Auto merge of #146341 - Qelxiros:dirfd-minimum, r=tgross35

minimal dirfd implementation (1/4)

This is the first of four smaller PRs that will eventually be equivalent to rust-lang/rust#139514.

A few notes:

- I renamed `new` to `open` because `open_dir` takes `&self` and opens a subdirectory.
  - I also renamed `open` to `open_file`.
- I'm not sure how to `impl AsRawFd` and friends because the `common` implementation uses `PathBuf`s. How should I proceed here?

The other PRs will be based on this one, so I'll make drafts and mark them ready as their predecessors get merged. They might take a bit though; I've never done this particular thing with git before.

Tracking issue: https://github.com/rust-lang/rust/issues/120426

try-job: aarch64-apple
try-job: dist-various*
try-job: test-various*
try-job: x86_64-msvc-1
try-job: x86_64-mingw*
This commit is contained in:
bors
2026-01-04 10:12:47 +00:00
12 changed files with 619 additions and 124 deletions

View File

@@ -152,6 +152,43 @@ pub enum TryLockError {
WouldBlock,
}
/// An object providing access to a directory on the filesystem.
///
/// Directories are automatically closed when they go out of scope. Errors detected
/// on closing are ignored by the implementation of `Drop`.
///
/// # Platform-specific behavior
///
/// On supported systems (including Windows and some UNIX-based OSes), this function acquires a
/// handle/file descriptor for the directory. This allows functions like [`Dir::open_file`] to
/// avoid [TOCTOU] errors when the directory itself is being moved.
///
/// On other systems, it stores an absolute path (see [`canonicalize()`]). In the latter case, no
/// [TOCTOU] guarantees are made.
///
/// # Examples
///
/// Opens a directory and then a file inside it.
///
/// ```no_run
/// #![feature(dirfd)]
/// use std::{fs::Dir, io};
///
/// fn main() -> std::io::Result<()> {
/// let dir = Dir::open("foo")?;
/// let mut file = dir.open_file("bar.txt")?;
/// let contents = io::read_to_string(file)?;
/// assert_eq!(contents, "Hello, world!");
/// Ok(())
/// }
/// ```
///
/// [TOCTOU]: self#time-of-check-to-time-of-use-toctou
#[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
@@ -1554,6 +1591,87 @@ impl Seek for Arc<File> {
}
}
impl Dir {
/// Attempts to open a directory at `path` 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};
///
/// fn main() -> std::io::Result<()> {
/// let dir = Dir::open("foo")?;
/// let mut f = dir.open_file("bar.txt")?;
/// let contents = io::read_to_string(f)?;
/// assert_eq!(contents, "Hello, world!");
/// Ok(())
/// }
/// ```
#[unstable(feature = "dirfd", issue = "120426")]
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
fs_imp::Dir::open(path.as_ref(), &OpenOptions::new().read(true).0)
.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};
///
/// fn main() -> std::io::Result<()> {
/// let dir = Dir::open("foo")?;
/// let mut f = dir.open_file("bar.txt")?;
/// let contents = io::read_to_string(f)?;
/// assert_eq!(contents, "Hello, world!");
/// Ok(())
/// }
/// ```
#[unstable(feature = "dirfd", issue = "120426")]
pub fn open_file<P: AsRef<Path>>(&self, path: P) -> io::Result<File> {
self.inner
.open_file(path.as_ref(), &OpenOptions::new().read(true).0)
.map(|f| File { inner: f })
}
}
impl AsInner<fs_imp::Dir> for Dir {
#[inline]
fn as_inner(&self) -> &fs_imp::Dir {
&self.inner
}
}
impl FromInner<fs_imp::Dir> for Dir {
fn from_inner(f: fs_imp::Dir) -> Dir {
Dir { inner: f }
}
}
impl IntoInner<fs_imp::Dir> for Dir {
fn into_inner(self) -> fs_imp::Dir {
self.inner
}
}
#[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.
///

View File

@@ -1,7 +1,11 @@
use rand::RngCore;
#[cfg(not(miri))]
use super::Dir;
use crate::assert_matches::assert_matches;
use crate::fs::{self, File, FileTimes, OpenOptions, TryLockError};
#[cfg(not(miri))]
use crate::io;
use crate::io::prelude::*;
use crate::io::{BorrowedBuf, ErrorKind, SeekFrom};
use crate::mem::MaybeUninit;
@@ -2465,3 +2469,30 @@ fn test_fs_set_times_nofollow() {
assert_ne!(target_metadata.accessed().unwrap(), accessed);
assert_ne!(target_metadata.modified().unwrap(), modified);
}
#[test]
// FIXME: libc calls fail on miri
#[cfg(not(miri))]
fn test_dir_smoke_test() {
let tmpdir = tmpdir();
let dir = Dir::open(tmpdir.path());
check!(dir);
}
#[test]
// FIXME: libc calls fail on miri
#[cfg(not(miri))]
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::open(tmpdir.path()));
let f = check!(dir.open_file("foo.txt"));
let buf = check!(io::read_to_string(f));
assert_eq!("bar", &buf);
let f = check!(dir.open_file(tmpdir.join("foo.txt")));
let buf = check!(io::read_to_string(f));
assert_eq!("bar", &buf);
}

View File

@@ -1,9 +1,10 @@
#![allow(dead_code)] // not used on all platforms
use crate::fs;
use crate::io::{self, Error, ErrorKind};
use crate::path::Path;
use crate::path::{Path, PathBuf};
use crate::sys::fs::{File, OpenOptions};
use crate::sys::helpers::ignore_notfound;
use crate::{fmt, fs};
pub(crate) const NOT_FILE_ERROR: Error = io::const_error!(
ErrorKind::InvalidInput,
@@ -58,3 +59,23 @@ pub fn exists(path: &Path) -> io::Result<bool> {
Err(error) => Err(error),
}
}
pub struct Dir {
path: PathBuf,
}
impl Dir {
pub fn open(path: &Path, _opts: &OpenOptions) -> io::Result<Self> {
path.canonicalize().map(|path| Self { path })
}
pub fn open_file(&self, path: &Path, opts: &OpenOptions) -> io::Result<File> {
File::open(&self.path.join(path), &opts)
}
}
impl fmt::Debug for Dir {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Dir").field("path", &self.path).finish()
}
}

View File

@@ -10,7 +10,7 @@ use crate::os::hermit::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, Raw
use crate::path::{Path, PathBuf};
use crate::sync::Arc;
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::helpers::run_path_with_cstr;
use crate::sys::time::SystemTime;
use crate::sys::{AsInner, AsInnerMut, FromInner, IntoInner, cvt, unsupported, unsupported_err};
@@ -18,6 +18,7 @@ use crate::{fmt, mem};
#[derive(Debug)]
pub struct File(FileDesc);
#[derive(Clone)]
pub struct FileAttr {
stat_val: stat_struct,

View File

@@ -59,7 +59,7 @@ pub fn with_native_path<T>(path: &Path, f: &dyn Fn(&Path) -> io::Result<T>) -> i
}
pub use imp::{
DirBuilder, DirEntry, File, FileAttr, FilePermissions, FileTimes, FileType, OpenOptions,
Dir, DirBuilder, DirEntry, File, FileAttr, FilePermissions, FileTimes, FileType, OpenOptions,
ReadDir,
};

View File

@@ -9,7 +9,7 @@ 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::helpers::ignore_notfound;
use crate::sys::pal::{abi, error};
use crate::sys::time::SystemTime;

View File

@@ -6,6 +6,7 @@ use crate::fs::TryLockError;
use crate::hash::Hash;
use crate::io::{self, BorrowedCursor, IoSlice, IoSliceMut, SeekFrom};
use crate::path::{Path, PathBuf};
pub use crate::sys::fs::common::Dir;
use crate::sys::pal::{helpers, unsupported};
use crate::sys::time::SystemTime;

View File

@@ -257,7 +257,7 @@ cfg_has_statx! {{
// all DirEntry's will have a reference to this struct
struct InnerReadDir {
dirp: Dir,
dirp: DirStream,
root: PathBuf,
}
@@ -272,10 +272,134 @@ impl ReadDir {
}
}
struct Dir(*mut libc::DIR);
struct DirStream(*mut libc::DIR);
unsafe impl Send for Dir {}
unsafe impl Sync for Dir {}
// dir::Dir requires openat support
cfg_select! {
any(
target_os = "redox",
target_os = "espidf",
target_os = "horizon",
target_os = "vita",
target_os = "nto",
target_os = "vxworks",
) => {
pub use crate::sys::fs::common::Dir;
}
_ => {
mod dir;
pub use dir::Dir;
}
}
fn debug_path_fd<'a, 'b>(
fd: c_int,
f: &'a mut fmt::Formatter<'b>,
name: &str,
) -> fmt::DebugStruct<'a, 'b> {
let mut b = f.debug_struct(name);
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,
}
}
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
}
fn get_path_from_fd(fd: c_int) -> Option<PathBuf> {
#[cfg(any(target_os = "linux", target_os = "illumos", target_os = "solaris"))]
fn get_path(fd: c_int) -> Option<PathBuf> {
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<PathBuf> {
// 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_select! {
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()
}
_ => {
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<PathBuf> {
let info = Box::<libc::kinfo_file>::new_zeroed();
let mut info = unsafe { info.assume_init() };
info.kf_structsize = size_of::<libc::kinfo_file>() 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<PathBuf> {
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<PathBuf> {
// FIXME(#24570): implement this for other Unix platforms
None
}
get_path(fd)
}
#[cfg(any(
target_os = "aix",
@@ -874,7 +998,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(
@@ -902,6 +1026,11 @@ impl Drop 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 {}
impl DirEntry {
pub fn path(&self) -> PathBuf {
self.dir.root.join(self.file_name_os_str())
@@ -1860,102 +1989,8 @@ 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<PathBuf> {
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<PathBuf> {
// 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_select! {
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()
}
_ => {
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<PathBuf> {
let info = Box::<libc::kinfo_file>::new_zeroed();
let mut info = unsafe { info.assume_init() };
info.kf_structsize = size_of::<libc::kinfo_file>() 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<PathBuf> {
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<PathBuf> {
// 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 {
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.as_raw_fd();
let mut b = f.debug_struct("File");
b.field("fd", &fd);
if let Some(path) = get_path(fd) {
b.field("path", &path);
}
if let Some((read, write)) = get_mode(fd) {
b.field("read", &read).field("write", &write);
}
let mut b = debug_path_fd(fd, f, "File");
b.finish()
}
}
@@ -2033,7 +2068,7 @@ pub fn readdir(path: &Path) -> io::Result<ReadDir> {
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))
}
}
@@ -2493,7 +2528,8 @@ mod remove_dir_impl {
use libc::{fdopendir, openat64 as openat, unlinkat};
use super::{
AsRawFd, Dir, DirEntry, FromRawFd, InnerReadDir, IntoRawFd, OwnedFd, RawFd, ReadDir, lstat,
AsRawFd, DirEntry, DirStream, FromRawFd, InnerReadDir, IntoRawFd, OwnedFd, RawFd, ReadDir,
lstat,
};
use crate::ffi::CStr;
use crate::io;
@@ -2517,7 +2553,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

View File

@@ -0,0 +1,114 @@
use libc::c_int;
cfg_select! {
not(
any(
all(target_os = "linux", not(target_env = "musl")),
target_os = "l4re",
target_os = "android",
target_os = "hurd",
)
) => {
use libc::{open as open64, openat as openat64};
}
_ => {
use libc::{open64, openat64};
}
}
use crate::ffi::CStr;
use crate::os::fd::{AsFd, BorrowedFd, IntoRawFd, OwnedFd, RawFd};
#[cfg(target_family = "unix")]
use crate::os::unix::io::{AsRawFd, FromRawFd};
#[cfg(target_os = "wasi")]
use crate::os::wasi::io::{AsRawFd, FromRawFd};
use crate::path::Path;
use crate::sys::fd::FileDesc;
use crate::sys::fs::OpenOptions;
use crate::sys::fs::unix::{File, debug_path_fd};
use crate::sys::helpers::run_path_with_cstr;
use crate::sys::{AsInner, FromInner, IntoInner, cvt_r};
use crate::{fmt, fs, io};
pub struct Dir(OwnedFd);
impl Dir {
pub fn open(path: &Path, opts: &OpenOptions) -> io::Result<Self> {
run_path_with_cstr(path, &|path| Self::open_with_c(path, opts))
}
pub fn open_file(&self, path: &Path, opts: &OpenOptions) -> io::Result<File> {
run_path_with_cstr(path.as_ref(), &|path| self.open_file_c(path, &opts))
}
pub fn open_with_c(path: &CStr, opts: &OpenOptions) -> io::Result<Self> {
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) }))
}
fn open_file_c(&self, path: &CStr, opts: &OpenOptions) -> io::Result<File> {
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) }))
}
}
impl fmt::Debug for Dir {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let fd = self.0.as_raw_fd();
let mut b = debug_path_fd(fd, f, "Dir");
b.finish()
}
}
#[unstable(feature = "dirfd", issue = "120426")]
impl AsRawFd for fs::Dir {
fn as_raw_fd(&self) -> RawFd {
self.as_inner().0.as_raw_fd()
}
}
#[unstable(feature = "dirfd", issue = "120426")]
impl IntoRawFd for fs::Dir {
fn into_raw_fd(self) -> RawFd {
self.into_inner().0.into_raw_fd()
}
}
#[unstable(feature = "dirfd", issue = "120426")]
impl FromRawFd for fs::Dir {
unsafe fn from_raw_fd(fd: RawFd) -> Self {
Self::from_inner(Dir(unsafe { FromRawFd::from_raw_fd(fd) }))
}
}
#[unstable(feature = "dirfd", issue = "120426")]
impl AsFd for fs::Dir {
fn as_fd(&self) -> BorrowedFd<'_> {
self.as_inner().0.as_fd()
}
}
#[unstable(feature = "dirfd", issue = "120426")]
impl From<fs::Dir> for OwnedFd {
fn from(value: fs::Dir) -> Self {
value.into_inner().0
}
}
#[unstable(feature = "dirfd", issue = "120426")]
impl From<OwnedFd> for fs::Dir {
fn from(value: OwnedFd) -> Self {
Self::from_inner(Dir(value))
}
}

View File

@@ -4,6 +4,7 @@ use crate::fs::TryLockError;
use crate::hash::{Hash, Hasher};
use crate::io::{self, BorrowedCursor, IoSlice, IoSliceMut, SeekFrom};
use crate::path::{Path, PathBuf};
pub use crate::sys::fs::common::Dir;
use crate::sys::time::SystemTime;
use crate::sys::unsupported;

View File

@@ -18,6 +18,8 @@ use crate::sys::time::SystemTime;
use crate::sys::{Align8, AsInner, FromInner, IntoInner, c, cvt};
use crate::{fmt, ptr, slice};
mod dir;
pub use dir::Dir;
mod remove_dir_all;
use remove_dir_all::remove_dir_all_iterative;
@@ -274,7 +276,7 @@ impl OpenOptions {
}
}
fn get_creation_mode(&self) -> io::Result<u32> {
fn get_cmode_disposition(&self) -> io::Result<(u32, u32)> {
match (self.write, self.append) {
(true, false) => {}
(false, false) => {
@@ -296,16 +298,24 @@ impl OpenOptions {
}
Ok(match (self.create, self.truncate, self.create_new) {
(false, false, false) => c::OPEN_EXISTING,
(true, false, false) => c::OPEN_ALWAYS,
(false, true, false) => c::TRUNCATE_EXISTING,
(false, false, false) => (c::OPEN_EXISTING, c::FILE_OPEN),
(true, false, false) => (c::OPEN_ALWAYS, c::FILE_OPEN_IF),
(false, true, false) => (c::TRUNCATE_EXISTING, c::FILE_OVERWRITE),
// `CREATE_ALWAYS` has weird semantics so we emulate it using
// `OPEN_ALWAYS` and a manual truncation step. See #115745.
(true, true, false) => c::OPEN_ALWAYS,
(_, _, true) => c::CREATE_NEW,
(true, true, false) => (c::OPEN_ALWAYS, c::FILE_OVERWRITE_IF),
(_, _, true) => (c::CREATE_NEW, c::FILE_CREATE),
})
}
fn get_creation_mode(&self) -> io::Result<u32> {
self.get_cmode_disposition().map(|(mode, _)| mode)
}
fn get_disposition(&self) -> io::Result<u32> {
self.get_cmode_disposition().map(|(_, mode)| mode)
}
fn get_flags_and_attributes(&self) -> u32 {
self.custom_flags
| self.attributes
@@ -1019,14 +1029,23 @@ impl FromRawHandle for File {
}
}
fn debug_path_handle<'a, 'b>(
handle: BorrowedHandle<'a>,
f: &'a mut fmt::Formatter<'b>,
name: &str,
) -> fmt::DebugStruct<'a, 'b> {
// FIXME(#24570): add more info here (e.g., mode)
let mut b = f.debug_struct(name);
b.field("handle", &handle.as_raw_handle());
if let Ok(path) = get_path(handle) {
b.field("path", &path);
}
b
}
impl fmt::Debug for File {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// 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) {
b.field("path", &path);
}
let mut b = debug_path_handle(self.handle.as_handle(), f, "File");
b.finish()
}
}
@@ -1292,8 +1311,8 @@ pub fn rename(old: &WCStr, new: &WCStr) -> io::Result<()> {
Layout::from_size_align(struct_size as usize, align_of::<c::FILE_RENAME_INFO>())
.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::<c::FILE_RENAME_INFO>();
if file_rename_info.is_null() {
@@ -1530,10 +1549,10 @@ pub fn set_times_nofollow(p: &WCStr, times: FileTimes) -> io::Result<()> {
file.set_times(times)
}
fn get_path(f: &File) -> io::Result<PathBuf> {
fn get_path(f: impl AsRawHandle) -> io::Result<PathBuf> {
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)),
)
@@ -1546,7 +1565,7 @@ pub fn canonicalize(p: &WCStr) -> io::Result<PathBuf> {
// 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<u64> {

View File

@@ -0,0 +1,153 @@
use crate::os::windows::io::{
AsHandle, AsRawHandle, BorrowedHandle, FromRawHandle, HandleOrInvalid, IntoRawHandle,
OwnedHandle, RawHandle,
};
use crate::path::Path;
use crate::sys::api::{UnicodeStrRef, WinError};
use crate::sys::fs::windows::debug_path_handle;
use crate::sys::fs::{File, OpenOptions};
use crate::sys::handle::Handle;
use crate::sys::path::{WCStr, with_native_path};
use crate::sys::{AsInner, FromInner, IntoInner, IoResult, c, to_u16s};
use crate::{fmt, fs, io, ptr};
pub struct Dir {
handle: Handle,
}
/// A wrapper around a raw NtCreateFile call.
///
/// This isn't completely safe because `OBJECT_ATTRIBUTES` contains raw pointers.
unsafe fn nt_create_file(
opts: &OpenOptions,
object_attributes: &c::OBJECT_ATTRIBUTES,
create_options: c::NTCREATEFILE_CREATE_OPTIONS,
) -> io::Result<Handle> {
let mut handle = ptr::null_mut();
let mut io_status = c::IO_STATUS_BLOCK::PENDING;
// SYNCHRONIZE is included in FILE_GENERIC_READ, but not GENERIC_READ, so we add it manually
let access = opts.get_access_mode()? | c::SYNCHRONIZE;
// one of FILE_SYNCHRONOUS_IO_{,NON}ALERT is required for later operations to succeed.
let options = create_options | c::FILE_SYNCHRONOUS_IO_NONALERT;
let status = unsafe {
c::NtCreateFile(
&mut handle,
access,
object_attributes,
&mut io_status,
ptr::null(),
c::FILE_ATTRIBUTE_NORMAL,
opts.share_mode,
opts.get_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 {
Err(WinError::new(unsafe { c::RtlNtStatusToDosError(status) })).io_result()
}
}
impl Dir {
pub fn open(path: &Path, opts: &OpenOptions) -> io::Result<Self> {
with_native_path(path, &|path| Self::open_with_native(path, opts))
}
pub fn open_file(&self, path: &Path, opts: &OpenOptions) -> io::Result<File> {
// NtCreateFile will fail if given an absolute path and a non-null RootDirectory
if path.is_absolute() {
return File::open(path, opts);
}
let path = to_u16s(path)?;
let path = &path[..path.len() - 1]; // trim 0 byte
self.open_file_native(&path, opts).map(|handle| File { handle })
}
fn open_with_native(path: &WCStr, opts: &OpenOptions) -> io::Result<Self> {
let creation = opts.get_creation_mode()?;
let sa = c::SECURITY_ATTRIBUTES {
nLength: size_of::<c::SECURITY_ATTRIBUTES>() as u32,
lpSecurityDescriptor: ptr::null_mut(),
bInheritHandle: opts.inherit_handle as c::BOOL,
};
let handle = unsafe {
c::CreateFileW(
path.as_ptr(),
opts.get_access_mode()?,
opts.share_mode,
&raw const sa,
creation,
// FILE_FLAG_BACKUP_SEMANTICS is required to open a directory
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(io::Error::last_os_error()),
}
}
fn open_file_native(&self, path: &[u16], opts: &OpenOptions) -> io::Result<Handle> {
let name = UnicodeStrRef::from_slice(path);
let object_attributes = c::OBJECT_ATTRIBUTES {
RootDirectory: self.handle.as_raw_handle(),
ObjectName: name.as_ptr(),
..c::OBJECT_ATTRIBUTES::with_length()
};
unsafe { nt_create_file(opts, &object_attributes, c::FILE_NON_DIRECTORY_FILE) }
}
}
impl fmt::Debug for Dir {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut b = debug_path_handle(self.handle.as_handle(), f, "Dir");
b.finish()
}
}
#[unstable(feature = "dirfd", issue = "120426")]
impl AsRawHandle for fs::Dir {
fn as_raw_handle(&self) -> RawHandle {
self.as_inner().handle.as_raw_handle()
}
}
#[unstable(feature = "dirhandle", issue = "120426")]
impl IntoRawHandle for fs::Dir {
fn into_raw_handle(self) -> RawHandle {
self.into_inner().handle.into_raw_handle()
}
}
#[unstable(feature = "dirhandle", issue = "120426")]
impl FromRawHandle for fs::Dir {
unsafe fn from_raw_handle(handle: RawHandle) -> Self {
Self::from_inner(Dir { handle: unsafe { FromRawHandle::from_raw_handle(handle) } })
}
}
#[unstable(feature = "dirhandle", issue = "120426")]
impl AsHandle for fs::Dir {
fn as_handle(&self) -> BorrowedHandle<'_> {
self.as_inner().handle.as_handle()
}
}
#[unstable(feature = "dirhandle", issue = "120426")]
impl From<fs::Dir> for OwnedHandle {
fn from(value: fs::Dir) -> Self {
value.into_inner().handle.into_inner()
}
}
#[unstable(feature = "dirhandle", issue = "120426")]
impl From<OwnedHandle> for fs::Dir {
fn from(value: OwnedHandle) -> Self {
Self::from_inner(Dir { handle: Handle::from_inner(value) })
}
}