From 817582b016a6b50f9135feed593a5295e33ff15b Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Sat, 19 Sep 2026 13:58:36 +0200 Subject: [PATCH 1/3] unix DirEntry: make fallback handling more clear --- library/std/src/sys/fs/unix.rs | 129 ++++++++++++++------------------- 1 file changed, 55 insertions(+), 74 deletions(-) diff --git a/library/std/src/sys/fs/unix.rs b/library/std/src/sys/fs/unix.rs index 46d5d33d23ac2..2e297cd10b537 100644 --- a/library/std/src/sys/fs/unix.rs +++ b/library/std/src/sys/fs/unix.rs @@ -1016,89 +1016,70 @@ 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 { - let fd = cvt(unsafe { dirfd(self.dir.dirp.0) })?; - let name = self.name.as_ptr(); + cfg_select! { + // Use fstatat (or similar) 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 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_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; + let mut stat: stat64 = unsafe { mem::zeroed() }; + cvt(unsafe { fstatat64(fd, name, &mut stat, libc::AT_SYMLINK_NOFOLLOW) })?; + Ok(FileAttr::from_stat64(stat)) } - } - - let mut stat: stat64 = unsafe { mem::zeroed() }; - cvt(unsafe { fstatat64(fd, name, &mut stat, libc::AT_SYMLINK_NOFOLLOW) })?; - Ok(FileAttr::from_stat64(stat)) - } - #[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 { - run_path_with_cstr(&self.path(), &lstat) - } - - #[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 { - 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 { + // 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 { From 07bc2b4fa8300e7ee27d06b41f6eb45d02e6b09e Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Sat, 19 Sep 2026 16:14:44 +0200 Subject: [PATCH 2/3] add Dir::(symlink_)metadata_at --- library/std/src/fs.rs | 48 +++++++++++++++++++++++++++ library/std/src/fs/tests.rs | 31 +++++++++++++---- library/std/src/sys/fs/common.rs | 8 +++++ library/std/src/sys/fs/unix/dir.rs | 36 ++++++++++++++++++-- library/std/src/sys/fs/windows.rs | 4 ++- library/std/src/sys/fs/windows/dir.rs | 46 +++++++++++++++++++++++-- 6 files changed, 160 insertions(+), 13 deletions(-) diff --git a/library/std/src/fs.rs b/library/std/src/fs.rs index 65b8ed634bc05..e7efa32af6964 100644 --- a/library/std/src/fs.rs +++ b/library/std/src/fs.rs @@ -1891,6 +1891,54 @@ impl Dir { pub fn try_clone(&self) -> io::Result { 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>(&self, path: P) -> io::Result { + 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>(&self, path: P) -> io::Result { + self.inner.symlink_metadata_at(path.as_ref()).map(Metadata) + } } impl AsInner for Dir { diff --git a/library/std/src/fs/tests.rs b/library/std/src/fs/tests.rs index e79b5cf17bc2b..a44fc328a209f 100644 --- a/library/std/src/fs/tests.rs +++ b/library/std/src/fs/tests.rs @@ -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}; @@ -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"); @@ -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()); +} diff --git a/library/std/src/sys/fs/common.rs b/library/std/src/sys/fs/common.rs index 96bafb26bb969..a754699696ed8 100644 --- a/library/std/src/sys/fs/common.rs +++ b/library/std/src/sys/fs/common.rs @@ -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 { + self.path.join(path).metadata().map(|m| m.into_inner()) + } + + pub fn symlink_metadata_at(&self, path: &Path) -> io::Result { + self.path.join(path).symlink_metadata().map(|m| m.into_inner()) + } } 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 cf0dece265054..a1e336b7fc237 100644 --- a/library/std/src/sys/fs/unix/dir.rs +++ b/library/std/src/sys/fs/unix/dir.rs @@ -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! { @@ -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<()> { @@ -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 { + run_path_with_cstr(path, &|path| { + self.metadata_at_c(path, /* symlink_nofollow */ false) + }) + } + + pub fn symlink_metadata_at(&self, path: &Path) -> io::Result { + run_path_with_cstr(path, &|path| self.metadata_at_c(path, /* symlink_nofollow */ true)) } fn open_with_c(path: &CStr, opts: &OpenOptions) -> io::Result { @@ -143,6 +153,26 @@ impl Dir { fn create_dir_c(&self, path: &CStr) -> io::Result<()> { cvt(unsafe { mkdirat(self.0.as_raw_fd(), path.as_ptr(), 0o777) }).map(|_| ()) } + + fn metadata_at_c(&self, path: &CStr, symlink_nofollow: bool) -> io::Result { + 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 { super::fstatat64(fd, path.as_ptr(), &mut stat, flag) })?; + Ok(FileAttr::from_stat64(stat)) + } } impl fmt::Debug for Dir { diff --git a/library/std/src/sys/fs/windows.rs b/library/std/src/sys/fs/windows.rs index b703ad9bd0ae8..40c247d9d011e 100644 --- a/library/std/src/sys/fs/windows.rs +++ b/library/std/src/sys/fs/windows.rs @@ -1474,7 +1474,9 @@ pub fn link(_original: &WCStr, _link: &WCStr) -> io::Result<()> { pub fn stat(path: &WCStr) -> io::Result { 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 . + if let Ok(attrs) = metadata(path, ReparsePoint::Open) { if !attrs.file_type().is_symlink() { return Ok(attrs); } diff --git a/library/std/src/sys/fs/windows/dir.rs b/library/std/src/sys/fs/windows/dir.rs index d4674ad24f87e..4940c60aa51f8 100644 --- a/library/std/src/sys/fs/windows/dir.rs +++ b/library/std/src/sys/fs/windows/dir.rs @@ -7,7 +7,7 @@ use crate::os::windows::io::{ }; use crate::path::Path; use crate::sys::api::{UnicodeStrRef, WinError}; -use crate::sys::fs::windows::debug_path_handle; +use crate::sys::fs::windows::{ReparsePoint, debug_path_handle}; use crate::sys::fs::{File, FileAttr, OpenOptions}; use crate::sys::handle::Handle; use crate::sys::path::{WCStr, with_native_path}; @@ -82,7 +82,7 @@ impl Dir { return File::open(path, opts); } let path = to_u16s_without_nul(path)?; - self.open_file_native(&path, opts, false).map(|handle| File { handle }) + self.open_file_native(&path, opts, /* dir */ false).map(|handle| File { handle }) } pub fn remove_file(&self, path: &Path) -> io::Result<()> { @@ -107,7 +107,7 @@ impl Dir { 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 }) + self.open_file_native(&path, &opts, /* dir */ true).map(|handle| Self { handle }) } pub fn remove_dir(&self, path: &Path) -> io::Result<()> { @@ -161,6 +161,7 @@ impl Dir { fn rename_native(&self, from: &[u16], to_dir: &Self, to: &[u16], dir: bool) -> io::Result<()> { let mut opts = OpenOptions::new(); opts.access_mode(c::DELETE); + // FIXME: custom_flags is ignored by `open_file_native`! opts.custom_flags(c::FILE_FLAG_OPEN_REPARSE_POINT | c::FILE_FLAG_BACKUP_SEMANTICS); let handle = self.open_file_native(from, &opts, dir)?; // Calculate the layout of the `FILE_RENAME_INFORMATION` we pass to `NtSetInformationFile` @@ -227,6 +228,45 @@ impl Dir { }); f.file_attr() } + + pub fn metadata_at(&self, path: &Path) -> io::Result { + let path = to_u16s_without_nul(path)?; + // Same as the `stat` logic used for `fs::metadata` + match self.metadata_at_native(&path, ReparsePoint::Follow) { + Err(err) if err.raw_os_error() == Some(c::ERROR_CANT_ACCESS_FILE as i32) => { + // Fallback to opening reparse points when following fails. Needed for UNIX domain + // sockets. See . + if let Ok(attrs) = self.metadata_at_native(&path, ReparsePoint::Open) { + if !attrs.file_type().is_symlink() { + return Ok(attrs); + } + } + Err(err) + } + result => result, + } + } + + pub fn symlink_metadata_at(&self, path: &Path) -> io::Result { + let path = to_u16s_without_nul(path)?; + self.metadata_at_native(&path, ReparsePoint::Open) + } + + fn metadata_at_native(&self, path: &[u16], reparse: ReparsePoint) -> io::Result { + let mut opts = OpenOptions::new(); + // the NT functions need at least c::FILE_READ_ATTRIBUTES + opts.access_mode(c::FILE_READ_ATTRIBUTES); + let create_opt = if reparse == ReparsePoint::Open { c::FILE_OPEN_REPARSE_POINT } else { 0 }; + + let name = UnicodeStrRef::from_slice(path); + let object_attributes = c::OBJECT_ATTRIBUTES { + RootDirectory: self.handle.as_raw_handle(), + ObjectName: name.as_ptr().cast_mut(), + ..c::OBJECT_ATTRIBUTES::with_length() + }; + let handle = unsafe { nt_create_file(&opts, &object_attributes, create_opt)? }; + File { handle }.file_attr() + } } impl fmt::Debug for Dir { From 626622f15706303a0db11fb8623f272fe25f75ad Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Sat, 19 Sep 2026 16:22:23 +0200 Subject: [PATCH 3/3] reuse Dir::metadata_at code for DirEntry::metadata --- library/std/src/sys/fs/unix.rs | 27 +++++++-------------------- library/std/src/sys/fs/unix/dir.rs | 14 +++++++++----- 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/library/std/src/sys/fs/unix.rs b/library/std/src/sys/fs/unix.rs index 2e297cd10b537..988bacd57d1b2 100644 --- a/library/std/src/sys/fs/unix.rs +++ b/library/std/src/sys/fs/unix.rs @@ -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::{ @@ -1018,7 +1014,7 @@ impl DirEntry { pub fn metadata(&self) -> io::Result { cfg_select! { - // Use fstatat (or similar) where possible + // Use directory handle where possible all( any( all(target_os = "linux", not(target_env = "musl")), @@ -1031,22 +1027,13 @@ impl DirEntry { not(miri) // no dirfd on Miri ) => { 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; - } - } - 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) })); + + dir_handle.metadata_at_c(&self.name, /* symlink_nofollow */ true) } // Fallback based on path diff --git a/library/std/src/sys/fs/unix/dir.rs b/library/std/src/sys/fs/unix/dir.rs index a1e336b7fc237..087916fd9ff63 100644 --- a/library/std/src/sys/fs/unix/dir.rs +++ b/library/std/src/sys/fs/unix/dir.rs @@ -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}; } } @@ -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 { @@ -154,7 +154,11 @@ impl Dir { cvt(unsafe { mkdirat(self.0.as_raw_fd(), path.as_ptr(), 0o777) }).map(|_| ()) } - fn metadata_at_c(&self, path: &CStr, symlink_nofollow: bool) -> io::Result { + pub(super) fn metadata_at_c( + &self, + path: &CStr, + symlink_nofollow: bool, + ) -> io::Result { let fd = self.0.as_raw_fd(); let flag = if symlink_nofollow { libc::AT_SYMLINK_NOFOLLOW } else { 0 }; @@ -170,7 +174,7 @@ impl Dir { } let mut stat: super::stat64 = unsafe { mem::zeroed() }; - cvt(unsafe { super::fstatat64(fd, path.as_ptr(), &mut stat, flag) })?; + cvt(unsafe { fstatat64(fd, path.as_ptr(), &mut stat, flag) })?; Ok(FileAttr::from_stat64(stat)) } }