diff --git a/library/std/src/fs.rs b/library/std/src/fs.rs index 3ff08e3a5664b..99655c8946824 100644 --- a/library/std/src/fs.rs +++ b/library/std/src/fs.rs @@ -132,6 +132,30 @@ pub enum TryLockError { WouldBlock, } +#[unstable(feature = "dirfd", issue = "120426")] +/// 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 +/// #![feature(dirfd)] +/// use std::fs::Dir; +/// +/// fn main() -> std::io::Result<()> { +/// let dir = Dir::new("foo")?; +/// let file = dir.open("bar.txt")?; +/// Ok(()) +/// } +/// ``` +pub struct Dir { + inner: fs_imp::Dir, +} + /// Metadata information about a file. /// /// This structure is returned from the [`metadata`] or @@ -1402,6 +1426,241 @@ impl Seek for Arc { } } +impl Dir { + /// 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 + /// #![feature(dirfd)] + /// use std::{fs::Dir, io::Read}; + /// + /// 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 + /// #![feature(dirfd)] + /// use std::fs::{Dir, OpenOptions}; + /// + /// 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 in read-only mode 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 + /// #![feature(dirfd)] + /// use std::{fs::Dir, io::Read}; + /// + /// 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 }) + } + + /// 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 + /// #![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 open_with>(&self, path: P, opts: &OpenOptions) -> io::Result { + 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 + /// + /// 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 + /// #![feature(dirfd)] + /// 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 + /// #![feature(dirfd)] + /// 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 + /// #![feature(dirfd)] + /// 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")] +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/fs/tests.rs b/library/std/src/fs/tests.rs index 46b0d832fec45..7310b2ab14a55 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,93 @@ 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); +} + +#[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"))); +} diff --git a/library/std/src/sys/fs/mod.rs b/library/std/src/sys/fs/mod.rs index d55e28074fe8c..ceee69d4dd7a6 100644 --- a/library/std/src/sys/fs/mod.rs +++ b/library/std/src/sys/fs/mod.rs @@ -47,7 +47,7 @@ pub fn with_native_path(path: &Path, f: &dyn Fn(&Path) -> io::Result) -> i } 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 863358596c199..0da11d3b44744 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, stat as stat64, + 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")), @@ -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, stat as stat64, + lstat as lstat64, mkdirat, 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, stat64}; +use libc::{ + dirent64, fstat64, ftruncate64, lseek64, lstat64, mkdirat, 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,10 +269,205 @@ impl ReadDir { } } -struct Dir(*mut libc::DIR); +struct DirStream(*mut libc::DIR); + +pub struct Dir(OwnedFd); + +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); + 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 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)) + } -unsafe impl Send for Dir {} -unsafe impl Sync for Dir {} + 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(self.0.as_raw_fd(), path.as_ptr(), flags, opts.mode as c_int) + })?; + Ok(File(unsafe { FileDesc::from_raw_fd(fd) })) + } + + 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 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( + self.0.as_raw_fd(), + path.as_ptr(), + if remove_dir { libc::AT_REMOVEDIR } else { 0 }, + ) + }) + .map(|_| ()) + } + + 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(|_| ()) + } +} + +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 = 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) { + 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 DirStream {} +unsafe impl Sync for DirStream {} #[cfg(any( target_os = "android", @@ -852,7 +1052,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( @@ -1653,79 +1853,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 +1869,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) { @@ -1825,7 +1952,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)) } } @@ -2186,7 +2313,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}; @@ -2212,7 +2339,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 diff --git a/library/std/src/sys/fs/windows.rs b/library/std/src/sys/fs/windows.rs index 9039fd00f5d62..d03f916dcd770 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<()> { + 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<()> { + 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 {