diff --git a/library/std/src/fs.rs b/library/std/src/fs.rs index 6cbf8301e01b9..68e7a6f8ef170 100644 --- a/library/std/src/fs.rs +++ b/library/std/src/fs.rs @@ -132,6 +132,33 @@ pub enum TryLockError { WouldBlock, } +/// 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, io::Read}; +/// +/// fn main() -> std::io::Result<()> { +/// let dir = Dir::new("foo")?; +/// let mut file = dir.open("bar.txt")?; +/// let mut s = String::new(); +/// file.read_to_string(&mut s)?; +/// println!("{}", s); +/// Ok(()) +/// } +/// ``` +#[unstable(feature = "dirfd", issue = "120426")] +pub struct Dir { + inner: fs_imp::Dir, +} + /// Metadata information about a file. /// /// This structure is returned from the [`metadata`] or @@ -1453,6 +1480,309 @@ 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 if `path` does not point to an existing directory. + /// Other errors may also be returned according to [`OpenOptions::open`]. + /// + /// # 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 { + fs_imp::Dir::new(path).map(|inner| Self { inner }) + } + + /// Attempts to open a directory at `path` with the options specified by `opts`. + /// + /// # Errors + /// + /// This function may return an error according to [`OpenOptions::open`]. + /// + /// # 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 { + fs_imp::Dir::new_with(path, &opts.0).map(|inner| Self { inner }) + } + + /// Attempts to open a directory at `path` with the minimum permissions for traversal. + /// + /// # Errors + /// + /// This function may return an error according to [`OpenOptions::open`]. + /// + /// # Examples + /// + /// ```no_run + /// #![feature(dirfd)] + /// use std::{fs::Dir, io::Read}; + /// + /// fn main() -> std::io::Result<()> { + /// let foo = Dir::new_for_traversal("foo")?; + /// let foobar = foo.open_dir("bar")?; + /// let mut s = String::new(); + /// foobar.open("baz")?.read_to_string(&mut s)?; + /// println!("{s}"); + /// Ok(()) + /// } + /// ``` + #[unstable(feature = "dirfd", issue = "120426")] + pub fn new_for_traversal>(path: P) -> io::Result { + fs_imp::Dir::new_for_traversal(path).map(|inner| Self { inner }) + } + + /// Attempts to open a file in read-only mode relative to this directory. + /// + /// # Errors + /// + /// This function will return an error if `path` does not point to an existing file. + /// Other errors may also be returned according to [`OpenOptions::open`]. + /// + /// # Examples + /// + /// ```no_run + /// #![feature(dirfd)] + /// use std::{fs::Dir, 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 may return an error according to [`OpenOptions::open`]. + /// + /// # 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 if `path` points to an existing file or directory. + /// Other errors may also be returned according to [`OpenOptions::open`]. + /// + /// # 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 open a directory relative to this directory in read-only mode. + /// + /// # Errors + /// + /// This function will return an error if `path` does not point to an existing directory. + /// Other errors may also be returned according to [`OpenOptions::open`]. + /// + /// # Examples + /// + /// ```no_run + /// #![feature(dirfd)] + /// use std::{fs::Dir, io::Read}; + /// + /// fn main() -> std::io::Result<()> { + /// let foo = Dir::new("foo")?; + /// let foobar = foo.open_dir("bar")?; + /// let mut s = String::new(); + /// foobar.open("baz")?.read_to_string(&mut s)?; + /// println!("{s}"); + /// Ok(()) + /// } + /// ``` + #[unstable(feature = "dirfd", issue = "120426")] + pub fn open_dir>(&self, path: P) -> io::Result { + self.inner.open_dir(path).map(|inner| Self { inner }) + } + + /// Attempts to open a directory relative to this directory in read-only mode. + /// + /// # Errors + /// + /// This function will return an error according to [`OpenOptions::open`]. + /// + /// # Examples + /// + /// ```no_run + /// #![feature(dirfd)] + /// use std::{fs::{Dir, OpenOptions}, io::Write}; + /// + /// fn main() -> std::io::Result<()> { + /// let foo = Dir::new("foo")?; + /// let foobar = foo.open_dir_with("bar", OpenOptions::new().write(true))?; + /// foobar.open("baz")?.write(b"baz")?; + /// Ok(()) + /// } + /// ``` + #[unstable(feature = "dirfd", issue = "120426")] + pub fn open_dir_with>(&self, path: P, opts: &OpenOptions) -> io::Result { + self.inner.open_dir_with(path, &opts.0).map(|inner| Self { inner }) + } + + /// Attempts to remove a file relative to this directory. + /// + /// # Errors + /// + /// This function will return an error if `path` does not point to an existing file. + /// Other errors may also be returned according to [`OpenOptions::open`]. + /// + /// # Examples + /// + /// ```no_run + /// #![feature(dirfd)] + /// use std::fs::Dir; + /// + /// 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 if `path` does not point to an existing, non-empty directory. + /// Other errors may also be returned according to [`OpenOptions::open`]. + /// + /// # 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 if `from` does not point to an existing file or directory. + /// Other errors may also be returned according to [`OpenOptions::open`]. + /// + /// # 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) + } + + /// Attempts to create a new symbolic link on the filesystem. + /// + /// If `original` is a relative path, it is interpreted relative to the created link. + /// If `link` is a relative path, it is interpreted relative to `self`. + #[unstable(feature = "dirfd", issue = "120426")] + pub fn symlink, Q: AsRef>(&self, original: P, link: Q) -> io::Result<()> { + self.inner.symlink(original, link) + } +} + +#[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. /// @@ -2413,6 +2743,178 @@ impl DirEntry { pub fn file_name(&self) -> OsString { self.0.file_name() } + + /// Attempts to open the file represented by `self` in read-only mode. + /// + /// # Errors + /// + /// This function will error whenever [`File::open`] would error, including when `self` + /// represents a directory. + /// + /// # Examples + /// + /// ```no_run + /// #![feature(dirfd)] + /// use std::{fs, io::read_to_string}; + /// + /// if let Ok(entries) = fs::read_dir(".") { + /// for entry in entries { + /// if let Ok(entry) = entry { + /// // Here, `entry` is a `DirEntry`. + /// if let Ok(file) = entry.open_file() && let Ok(text) = read_to_string(file) { + /// println!("{}", text); + /// } + /// } + /// } + /// } + /// ``` + #[unstable(feature = "dirfd", issue = "120426")] + pub fn open_file(&self) -> io::Result { + self.0.open_file().map(|inner| File { inner }) + } + + /// Attempts to open the file represented by `self` with the options specified by `opts`. + /// + /// # Errors + /// + /// This function will error whenever [`OpenOptions::open`] would error, including when `self` + /// represents a directory. + /// + /// # Examples + /// + /// ```no_run + /// #![feature(dirfd)] + /// use std::{fs, io::Write}; + /// + /// if let Ok(entries) = fs::read_dir(".") { + /// for entry in entries { + /// if let Ok(entry) = entry { + /// // Here, `entry` is a `DirEntry`. + /// if let Ok(mut file) = entry.open_file_with(fs::OpenOptions::new().write(true)) { + /// let _ = file.write(b"foo"); + /// } + /// } + /// } + /// } + /// ``` + #[unstable(feature = "dirfd", issue = "120426")] + pub fn open_file_with(&self, opts: &OpenOptions) -> io::Result { + self.0.open_file_with(&opts.0).map(|inner| File { inner }) + } + + /// Attempts to open the directory represented by `self` in read-only mode. + /// + /// # Errors + /// + /// This function will error whenever [`Dir::new`] would error, including when `self` + /// represents a file. + /// + /// # Examples + /// + /// ```no_run + /// #![feature(dirfd)] + /// use std::{fs, io::read_to_string}; + /// + /// if let Ok(entries) = fs::read_dir(".") { + /// for entry in entries { + /// if let Ok(entry) = entry { + /// // Here, `entry` is a `DirEntry`. + /// if let Ok(dir) = entry.open_dir() && let Ok(file) = dir.open("foo") && let Ok(text) = read_to_string(file) { + /// println!("{}", text); + /// } + /// } + /// } + /// } + /// ``` + #[unstable(feature = "dirfd", issue = "120426")] + pub fn open_dir(&self) -> io::Result { + self.0.open_dir().map(|inner| Dir { inner }) + } + + /// Attempts to open the directory represented by `self` with the options specified by `opts`. + /// + /// # Errors + /// + /// This function will error whenever [`Dir::new_with`] would error, including when `self` + /// represents a file. + /// + /// # Examples + /// + /// ```no_run + /// #![feature(dirfd)] + /// use std::fs; + /// + /// if let Ok(entries) = fs::read_dir(".") { + /// for entry in entries { + /// if let Ok(entry) = entry { + /// // Here, `entry` is a `DirEntry`. + /// if let Ok(dir) = entry.open_dir_with(fs::OpenOptions::new().write(true)) { + /// let _ = dir.remove_file("foo"); + /// } + /// } + /// } + /// } + /// ``` + #[unstable(feature = "dirfd", issue = "120426")] + pub fn open_dir_with(&self, opts: &OpenOptions) -> io::Result { + self.0.open_dir_with(&opts.0).map(|inner| Dir { inner }) + } + + /// Attempts to remove the file represented by `self`. + /// + /// # Errors + /// + /// This function will return an error whenever [`remove_file`] would error. + /// + /// # Examples + /// + /// ```no_run + /// #![feature(dirfd)] + /// use std::fs; + /// + /// if let Ok(entries) = fs::read_dir(".") { + /// for entry in entries { + /// if let Ok(entry) = entry { + /// // Here, `entry` is a `DirEntry`. + /// if let Ok(ty) = entry.file_type() && ty.is_file() { + /// let _ = entry.remove_file(); + /// } + /// } + /// } + /// } + /// ``` + #[unstable(feature = "dirfd", issue = "120426")] + pub fn remove_file(&self) -> io::Result<()> { + self.0.remove_file() + } + + /// Attempts to remove the directory represented by `self`. + /// + /// # Errors + /// + /// This function will return an error whenever [`remove_dir`] would error. + /// + /// # Examples + /// + /// ```no_run + /// #![feature(dirfd)] + /// use std::fs; + /// + /// if let Ok(entries) = fs::read_dir(".") { + /// for entry in entries { + /// if let Ok(entry) = entry { + /// // Here, `entry` is a `DirEntry`. + /// if let Ok(ty) = entry.file_type() && ty.is_dir() { + /// let _ = entry.remove_dir(); + /// } + /// } + /// } + /// } + /// ``` + #[unstable(feature = "dirfd", issue = "120426")] + pub fn remove_dir(&self) -> io::Result<()> { + self.0.remove_dir() + } } #[stable(feature = "dir_entry_debug", since = "1.13.0")] diff --git a/library/std/src/fs/tests.rs b/library/std/src/fs/tests.rs index c81e3af2f0d4c..f255fde7e2125 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; @@ -2084,3 +2085,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/common.rs b/library/std/src/sys/fs/common.rs index bfd684d295b89..4434568a21859 100644 --- a/library/std/src/sys/fs/common.rs +++ b/library/std/src/sys/fs/common.rs @@ -1,8 +1,10 @@ #![allow(dead_code)] // not used on all platforms -use crate::fs; +use crate::fmt; +use crate::fs::{self, create_dir, remove_dir, remove_file, rename}; use crate::io::{self, Error, ErrorKind}; -use crate::path::Path; +use crate::path::{Path, PathBuf}; +use crate::sys::fs::{File, OpenOptions, symlink}; use crate::sys_common::ignore_notfound; pub(crate) const NOT_FILE_ERROR: Error = io::const_error!( @@ -58,3 +60,70 @@ pub fn exists(path: &Path) -> io::Result { Err(error) => Err(error), } } + +pub struct Dir { + path: PathBuf, +} + +impl Dir { + pub fn new>(path: P) -> io::Result { + Ok(Self { path: path.as_ref().to_path_buf() }) + } + + pub fn new_with>(path: P, _opts: &OpenOptions) -> io::Result { + Ok(Self { path: path.as_ref().to_path_buf() }) + } + + pub fn new_for_traversal>(path: P) -> io::Result { + Ok(Self { path: path.as_ref().to_path_buf() }) + } + + pub fn open>(&self, path: P) -> io::Result { + let mut opts = OpenOptions::new(); + opts.read(true); + File::open(&self.path.join(path), &opts) + } + + pub fn open_with>(&self, path: P, opts: &OpenOptions) -> io::Result { + File::open(&self.path.join(path), opts) + } + + pub fn create_dir>(&self, path: P) -> io::Result<()> { + create_dir(self.path.join(path)) + } + + pub fn open_dir>(&self, path: P) -> io::Result { + Self::new(self.path.join(path)) + } + + pub fn open_dir_with>(&self, path: P, opts: &OpenOptions) -> io::Result { + Self::new_with(self.path.join(path), opts) + } + + pub fn remove_file>(&self, path: P) -> io::Result<()> { + remove_file(self.path.join(path)) + } + + pub fn remove_dir>(&self, path: P) -> io::Result<()> { + remove_dir(self.path.join(path)) + } + + pub fn rename, Q: AsRef>( + &self, + from: P, + to_dir: &Self, + to: Q, + ) -> io::Result<()> { + rename(self.path.join(from), to_dir.path.join(to)) + } + + pub fn symlink, Q: AsRef>(&self, original: P, link: Q) -> io::Result<()> { + symlink(original.as_ref(), link.as_ref()) + } +} + +impl fmt::Debug for Dir { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Dir").field("path", &self.path).finish() + } +} diff --git a/library/std/src/sys/fs/hermit.rs b/library/std/src/sys/fs/hermit.rs index 175d919c289dd..5af353d8f1a3f 100644 --- a/library/std/src/sys/fs/hermit.rs +++ b/library/std/src/sys/fs/hermit.rs @@ -11,7 +11,8 @@ use crate::path::{Path, PathBuf}; use crate::sync::Arc; use crate::sys::common::small_c_string::run_path_with_cstr; use crate::sys::fd::FileDesc; -pub use crate::sys::fs::common::{copy, exists}; +pub use crate::sys::fs::common::{Dir, copy, exists}; +use crate::sys::fs::{remove_dir, remove_file}; use crate::sys::time::SystemTime; use crate::sys::{cvt, unsupported, unsupported_err}; use crate::sys_common::{AsInner, AsInnerMut, FromInner, IntoInner}; @@ -19,6 +20,7 @@ use crate::{fmt, mem}; #[derive(Debug)] pub struct File(FileDesc); + #[derive(Clone)] pub struct FileAttr { stat_val: stat_struct, @@ -251,6 +253,32 @@ impl DirEntry { pub fn file_name_os_str(&self) -> &OsStr { self.name.as_os_str() } + + pub fn open_file(&self) -> io::Result { + let mut opts = OpenOptions::new(); + opts.read(true); + File::open(&self.path(), &opts) + } + + pub fn open_file_with(&self, opts: &OpenOptions) -> io::Result { + File::open(&self.path(), opts) + } + + pub fn open_dir(&self) -> io::Result { + Dir::new(self.path()) + } + + pub fn open_dir_with(&self, opts: &OpenOptions) -> io::Result { + Dir::new_with(self.path(), opts) + } + + pub fn remove_file(&self) -> io::Result<()> { + remove_file(&self.path()) + } + + pub fn remove_dir(&self) -> io::Result<()> { + remove_dir(&self.path()) + } } impl OpenOptions { 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/solid.rs b/library/std/src/sys/fs/solid.rs index 808a95829114e..5505f66faa2a5 100644 --- a/library/std/src/sys/fs/solid.rs +++ b/library/std/src/sys/fs/solid.rs @@ -9,7 +9,8 @@ use crate::os::raw::{c_int, c_short}; use crate::os::solid::ffi::OsStrExt; use crate::path::{Path, PathBuf}; use crate::sync::Arc; -pub use crate::sys::fs::common::exists; +pub use crate::sys::fs::common::{Dir, exists}; +use crate::sys::fs::{remove_dir, remove_file}; use crate::sys::pal::{abi, error}; use crate::sys::time::SystemTime; use crate::sys::{unsupported, unsupported_err}; @@ -219,6 +220,32 @@ impl DirEntry { _ => lstat(&self.path()).map(|m| m.file_type()), } } + + pub fn open_file(&self) -> io::Result { + let mut opts = OpenOptions::new(); + opts.read(true); + File::open(&self.path(), &opts) + } + + pub fn open_file_with(&self, opts: &OpenOptions) -> io::Result { + File::open(&self.path(), opts) + } + + pub fn open_dir(&self) -> io::Result { + Dir::new(self.path()) + } + + pub fn open_dir_with(&self, opts: &OpenOptions) -> io::Result { + Dir::new_with(self.path(), opts) + } + + pub fn remove_file(&self) -> io::Result<()> { + remove_file(&self.path()) + } + + pub fn remove_dir(&self) -> io::Result<()> { + remove_dir(&self.path()) + } } impl OpenOptions { diff --git a/library/std/src/sys/fs/uefi.rs b/library/std/src/sys/fs/uefi.rs index 5763d7862f5ae..261c6ffc01527 100644 --- a/library/std/src/sys/fs/uefi.rs +++ b/library/std/src/sys/fs/uefi.rs @@ -20,6 +20,8 @@ pub struct FileAttr { size: u64, } +pub struct Dir(!); + pub struct ReadDir(!); pub struct DirEntry(!); @@ -115,6 +117,71 @@ impl FileType { } } +impl Dir { + pub fn new>(_path: P) -> io::Result { + unsupported() + } + + pub fn new_with>(_path: P, _opts: &OpenOptions) -> io::Result { + unsupported() + } + + pub fn new_for_traversal>(_path: P) -> io::Result { + unsupported() + } + + pub fn open>(&self, _path: P) -> io::Result { + self.0 + } + + pub fn open_with>(&self, _path: P, _opts: &OpenOptions) -> io::Result { + self.0 + } + + pub fn create_dir>(&self, _path: P) -> io::Result<()> { + self.0 + } + + pub fn open_dir>(&self, _path: P) -> io::Result { + self.0 + } + + pub fn open_dir_with>(&self, _path: P, _opts: &OpenOptions) -> io::Result { + self.0 + } + + pub fn remove_file>(&self, _path: P) -> io::Result<()> { + self.0 + } + + pub fn remove_dir>(&self, _path: P) -> io::Result<()> { + self.0 + } + + pub fn rename, Q: AsRef>( + &self, + _from: P, + _to_dir: &Self, + _to: Q, + ) -> io::Result<()> { + self.0 + } + + pub fn symlink, Q: AsRef>( + &self, + _original: P, + _link: Q, + ) -> io::Result<()> { + self.0 + } +} + +impl fmt::Debug for Dir { + fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0 + } +} + impl fmt::Debug for ReadDir { fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0 @@ -145,6 +212,30 @@ impl DirEntry { pub fn file_type(&self) -> io::Result { self.0 } + + pub fn open_file(&self) -> io::Result { + self.0 + } + + pub fn open_file_with(&self, _opts: &OpenOptions) -> io::Result { + self.0 + } + + pub fn open_dir(&self) -> io::Result { + self.0 + } + + pub fn open_dir_with(&self, _opts: &OpenOptions) -> io::Result { + self.0 + } + + pub fn remove_file(&self) -> io::Result<()> { + self.0 + } + + pub fn remove_dir(&self) -> io::Result<()> { + self.0 + } } impl OpenOptions { diff --git a/library/std/src/sys/fs/unix.rs b/library/std/src/sys/fs/unix.rs index dc278274f00f4..5f1115298203a 100644 --- a/library/std/src/sys/fs/unix.rs +++ b/library/std/src/sys/fs/unix.rs @@ -72,6 +72,52 @@ use libc::{ target_os = "hurd" ))] use libc::{dirent64, fstat64, ftruncate64, lseek64, lstat64, off64_t, open64, stat64}; +#[cfg(any( + target_os = "aix", + target_os = "freebsd", + target_os = "fuchsia", + target_os = "solaris", + target_vendor = "apple", +))] +use libc::{mkdirat, openat as openat64, renameat, symlinkat, unlinkat}; +#[cfg(not(any( + target_os = "redox", + target_os = "espidf", + target_os = "horizon", + target_os = "vita", + target_os = "nto", + target_os = "vxworks", + target_os = "fuchsia", + target_os = "freebsd", + target_os = "solaris", + target_os = "aix", + target_vendor = "apple", + miri +)))] +use libc::{mkdirat, openat64, renameat, symlinkat, unlinkat}; + +#[cfg(any(target_os = "freebsd", target_os = "aix"))] +const TRAVERSE_DIRECTORY: i32 = libc::O_EXEC; +#[cfg(any(target_os = "linux", target_os = "android", target_os = "l4re"))] +const TRAVERSE_DIRECTORY: i32 = libc::O_PATH; +#[cfg(target_os = "illumos")] +const TRAVERSE_DIRECTORY: i32 = libc::O_SEARCH; +#[cfg(not(any( + target_os = "aix", + target_os = "android", + target_os = "freebsd", + target_os = "illumos", + target_os = "l4re", + target_os = "linux", + target_os = "redox", + target_os = "espidf", + target_os = "horizon", + target_os = "vita", + target_os = "nto", + target_os = "vxworks", + miri +)))] +const TRAVERSE_DIRECTORY: i32 = libc::O_RDONLY; use crate::ffi::{CStr, OsStr, OsString}; use crate::fmt::{self, Write as _}; @@ -249,7 +295,7 @@ cfg_has_statx! {{ // all DirEntry's will have a reference to this struct struct InnerReadDir { - dirp: Dir, + dirp: DirStream, root: PathBuf, } @@ -264,10 +310,285 @@ impl ReadDir { } } -struct Dir(*mut libc::DIR); +struct DirStream(*mut libc::DIR); + +#[cfg(any( + target_os = "redox", + target_os = "espidf", + target_os = "horizon", + target_os = "vita", + target_os = "nto", + target_os = "vxworks", + miri +))] +pub use crate::sys::fs::common::Dir; + +#[cfg(not(any( + target_os = "redox", + target_os = "espidf", + target_os = "horizon", + target_os = "vita", + target_os = "nto", + target_os = "vxworks", + miri +)))] +pub struct Dir(OwnedFd); + +#[cfg(not(any( + target_os = "redox", + target_os = "espidf", + target_os = "horizon", + target_os = "vita", + target_os = "nto", + target_os = "vxworks", + miri +)))] +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::new_with_c(path, &opts)) + } + + pub fn new_with>(path: P, opts: &OpenOptions) -> io::Result { + run_path_with_cstr(path.as_ref(), &|path| Self::new_with_c(path, opts)) + } + + pub fn new_for_traversal>(path: P) -> io::Result { + run_path_with_cstr(path.as_ref(), &|path| Self::new_c(path)) + } + + 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 open_dir>(&self, path: P) -> io::Result { + run_path_with_cstr(path.as_ref(), &|path| self.open_c_dir(path, &OpenOptions::new())) + } + + pub fn open_dir_with>(&self, path: P, opts: &OpenOptions) -> io::Result { + run_path_with_cstr(path.as_ref(), &|path| self.open_c_dir(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 symlink, Q: AsRef>(&self, original: P, link: Q) -> io::Result<()> { + run_path_with_cstr(original.as_ref(), &|original| { + run_path_with_cstr(link.as_ref(), &|link| self.symlink_c(original, link)) + }) + } + + 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(&self, 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 { + openat64(self.0.as_raw_fd(), path.as_ptr(), flags, opts.mode as c_int) + })?; + Ok(Self(unsafe { OwnedFd::from_raw_fd(fd) })) + } + + pub fn new_c(path: &CStr) -> io::Result { + let flags = libc::O_CLOEXEC | libc::O_DIRECTORY | TRAVERSE_DIRECTORY; + let fd = cvt_r(|| unsafe { open64(path.as_ptr(), flags, 0) })?; + Ok(Self(unsafe { OwnedFd::from_raw_fd(fd) })) + } + + pub fn new_with_c(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(|_| ()) + } + + pub fn symlink_c(&self, original: &CStr, link: &CStr) -> io::Result<()> { + cvt(unsafe { symlinkat(original.as_ptr(), self.0.as_raw_fd(), link.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) +} + +#[cfg(not(any( + target_os = "redox", + target_os = "espidf", + target_os = "horizon", + target_os = "vita", + target_os = "nto", + target_os = "vxworks", + miri +)))] +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 Dir {} -unsafe impl Sync for Dir {} +// SAFETY: `int dirfd (DIR *dirstream)` is MT-safe, implying that the pointer +// may be safely sent among threads. +unsafe impl Send for DirStream {} +unsafe impl Sync for DirStream {} #[cfg(any( target_os = "android", @@ -852,7 +1173,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( @@ -1071,6 +1392,171 @@ impl DirEntry { pub fn file_name_os_str(&self) -> &OsStr { OsStr::from_bytes(self.name_bytes()) } + + #[cfg(not(any( + target_os = "redox", + target_os = "espidf", + target_os = "horizon", + target_os = "vita", + target_os = "nto", + target_os = "vxworks", + miri + )))] + pub fn open_file(&self) -> io::Result { + Dir(unsafe { OwnedFd::from_raw_fd(libc::dirfd(self.dir.dirp.0)) }).open(self.file_name()) + } + + #[cfg(any( + target_os = "redox", + target_os = "espidf", + target_os = "horizon", + target_os = "vita", + target_os = "nto", + target_os = "vxworks", + miri + ))] + pub fn open_file(&self) -> io::Result { + let mut opts = OpenOptions::new(); + opts.read(true); + File::open(&self.path(), &opts) + } + + #[cfg(not(any( + target_os = "redox", + target_os = "espidf", + target_os = "horizon", + target_os = "vita", + target_os = "nto", + target_os = "vxworks", + miri + )))] + pub fn open_file_with(&self, opts: &OpenOptions) -> io::Result { + Dir(unsafe { OwnedFd::from_raw_fd(libc::dirfd(self.dir.dirp.0)) }) + .open_with(self.file_name(), opts) + } + + #[cfg(any( + target_os = "redox", + target_os = "espidf", + target_os = "horizon", + target_os = "vita", + target_os = "nto", + target_os = "vxworks", + miri + ))] + pub fn open_file_with(&self, opts: &OpenOptions) -> io::Result { + File::open(&self.path(), &opts) + } + + #[cfg(not(any( + target_os = "redox", + target_os = "espidf", + target_os = "horizon", + target_os = "vita", + target_os = "nto", + target_os = "vxworks", + miri + )))] + pub fn open_dir(&self) -> io::Result { + Dir(unsafe { OwnedFd::from_raw_fd(libc::dirfd(self.dir.dirp.0)) }) + .open_dir(self.file_name()) + } + + #[cfg(any( + target_os = "redox", + target_os = "espidf", + target_os = "horizon", + target_os = "vita", + target_os = "nto", + target_os = "vxworks", + miri + ))] + pub fn open_dir(&self) -> io::Result { + Dir::new(self.path()) + } + + #[cfg(not(any( + target_os = "redox", + target_os = "espidf", + target_os = "horizon", + target_os = "vita", + target_os = "nto", + target_os = "vxworks", + miri + )))] + pub fn open_dir_with(&self, opts: &OpenOptions) -> io::Result { + Dir(unsafe { OwnedFd::from_raw_fd(libc::dirfd(self.dir.dirp.0)) }) + .open_dir_with(self.file_name(), opts) + } + + #[cfg(any( + target_os = "redox", + target_os = "espidf", + target_os = "horizon", + target_os = "vita", + target_os = "nto", + target_os = "vxworks", + miri + ))] + pub fn open_dir_with(&self, opts: &OpenOptions) -> io::Result { + Dir::new_with(self.path(), opts) + } + + #[cfg(not(any( + target_os = "redox", + target_os = "espidf", + target_os = "horizon", + target_os = "vita", + target_os = "nto", + target_os = "vxworks", + miri + )))] + pub fn remove_file(&self) -> io::Result<()> { + Dir(unsafe { OwnedFd::from_raw_fd(libc::dirfd(self.dir.dirp.0)) }) + .remove_file(self.file_name()) + } + + #[cfg(any( + target_os = "redox", + target_os = "espidf", + target_os = "horizon", + target_os = "vita", + target_os = "nto", + target_os = "vxworks", + miri + ))] + pub fn remove_file(&self) -> io::Result<()> { + use crate::sys::fs::remove_file; + remove_file(&self.path()) + } + + #[cfg(not(any( + target_os = "redox", + target_os = "espidf", + target_os = "horizon", + target_os = "vita", + target_os = "nto", + target_os = "vxworks", + miri + )))] + pub fn remove_dir(&self) -> io::Result<()> { + Dir(unsafe { OwnedFd::from_raw_fd(libc::dirfd(self.dir.dirp.0)) }) + .remove_dir(self.file_name()) + } + + #[cfg(any( + target_os = "redox", + target_os = "espidf", + target_os = "horizon", + target_os = "vita", + target_os = "nto", + target_os = "vxworks", + miri + ))] + pub fn remove_dir(&self) -> io::Result<()> { + use crate::sys::fs::remove_dir; + remove_dir(&self.path()) + } } impl OpenOptions { @@ -1661,79 +2147,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 { @@ -1750,7 +2163,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) { @@ -1833,7 +2246,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)) } } @@ -2194,7 +2607,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}; @@ -2220,7 +2633,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/unsupported.rs b/library/std/src/sys/fs/unsupported.rs index efaddb51b3751..fd0e3b25a9381 100644 --- a/library/std/src/sys/fs/unsupported.rs +++ b/library/std/src/sys/fs/unsupported.rs @@ -13,6 +13,8 @@ pub struct FileAttr(!); pub struct ReadDir(!); +pub struct Dir(!); + pub struct DirEntry(!); #[derive(Clone, Debug)] @@ -151,6 +153,71 @@ impl Iterator for ReadDir { } } +impl Dir { + pub fn new>(_path: P) -> io::Result { + unsupported() + } + + pub fn new_with>(_path: P, _opts: &OpenOptions) -> io::Result { + unsupported() + } + + pub fn new_for_traversal>(_path: P) -> io::Result { + unsupported() + } + + pub fn open>(&self, _path: P) -> io::Result { + self.0 + } + + pub fn open_with>(&self, _path: P, _opts: &OpenOptions) -> io::Result { + self.0 + } + + pub fn create_dir>(&self, _path: P) -> io::Result<()> { + self.0 + } + + pub fn open_dir>(&self, _path: P) -> io::Result { + self.0 + } + + pub fn open_dir_with>(&self, _path: P, _opts: &OpenOptions) -> io::Result { + self.0 + } + + pub fn remove_file>(&self, _path: P) -> io::Result<()> { + self.0 + } + + pub fn remove_dir>(&self, _path: P) -> io::Result<()> { + self.0 + } + + pub fn rename, Q: AsRef>( + &self, + _from: P, + _to_dir: &Self, + _to: Q, + ) -> io::Result<()> { + self.0 + } + + pub fn symlink, Q: AsRef>( + &self, + _original: P, + _link: Q, + ) -> io::Result<()> { + self.0 + } +} + +impl fmt::Debug for Dir { + fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0 + } +} + impl DirEntry { pub fn path(&self) -> PathBuf { self.0 @@ -167,6 +234,30 @@ impl DirEntry { pub fn file_type(&self) -> io::Result { self.0 } + + pub fn open_file(&self) -> io::Result { + self.0 + } + + pub fn open_file_with(&self, _opts: &OpenOptions) -> io::Result { + self.0 + } + + pub fn open_dir(&self) -> io::Result { + self.0 + } + + pub fn open_dir_with(&self, _opts: &OpenOptions) -> io::Result { + self.0 + } + + pub fn remove_file(&self) -> io::Result<()> { + self.0 + } + + pub fn remove_dir(&self) -> io::Result<()> { + self.0 + } } impl OpenOptions { diff --git a/library/std/src/sys/fs/wasi.rs b/library/std/src/sys/fs/wasi.rs index b65d86de12a3d..39f993c70f66a 100644 --- a/library/std/src/sys/fs/wasi.rs +++ b/library/std/src/sys/fs/wasi.rs @@ -9,7 +9,8 @@ use crate::path::{Path, PathBuf}; use crate::sync::Arc; use crate::sys::common::small_c_string::run_path_with_cstr; use crate::sys::fd::WasiFd; -pub use crate::sys::fs::common::exists; +pub use crate::sys::fs::common::{Dir, exists}; +use crate::sys::fs::{remove_dir, remove_file}; use crate::sys::time::SystemTime; use crate::sys::{unsupported, unsupported_err}; use crate::sys_common::{AsInner, FromInner, IntoInner, ignore_notfound}; @@ -293,6 +294,32 @@ impl DirEntry { pub fn ino(&self) -> wasi::Inode { self.meta.d_ino } + + pub fn open_file(&self) -> io::Result { + let mut opts = OpenOptions::new(); + opts.read(true); + File::open(&self.path(), &opts) + } + + pub fn open_file_with(&self, opts: &OpenOptions) -> io::Result { + File::open(&self.path(), opts) + } + + pub fn open_dir(&self) -> io::Result { + Dir::new(self.path()) + } + + pub fn open_dir_with(&self, opts: &OpenOptions) -> io::Result { + Dir::new_with(self.path(), opts) + } + + pub fn remove_file(&self) -> io::Result<()> { + remove_file(&self.path()) + } + + pub fn remove_dir(&self) -> io::Result<()> { + remove_dir(&self.path()) + } } impl OpenOptions { diff --git a/library/std/src/sys/fs/windows.rs b/library/std/src/sys/fs/windows.rs index a95709b489143..dd8f2a59fbaf9 100644 --- a/library/std/src/sys/fs/windows.rs +++ b/library/std/src/sys/fs/windows.rs @@ -10,6 +10,8 @@ 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::fs::{remove_dir, remove_file}; 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 +28,10 @@ pub struct File { handle: Handle, } +pub struct Dir { + handle: Handle, +} + #[derive(Clone)] pub struct FileAttr { attributes: u32, @@ -184,6 +190,32 @@ impl DirEntry { pub fn metadata(&self) -> io::Result { Ok(self.data.into()) } + + pub fn open_file(&self) -> io::Result { + let mut opts = OpenOptions::new(); + opts.read(true); + File::open(&self.path(), &opts) + } + + pub fn open_file_with(&self, opts: &OpenOptions) -> io::Result { + File::open(&self.path(), opts) + } + + pub fn open_dir(&self) -> io::Result { + Dir::new(&self.path()) + } + + pub fn open_dir_with(&self, opts: &OpenOptions) -> io::Result { + Dir::new_with(&self.path(), opts) + } + + pub fn remove_file(&self) -> io::Result<()> { + remove_file(&self.path()) + } + + pub fn remove_dir(&self) -> io::Result<()> { + remove_dir(&self.path()) + } } impl OpenOptions { @@ -288,6 +320,30 @@ impl OpenOptions { }) } + fn get_disposition(&self) -> io::Result { + match (self.write, self.append) { + (true, false) => {} + (false, false) => { + if self.truncate || self.create || self.create_new { + return Err(Error::from_raw_os_error(c::ERROR_INVALID_PARAMETER as i32)); + } + } + (_, true) => { + if self.truncate && !self.create_new { + return Err(Error::from_raw_os_error(c::ERROR_INVALID_PARAMETER as i32)); + } + } + } + + Ok(match (self.create, self.truncate, self.create_new) { + (false, false, false) => c::FILE_OPEN, + (true, false, false) => c::FILE_OPEN_IF, + (false, true, false) => c::FILE_OVERWRITE, + (true, true, false) => c::FILE_OVERWRITE_IF, + (_, _, true) => c::FILE_CREATE, + }) + } + fn get_flags_and_attributes(&self) -> u32 { self.custom_flags | self.attributes @@ -848,6 +904,360 @@ impl File { } } +unsafe fn nt_create_file( + access: u32, + disposition: 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 access = access | c::SYNCHRONIZE; + let options = if dir { c::FILE_DIRECTORY_FILE } else { c::FILE_NON_DIRECTORY_FILE } + | c::FILE_SYNCHRONOUS_IO_NONALERT; + let status = unsafe { + c::NtCreateFile( + &mut handle, + access, + object_attributes, + &mut io_status, + ptr::null(), + c::FILE_ATTRIBUTE_NORMAL, + share, + disposition, + options, + 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) +} + +fn run_path_with_utf16>( + path: P, + f: &dyn Fn(&[u16]) -> io::Result, +) -> io::Result { + let utf16: Vec = path.as_ref().as_os_str().encode_wide().collect(); + f(&utf16) +} + +impl Dir { + pub fn new>(path: P) -> io::Result { + let opts = OpenOptions::new(); + run_path_with_wcstr(path.as_ref(), &|path| Self::new_with_native(path, &opts)) + } + + pub fn new_with>(path: P, opts: &OpenOptions) -> io::Result { + run_path_with_wcstr(path.as_ref(), &|path| Self::new_with_native(path, &opts)) + } + + pub fn new_for_traversal>(path: P) -> io::Result { + run_path_with_wcstr(path.as_ref(), &|path| Self::new_native(path)) + } + + pub fn open>(&self, path: P) -> io::Result { + let mut opts = OpenOptions::new(); + let path = path.as_ref().as_os_str().encode_wide().collect::>(); + opts.read(true); + self.open_native(&path, &opts).map(|handle| File { handle }) + } + + pub fn open_with>(&self, path: P, opts: &OpenOptions) -> io::Result { + let path = path.as_ref().as_os_str().encode_wide().collect::>(); + self.open_native(&path, &opts).map(|handle| File { handle }) + } + + pub fn open_dir>(&self, path: P) -> io::Result { + let mut opts = OpenOptions::new(); + let path = path.as_ref().as_os_str().encode_wide().collect::>(); + opts.read(true); + self.open_native(&path, &opts).map(|handle| Self { handle }) + } + + pub fn open_dir_with>(&self, path: P, opts: &OpenOptions) -> io::Result { + let path = path.as_ref().as_os_str().encode_wide().collect::>(); + self.open_native(&path, &opts).map(|handle| Self { handle }) + } + + pub fn create_dir>(&self, path: P) -> io::Result<()> { + let mut opts = OpenOptions::new(); + opts.write(true); + run_path_with_utf16(path, &|path| self.create_dir_native(path, &opts).map(|_| ())) + } + + pub fn remove_file>(&self, path: P) -> io::Result<()> { + run_path_with_utf16(path, &|path| self.remove_native(path, false)) + } + + pub fn remove_dir>(&self, path: P) -> io::Result<()> { + run_path_with_utf16(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(to.as_ref(), &|to| self.rename_native(from.as_ref(), to_dir, to)) + } + + pub fn symlink, Q: AsRef>(&self, original: P, link: Q) -> io::Result<()> { + run_path_with_utf16(original.as_ref(), &|orig| { + self.symlink_native(orig, link.as_ref(), original.as_ref().is_relative()) + }) + } + + fn new_native(path: &WCStr) -> io::Result { + let mut opts = OpenOptions::new(); + opts.access_mode(c::FILE_TRAVERSE); + Self::new_with_native(path, &opts) + } + + fn new_with_native(path: &WCStr, opts: &OpenOptions) -> io::Result { + let creation = opts.get_creation_mode()?; + let handle = unsafe { + c::CreateFileW( + path.as_ptr(), + opts.get_access_mode()?, + opts.share_mode, + opts.security_attributes, + creation, + opts.get_flags_and_attributes() | c::FILE_FLAG_BACKUP_SEMANTICS, + ptr::null_mut(), + ) + }; + match OwnedHandle::try_from(unsafe { HandleOrInvalid::from_raw_handle(handle) }) { + Ok(handle) => Ok(Self { handle: Handle::from_inner(handle) }), + Err(_) => Err(Error::last_os_error()), + } + } + + fn open_native(&self, path: &[u16], opts: &OpenOptions) -> io::Result { + let name = c::UNICODE_STRING { + Length: path.len() as _, + MaximumLength: path.len() 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()?, + opts.get_disposition()?, + &object_attributes, + share, + false, + ) + } + .io_result() + } + + fn create_dir_native(&self, path: &[u16], opts: &OpenOptions) -> io::Result { + let name = c::UNICODE_STRING { + Length: path.len() as _, + MaximumLength: path.len() 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()?, + opts.get_disposition()?, + &object_attributes, + share, + true, + ) + } + .io_result() + } + + fn remove_native(&self, path: &[u16], 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: &Path, to_dir: &Self, to: &WCStr) -> io::Result<()> { + let mut opts = OpenOptions::new(); + opts.access_mode(c::GENERIC_WRITE); + let handle = run_path_with_utf16(from, &|u| self.open_native(u, &opts))?; + // Calculate the layout of the `FILE_RENAME_INFO` we pass to `SetFileInformation` + // This is a dynamically sized struct so we need to get the position of the last field to calculate the actual size. + const too_long_err: io::Error = + io::const_error!(io::ErrorKind::InvalidFilename, "Filename too long"); + let struct_size = to + .count_bytes() + .checked_mul(2) + .and_then(|x| x.checked_add(offset_of!(c::FILE_RENAME_INFO, FileName))) + .ok_or(too_long_err)?; + let layout = Layout::from_size_align(struct_size, align_of::()) + .map_err(|_| too_long_err)?; + let to_byte_len_without_nul = + u32::try_from((to.count_bytes() - 1) * 2).map_err(|_| too_long_err)?; + let struct_size = u32::try_from(struct_size).map_err(|_| too_long_err)?; + + let file_rename_info; + // SAFETY: We allocate enough memory for a full FILE_RENAME_INFO struct and a filename. + unsafe { + file_rename_info = alloc(layout).cast::(); + if file_rename_info.is_null() { + return Err(io::ErrorKind::OutOfMemory.into()); + } + + (&raw mut (*file_rename_info).Anonymous).write(c::FILE_RENAME_INFO_0 { + Flags: c::FILE_RENAME_FLAG_REPLACE_IF_EXISTS | c::FILE_RENAME_FLAG_POSIX_SEMANTICS, + }); + + (&raw mut (*file_rename_info).RootDirectory).write(to_dir.handle.as_raw_handle()); + // Don't include the NULL in the size + (&raw mut (*file_rename_info).FileNameLength).write(to_byte_len_without_nul); + + to.as_ptr().copy_to_nonoverlapping( + (&raw mut (*file_rename_info).FileName).cast::(), + run_path_with_wcstr(from, &|s| Ok(s.count_bytes())).unwrap(), + ); + } + + let result = unsafe { + c::SetFileInformationByHandle( + handle.as_raw_handle(), + c::FileRenameInfoEx, + file_rename_info.cast::(), + struct_size, + ) + }; + unsafe { dealloc(file_rename_info.cast::(), layout) }; + if result == 0 { Err(api::get_last_error()).io_result() } else { Ok(()) } + } + + fn symlink_native(&self, original: &[u16], link: &Path, relative: bool) -> io::Result<()> { + const TOO_LONG_ERR: io::Error = + io::const_error!(io::ErrorKind::InvalidFilename, "File name is too long"); + let mut opts = OpenOptions::new(); + opts.write(true); + let linkfile = File::open(link, &opts)?; + let utf16: Vec = original.iter().chain(original).copied().collect(); + let file_name_len = u16::try_from(original.len()).or(Err(TOO_LONG_ERR))?; + let sym_buffer = c::SYMBOLIC_LINK_REPARSE_BUFFER { + SubstituteNameOffset: 0, + SubstituteNameLength: file_name_len, + PrintNameOffset: file_name_len, + PrintNameLength: file_name_len, + Flags: if relative { c::SYMLINK_FLAG_RELATIVE } else { 0 }, + PathBuffer: 0, + }; + let layout = Layout::new::(); + let layout = layout + .extend(Layout::new::()) + .or(Err(TOO_LONG_ERR))? + .0; + let layout = Layout::array::(original.len() * 2) + .and_then(|arr| layout.extend(arr)) + .or(Err(TOO_LONG_ERR))? + .0; + let buffer = unsafe { alloc(layout) }.cast::(); + unsafe { + buffer.write(c::REPARSE_DATA_BUFFER { + ReparseTag: c::IO_REPARSE_TAG_SYMLINK, + ReparseDataLength: u16::try_from(size_of_val(&sym_buffer)).or(Err(TOO_LONG_ERR))?, + Reserved: 0, + rest: (), + }); + buffer + .add(offset_of!(c::REPARSE_DATA_BUFFER, rest)) + .cast::() + .write(sym_buffer); + ptr::copy_nonoverlapping( + utf16.as_ptr(), + buffer + .add(offset_of!(c::REPARSE_DATA_BUFFER, rest)) + .add(offset_of!(c::SYMBOLIC_LINK_REPARSE_BUFFER, PathBuffer)) + .cast::(), + original.len() * 2, + ); + }; + let result = unsafe { + c::DeviceIoControl( + linkfile.handle.as_raw_handle(), + c::FSCTL_SET_REPARSE_POINT, + &raw const buffer as *const c_void, + u32::try_from(size_of_val(&buffer)).or(Err(TOO_LONG_ERR))?, + ptr::null_mut(), + 0, + ptr::null_mut(), + ptr::null_mut(), + ) + }; + unsafe { + dealloc(buffer.cast(), layout); + } + + 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]>>, @@ -997,7 +1407,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() @@ -1265,8 +1675,8 @@ pub fn rename(old: &WCStr, new: &WCStr) -> io::Result<()> { Layout::from_size_align(struct_size as usize, align_of::()) .unwrap(); - // SAFETY: We allocate enough memory for a full FILE_RENAME_INFO struct and a filename. let file_rename_info; + // SAFETY: We allocate enough memory for a full FILE_RENAME_INFO struct and a filename. unsafe { file_rename_info = alloc(layout).cast::(); if file_rename_info.is_null() { @@ -1486,10 +1896,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)), ) @@ -1502,7 +1912,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 {