From 003285e9b2ebd0557ad7b5e6a77cd1f35e4507b3 Mon Sep 17 00:00:00 2001 From: Jeremy Smart Date: Sun, 4 Jan 2026 13:10:24 -0500 Subject: [PATCH] add file operations to Dir --- library/std/src/fs.rs | 83 ++++++++++++ library/std/src/fs/tests.rs | 49 ++++++++ library/std/src/sys/fs/common.rs | 9 ++ library/std/src/sys/fs/unix/dir.rs | 32 ++++- library/std/src/sys/fs/windows/dir.rs | 118 ++++++++++++++++-- .../std/src/sys/pal/windows/c/bindings.txt | 4 + .../std/src/sys/pal/windows/c/windows_sys.rs | 28 +++++ 7 files changed, 310 insertions(+), 13 deletions(-) diff --git a/library/std/src/fs.rs b/library/std/src/fs.rs index 4edd35b310bd0..5705e7b2dfb72 100644 --- a/library/std/src/fs.rs +++ b/library/std/src/fs.rs @@ -1646,6 +1646,89 @@ impl Dir { .open_file(path.as_ref(), &OpenOptions::new().read(true).0) .map(|f| File { inner: f }) } + + /// Attempts to open a file according to `opts` 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, OpenOptions}, io::{self, Write}}; + /// + /// fn main() -> io::Result<()> { + /// let dir = Dir::open("foo")?; + /// let mut opts = OpenOptions::new(); + /// opts.read(true).write(true); + /// let mut f = dir.open_file_with("bar.txt", &opts)?; + /// f.write(b"Hello, world!")?; + /// let contents = io::read_to_string(f)?; + /// assert_eq!(contents, "Hello, world!"); + /// Ok(()) + /// } + /// ``` + #[unstable(feature = "dirfd", issue = "120426")] + pub fn open_file_with>(&self, path: P, opts: &OpenOptions) -> io::Result { + self.inner.open_file(path.as_ref(), &opts.0).map(|f| File { inner: f }) + } + + /// 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::open("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.as_ref()) + } + + /// 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::open("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.as_ref(), &to_dir.inner, to.as_ref()) + } } impl AsInner for Dir { diff --git a/library/std/src/fs/tests.rs b/library/std/src/fs/tests.rs index 6a86705f2fadb..c8df25986e471 100644 --- a/library/std/src/fs/tests.rs +++ b/library/std/src/fs/tests.rs @@ -3,6 +3,8 @@ use rand::RngCore; #[cfg(not(miri))] use super::Dir; use crate::assert_matches::assert_matches; +#[cfg(not(miri))] +use crate::fs::exists; use crate::fs::{self, File, FileTimes, OpenOptions, TryLockError}; #[cfg(not(miri))] use crate::io; @@ -2496,3 +2498,50 @@ fn test_dir_read_file() { let buf = check!(io::read_to_string(f)); assert_eq!("bar", &buf); } + +#[test] +// FIXME: libc calls fail on miri +#[cfg(not(miri))] +fn test_dir_write_file() { + let tmpdir = tmpdir(); + let dir = check!(Dir::open(tmpdir.path())); + let mut f = check!(dir.open_file_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] +// FIXME: libc calls fail on miri +#[cfg(not(miri))] +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::open(tmpdir.path())); + check!(dir.remove_file("foo.txt")); + assert!(!matches!(exists(tmpdir.join("foo.txt")), Ok(true))); +} + +#[test] +// FIXME: libc calls fail on miri +#[cfg(not(miri))] +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::open(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); +} diff --git a/library/std/src/sys/fs/common.rs b/library/std/src/sys/fs/common.rs index 4d47d392a8268..34a4fd225e99e 100644 --- a/library/std/src/sys/fs/common.rs +++ b/library/std/src/sys/fs/common.rs @@ -1,5 +1,6 @@ #![allow(dead_code)] // not used on all platforms +use crate::fs::{remove_file, rename}; use crate::io::{self, Error, ErrorKind}; use crate::path::{Path, PathBuf}; use crate::sys::fs::{File, OpenOptions}; @@ -72,6 +73,14 @@ impl Dir { pub fn open_file(&self, path: &Path, opts: &OpenOptions) -> io::Result { File::open(&self.path.join(path), &opts) } + + pub fn remove_file(&self, path: &Path) -> io::Result<()> { + remove_file(self.path.join(path)) + } + + pub fn rename(&self, from: &Path, to_dir: &Self, to: &Path) -> io::Result<()> { + rename(self.path.join(from), to_dir.path.join(to)) + } } impl fmt::Debug for Dir { diff --git a/library/std/src/sys/fs/unix/dir.rs b/library/std/src/sys/fs/unix/dir.rs index 094d1bfd31685..6437de4b5f152 100644 --- a/library/std/src/sys/fs/unix/dir.rs +++ b/library/std/src/sys/fs/unix/dir.rs @@ -1,4 +1,4 @@ -use libc::c_int; +use libc::{c_int, renameat, unlinkat}; cfg_select! { not( @@ -27,7 +27,7 @@ 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::sys::{AsInner, FromInner, IntoInner, cvt, cvt_r}; use crate::{fmt, fs, io}; pub struct Dir(OwnedFd); @@ -41,6 +41,16 @@ impl Dir { run_path_with_cstr(path.as_ref(), &|path| self.open_file_c(path, &opts)) } + pub fn remove_file(&self, path: &Path) -> io::Result<()> { + run_path_with_cstr(path, &|path| self.remove_c(path, false)) + } + + pub fn rename(&self, from: &Path, to_dir: &Self, to: &Path) -> io::Result<()> { + run_path_with_cstr(from, &|from| { + run_path_with_cstr(to, &|to| self.rename_c(from, to_dir, to)) + }) + } + pub fn open_with_c(path: &CStr, opts: &OpenOptions) -> io::Result { let flags = libc::O_CLOEXEC | libc::O_DIRECTORY @@ -61,6 +71,24 @@ impl Dir { })?; Ok(File(unsafe { FileDesc::from_raw_fd(fd) })) } + + 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(|_| ()) + } + + fn rename_c(&self, from: &CStr, to_dir: &Self, to: &CStr) -> io::Result<()> { + cvt(unsafe { + renameat(self.0.as_raw_fd(), from.as_ptr(), to_dir.0.as_raw_fd(), to.as_ptr()) + }) + .map(|_| ()) + } } impl fmt::Debug for Dir { diff --git a/library/std/src/sys/fs/windows/dir.rs b/library/std/src/sys/fs/windows/dir.rs index 3f617806c6c36..6cba24603fda4 100644 --- a/library/std/src/sys/fs/windows/dir.rs +++ b/library/std/src/sys/fs/windows/dir.rs @@ -1,9 +1,12 @@ +use crate::alloc::{Layout, alloc, dealloc}; +use crate::ffi::c_void; +use crate::mem::offset_of; 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::api::{self, SetFileInformation, UnicodeStrRef, WinError}; use crate::sys::fs::windows::debug_path_handle; use crate::sys::fs::{File, OpenOptions}; use crate::sys::handle::Handle; @@ -15,6 +18,11 @@ pub struct Dir { handle: Handle, } +fn run_path_with_u16s(path: &Path, f: &dyn Fn(&[u16]) -> io::Result) -> io::Result { + let path = to_u16s(path)?; + f(&path[..path.len() - 1]) +} + /// A wrapper around a raw NtCreateFile call. /// /// This isn't completely safe because `OBJECT_ATTRIBUTES` contains raw pointers. @@ -62,9 +70,20 @@ impl Dir { 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 }) + run_path_with_u16s(path, &|path| { + self.open_file_native(&path, opts, false).map(|handle| File { handle }) + }) + } + + pub fn remove_file(&self, path: &Path) -> io::Result<()> { + run_path_with_u16s(path, &|path| self.remove_native(path, false)) + } + + pub fn rename(&self, from: &Path, to_dir: &Self, to: &Path) -> io::Result<()> { + let is_dir = from.is_dir(); + run_path_with_u16s(from, &|from| { + run_path_with_u16s(to, &|to| self.rename_native(from, to_dir, to, is_dir)) + }) } fn open_with_native(path: &WCStr, opts: &OpenOptions) -> io::Result { @@ -92,14 +111,91 @@ impl Dir { } } - fn open_file_native(&self, path: &[u16], opts: &OpenOptions) -> io::Result { + fn open_file_native(&self, path: &[u16], opts: &OpenOptions, dir: bool) -> io::Result { 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) } + let create_opt = if dir { c::FILE_DIRECTORY_FILE } else { c::FILE_NON_DIRECTORY_FILE }; + unsafe { nt_create_file(opts, &object_attributes, create_opt) } + } + + fn remove_native(&self, path: &[u16], dir: bool) -> io::Result<()> { + let mut opts = OpenOptions::new(); + opts.access_mode(c::DELETE); + let handle = self.open_file_native(path, &opts, dir)?; + 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: &[u16], to_dir: &Self, to: &[u16], dir: bool) -> io::Result<()> { + let mut opts = OpenOptions::new(); + opts.access_mode(c::DELETE); + opts.custom_flags(c::FILE_FLAG_OPEN_REPARSE_POINT | c::FILE_FLAG_BACKUP_SEMANTICS); + let handle = self.open_file_native(from, &opts, dir)?; + // Calculate the layout of the `FILE_RENAME_INFORMATION` we pass to `NtSetInformationFile` + // 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 + .len() + .checked_mul(2) + .and_then(|x| x.checked_add(offset_of!(c::FILE_RENAME_INFORMATION, FileName))) + .ok_or(too_long_err)?; + let layout = Layout::from_size_align(struct_size, align_of::()) + .map_err(|_| too_long_err)?; + let struct_size = u32::try_from(struct_size).map_err(|_| too_long_err)?; + let to_byte_len = u32::try_from(to.len() * 2).map_err(|_| too_long_err)?; + + let file_rename_info; + // SAFETY: We allocate enough memory for a full FILE_RENAME_INFORMATION struct and the 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_INFORMATION_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); + + to.as_ptr().copy_to_nonoverlapping( + (&raw mut (*file_rename_info).FileName).cast::(), + to.len(), + ); + } + + let status = unsafe { + c::NtSetInformationFile( + handle.as_raw_handle(), + &mut c::IO_STATUS_BLOCK::default(), + file_rename_info.cast::(), + struct_size, + c::FileRenameInformation, + ) + }; + unsafe { dealloc(file_rename_info.cast::(), layout) }; + if c::nt_success(status) { + // SAFETY: nt_success guarantees that handle is no longer null + Ok(()) + } else { + Err(WinError::new(unsafe { c::RtlNtStatusToDosError(status) })) + } + .io_result() } } @@ -117,35 +213,35 @@ impl AsRawHandle for fs::Dir { } } -#[unstable(feature = "dirhandle", issue = "120426")] +#[unstable(feature = "dirfd", 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")] +#[unstable(feature = "dirfd", 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")] +#[unstable(feature = "dirfd", issue = "120426")] impl AsHandle for fs::Dir { fn as_handle(&self) -> BorrowedHandle<'_> { self.as_inner().handle.as_handle() } } -#[unstable(feature = "dirhandle", issue = "120426")] +#[unstable(feature = "dirfd", issue = "120426")] impl From for OwnedHandle { fn from(value: fs::Dir) -> Self { value.into_inner().handle.into_inner() } } -#[unstable(feature = "dirhandle", issue = "120426")] +#[unstable(feature = "dirfd", issue = "120426")] impl From for fs::Dir { fn from(value: OwnedHandle) -> Self { Self::from_inner(Dir { handle: Handle::from_inner(value) }) diff --git a/library/std/src/sys/pal/windows/c/bindings.txt b/library/std/src/sys/pal/windows/c/bindings.txt index 9009aa09f48ed..f0226944eb0e4 100644 --- a/library/std/src/sys/pal/windows/c/bindings.txt +++ b/library/std/src/sys/pal/windows/c/bindings.txt @@ -2075,6 +2075,7 @@ FILE_READ_EA FILE_RENAME_FLAG_POSIX_SEMANTICS FILE_RENAME_FLAG_REPLACE_IF_EXISTS FILE_RENAME_INFO +FILE_RENAME_INFORMATION FILE_RESERVE_OPFILTER FILE_SEQUENTIAL_ONLY FILE_SESSION_AWARE @@ -2120,6 +2121,8 @@ FileNormalizedNameInfo FileRemoteProtocolInfo FileRenameInfo FileRenameInfoEx +FileRenameInformation +FileRenameInformationEx FileStandardInfo FileStorageInfo FileStreamInfo @@ -2303,6 +2306,7 @@ NTCREATEFILE_CREATE_DISPOSITION NTCREATEFILE_CREATE_OPTIONS NtOpenFile NtReadFile +NtSetInformationFile NTSTATUS NtWriteFile OBJ_CASE_INSENSITIVE diff --git a/library/std/src/sys/pal/windows/c/windows_sys.rs b/library/std/src/sys/pal/windows/c/windows_sys.rs index 98f277b33780c..73edfeda34fe9 100644 --- a/library/std/src/sys/pal/windows/c/windows_sys.rs +++ b/library/std/src/sys/pal/windows/c/windows_sys.rs @@ -75,6 +75,7 @@ windows_targets::link!("kernel32.dll" "system" fn MultiByteToWideChar(codepage : windows_targets::link!("ntdll.dll" "system" fn NtCreateFile(filehandle : *mut HANDLE, desiredaccess : FILE_ACCESS_RIGHTS, objectattributes : *const OBJECT_ATTRIBUTES, iostatusblock : *mut IO_STATUS_BLOCK, allocationsize : *const i64, fileattributes : FILE_FLAGS_AND_ATTRIBUTES, shareaccess : FILE_SHARE_MODE, createdisposition : NTCREATEFILE_CREATE_DISPOSITION, createoptions : NTCREATEFILE_CREATE_OPTIONS, eabuffer : *const core::ffi::c_void, ealength : u32) -> NTSTATUS); windows_targets::link!("ntdll.dll" "system" fn NtOpenFile(filehandle : *mut HANDLE, desiredaccess : u32, objectattributes : *const OBJECT_ATTRIBUTES, iostatusblock : *mut IO_STATUS_BLOCK, shareaccess : u32, openoptions : u32) -> NTSTATUS); windows_targets::link!("ntdll.dll" "system" fn NtReadFile(filehandle : HANDLE, event : HANDLE, apcroutine : PIO_APC_ROUTINE, apccontext : *const core::ffi::c_void, iostatusblock : *mut IO_STATUS_BLOCK, buffer : *mut core::ffi::c_void, length : u32, byteoffset : *const i64, key : *const u32) -> NTSTATUS); +windows_targets::link!("ntdll.dll" "system" fn NtSetInformationFile(filehandle : HANDLE, iostatusblock : *mut IO_STATUS_BLOCK, fileinformation : *const core::ffi::c_void, length : u32, fileinformationclass : FILE_INFORMATION_CLASS) -> NTSTATUS); windows_targets::link!("ntdll.dll" "system" fn NtWriteFile(filehandle : HANDLE, event : HANDLE, apcroutine : PIO_APC_ROUTINE, apccontext : *const core::ffi::c_void, iostatusblock : *mut IO_STATUS_BLOCK, buffer : *const core::ffi::c_void, length : u32, byteoffset : *const i64, key : *const u32) -> NTSTATUS); windows_targets::link!("advapi32.dll" "system" fn OpenProcessToken(processhandle : HANDLE, desiredaccess : TOKEN_ACCESS_MASK, tokenhandle : *mut HANDLE) -> BOOL); windows_targets::link!("kernel32.dll" "system" fn QueryPerformanceCounter(lpperformancecount : *mut i64) -> BOOL); @@ -2532,6 +2533,7 @@ impl Default for FILE_ID_BOTH_DIR_INFO { unsafe { core::mem::zeroed() } } } +pub type FILE_INFORMATION_CLASS = i32; pub type FILE_INFO_BY_HANDLE_CLASS = i32; #[repr(C)] #[derive(Clone, Copy, Default)] @@ -2593,6 +2595,30 @@ impl Default for FILE_RENAME_INFO_0 { unsafe { core::mem::zeroed() } } } +#[repr(C)] +#[derive(Clone, Copy)] +pub struct FILE_RENAME_INFORMATION { + pub Anonymous: FILE_RENAME_INFORMATION_0, + pub RootDirectory: HANDLE, + pub FileNameLength: u32, + pub FileName: [u16; 1], +} +impl Default for FILE_RENAME_INFORMATION { + fn default() -> Self { + unsafe { core::mem::zeroed() } + } +} +#[repr(C)] +#[derive(Clone, Copy)] +pub union FILE_RENAME_INFORMATION_0 { + pub ReplaceIfExists: bool, + pub Flags: u32, +} +impl Default for FILE_RENAME_INFORMATION_0 { + fn default() -> Self { + unsafe { core::mem::zeroed() } + } +} pub const FILE_RESERVE_OPFILTER: NTCREATEFILE_CREATE_OPTIONS = 1048576u32; pub const FILE_SEQUENTIAL_ONLY: NTCREATEFILE_CREATE_OPTIONS = 4u32; pub const FILE_SESSION_AWARE: NTCREATEFILE_CREATE_OPTIONS = 262144u32; @@ -2700,6 +2726,8 @@ pub const FileNormalizedNameInfo: FILE_INFO_BY_HANDLE_CLASS = 24i32; pub const FileRemoteProtocolInfo: FILE_INFO_BY_HANDLE_CLASS = 13i32; pub const FileRenameInfo: FILE_INFO_BY_HANDLE_CLASS = 3i32; pub const FileRenameInfoEx: FILE_INFO_BY_HANDLE_CLASS = 22i32; +pub const FileRenameInformation: FILE_INFORMATION_CLASS = 10i32; +pub const FileRenameInformationEx: FILE_INFORMATION_CLASS = 65i32; pub const FileStandardInfo: FILE_INFO_BY_HANDLE_CLASS = 1i32; pub const FileStorageInfo: FILE_INFO_BY_HANDLE_CLASS = 16i32; pub const FileStreamInfo: FILE_INFO_BY_HANDLE_CLASS = 7i32;