Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 89 additions & 4 deletions library/std/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1545,6 +1545,8 @@ impl crate::io::IoHandle for File {}
impl Dir {
/// Attempts to open a directory at `path` in read-only mode.
///
/// 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.
Expand All @@ -1570,8 +1572,30 @@ impl Dir {
.map(|inner| Self { inner })
}

/// Queries metadata about the underlying directory.
///
/// # Examples
///
/// ```no_run
/// #![feature(dirfd)]
/// use std::fs::Dir;
///
/// fn main() -> std::io::Result<()> {
/// let dir = Dir::open("foo")?;
/// let metadata = dir.metadata()?;
/// Ok(())
/// }
/// ```
#[unstable(feature = "dirfd", issue = "120426")]
pub fn metadata(&self) -> io::Result<Metadata> {
self.inner.metadata().map(Metadata)
}

/// Attempts to open a file in read-only mode relative to this directory.
///
/// This function interprets `path` relative to the directory provided by `self`. To open a file
/// relative to the current working directory, or at an absolute path, see [`File::open`].
///
/// # Errors
///
/// This function will return an error if `path` does not point to an existing file.
Expand All @@ -1598,7 +1622,63 @@ impl Dir {
.map(|f| File { inner: f })
}

/// Queries metadata about the underlying directory.
/// Attempts to open a file according to `opts` relative to this directory.
///
/// This function interprets `path` relative to the directory provided by `self`. To open a file
/// relative to the current working directory, or at an absolute path, see [`File::open`].
///
/// # Errors
///
/// This function will return an error if `path` does not point to an existing file.
/// Other errors may also be returned according to [`OpenOptions::open`].
///
/// # Examples
///
/// ```no_run
/// #![feature(dirfd)]
/// use std::{fs::{Dir, OpenOptions}, io::{self, Write}};
///
/// fn main() -> io::Result<()> {
/// let dir = Dir::open("foo")?;
/// let mut opts = OpenOptions::new();
/// opts.read(true).write(true);
/// let mut f = dir.open_file_with("bar.txt", &opts)?;
/// f.write_all(b"Hello, world!")?;
/// let contents = io::read_to_string(f)?;
/// assert_eq!(contents, "Hello, world!");
/// Ok(())
/// }
/// ```
#[unstable(feature = "dirfd", issue = "120426")]
pub fn open_file_with<P: AsRef<Path>>(&self, path: P, opts: &OpenOptions) -> io::Result<File> {
self.inner.open_file(path.as_ref(), &opts.0).map(|f| File { inner: f })
}

/// Attempts to remove a file relative to this directory.
///
/// This function interprets `path` relative to the directory provided by `self`. To remove a file
/// relative to the current working directory, or at an absolute path, see [`fs::remove_file`][remove_file].
///
/// # Errors
///
/// This function will return an error if `path` does not point to an existing file.
/// Other errors may also be returned according to [`OpenOptions::open`].
/// dir.remove_file("bar.txt")?;
#[unstable(feature = "dirfd", issue = "120426")]
pub fn remove_file<P: AsRef<Path>>(&self, path: P) -> io::Result<()> {
self.inner.remove_file(path.as_ref())
}

/// Attempts to rename a file or directory relative to this directory to a new name, replacing
/// the destination file if present.
///
/// This function interprets `from` relative to the directory provided by `self` and `to` relative to the directory
/// provided by `to_dir`. To rename a file relative to the current working directory, or at an absolute path, see [`fs::rename`][rename].
///
/// # Errors
///
/// This function will return an error if `from` does not point to an existing file or directory.
/// Other errors may also be returned according to [`OpenOptions::open`].
///
/// # Examples
///
Expand All @@ -1608,13 +1688,18 @@ impl Dir {
///
/// fn main() -> std::io::Result<()> {
/// let dir = Dir::open("foo")?;
/// let metadata = dir.metadata()?;
/// dir.rename("bar.txt", &dir, "quux.txt")?;
/// Ok(())
/// }
/// ```
#[unstable(feature = "dirfd", issue = "120426")]
pub fn metadata(&self) -> io::Result<Metadata> {
self.inner.metadata().map(Metadata)
pub fn rename<P: AsRef<Path>, Q: AsRef<Path>>(
&self,
from: P,
to_dir: &Self,
to: Q,
) -> io::Result<()> {
self.inner.rename(from.as_ref(), &to_dir.inner, to.as_ref())
}
}

Expand Down
50 changes: 47 additions & 3 deletions library/std/src/fs/tests.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use rand::RngCore;

use super::Dir;
use crate::fs::{self, File, FileTimes, OpenOptions, TryLockError};
use crate::fs::{self, File, FileTimes, OpenOptions, TryLockError, exists};
use crate::io::prelude::*;
use crate::io::{BorrowedBuf, ErrorKind, SeekFrom};
use crate::mem::MaybeUninit;
Expand Down Expand Up @@ -2530,18 +2530,19 @@ fn test_fs_set_times_nofollow() {
}

#[test]
#[cfg_attr(miri, ignore)] // FIXME: Miri does not support directory handles
fn test_dir_smoke_test() {
let tmpdir = tmpdir();
let dir = Dir::open(tmpdir.path());
check!(dir);
}

#[test]
#[cfg_attr(miri, ignore)] // FIXME: Miri does not support directory handles
fn test_dir_read_file() {
let tmpdir = tmpdir();
let mut f = check!(File::create(tmpdir.join("foo.txt")));
check!(f.write(b"bar"));
check!(f.flush());
check!(f.write_all(b"bar"));
drop(f);
let dir = check!(Dir::open(tmpdir.path()));
let f = check!(dir.open_file("foo.txt"));
Expand All @@ -2559,3 +2560,46 @@ fn test_dir_metadata() {
let metadata = check!(dir.metadata());
assert!(metadata.is_dir());
}

#[test]
#[cfg_attr(miri, ignore)] // FIXME: Miri does not support directory handles
fn test_dir_write_file() {
let tmpdir = tmpdir();
let dir = check!(Dir::open(tmpdir.path()));
let mut f = check!(dir.open_file_with("foo.txt", &OpenOptions::new().write(true).create(true)));
check!(f.write(b"bar"));
check!(f.flush());
drop(f);
let mut f = check!(File::open(tmpdir.join("foo.txt")));
let mut buf = [0u8; 3];
check!(f.read_exact(&mut buf));
assert_eq!(b"bar", &buf);
}

#[test]
#[cfg_attr(miri, ignore)] // FIXME: Miri does not support directory handles
fn test_dir_remove_file() {
let tmpdir = tmpdir();
let mut f = check!(File::create(tmpdir.join("foo.txt")));
check!(f.write(b"bar"));
check!(f.flush());
drop(f);
let dir = check!(Dir::open(tmpdir.path()));
check!(dir.remove_file("foo.txt"));
assert!(!matches!(exists(tmpdir.join("foo.txt")), Ok(true)));
}

#[test]
#[cfg_attr(miri, ignore)] // FIXME: Miri does not support directory handles
fn test_dir_rename_file() {
let tmpdir = tmpdir();
let mut f = check!(File::create(tmpdir.join("foo.txt")));
check!(f.write_all(b"bar"));
drop(f);
let dir = check!(Dir::open(tmpdir.path()));
check!(dir.rename("foo.txt", &dir, "baz.txt"));
let mut f = check!(File::open(tmpdir.join("baz.txt")));
let mut buf = [0u8; 3];
check!(f.read_exact(&mut buf));
assert_eq!(b"bar", &buf);
}
9 changes: 9 additions & 0 deletions library/std/src/sys/fs/common.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#![allow(dead_code)] // not used on all platforms

use crate::fs::{remove_file, rename};
use crate::io::{self, Error, ErrorKind};
use crate::path::{Path, PathBuf};
use crate::sys::IntoInner;
Expand Down Expand Up @@ -77,6 +78,14 @@ impl Dir {
pub fn metadata(&self) -> io::Result<FileAttr> {
self.path.metadata().map(|m| m.into_inner())
}

pub fn remove_file(&self, path: &Path) -> io::Result<()> {
remove_file(self.path.join(path))
}

pub fn rename(&self, from: &Path, to_dir: &Self, to: &Path) -> io::Result<()> {
rename(self.path.join(from), to_dir.path.join(to))
}
}

impl fmt::Debug for Dir {
Expand Down
32 changes: 30 additions & 2 deletions library/std/src/sys/fs/unix/dir.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use libc::c_int;
use libc::{c_int, renameat, unlinkat};

cfg_select! {
not(
Expand Down Expand Up @@ -27,7 +27,7 @@ use crate::sys::fd::FileDesc;
use crate::sys::fs::OpenOptions;
use crate::sys::fs::unix::{File, FileAttr, debug_path_fd};
use crate::sys::helpers::run_path_with_cstr;
use crate::sys::{AsInner, FromInner, IntoInner, cvt_r};
use crate::sys::{AsInner, FromInner, IntoInner, cvt, cvt_r};
use crate::{fmt, fs, io};

pub struct Dir(OwnedFd);
Expand All @@ -51,6 +51,16 @@ impl Dir {
f.file_attr()
}

pub fn remove_file(&self, path: &Path) -> io::Result<()> {
run_path_with_cstr(path, &|path| self.remove_c(path, false))

@Mark-Simulacrum Mark-Simulacrum Jun 7, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this check somewhere that the passed path is relative? Otherwise we're not actually getting the promised semantics: "If path is absolute, then dirfd is ignored." (https://www.man7.org/linux/man-pages/man2/unlink.2.html)

View changes since the review

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems like a question for all dirfd-relative operations that take a path: should they require the path to be relative, or should they allow absolute paths and do the obvious thing for them? IMO it makes sense to allow absolute paths. That basically lets you use dirfd as a "local working directory" without the downsides of set_current_dir.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The argument against is that people would like to use dirfd in security relevant contexts. You can build a less secure variant on top of dirfd but not really the other way around (well technically you can but defaulting to the less secure option has historically been a footgun).

But in any case, I think this is a question for libs-api.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unstable, so you can put it on the open questions in the tracking issue? I don't think this needs to be settled immediately.

On linux there's openat2 which adds RESOLVE_BENEATH and RESOLVE_IN_ROOT flags which give you certain security properties, depending on which you want.

@RalfJung RalfJung Jun 10, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tracking issue explicitly says that this is NOT using O_BENEATH. We also have a fallback implementation that just stores the dir name as a string. So anything security critical seems to be already ruled out.

I have added this to #120426.

}

pub fn rename(&self, from: &Path, to_dir: &Self, to: &Path) -> io::Result<()> {
run_path_with_cstr(from, &|from| {
run_path_with_cstr(to, &|to| self.rename_c(from, to_dir, to))
})
}

pub fn open_with_c(path: &CStr, opts: &OpenOptions) -> io::Result<Self> {
let flags = libc::O_CLOEXEC
| libc::O_DIRECTORY
Expand All @@ -71,6 +81,24 @@ impl Dir {
})?;
Ok(File(unsafe { FileDesc::from_raw_fd(fd) }))
}

fn remove_c(&self, path: &CStr, remove_dir: bool) -> io::Result<()> {
cvt(unsafe {
unlinkat(
self.0.as_raw_fd(),
path.as_ptr(),
if remove_dir { libc::AT_REMOVEDIR } else { 0 },
)
})
.map(|_| ())
}

fn rename_c(&self, from: &CStr, to_dir: &Self, to: &CStr) -> io::Result<()> {
cvt(unsafe {
renameat(self.0.as_raw_fd(), from.as_ptr(), to_dir.0.as_raw_fd(), to.as_ptr())
})
.map(|_| ())
}
}

impl fmt::Debug for Dir {
Expand Down
Loading
Loading