From 80ab2d4c76d48129363c8d808e9c46bf88ce3d99 Mon Sep 17 00:00:00 2001 From: Jeremy Smart Date: Tue, 8 Apr 2025 01:38:40 -0400 Subject: [PATCH 1/7] dirfd: initial quick and dirty implementation for unix --- library/std/src/fs.rs | 26 ++++ library/std/src/sys/fs/mod.rs | 2 + library/std/src/sys/fs/unix.rs | 230 ++++++++++++++++++++++----------- 3 files changed, 180 insertions(+), 78 deletions(-) diff --git a/library/std/src/fs.rs b/library/std/src/fs.rs index 3ff08e3a5664b..a5b91dc5f68de 100644 --- a/library/std/src/fs.rs +++ b/library/std/src/fs.rs @@ -132,6 +132,13 @@ pub enum TryLockError { WouldBlock, } +#[unstable(feature = "dirfd", issue = "120426")] +#[cfg(target_family = "unix")] +/// An object providing access to a directory on the filesystem. +pub struct Dir { + inner: fs_imp::Dir, +} + /// Metadata information about a file. /// /// This structure is returned from the [`metadata`] or @@ -1402,6 +1409,25 @@ impl Seek for Arc { } } +#[unstable(feature = "dirfd", issue = "120426")] +impl Dir { + /// Opens a file relative to this directory. + pub fn open>(&self, path: P) -> io::Result { + self.inner.open(path).map(|f| File { inner: f }) + } + /// Opens a file relative to this directory with the specified options. + pub fn open_with>(&self, path: P, opts: &OpenOptions) -> io::Result { + self.inner.open_with(path, &opts.0).map(|f| File { inner: f }) + } +} + +#[unstable(feature = "dirfd", issue = "120426")] +impl fmt::Debug for Dir { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.inner.fmt(f) + } +} + impl OpenOptions { /// Creates a blank new set of options ready for configuration. /// diff --git a/library/std/src/sys/fs/mod.rs b/library/std/src/sys/fs/mod.rs index d55e28074fe8c..458c097339d90 100644 --- a/library/std/src/sys/fs/mod.rs +++ b/library/std/src/sys/fs/mod.rs @@ -46,6 +46,8 @@ pub fn with_native_path(path: &Path, f: &dyn Fn(&Path) -> io::Result) -> i f(path) } +#[cfg(target_family = "unix")] +pub use imp::Dir; pub use imp::{ DirBuilder, DirEntry, File, FileAttr, FilePermissions, FileTimes, FileType, OpenOptions, ReadDir, diff --git a/library/std/src/sys/fs/unix.rs b/library/std/src/sys/fs/unix.rs index 863358596c199..a50afc4c3231e 100644 --- a/library/std/src/sys/fs/unix.rs +++ b/library/std/src/sys/fs/unix.rs @@ -54,7 +54,7 @@ use libc::{c_int, mode_t}; #[cfg(target_os = "android")] use libc::{ dirent as dirent64, fstat as fstat64, fstatat as fstatat64, ftruncate64, lseek64, - lstat as lstat64, off64_t, open as open64, stat as stat64, + lstat as lstat64, off64_t, open as open64, openat as openat64, stat as stat64, }; #[cfg(not(any( all(target_os = "linux", not(target_env = "musl")), @@ -64,14 +64,14 @@ use libc::{ )))] use libc::{ dirent as dirent64, fstat as fstat64, ftruncate as ftruncate64, lseek as lseek64, - lstat as lstat64, off_t as off64_t, open as open64, stat as stat64, + lstat as lstat64, off_t as off64_t, open as open64, openat as openat64, stat as stat64, }; #[cfg(any( all(target_os = "linux", not(target_env = "musl")), target_os = "l4re", target_os = "hurd" ))] -use libc::{dirent64, fstat64, ftruncate64, lseek64, lstat64, off64_t, open64, stat64}; +use libc::{dirent64, fstat64, ftruncate64, lseek64, lstat64, off64_t, open64, openat64, stat64}; use crate::ffi::{CStr, OsStr, OsString}; use crate::fmt::{self, Write as _}; @@ -264,7 +264,154 @@ impl ReadDir { } } -struct Dir(*mut libc::DIR); +pub struct Dir(*mut libc::DIR); + +// dirfd isn't supported everywhere +#[cfg(not(any( + miri, + target_os = "redox", + target_os = "nto", + target_os = "vita", + target_os = "hurd", + target_os = "espidf", + target_os = "horizon", + target_os = "vxworks", + target_os = "rtems", + target_os = "nuttx", +)))] +impl Dir { + pub fn open>(&self, path: P) -> io::Result { + let mut opts = OpenOptions::new(); + opts.read(true); + run_path_with_cstr(path.as_ref(), &|path| self.open_c(path, &opts)) + } + + pub fn open_with>(&self, path: P, opts: &OpenOptions) -> io::Result { + run_path_with_cstr(path.as_ref(), &|path| self.open_c(path, opts)) + } + + pub fn open_c(&self, path: &CStr, opts: &OpenOptions) -> io::Result { + let flags = libc::O_CLOEXEC + | opts.get_access_mode()? + | opts.get_creation_mode()? + | (opts.custom_flags as c_int & !libc::O_ACCMODE); + let fd = cvt_r(|| unsafe { + openat64(libc::dirfd(self.0), path.as_ptr(), flags, opts.mode as c_int) + })?; + Ok(File(unsafe { FileDesc::from_raw_fd(fd) })) + } + + // pub fn create_dir>(&self, path: P) -> Result<()> + // pub fn rename, Q: AsRef>(&self, from: P, to_dir: &Self, to: Q) -> Result<()> + // pub fn remove_file>(&self, path: P) -> Result<()> + // pub fn remove_dir>(&self, path: P) -> Result<()> + // pub fn symlink, Q: AsRef>(&self, original: P, link: Q) +} + +fn get_path_from_fd(fd: c_int) -> Option { + #[cfg(any(target_os = "linux", target_os = "illumos", target_os = "solaris"))] + fn get_path(fd: c_int) -> Option { + let mut p = PathBuf::from("/proc/self/fd"); + p.push(&fd.to_string()); + run_path_with_cstr(&p, &readlink).ok() + } + + #[cfg(any(target_vendor = "apple", target_os = "netbsd"))] + fn get_path(fd: c_int) -> Option { + // FIXME: The use of PATH_MAX is generally not encouraged, but it + // is inevitable in this case because Apple targets and NetBSD define `fcntl` + // with `F_GETPATH` in terms of `MAXPATHLEN`, and there are no + // alternatives. If a better method is invented, it should be used + // instead. + let mut buf = vec![0; libc::PATH_MAX as usize]; + let n = unsafe { libc::fcntl(fd, libc::F_GETPATH, buf.as_ptr()) }; + if n == -1 { + cfg_if::cfg_if! { + if #[cfg(target_os = "netbsd")] { + // fallback to procfs as last resort + let mut p = PathBuf::from("/proc/self/fd"); + p.push(&fd.to_string()); + return run_path_with_cstr(&p, &readlink).ok() + } else { + return None; + } + } + } + let l = buf.iter().position(|&c| c == 0).unwrap(); + buf.truncate(l as usize); + buf.shrink_to_fit(); + Some(PathBuf::from(OsString::from_vec(buf))) + } + + #[cfg(target_os = "freebsd")] + fn get_path(fd: c_int) -> Option { + let info = Box::::new_zeroed(); + let mut info = unsafe { info.assume_init() }; + info.kf_structsize = size_of::() as libc::c_int; + let n = unsafe { libc::fcntl(fd, libc::F_KINFO, &mut *info) }; + if n == -1 { + return None; + } + let buf = unsafe { CStr::from_ptr(info.kf_path.as_mut_ptr()).to_bytes().to_vec() }; + Some(PathBuf::from(OsString::from_vec(buf))) + } + + #[cfg(target_os = "vxworks")] + fn get_path(fd: c_int) -> Option { + let mut buf = vec![0; libc::PATH_MAX as usize]; + let n = unsafe { libc::ioctl(fd, libc::FIOGETNAME, buf.as_ptr()) }; + if n == -1 { + return None; + } + let l = buf.iter().position(|&c| c == 0).unwrap(); + buf.truncate(l as usize); + Some(PathBuf::from(OsString::from_vec(buf))) + } + + #[cfg(not(any( + target_os = "linux", + target_os = "vxworks", + target_os = "freebsd", + target_os = "netbsd", + target_os = "illumos", + target_os = "solaris", + target_vendor = "apple", + )))] + fn get_path(_fd: c_int) -> Option { + // FIXME(#24570): implement this for other Unix platforms + None + } + + get_path(fd) +} + +impl fmt::Debug for Dir { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn get_mode(fd: c_int) -> Option<(bool, bool)> { + let mode = unsafe { libc::fcntl(fd, libc::F_GETFL) }; + if mode == -1 { + return None; + } + match mode & libc::O_ACCMODE { + libc::O_RDONLY => Some((true, false)), + libc::O_RDWR => Some((true, true)), + libc::O_WRONLY => Some((false, true)), + _ => None, + } + } + + let fd = unsafe { dirfd(self.0) }; + let mut b = f.debug_struct("Dir"); + b.field("fd", &fd); + if let Some(path) = get_path_from_fd(fd) { + b.field("path", &path); + } + if let Some((read, write)) = get_mode(fd) { + b.field("read", &read).field("write", &write); + } + b.finish() + } +} unsafe impl Send for Dir {} unsafe impl Sync for Dir {} @@ -1653,79 +1800,6 @@ impl FromRawFd for File { impl fmt::Debug for File { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - #[cfg(any(target_os = "linux", target_os = "illumos", target_os = "solaris"))] - fn get_path(fd: c_int) -> Option { - let mut p = PathBuf::from("/proc/self/fd"); - p.push(&fd.to_string()); - run_path_with_cstr(&p, &readlink).ok() - } - - #[cfg(any(target_vendor = "apple", target_os = "netbsd"))] - fn get_path(fd: c_int) -> Option { - // FIXME: The use of PATH_MAX is generally not encouraged, but it - // is inevitable in this case because Apple targets and NetBSD define `fcntl` - // with `F_GETPATH` in terms of `MAXPATHLEN`, and there are no - // alternatives. If a better method is invented, it should be used - // instead. - let mut buf = vec![0; libc::PATH_MAX as usize]; - let n = unsafe { libc::fcntl(fd, libc::F_GETPATH, buf.as_ptr()) }; - if n == -1 { - cfg_if::cfg_if! { - if #[cfg(target_os = "netbsd")] { - // fallback to procfs as last resort - let mut p = PathBuf::from("/proc/self/fd"); - p.push(&fd.to_string()); - return run_path_with_cstr(&p, &readlink).ok() - } else { - return None; - } - } - } - let l = buf.iter().position(|&c| c == 0).unwrap(); - buf.truncate(l as usize); - buf.shrink_to_fit(); - Some(PathBuf::from(OsString::from_vec(buf))) - } - - #[cfg(target_os = "freebsd")] - fn get_path(fd: c_int) -> Option { - let info = Box::::new_zeroed(); - let mut info = unsafe { info.assume_init() }; - info.kf_structsize = size_of::() as libc::c_int; - let n = unsafe { libc::fcntl(fd, libc::F_KINFO, &mut *info) }; - if n == -1 { - return None; - } - let buf = unsafe { CStr::from_ptr(info.kf_path.as_mut_ptr()).to_bytes().to_vec() }; - Some(PathBuf::from(OsString::from_vec(buf))) - } - - #[cfg(target_os = "vxworks")] - fn get_path(fd: c_int) -> Option { - let mut buf = vec![0; libc::PATH_MAX as usize]; - let n = unsafe { libc::ioctl(fd, libc::FIOGETNAME, buf.as_ptr()) }; - if n == -1 { - return None; - } - let l = buf.iter().position(|&c| c == 0).unwrap(); - buf.truncate(l as usize); - Some(PathBuf::from(OsString::from_vec(buf))) - } - - #[cfg(not(any( - target_os = "linux", - target_os = "vxworks", - target_os = "freebsd", - target_os = "netbsd", - target_os = "illumos", - target_os = "solaris", - target_vendor = "apple", - )))] - fn get_path(_fd: c_int) -> Option { - // FIXME(#24570): implement this for other Unix platforms - None - } - fn get_mode(fd: c_int) -> Option<(bool, bool)> { let mode = unsafe { libc::fcntl(fd, libc::F_GETFL) }; if mode == -1 { @@ -1742,7 +1816,7 @@ impl fmt::Debug for File { let fd = self.as_raw_fd(); let mut b = f.debug_struct("File"); b.field("fd", &fd); - if let Some(path) = get_path(fd) { + if let Some(path) = get_path_from_fd(fd) { b.field("path", &path); } if let Some((read, write)) = get_mode(fd) { From 4691b2551976474b6e1b2c1a736d660bb85ab38a Mon Sep 17 00:00:00 2001 From: Jeremy Smart Date: Sat, 3 May 2025 11:59:23 -0400 Subject: [PATCH 2/7] add documentation, implementations for unsupported platforms --- library/std/src/fs.rs | 28 ++++++++++++++++++++++-- library/std/src/sys/fs/unix.rs | 39 +++++++++++++++++++++++++++++----- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/library/std/src/fs.rs b/library/std/src/fs.rs index a5b91dc5f68de..b30b02f8db86b 100644 --- a/library/std/src/fs.rs +++ b/library/std/src/fs.rs @@ -133,8 +133,24 @@ pub enum TryLockError { } #[unstable(feature = "dirfd", issue = "120426")] -#[cfg(target_family = "unix")] /// An object providing access to a directory on the filesystem. +/// +/// Files are automatically closed when they go out of scope. Errors detected +/// on closing are ignored by the implementation of `Drop`. +/// +/// # Examples +/// +/// Opens a directory and then a file inside it. +/// +/// ```no_run +/// use std::fs::Dir; +/// +/// fn main() -> std::io::Result<()> { +/// let dir = Dir::new("/home/foo")?; +/// let file = dir.open("bar.txt")?; +/// Ok(()) +/// } +/// ``` pub struct Dir { inner: fs_imp::Dir, } @@ -1409,13 +1425,21 @@ impl Seek for Arc { } } -#[unstable(feature = "dirfd", issue = "120426")] impl Dir { /// Opens a file relative to this directory. + /// + /// # Examples + /// ```no_run + /// use std::fs::Dir; + /// + /// let dir = Dir::new("foo")?; + /// ``` + #[unstable(feature = "dirfd", issue = "120426")] pub fn open>(&self, path: P) -> io::Result { self.inner.open(path).map(|f| File { inner: f }) } /// Opens a file relative to this directory with the specified options. + #[unstable(feature = "dirfd", issue = "120426")] pub fn open_with>(&self, path: P, opts: &OpenOptions) -> io::Result { self.inner.open_with(path, &opts.0).map(|f| File { inner: f }) } diff --git a/library/std/src/sys/fs/unix.rs b/library/std/src/sys/fs/unix.rs index a50afc4c3231e..598e9b2b7a41c 100644 --- a/library/std/src/sys/fs/unix.rs +++ b/library/std/src/sys/fs/unix.rs @@ -300,12 +300,41 @@ impl Dir { })?; Ok(File(unsafe { FileDesc::from_raw_fd(fd) })) } +} - // pub fn create_dir>(&self, path: P) -> Result<()> - // pub fn rename, Q: AsRef>(&self, from: P, to_dir: &Self, to: Q) -> Result<()> - // pub fn remove_file>(&self, path: P) -> Result<()> - // pub fn remove_dir>(&self, path: P) -> Result<()> - // pub fn symlink, Q: AsRef>(&self, original: P, link: Q) +#[cfg(any( + miri, + target_os = "redox", + target_os = "nto", + target_os = "vita", + target_os = "hurd", + target_os = "espidf", + target_os = "horizon", + target_os = "vxworks", + target_os = "rtems", + target_os = "nuttx", +))] +impl Dir { + pub fn open>(&self, path: P) -> io::Result { + Err(io::const_error!( + io::ErrorKind::Unsupported, + "directory handles are not available for this platform", + )) + } + + pub fn open_with>(&self, path: P, opts: &OpenOptions) -> io::Result { + Err(io::const_error!( + io::ErrorKind::Unsupported, + "directory handles are not available for this platform", + )) + } + + pub fn open_c(&self, path: &CStr, opts: &OpenOptions) -> io::Result { + Err(io::const_error!( + io::ErrorKind::Unsupported, + "directory handles are not available for this platform", + )) + } } fn get_path_from_fd(fd: c_int) -> Option { From 148fd2489c02b685d78fbe458f8ca25adc2e3cbf Mon Sep 17 00:00:00 2001 From: Jeremy Smart Date: Mon, 5 May 2025 03:54:02 -0400 Subject: [PATCH 3/7] add new Dir struct, rename/remove functions, constructors, and documentation --- library/std/src/fs.rs | 182 ++++++++++++++++++++++++++++++++- library/std/src/sys/fs/mod.rs | 4 +- library/std/src/sys/fs/unix.rs | 126 +++++++++++++---------- 3 files changed, 250 insertions(+), 62 deletions(-) diff --git a/library/std/src/fs.rs b/library/std/src/fs.rs index b30b02f8db86b..557e44c1c807b 100644 --- a/library/std/src/fs.rs +++ b/library/std/src/fs.rs @@ -146,7 +146,7 @@ pub enum TryLockError { /// use std::fs::Dir; /// /// fn main() -> std::io::Result<()> { -/// let dir = Dir::new("/home/foo")?; +/// let dir = Dir::new("foo")?; /// let file = dir.open("bar.txt")?; /// Ok(()) /// } @@ -1426,23 +1426,197 @@ impl Seek for Arc { } impl Dir { - /// Opens a file relative to this directory. + /// Attempts to open a directory at `path` in read-only mode. + /// + /// See [`new_with`] for more options. + /// + /// # Errors + /// + /// This function will return an error in these (and other) situations: + /// * The path doesn't exist + /// * The path doesn't specify a directory + /// * The process doesn't have permission to read the directory + /// + /// # Examples + /// + /// ```no_run + /// use std::fs::Dir; + /// + /// fn main() -> std::io::Result<()> { + /// let dir = Dir::new("foo")?; + /// let mut f = dir.open("bar.txt")?; + /// let mut data = vec![]; + /// f.read_to_end(&mut data)?; + /// Ok(()) + /// } + /// ``` + /// + /// [`new_with`]: Dir::new_with + #[unstable(feature = "dirfd", issue = "120426")] + pub fn new>(path: P) -> io::Result { + Ok(Self { inner: fs_imp::Dir::new(path)? }) + } + + /// Attempts to open a directory at `path` with the options specified by `opts`. + /// + /// # Errors + /// + /// This function will return an error in these (and other) situations: + /// * The path doesn't exist + /// * The path doesn't specify a directory + /// * The process doesn't have permission to read/write (according to `opts`) the directory /// /// # Examples + /// /// ```no_run /// use std::fs::Dir; /// - /// let dir = Dir::new("foo")?; + /// fn main() -> std::io::Result<()> { + /// let dir = Dir::new_with("foo", OpenOptions::new().write(true))?; + /// let mut f = dir.remove_file("bar.txt")?; + /// Ok(()) + /// } + /// ``` + #[unstable(feature = "dirfd", issue = "120426")] + pub fn new_with>(path: P, opts: &OpenOptions) -> io::Result { + Ok(Self { inner: fs_imp::Dir::new_with(path, &opts.0)? }) + } + + /// Attempts to open a file relative to this directory. + /// + /// # Errors + /// + /// This function will return an error in these (and other) situations: + /// * The path doesn't exist + /// * The path doesn't specify a regular file + /// * The process doesn't have permission to read/write (according to `opts`) the directory + /// + /// # Examples + /// + /// ```no_run + /// use std::fs::Dir; + /// + /// fn main() -> std::io::Result<()> { + /// let dir = Dir::new("foo")?; + /// let mut f = dir.open("bar.txt")?; + /// let mut data = vec![]; + /// f.read_to_end(&mut data)?; + /// Ok(()) + /// } /// ``` #[unstable(feature = "dirfd", issue = "120426")] pub fn open>(&self, path: P) -> io::Result { self.inner.open(path).map(|f| File { inner: f }) } - /// Opens a file relative to this directory with the specified options. + + /// Attempts to open a file relative to this directory with the options specified by `opts`. + /// + /// # Errors + /// + /// This function will return an error in these (and other) situations: + /// * The path doesn't exist + /// * The path doesn't specify a regular file + /// * The process doesn't have permission to read/write (according to `opts`) the directory + /// + /// # Examples + /// + /// ```no_run + /// use std::fs::Dir; + /// + /// fn main() -> std::io::Result<()> { + /// let dir = Dir::new("foo")?; + /// let mut f = dir.open_with("bar.txt", OpenOptions::new().read(true))?; + /// let mut data = vec![]; + /// f.read_to_end(&mut data)?; + /// Ok(()) + /// } + /// ``` #[unstable(feature = "dirfd", issue = "120426")] pub fn open_with>(&self, path: P, opts: &OpenOptions) -> io::Result { self.inner.open_with(path, &opts.0).map(|f| File { inner: f }) } + + /// Attempts to remove a file relative to this directory. + /// + /// # Errors + /// + /// This function will return an error in these (and other) situations: + /// * The path doesn't exist + /// * The path doesn't specify a regular file + /// * The process doesn't have permission to delete the file. + /// + /// # Examples + /// + /// ```no_run + /// use std::fs::Dir; + /// + /// fn main() -> std::io::Result<()> { + /// let dir = Dir::new("foo")?; + /// dir.remove_file("bar.txt")?; + /// Ok(()) + /// } + /// ``` + #[unstable(feature = "dirfd", issue = "120426")] + pub fn remove_file>(&self, path: P) -> io::Result<()> { + self.inner.remove_file(path) + } + + /// Attempts to remove a directory relative to this directory. + /// + /// # Errors + /// + /// This function will return an error in these (and other) situations: + /// * The path doesn't exist + /// * The path doesn't specify a directory + /// * The directory isn't empty + /// * The process doesn't have permission to delete the directory. + /// + /// # Examples + /// + /// ```no_run + /// use std::fs::Dir; + /// + /// fn main() -> std::io::Result<()> { + /// let dir = Dir::new("foo")?; + /// dir.remove_dir("baz")?; + /// Ok(()) + /// } + /// ``` + #[unstable(feature = "dirfd", issue = "120426")] + pub fn remove_dir>(&self, path: P) -> io::Result<()> { + self.inner.remove_dir(path) + } + + /// Attempts to rename a file or directory relative to this directory to a new name, replacing + /// the destination file if present. + /// + /// # Errors + /// + /// This function will return an error in these (and other) situations: + /// * The `from` path doesn't exist + /// * The `from` path doesn't specify a directory + /// * `self` and `to_dir` are on different mount points + /// + /// # Examples + /// + /// ```no_run + /// use std::fs::Dir; + /// + /// fn main() -> std::io::Result<()> { + /// let dir = Dir::new("foo")?; + /// dir.rename("bar.txt", &dir, "quux.txt")?; + /// Ok(()) + /// } + /// ``` + #[unstable(feature = "dirfd", issue = "120426")] + pub fn rename, Q: AsRef>( + &self, + from: P, + to_dir: &Self, + to: Q, + ) -> io::Result<()> { + self.inner.rename(from, &to_dir.inner, to) + } } #[unstable(feature = "dirfd", issue = "120426")] diff --git a/library/std/src/sys/fs/mod.rs b/library/std/src/sys/fs/mod.rs index 458c097339d90..ceee69d4dd7a6 100644 --- a/library/std/src/sys/fs/mod.rs +++ b/library/std/src/sys/fs/mod.rs @@ -46,10 +46,8 @@ pub fn with_native_path(path: &Path, f: &dyn Fn(&Path) -> io::Result) -> i f(path) } -#[cfg(target_family = "unix")] -pub use imp::Dir; pub use imp::{ - DirBuilder, DirEntry, File, FileAttr, FilePermissions, FileTimes, FileType, OpenOptions, + Dir, DirBuilder, DirEntry, File, FileAttr, FilePermissions, FileTimes, FileType, OpenOptions, ReadDir, }; diff --git a/library/std/src/sys/fs/unix.rs b/library/std/src/sys/fs/unix.rs index 598e9b2b7a41c..fab52e668da29 100644 --- a/library/std/src/sys/fs/unix.rs +++ b/library/std/src/sys/fs/unix.rs @@ -54,7 +54,8 @@ use libc::{c_int, mode_t}; #[cfg(target_os = "android")] use libc::{ dirent as dirent64, fstat as fstat64, fstatat as fstatat64, ftruncate64, lseek64, - lstat as lstat64, off64_t, open as open64, openat as openat64, stat as stat64, + lstat as lstat64, off64_t, open as open64, openat as openat64, renameat, stat as stat64, + unlinkat, }; #[cfg(not(any( all(target_os = "linux", not(target_env = "musl")), @@ -64,14 +65,18 @@ use libc::{ )))] use libc::{ dirent as dirent64, fstat as fstat64, ftruncate as ftruncate64, lseek as lseek64, - lstat as lstat64, off_t as off64_t, open as open64, openat as openat64, stat as stat64, + lstat as lstat64, off_t as off64_t, open as open64, openat as openat64, renameat, + stat as stat64, unlinkat, }; #[cfg(any( all(target_os = "linux", not(target_env = "musl")), target_os = "l4re", target_os = "hurd" ))] -use libc::{dirent64, fstat64, ftruncate64, lseek64, lstat64, off64_t, open64, openat64, stat64}; +use libc::{ + dirent64, fstat64, ftruncate64, lseek64, lstat64, off64_t, open64, openat64, renameat, stat64, + unlinkat, +}; use crate::ffi::{CStr, OsStr, OsString}; use crate::fmt::{self, Write as _}; @@ -249,7 +254,7 @@ cfg_has_statx! {{ // all DirEntry's will have a reference to this struct struct InnerReadDir { - dirp: Dir, + dirp: DirStream, root: PathBuf, } @@ -264,22 +269,21 @@ impl ReadDir { } } -pub struct Dir(*mut libc::DIR); +struct DirStream(*mut libc::DIR); + +pub struct Dir(OwnedFd); -// dirfd isn't supported everywhere -#[cfg(not(any( - miri, - target_os = "redox", - target_os = "nto", - target_os = "vita", - target_os = "hurd", - target_os = "espidf", - target_os = "horizon", - target_os = "vxworks", - target_os = "rtems", - target_os = "nuttx", -)))] impl Dir { + pub fn new>(path: P) -> io::Result { + let mut opts = OpenOptions::new(); + opts.read(true); + run_path_with_cstr(path.as_ref(), &|path| Self::open_c_dir(path, &opts)) + } + + pub fn new_with>(path: P, opts: &OpenOptions) -> io::Result { + run_path_with_cstr(path.as_ref(), &|path| Self::open_c_dir(path, &opts)) + } + pub fn open>(&self, path: P) -> io::Result { let mut opts = OpenOptions::new(); opts.read(true); @@ -290,50 +294,62 @@ impl Dir { run_path_with_cstr(path.as_ref(), &|path| self.open_c(path, opts)) } + pub fn remove_file>(&self, path: P) -> io::Result<()> { + run_path_with_cstr(path.as_ref(), &|path| self.remove_c(path, false)) + } + + pub fn remove_dir>(&self, path: P) -> io::Result<()> { + run_path_with_cstr(path.as_ref(), &|path| self.remove_c(path, true)) + } + + pub fn rename, Q: AsRef>( + &self, + from: P, + to_dir: &Self, + to: Q, + ) -> io::Result<()> { + run_path_with_cstr(from.as_ref(), &|from| { + run_path_with_cstr(to.as_ref(), &|to| self.rename_c(from, to_dir, to)) + }) + } + pub fn open_c(&self, path: &CStr, opts: &OpenOptions) -> io::Result { let flags = libc::O_CLOEXEC | opts.get_access_mode()? | opts.get_creation_mode()? | (opts.custom_flags as c_int & !libc::O_ACCMODE); let fd = cvt_r(|| unsafe { - openat64(libc::dirfd(self.0), path.as_ptr(), flags, opts.mode as c_int) + openat64(self.0.as_raw_fd(), path.as_ptr(), flags, opts.mode as c_int) })?; Ok(File(unsafe { FileDesc::from_raw_fd(fd) })) } -} -#[cfg(any( - miri, - target_os = "redox", - target_os = "nto", - target_os = "vita", - target_os = "hurd", - target_os = "espidf", - target_os = "horizon", - target_os = "vxworks", - target_os = "rtems", - target_os = "nuttx", -))] -impl Dir { - pub fn open>(&self, path: P) -> io::Result { - Err(io::const_error!( - io::ErrorKind::Unsupported, - "directory handles are not available for this platform", - )) + pub fn open_c_dir(path: &CStr, opts: &OpenOptions) -> io::Result { + let flags = libc::O_CLOEXEC + | libc::O_DIRECTORY + | opts.get_access_mode()? + | opts.get_creation_mode()? + | (opts.custom_flags as c_int & !libc::O_ACCMODE); + let fd = cvt_r(|| unsafe { open64(path.as_ptr(), flags, opts.mode as c_int) })?; + Ok(Self(unsafe { OwnedFd::from_raw_fd(fd) })) } - pub fn open_with>(&self, path: P, opts: &OpenOptions) -> io::Result { - Err(io::const_error!( - io::ErrorKind::Unsupported, - "directory handles are not available for this platform", - )) + pub 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(|_| ()) } - pub fn open_c(&self, path: &CStr, opts: &OpenOptions) -> io::Result { - Err(io::const_error!( - io::ErrorKind::Unsupported, - "directory handles are not available for this platform", - )) + pub fn rename_c(&self, p1: &CStr, new_dir: &Self, p2: &CStr) -> io::Result<()> { + cvt(unsafe { + renameat(self.0.as_raw_fd(), p1.as_ptr(), new_dir.0.as_raw_fd(), p2.as_ptr()) + }) + .map(|_| ()) } } @@ -429,7 +445,7 @@ impl fmt::Debug for Dir { } } - let fd = unsafe { dirfd(self.0) }; + let fd = self.0.as_raw_fd(); let mut b = f.debug_struct("Dir"); b.field("fd", &fd); if let Some(path) = get_path_from_fd(fd) { @@ -442,8 +458,8 @@ impl fmt::Debug for Dir { } } -unsafe impl Send for Dir {} -unsafe impl Sync for Dir {} +unsafe impl Send for DirStream {} +unsafe impl Sync for DirStream {} #[cfg(any( target_os = "android", @@ -1028,7 +1044,7 @@ pub(crate) fn debug_assert_fd_is_open(fd: RawFd) { } } -impl Drop for Dir { +impl Drop for DirStream { fn drop(&mut self) { // dirfd isn't supported everywhere #[cfg(not(any( @@ -1928,7 +1944,7 @@ pub fn readdir(path: &Path) -> io::Result { Err(Error::last_os_error()) } else { let root = path.to_path_buf(); - let inner = InnerReadDir { dirp: Dir(ptr), root }; + let inner = InnerReadDir { dirp: DirStream(ptr), root }; Ok(ReadDir::new(inner)) } } @@ -2289,7 +2305,7 @@ mod remove_dir_impl { #[cfg(all(target_os = "linux", target_env = "gnu"))] use libc::{fdopendir, openat64 as openat, unlinkat}; - use super::{Dir, DirEntry, InnerReadDir, ReadDir, lstat}; + use super::{DirEntry, DirStream, InnerReadDir, ReadDir, lstat}; use crate::ffi::CStr; use crate::io; use crate::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd}; @@ -2315,7 +2331,7 @@ mod remove_dir_impl { if ptr.is_null() { return Err(io::Error::last_os_error()); } - let dirp = Dir(ptr); + let dirp = DirStream(ptr); // file descriptor is automatically closed by libc::closedir() now, so give up ownership let new_parent_fd = dir_fd.into_raw_fd(); // a valid root is not needed because we do not call any functions involving the full path From bf718d83341fcd88627afa2c04d93f7e81372083 Mon Sep 17 00:00:00 2001 From: Jeremy Smart Date: Mon, 5 May 2025 05:17:42 -0400 Subject: [PATCH 4/7] fix doctests, add real tests --- library/std/src/fs.rs | 18 +++++--- library/std/src/fs/tests.rs | 85 ++++++++++++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 6 deletions(-) diff --git a/library/std/src/fs.rs b/library/std/src/fs.rs index 557e44c1c807b..36c57ae7a214e 100644 --- a/library/std/src/fs.rs +++ b/library/std/src/fs.rs @@ -143,6 +143,7 @@ pub enum TryLockError { /// Opens a directory and then a file inside it. /// /// ```no_run +/// #![feature(dirfd)] /// use std::fs::Dir; /// /// fn main() -> std::io::Result<()> { @@ -1440,7 +1441,8 @@ impl Dir { /// # Examples /// /// ```no_run - /// use std::fs::Dir; + /// #![feature(dirfd)] + /// use std::{fs::Dir, io::Read}; /// /// fn main() -> std::io::Result<()> { /// let dir = Dir::new("foo")?; @@ -1469,7 +1471,8 @@ impl Dir { /// # Examples /// /// ```no_run - /// use std::fs::Dir; + /// #![feature(dirfd)] + /// use std::fs::{Dir, OpenOptions}; /// /// fn main() -> std::io::Result<()> { /// let dir = Dir::new_with("foo", OpenOptions::new().write(true))?; @@ -1482,7 +1485,7 @@ impl Dir { Ok(Self { inner: fs_imp::Dir::new_with(path, &opts.0)? }) } - /// Attempts to open a file relative to this directory. + /// Attempts to open a file in read-only mode relative to this directory. /// /// # Errors /// @@ -1494,7 +1497,8 @@ impl Dir { /// # Examples /// /// ```no_run - /// use std::fs::Dir; + /// #![feature(dirfd)] + /// use std::{fs::Dir, io::Read}; /// /// fn main() -> std::io::Result<()> { /// let dir = Dir::new("foo")?; @@ -1521,7 +1525,8 @@ impl Dir { /// # Examples /// /// ```no_run - /// use std::fs::Dir; + /// #![feature(dirfd)] + /// use std::{fs::{Dir, OpenOptions}, io::Read}; /// /// fn main() -> std::io::Result<()> { /// let dir = Dir::new("foo")?; @@ -1548,6 +1553,7 @@ impl Dir { /// # Examples /// /// ```no_run + /// #![feature(dirfd)] /// use std::fs::Dir; /// /// fn main() -> std::io::Result<()> { @@ -1574,6 +1580,7 @@ impl Dir { /// # Examples /// /// ```no_run + /// #![feature(dirfd)] /// use std::fs::Dir; /// /// fn main() -> std::io::Result<()> { @@ -1600,6 +1607,7 @@ impl Dir { /// # Examples /// /// ```no_run + /// #![feature(dirfd)] /// use std::fs::Dir; /// /// fn main() -> std::io::Result<()> { diff --git a/library/std/src/fs/tests.rs b/library/std/src/fs/tests.rs index 46b0d832fec45..ae7001357120c 100644 --- a/library/std/src/fs/tests.rs +++ b/library/std/src/fs/tests.rs @@ -1,5 +1,6 @@ use rand::RngCore; +use super::Dir; #[cfg(any( windows, target_os = "freebsd", @@ -17,7 +18,7 @@ use crate::char::MAX_LEN_UTF8; target_vendor = "apple", ))] use crate::fs::TryLockError; -use crate::fs::{self, File, FileTimes, OpenOptions}; +use crate::fs::{self, File, FileTimes, OpenOptions, create_dir}; use crate::io::prelude::*; use crate::io::{BorrowedBuf, ErrorKind, SeekFrom}; use crate::mem::MaybeUninit; @@ -2024,3 +2025,85 @@ fn test_rename_junction() { // Junction links are always absolute so we just check the file name is correct. assert_eq!(fs::read_link(&dest).unwrap().file_name(), Some(not_exist.as_os_str())); } + +#[test] +fn test_dir_smoke_test() { + let tmpdir = tmpdir(); + check!(Dir::new(tmpdir.path())); +} + +#[test] +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()); + drop(f); + let dir = check!(Dir::new(tmpdir.path())); + let mut f = check!(dir.open("foo.txt")); + let mut buf = [0u8; 3]; + check!(f.read_exact(&mut buf)); + assert_eq!(b"bar", &buf); +} + +#[test] +fn test_dir_write_file() { + let tmpdir = tmpdir(); + let dir = check!(Dir::new(tmpdir.path())); + let mut f = check!(dir.open_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] +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::new(tmpdir.path())); + check!(dir.remove_file("foo.txt")); + let result = File::open(tmpdir.join("foo.txt")); + #[cfg(all(unix, not(target_os = "vxworks")))] + error!(result, "No such file or directory"); + #[cfg(target_os = "vxworks")] + error!(result, "no such file or directory"); + #[cfg(windows)] + error!(result, 2); // ERROR_FILE_NOT_FOUND +} + +#[test] +fn test_dir_remove_dir() { + let tmpdir = tmpdir(); + check!(create_dir(tmpdir.join("foo"))); + let dir = check!(Dir::new(tmpdir.path())); + check!(dir.remove_dir("foo")); + let result = Dir::new(tmpdir.join("foo")); + #[cfg(all(unix, not(target_os = "vxworks")))] + error!(result, "No such file or directory"); + #[cfg(target_os = "vxworks")] + error!(result, "no such file or directory"); + #[cfg(windows)] + error!(result, 2); // ERROR_FILE_NOT_FOUND +} + +#[test] +fn test_dir_rename_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::new(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); +} From 2e182a0b447935045bf64c8d9a4737ffc3131b16 Mon Sep 17 00:00:00 2001 From: Jeremy Smart Date: Mon, 5 May 2025 21:47:16 -0400 Subject: [PATCH 5/7] add windows implementation --- library/std/src/sys/fs/windows.rs | 224 +++++++++++++++++++++++++++++- 1 file changed, 220 insertions(+), 4 deletions(-) diff --git a/library/std/src/sys/fs/windows.rs b/library/std/src/sys/fs/windows.rs index 9039fd00f5d62..33f4b6f71e346 100644 --- a/library/std/src/sys/fs/windows.rs +++ b/library/std/src/sys/fs/windows.rs @@ -10,6 +10,7 @@ use crate::os::windows::io::{AsHandle, BorrowedHandle}; use crate::os::windows::prelude::*; use crate::path::{Path, PathBuf}; use crate::sync::Arc; +use crate::sys::api::SetFileInformation; use crate::sys::handle::Handle; use crate::sys::pal::api::{self, WinError, set_file_information_by_handle}; use crate::sys::pal::{IoResult, fill_utf16_buf, to_u16s, truncate_utf16_at_nul}; @@ -26,6 +27,10 @@ pub struct File { handle: Handle, } +pub struct Dir { + handle: Handle, +} + #[derive(Clone)] pub struct FileAttr { attributes: u32, @@ -846,6 +851,217 @@ impl File { } } +unsafe fn nt_create_file( + access: u32, + object_attributes: &c::OBJECT_ATTRIBUTES, + share: u32, + dir: bool, +) -> Result { + let mut handle = ptr::null_mut(); + let mut io_status = c::IO_STATUS_BLOCK::PENDING; + let disposition = match (access & c::GENERIC_READ > 0, access & c::GENERIC_WRITE > 0) { + (true, true) => c::FILE_OPEN_IF, + (true, false) => c::FILE_OPEN, + (false, true) => c::FILE_CREATE, + (false, false) => { + return Err(WinError::new(c::ERROR_INVALID_PARAMETER)); + } + }; + let status = unsafe { + c::NtCreateFile( + &mut handle, + access, + object_attributes, + &mut io_status, + ptr::null(), + c::FILE_ATTRIBUTE_NORMAL, + share, + disposition, + if dir { c::FILE_DIRECTORY_FILE } else { c::FILE_NON_DIRECTORY_FILE }, + ptr::null(), + 0, + ) + }; + if c::nt_success(status) { + // SAFETY: nt_success guarantees that handle is no longer null + unsafe { Ok(Handle::from_raw_handle(handle)) } + } else { + let win_error = if status == c::STATUS_DELETE_PENDING { + // We make a special exception for `STATUS_DELETE_PENDING` because + // otherwise this will be mapped to `ERROR_ACCESS_DENIED` which is + // very unhelpful because that can also mean a permission error. + WinError::DELETE_PENDING + } else { + WinError::new(unsafe { c::RtlNtStatusToDosError(status) }) + }; + Err(win_error) + } +} + +fn run_path_with_wcstr>( + path: P, + f: &dyn Fn(&WCStr) -> io::Result, +) -> io::Result { + let path = maybe_verbatim(path.as_ref())?; + // SAFETY: maybe_verbatim returns null-terminated strings + let path = unsafe { WCStr::from_wchars_with_null_unchecked(&path) }; + f(path) +} + +impl Dir { + pub fn new>(path: P) -> io::Result { + let opts = OpenOptions::new(); + run_path_with_wcstr(path, &|path| Self::new_native(path, &opts)) + } + + pub fn new_with>(path: P, opts: &OpenOptions) -> io::Result { + run_path_with_wcstr(path, &|path| Self::new_native(path, &opts)) + } + + pub fn open>(&self, path: P) -> io::Result { + let mut opts = OpenOptions::new(); + opts.read(true); + Ok(File { handle: run_path_with_wcstr(path, &|path| self.open_native(path, &opts))? }) + } + + pub fn open_with>(&self, path: P, opts: &OpenOptions) -> io::Result { + Ok(File { handle: run_path_with_wcstr(path, &|path| self.open_native(path, &opts))? }) + } + + pub fn create_dir>(&self, path: P) -> io::Result<()> { + run_path_with_wcstr(path, &|path| { + self.create_dir_native(path, &OpenOptions::new()).map(|_| ()) + }) + } + + pub fn remove_file>(&self, path: P) -> io::Result<()> { + run_path_with_wcstr(path, &|path| self.remove_native(path, false)) + } + + pub fn remove_dir>(&self, path: P) -> io::Result<()> { + run_path_with_wcstr(path, &|path| self.remove_native(path, true)) + } + + pub fn rename, Q: AsRef>( + &self, + from: P, + to_dir: &Self, + to: Q, + ) -> io::Result<()> { + run_path_with_wcstr(from.as_ref(), &|from| { + run_path_with_wcstr(to.as_ref(), &|to| self.rename_native(from, to_dir, to)) + }) + } + + fn new_native(path: &WCStr, opts: &OpenOptions) -> io::Result { + let name = c::UNICODE_STRING { + Length: path.count_bytes() as _, + MaximumLength: path.count_bytes() as _, + Buffer: path.as_ptr() as *mut _, + }; + let object_attributes = c::OBJECT_ATTRIBUTES { + Length: size_of::() as _, + RootDirectory: ptr::null_mut(), + ObjectName: &name, + Attributes: 0, + SecurityDescriptor: ptr::null(), + SecurityQualityOfService: ptr::null(), + }; + let share = c::FILE_SHARE_READ | c::FILE_SHARE_WRITE | c::FILE_SHARE_DELETE; + let handle = + unsafe { nt_create_file(opts.get_access_mode()?, &object_attributes, share, true) } + .io_result()?; + Ok(Self { handle }) + } + + fn open_native(&self, path: &WCStr, opts: &OpenOptions) -> io::Result { + let name = c::UNICODE_STRING { + Length: path.count_bytes() as _, + MaximumLength: path.count_bytes() as _, + Buffer: path.as_ptr() as *mut _, + }; + let object_attributes = c::OBJECT_ATTRIBUTES { + Length: size_of::() as _, + RootDirectory: self.handle.as_raw_handle(), + ObjectName: &name, + Attributes: 0, + SecurityDescriptor: ptr::null(), + SecurityQualityOfService: ptr::null(), + }; + let share = c::FILE_SHARE_READ | c::FILE_SHARE_WRITE | c::FILE_SHARE_DELETE; + unsafe { nt_create_file(opts.get_access_mode()?, &object_attributes, share, false) } + .io_result() + } + + fn create_dir_native(&self, path: &WCStr, opts: &OpenOptions) -> io::Result { + let name = c::UNICODE_STRING { + Length: path.count_bytes() as _, + MaximumLength: path.count_bytes() as _, + Buffer: path.as_ptr() as *mut _, + }; + let object_attributes = c::OBJECT_ATTRIBUTES { + Length: size_of::() as _, + RootDirectory: self.handle.as_raw_handle(), + ObjectName: &name, + Attributes: 0, + SecurityDescriptor: ptr::null(), + SecurityQualityOfService: ptr::null(), + }; + let share = c::FILE_SHARE_READ | c::FILE_SHARE_WRITE | c::FILE_SHARE_DELETE; + unsafe { nt_create_file(opts.get_access_mode()?, &object_attributes, share, true) } + .io_result() + } + + fn remove_native(&self, path: &WCStr, dir: bool) -> io::Result<()> { + let mut opts = OpenOptions::new(); + opts.access_mode(c::GENERIC_WRITE); + let handle = + if dir { self.create_dir_native(path, &opts) } else { self.open_native(path, &opts) }?; + let info = c::FILE_DISPOSITION_INFO_EX { Flags: c::FILE_DISPOSITION_FLAG_DELETE }; + let result = unsafe { + c::SetFileInformationByHandle( + handle.as_raw_handle(), + c::FileDispositionInfoEx, + (&info).as_ptr(), + size_of::() as _, + ) + }; + if result == 0 { Err(api::get_last_error()).io_result() } else { Ok(()) } + } + + fn rename_native(&self, from: &WCStr, to_dir: &Self, to: &WCStr) -> io::Result<()> { + let mut opts = OpenOptions::new(); + opts.access_mode(c::GENERIC_WRITE); + let handle = self.open_native(from, &opts)?; + let info = c::FILE_RENAME_INFO { + Anonymous: c::FILE_RENAME_INFO_0 { ReplaceIfExists: true }, + RootDirectory: to_dir.handle.as_raw_handle(), + FileNameLength: to.count_bytes() as _, + FileName: [to.as_ptr() as u16], + }; + let result = unsafe { + c::SetFileInformationByHandle( + handle.as_raw_handle(), + c::FileRenameInfo, + ptr::addr_of!(info) as _, + size_of::() as _, + ) + }; + if result == 0 { Err(api::get_last_error()).io_result() } else { Ok(()) } + } +} + +impl fmt::Debug for Dir { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut b = f.debug_struct("Dir"); + b.field("handle", &self.handle.as_raw_handle()); + if let Ok(path) = get_path(self.handle.as_handle()) { + b.field("path", &path); + } + b.finish() + } +} + /// A buffer for holding directory entries. struct DirBuff { buffer: Box; Self::BUFFER_SIZE]>>, @@ -995,7 +1211,7 @@ impl fmt::Debug for File { // FIXME(#24570): add more info here (e.g., mode) let mut b = f.debug_struct("File"); b.field("handle", &self.handle.as_raw_handle()); - if let Ok(path) = get_path(self) { + if let Ok(path) = get_path(self.handle.as_handle()) { b.field("path", &path); } b.finish() @@ -1484,10 +1700,10 @@ pub fn set_perm(p: &WCStr, perm: FilePermissions) -> io::Result<()> { } } -fn get_path(f: &File) -> io::Result { +fn get_path(f: impl AsRawHandle) -> io::Result { fill_utf16_buf( |buf, sz| unsafe { - c::GetFinalPathNameByHandleW(f.handle.as_raw_handle(), buf, sz, c::VOLUME_NAME_DOS) + c::GetFinalPathNameByHandleW(f.as_raw_handle(), buf, sz, c::VOLUME_NAME_DOS) }, |buf| PathBuf::from(OsString::from_wide(buf)), ) @@ -1500,7 +1716,7 @@ pub fn canonicalize(p: &WCStr) -> io::Result { // This flag is so we can open directories too opts.custom_flags(c::FILE_FLAG_BACKUP_SEMANTICS); let f = File::open_native(p, &opts)?; - get_path(&f) + get_path(f.handle) } pub fn copy(from: &WCStr, to: &WCStr) -> io::Result { From 7767828d1defc3b0c404c085191aecb4dda12641 Mon Sep 17 00:00:00 2001 From: Jeremy Smart Date: Mon, 5 May 2025 22:20:12 -0400 Subject: [PATCH 6/7] add unix implementation for create_dir --- library/std/src/fs.rs | 27 +++++++++++++++++++++++++++ library/std/src/sys/fs/unix.rs | 18 +++++++++++++----- library/std/src/sys/fs/windows.rs | 6 +++--- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/library/std/src/fs.rs b/library/std/src/fs.rs index 36c57ae7a214e..99655c8946824 100644 --- a/library/std/src/fs.rs +++ b/library/std/src/fs.rs @@ -1541,6 +1541,33 @@ impl Dir { self.inner.open_with(path, &opts.0).map(|f| File { inner: f }) } + /// Attempts to create a directory relative to this directory. + /// + /// # Errors + /// + /// This function will return an error in these (and other) situations: + /// * The path exists + /// * The process doesn't have permission to create the directory + /// + /// # Examples + /// + /// ```no_run + /// #![feature(dirfd)] + /// use std::{fs::{Dir, OpenOptions}, io::Read}; + /// + /// fn main() -> std::io::Result<()> { + /// let dir = Dir::new("foo")?; + /// let mut f = dir.open_with("bar.txt", OpenOptions::new().read(true))?; + /// let mut data = vec![]; + /// f.read_to_end(&mut data)?; + /// Ok(()) + /// } + /// ``` + #[unstable(feature = "dirfd", issue = "120426")] + pub fn create_dir>(&self, path: P) -> io::Result<()> { + self.inner.create_dir(path) + } + /// Attempts to remove a file relative to this directory. /// /// # Errors diff --git a/library/std/src/sys/fs/unix.rs b/library/std/src/sys/fs/unix.rs index fab52e668da29..0da11d3b44744 100644 --- a/library/std/src/sys/fs/unix.rs +++ b/library/std/src/sys/fs/unix.rs @@ -54,8 +54,8 @@ use libc::{c_int, mode_t}; #[cfg(target_os = "android")] use libc::{ dirent as dirent64, fstat as fstat64, fstatat as fstatat64, ftruncate64, lseek64, - lstat as lstat64, off64_t, open as open64, openat as openat64, renameat, stat as stat64, - unlinkat, + lstat as lstat64, mkdirat, off64_t, open as open64, openat as openat64, renameat, + stat as stat64, unlinkat, }; #[cfg(not(any( all(target_os = "linux", not(target_env = "musl")), @@ -65,7 +65,7 @@ use libc::{ )))] use libc::{ dirent as dirent64, fstat as fstat64, ftruncate as ftruncate64, lseek as lseek64, - lstat as lstat64, off_t as off64_t, open as open64, openat as openat64, renameat, + lstat as lstat64, mkdirat, off_t as off64_t, open as open64, openat as openat64, renameat, stat as stat64, unlinkat, }; #[cfg(any( @@ -74,8 +74,8 @@ use libc::{ target_os = "hurd" ))] use libc::{ - dirent64, fstat64, ftruncate64, lseek64, lstat64, off64_t, open64, openat64, renameat, stat64, - unlinkat, + dirent64, fstat64, ftruncate64, lseek64, lstat64, mkdirat, off64_t, open64, openat64, renameat, + stat64, unlinkat, }; use crate::ffi::{CStr, OsStr, OsString}; @@ -294,6 +294,10 @@ impl Dir { run_path_with_cstr(path.as_ref(), &|path| self.open_c(path, opts)) } + pub fn create_dir>(&self, path: P) -> io::Result<()> { + run_path_with_cstr(path.as_ref(), &|path| self.create_dir_c(path)) + } + pub fn remove_file>(&self, path: P) -> io::Result<()> { run_path_with_cstr(path.as_ref(), &|path| self.remove_c(path, false)) } @@ -334,6 +338,10 @@ impl Dir { Ok(Self(unsafe { OwnedFd::from_raw_fd(fd) })) } + pub fn create_dir_c(&self, path: &CStr) -> io::Result<()> { + cvt(unsafe { mkdirat(self.0.as_raw_fd(), path.as_ptr(), 0o777) }).map(|_| ()) + } + pub fn remove_c(&self, path: &CStr, remove_dir: bool) -> io::Result<()> { cvt(unsafe { unlinkat( diff --git a/library/std/src/sys/fs/windows.rs b/library/std/src/sys/fs/windows.rs index 33f4b6f71e346..d03f916dcd770 100644 --- a/library/std/src/sys/fs/windows.rs +++ b/library/std/src/sys/fs/windows.rs @@ -929,9 +929,9 @@ impl Dir { } pub fn create_dir>(&self, path: P) -> io::Result<()> { - run_path_with_wcstr(path, &|path| { - self.create_dir_native(path, &OpenOptions::new()).map(|_| ()) - }) + let mut opts = OpenOptions::new(); + opts.write(true); + run_path_with_wcstr(path, &|path| self.create_dir_native(path, &opts).map(|_| ())) } pub fn remove_file>(&self, path: P) -> io::Result<()> { From 3fab5f453d3835c08680d3704299a5b7a6c3e013 Mon Sep 17 00:00:00 2001 From: Jeremy Smart Date: Mon, 5 May 2025 23:28:27 -0400 Subject: [PATCH 7/7] add create_dir test --- library/std/src/fs/tests.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/library/std/src/fs/tests.rs b/library/std/src/fs/tests.rs index ae7001357120c..7310b2ab14a55 100644 --- a/library/std/src/fs/tests.rs +++ b/library/std/src/fs/tests.rs @@ -2107,3 +2107,11 @@ fn test_dir_rename_file() { check!(f.read_exact(&mut buf)); assert_eq!(b"bar", &buf); } + +#[test] +fn test_dir_create_dir() { + let tmpdir = tmpdir(); + let dir = check!(Dir::new(tmpdir.path())); + check!(dir.create_dir("foo")); + check!(Dir::new(tmpdir.join("foo"))); +}