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
48 changes: 48 additions & 0 deletions library/std/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1891,6 +1891,54 @@ impl Dir {
pub fn try_clone(&self) -> io::Result<Self> {
Ok(Dir { inner: self.inner.duplicate()? })
}

/// Queries the file system to get information about a file, directory, etc. relative to this
/// directory.
///
/// This function will traverse symbolic links to query information about the destination file.
/// To query metadata about the path itself without following symbolic links, use
/// [`symlink_metadata_at`][Self::symlink_metadata_at].
///
/// # Examples
///
/// ```no_run
/// #![feature(dirfd)]
/// use std::fs::Dir;
///
/// fn main() -> std::io::Result<()> {
/// let dir = Dir::open("foo")?;
/// let metadata = dir.metadata_at("subdir/file.txt")?;
/// Ok(())
/// }
/// ```
#[unstable(feature = "dirfd", issue = "120426")]
pub fn metadata_at<P: AsRef<Path>>(&self, path: P) -> io::Result<Metadata> {

@RalfJung RalfJung Sep 19, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The name does match the operation in fs that it corresponds to -- but Dir::metadata is already taken. So I went for a new name, inspired by the Linux naming scheme with the *at calls working on directory handles.

Alternatives that have been suggested or that I can think of:

  • Use just metadata for this, and ask people to write dir.metadata(".") to get the metadata of the directory itself. But that seems silly in terms of the extra syscalls it causes.
  • Use just metadata for this, and use self_metadata/metadata_self or so for the metadata of the directory handle itself.

View changes since the review

self.inner.metadata_at(path.as_ref()).map(Metadata)
}

/// Queries the file system to get information about a file, directory, etc. relative to this
/// directory.
///
/// This function will return the [`Metadata`] of the exact path without traversing symbolic
/// links to a resolved destination file. Using this function on a path that is a file or
/// directory (not a symbolic link) will behave the same as [`metadata_at`][Self::metadata_at].
///
/// # Examples
///
/// ```no_run
/// #![feature(dirfd)]
/// use std::fs::Dir;
///
/// fn main() -> std::io::Result<()> {
/// let dir = Dir::open("foo")?;
/// let metadata = dir.symlink_metadata_at("subdir/file.txt")?;
/// Ok(())
/// }
/// ```
#[unstable(feature = "dirfd", issue = "120426")]
pub fn symlink_metadata_at<P: AsRef<Path>>(&self, path: P) -> io::Result<Metadata> {
self.inner.symlink_metadata_at(path.as_ref()).map(Metadata)
}
}

impl AsInner<fs_imp::Dir> for Dir {
Expand Down
31 changes: 25 additions & 6 deletions library/std/src/fs/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::os::unix::fs::symlink as symlink_file;
use crate::os::unix::fs::symlink as junction_point;
#[cfg(windows)]
use crate::os::windows::fs::{OpenOptionsExt, junction_point, symlink_dir, symlink_file};
use crate::path::Path;
use crate::path::{Path, PathBuf};
use crate::sync::Arc;
use crate::test_helpers::{TempDir, tmpdir};
use crate::time::{Duration, Instant, SystemTime};
Expand Down Expand Up @@ -655,11 +655,6 @@ fn set_get_permissions_nofollows() {
not(any(target_os = "espidf", target_os = "horizon", target_os = "wasi"))
))]
fn set_get_permissions_nofollows_symlink() {
#[cfg(not(windows))]
use crate::os::unix::fs::symlink as symlink_file;
#[cfg(windows)]
use crate::os::windows::fs::symlink_file;

let tmpdir = tmpdir();
let filename = tmpdir.join("set_get_unix_permissions_file");
let symlink_name = tmpdir.join("set_get_unix_permissions");
Expand Down Expand Up @@ -3088,3 +3083,27 @@ fn test_dir_open_dir() {
check!(f.read_exact(&mut buf));
assert_eq!(b"baz", &buf);
}

#[test]
fn test_dir_metadata_at() {
let tmpdir = tmpdir();
let dir = check!(Dir::open(tmpdir.path()));
check!(dir.create_dir("subdir"));
// FIXME: `/` does not work as path separator on Windows.
let barpath = PathBuf::from("subdir").join("bar.txt");
drop(check!(dir.open_file_with(&barpath, &OpenOptions::new().create(true).write(true))));
check!(symlink_file(&tmpdir.join("subdir/bar.txt"), &tmpdir.join("link")));

let metadata = check!(dir.metadata_at(&barpath));
assert!(metadata.is_file());
let metadata = check!(dir.metadata_at("subdir"));
assert!(metadata.is_dir());
dir.metadata_at("does-not-exist").unwrap_err();

let metadata = check!(dir.metadata_at("link"));
assert!(metadata.is_file());
assert!(!metadata.is_symlink());
let metadata = check!(dir.symlink_metadata_at("link"));
assert!(!metadata.is_file());
assert!(metadata.is_symlink());
}
8 changes: 8 additions & 0 deletions library/std/src/sys/fs/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,14 @@ impl Dir {
pub fn remove_dir(&self, path: &Path) -> io::Result<()> {
remove_dir(path)
}

pub fn metadata_at(&self, path: &Path) -> io::Result<FileAttr> {
self.path.join(path).metadata().map(|m| m.into_inner())
}

pub fn symlink_metadata_at(&self, path: &Path) -> io::Result<FileAttr> {
self.path.join(path).symlink_metadata().map(|m| m.into_inner())
}
}

impl fmt::Debug for Dir {
Expand Down
124 changes: 46 additions & 78 deletions library/std/src/sys/fs/unix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ use libc::c_char;
target_vendor = "apple",
))]
use libc::dirfd;
#[cfg(any(target_os = "fuchsia", target_os = "illumos", target_vendor = "apple"))]
use libc::fstatat as fstatat64;
#[cfg(any(all(target_os = "linux", not(target_env = "musl")), target_os = "hurd"))]
use libc::fstatat64;
use libc::{c_int, mode_t};
#[cfg(target_os = "android")]
use libc::{
Expand Down Expand Up @@ -1016,89 +1012,61 @@ impl DirEntry {
self.file_name_os_str().to_os_string()
}

#[cfg(all(
any(
all(target_os = "linux", not(target_env = "musl")),
target_os = "android",
target_os = "fuchsia",
target_os = "hurd",
target_os = "illumos",
target_vendor = "apple",
),
not(miri) // no dirfd on Miri
))]
pub fn metadata(&self) -> io::Result<FileAttr> {
let fd = cvt(unsafe { dirfd(self.dir.dirp.0) })?;
let name = self.name.as_ptr();

cfg_has_statx! {
if let Some(ret) = unsafe { try_statx(
fd,
name,
libc::AT_SYMLINK_NOFOLLOW | libc::AT_STATX_SYNC_AS_STAT,
libc::STATX_BASIC_STATS | libc::STATX_BTIME,
) } {
return ret;
}
}
cfg_select! {
// Use directory handle where possible
all(
any(
all(target_os = "linux", not(target_env = "musl")),
target_os = "android",
target_os = "fuchsia",
target_os = "hurd",
target_os = "illumos",
target_vendor = "apple",
),
not(miri) // no dirfd on Miri
) => {
let fd = cvt(unsafe { dirfd(self.dir.dirp.0) })?;

let mut stat: stat64 = unsafe { mem::zeroed() };
cvt(unsafe { fstatat64(fd, name, &mut stat, libc::AT_SYMLINK_NOFOLLOW) })?;
Ok(FileAttr::from_stat64(stat))
}
// Make this FD into a directory handle. We don't actually drop it,
// so having an `OwnedFd` is fine.
let dir_handle =
mem::ManuallyDrop::new(dir::Dir(unsafe { OwnedFd::from_raw_fd(fd) }));

#[cfg(any(
not(any(
all(target_os = "linux", not(target_env = "musl")),
target_os = "android",
target_os = "fuchsia",
target_os = "hurd",
target_os = "illumos",
target_vendor = "apple",
)),
miri // no dirfd on Miri
))]
pub fn metadata(&self) -> io::Result<FileAttr> {
run_path_with_cstr(&self.path(), &lstat)
}
dir_handle.metadata_at_c(&self.name, /* symlink_nofollow */ true)
}

#[cfg(any(
target_os = "solaris",
target_os = "illumos",
target_os = "haiku",
target_os = "vxworks",
target_os = "aix",
target_os = "nto",
target_os = "qnx",
target_os = "vita",
target_os = "l4re",
))]
pub fn file_type(&self) -> io::Result<FileType> {
self.metadata().map(|m| m.file_type())
// Fallback based on path
_ => run_path_with_cstr(&self.path(), &lstat),
}
}

#[cfg(not(any(
target_os = "solaris",
target_os = "illumos",
target_os = "haiku",
target_os = "vxworks",
target_os = "aix",
target_os = "nto",
target_os = "qnx",
target_os = "vita",
target_os = "l4re",
)))]
pub fn file_type(&self) -> io::Result<FileType> {
// Use `entry.d_type` if available.
#[cfg(not(any(
target_os = "solaris",
target_os = "illumos",
target_os = "haiku",
target_os = "vxworks",
target_os = "aix",
target_os = "nto",
target_os = "qnx",
target_os = "vita",
target_os = "l4re",
)))]
match self.entry.d_type {
libc::DT_CHR => Ok(FileType { mode: libc::S_IFCHR }),
libc::DT_FIFO => Ok(FileType { mode: libc::S_IFIFO }),
libc::DT_LNK => Ok(FileType { mode: libc::S_IFLNK }),
libc::DT_REG => Ok(FileType { mode: libc::S_IFREG }),
libc::DT_SOCK => Ok(FileType { mode: libc::S_IFSOCK }),
libc::DT_DIR => Ok(FileType { mode: libc::S_IFDIR }),
libc::DT_BLK => Ok(FileType { mode: libc::S_IFBLK }),
_ => self.metadata().map(|m| m.file_type()),
libc::DT_CHR => return Ok(FileType { mode: libc::S_IFCHR }),
libc::DT_FIFO => return Ok(FileType { mode: libc::S_IFIFO }),
libc::DT_LNK => return Ok(FileType { mode: libc::S_IFLNK }),
libc::DT_REG => return Ok(FileType { mode: libc::S_IFREG }),
libc::DT_SOCK => return Ok(FileType { mode: libc::S_IFSOCK }),
libc::DT_DIR => return Ok(FileType { mode: libc::S_IFDIR }),
libc::DT_BLK => return Ok(FileType { mode: libc::S_IFBLK }),
_ => {}
}

// Fall back to loading the metadata.
self.metadata().map(|m| m.file_type())
}

pub fn ino(&self) -> u64 {
Expand Down
46 changes: 40 additions & 6 deletions library/std/src/sys/fs/unix/dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ cfg_select! {
target_os = "android",
target_os = "hurd",
)) => {
use libc::{open as open64, openat as openat64};
use libc::{fstatat as fstatat64, open as open64, openat as openat64};
}
_ => {
use libc::{open64, openat64};
use libc::{fstatat64, open64, openat64};
}
}

Expand All @@ -26,7 +26,7 @@ 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, cvt_r};
use crate::{fmt, fs, io};
use crate::{fmt, fs, io, mem};

const TRAVERSE_DIRECTORY: i32 =
cfg_select! {
Expand All @@ -36,7 +36,7 @@ const TRAVERSE_DIRECTORY: i32 =
_ => libc::O_RDONLY,
};

pub struct Dir(OwnedFd);
pub struct Dir(pub(super) OwnedFd);

impl Dir {
pub fn open(path: &Path, opts: &OpenOptions) -> io::Result<Self> {
Expand Down Expand Up @@ -68,7 +68,7 @@ impl Dir {
}

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

pub fn rename(&self, from: &Path, to_dir: &Self, to: &Path) -> io::Result<()> {
Expand All @@ -86,7 +86,17 @@ impl Dir {
}

pub fn remove_dir(&self, path: &Path) -> io::Result<()> {
run_path_with_cstr(path, &|path| self.remove_c(path, true))
run_path_with_cstr(path, &|path| self.remove_c(path, /* remove_dir */ true))
}

pub fn metadata_at(&self, path: &Path) -> io::Result<FileAttr> {
run_path_with_cstr(path, &|path| {
self.metadata_at_c(path, /* symlink_nofollow */ false)
})
}

pub fn symlink_metadata_at(&self, path: &Path) -> io::Result<FileAttr> {
run_path_with_cstr(path, &|path| self.metadata_at_c(path, /* symlink_nofollow */ true))
}

fn open_with_c(path: &CStr, opts: &OpenOptions) -> io::Result<Self> {
Expand Down Expand Up @@ -143,6 +153,30 @@ impl Dir {
fn create_dir_c(&self, path: &CStr) -> io::Result<()> {
cvt(unsafe { mkdirat(self.0.as_raw_fd(), path.as_ptr(), 0o777) }).map(|_| ())
}

pub(super) fn metadata_at_c(
&self,
path: &CStr,
symlink_nofollow: bool,
) -> io::Result<FileAttr> {
let fd = self.0.as_raw_fd();
let flag = if symlink_nofollow { libc::AT_SYMLINK_NOFOLLOW } else { 0 };

cfg_has_statx! {
if let Some(ret) = unsafe { super::try_statx(
fd,
path.as_ptr(),
flag | libc::AT_STATX_SYNC_AS_STAT,
libc::STATX_BASIC_STATS | libc::STATX_BTIME,
) } {
return ret;
}
}

let mut stat: super::stat64 = unsafe { mem::zeroed() };
cvt(unsafe { fstatat64(fd, path.as_ptr(), &mut stat, flag) })?;
Ok(FileAttr::from_stat64(stat))
}
}

impl fmt::Debug for Dir {
Expand Down
4 changes: 3 additions & 1 deletion library/std/src/sys/fs/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1474,7 +1474,9 @@ pub fn link(_original: &WCStr, _link: &WCStr) -> io::Result<()> {
pub fn stat(path: &WCStr) -> io::Result<FileAttr> {
match metadata(path, ReparsePoint::Follow) {
Err(err) if err.raw_os_error() == Some(c::ERROR_CANT_ACCESS_FILE as i32) => {
if let Ok(attrs) = lstat(path) {
// Fallback to opening reparse points when following fails. Needed for UNIX domain
// sockets. See <https://github.com/rust-lang/rust/issues/109106>.
if let Ok(attrs) = metadata(path, ReparsePoint::Open) {
if !attrs.file_type().is_symlink() {
return Ok(attrs);
}
Expand Down
Loading
Loading