From 40ebc040af366e1e09b80458bc3cc46cf49f8b16 Mon Sep 17 00:00:00 2001 From: Jeremy Smart Date: Sun, 9 Aug 2026 22:55:19 -0400 Subject: [PATCH 1/2] add symlink --- library/std/src/fs.rs | 9 ++++ library/std/src/fs/tests.rs | 19 ++++++++ library/std/src/sys/fs/common.rs | 6 ++- library/std/src/sys/fs/unix/dir.rs | 14 +++++- library/std/src/sys/fs/windows/dir.rs | 69 ++++++++++++++++++++++++++- 5 files changed, 113 insertions(+), 4 deletions(-) diff --git a/library/std/src/fs.rs b/library/std/src/fs.rs index 6ad20b192fa5c..24ef23854dfbe 100644 --- a/library/std/src/fs.rs +++ b/library/std/src/fs.rs @@ -1869,6 +1869,15 @@ impl Dir { pub fn remove_dir>(&self, path: P) -> io::Result<()> { self.inner.remove_dir(path.as_ref()) } + + /// 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.as_ref(), link.as_ref()) + } } impl AsInner for Dir { diff --git a/library/std/src/fs/tests.rs b/library/std/src/fs/tests.rs index 075814b379027..520655aa108a0 100644 --- a/library/std/src/fs/tests.rs +++ b/library/std/src/fs/tests.rs @@ -2791,3 +2791,22 @@ fn test_dir_open_dir() { check!(f.read_exact(&mut buf)); assert_eq!(b"baz", &buf); } + +#[test] +fn test_dir_symlink() { + let tmpdir = tmpdir(); + if !got_symlink_permission(&tmpdir) { + return; + }; + + 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"quux")); + check!(f.flush()); + drop(f); + check!(dir.symlink("foo.txt", "bar.txt")); + let mut f = check!(dir.open_file("bar.txt")); + let mut buf = [0u8; 4]; + check!(f.read_exact(&mut buf)); + assert_eq!(b"quux", &buf); +} diff --git a/library/std/src/sys/fs/common.rs b/library/std/src/sys/fs/common.rs index 17b98a4506544..95704fd48b139 100644 --- a/library/std/src/sys/fs/common.rs +++ b/library/std/src/sys/fs/common.rs @@ -4,7 +4,7 @@ use crate::fs::{create_dir, remove_dir, remove_file, rename}; use crate::io::{self, Error, ErrorKind}; use crate::path::{Path, PathBuf}; use crate::sys::IntoInner; -use crate::sys::fs::{File, FileAttr, OpenOptions}; +use crate::sys::fs::{File, FileAttr, OpenOptions, symlink}; use crate::sys::helpers::ignore_notfound; use crate::{fmt, fs}; @@ -104,6 +104,10 @@ impl Dir { pub fn remove_dir(&self, path: &Path) -> io::Result<()> { remove_dir(path) } + + pub fn symlink(&self, original: &Path, link: &Path) -> io::Result<()> { + symlink(original, link) + } } 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 3fe952d942927..e0c088025ad10 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, mkdirat, renameat, unlinkat}; +use libc::{c_int, mkdirat, renameat, symlinkat, unlinkat}; cfg_select! { not(any( @@ -78,13 +78,19 @@ impl Dir { } pub fn create_dir(&self, path: &Path) -> io::Result<()> { - run_path_with_cstr(path.as_ref(), &|path| self.create_dir_c(path)) + run_path_with_cstr(path, &|path| self.create_dir_c(path)) } pub fn remove_dir(&self, path: &Path) -> io::Result<()> { run_path_with_cstr(path, &|path| self.remove_c(path, true)) } + pub fn symlink(&self, original: &Path, link: &Path) -> io::Result<()> { + run_path_with_cstr(original, &|original| { + run_path_with_cstr(link, &|link| self.symlink_c(original, link)) + }) + } + fn open_with_c(path: &CStr, opts: &OpenOptions) -> io::Result { let flags = libc::O_CLOEXEC | libc::O_DIRECTORY @@ -139,6 +145,10 @@ impl Dir { fn create_dir_c(&self, path: &CStr) -> io::Result<()> { cvt(unsafe { mkdirat(self.0.as_raw_fd(), path.as_ptr(), 0o777) }).map(|_| ()) } + + 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(|_| ()) + } } 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 4fe0062af821b..eaa0e046ab524 100644 --- a/library/std/src/sys/fs/windows/dir.rs +++ b/library/std/src/sys/fs/windows/dir.rs @@ -6,7 +6,7 @@ use crate::os::windows::io::{ OwnedHandle, RawHandle, }; use crate::path::Path; -use crate::sys::api::{UnicodeStrRef, WinError}; +use crate::sys::api::{UnicodeStrRef, WinError, get_last_error}; use crate::sys::fs::windows::debug_path_handle; use crate::sys::fs::{File, FileAttr, OpenOptions}; use crate::sys::handle::Handle; @@ -111,6 +111,11 @@ impl Dir { self.remove_native(&path, true) } + pub fn symlink(&self, original: &Path, link: &Path) -> io::Result<()> { + let orig = to_u16s_without_nul(original)?; + self.symlink_native(&orig, link, original.is_relative()) + } + fn open_with_native(path: &WCStr, opts: &OpenOptions) -> io::Result { let creation = opts.get_creation_mode()?; let sa = c::SECURITY_ATTRIBUTES { @@ -223,6 +228,68 @@ impl Dir { }); f.file_attr() } + + 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); + opts.create(true); + opts.custom_flags(c::FILE_FLAG_OPEN_REPARSE_POINT | c::FILE_FLAG_BACKUP_SEMANTICS); + opts.attributes(c::FILE_ATTRIBUTE_REPARSE_POINT); + let linkfile = self.open_file(link, &opts)?; + let original_name_byte_len = + u16::try_from(size_of::() * original.len()).or(Err(TOO_LONG_ERR))?; + let layout = Layout::from_size_align( + size_of::() + + size_of::() + + usize::from(original_name_byte_len), + align_of::() + .max(align_of::()) + .max(align_of::()), + ) + .or(Err(TOO_LONG_ERR))?; + let buffer = unsafe { alloc(layout) }.cast::(); + if buffer.is_null() { + return Err(io::ErrorKind::OutOfMemory.into()); + } + unsafe { + (&raw mut (*buffer).ReparseTag).write(c::IO_REPARSE_TAG_SYMLINK); + (&raw mut (*buffer).ReparseDataLength).write(original_name_byte_len); + (&raw mut (*buffer).Reserved).write(0); + let rest = (&raw mut (*buffer).rest).cast::(); + + (&raw mut (*rest).SubstituteNameOffset).write(0); + (&raw mut (*rest).SubstituteNameLength).write(original_name_byte_len); + (&raw mut (*rest).PrintNameOffset).write(0); + (&raw mut (*rest).PrintNameLength).write(original_name_byte_len); + (&raw mut (*rest).Flags).write(if relative { c::SYMLINK_FLAG_RELATIVE } else { 0 }); + + original.as_ptr().copy_to_nonoverlapping(&raw mut (*rest).PathBuffer, original.len()); + }; + let result = unsafe { + c::DeviceIoControl( + linkfile.handle.as_raw_handle(), + c::FSCTL_SET_REPARSE_POINT, + buffer as *mut c_void as *const c_void, + u32::try_from( + size_of::() + + size_of::() + + usize::from(original_name_byte_len), + ) + .or(Err(TOO_LONG_ERR))?, + ptr::null_mut(), + 0, + ptr::null_mut(), + ptr::null_mut(), + ) + }; + unsafe { + dealloc(buffer.cast(), layout); + } + + if result == 0 { Err(get_last_error()).io_result() } else { Ok(()) } + } } impl fmt::Debug for Dir { From 6181dcb31e0fdaa3ec4d5b420aee125183f200f6 Mon Sep 17 00:00:00 2001 From: Jeremy Smart Date: Tue, 11 Aug 2026 21:11:22 -0400 Subject: [PATCH 2/2] add direntry api --- library/std/src/fs.rs | 101 +++++++++++++++++++++++++++++ library/std/src/fs/tests.rs | 58 +++++++++++++++-- library/std/src/sys/fs/mod.rs | 15 +++++ library/std/src/sys/fs/unix.rs | 21 ++++++ library/std/src/sys/fs/unix/dir.rs | 6 +- 5 files changed, 194 insertions(+), 7 deletions(-) diff --git a/library/std/src/fs.rs b/library/std/src/fs.rs index 24ef23854dfbe..7c21dadd3b3fc 100644 --- a/library/std/src/fs.rs +++ b/library/std/src/fs.rs @@ -2872,6 +2872,107 @@ impl DirEntry { pub fn file_name(&self) -> OsString { self.0.file_name() } + + /// Opens the file represented by `self` in read-only mode. + /// + /// # Errors + /// + /// This function will return an error if `self` does not represent a regular file. + /// Other errors may also be returned according to [`OpenOptions::open`]. + /// + /// # Examples + /// + /// ``` + /// use std::fs; + /// + /// if let Ok(entries) = fs::read_dir(".") { + /// for entry in entries { + /// if let Ok(entry) = entry && entry.path().is_file() { + /// println!("{}", fs::read_to_string(entry.open())); + /// } + /// } + /// } + /// ``` + #[unstable(feature = "dirfd", issue = "120426")] + pub fn open(&self) -> io::Result { + self.0.open_with(&OpenOptions::new().read(true).0).map(|inner| File { inner }) + } + + /// Opens the file represented by `self` according to `options`. + /// + /// # Errors + /// + /// Errors may be returned according to [`OpenOptions::open`]. + /// + /// # Examples + /// + /// ``` + /// use std::fs; + /// use std::io::Write; + /// + /// if let Ok(entries) = fs::read_dir(".") { + /// for entry in entries { + /// if let Ok(entry) = entry && entry.path().is_file() { + /// let file = entry.open_with(&OpenOptions::new().read(true).write(true)); + /// let _ = file.write_all(b"foo"); + /// } + /// } + /// } + /// ``` + #[unstable(feature = "dirfd", issue = "120426")] + pub fn open_with(&self, options: &OpenOptions) -> io::Result { + self.0.open_with(&options.0).map(|inner| File { inner }) + } + + /// Removes the file represented by `self`. + /// + /// # Errors + /// + /// This function returns an error if `self` isn't a file. Errors may also be returned for other + /// reasons such as incorrect permissions. + /// + /// # Examples + /// + /// ``` + /// use std::fs; + /// + /// if let Ok(entries) = fs::read_dir(".") { + /// for entry in entries { + /// if let Ok(entry) = entry && entry.path().is_file() { + /// let _ = entry.remove_file(); + /// } + /// } + /// } + /// ``` + #[unstable(feature = "dirfd", issue = "120426")] + pub fn remove_file(&self) -> io::Result<()> { + self.0.remove_file() + } + + /// Removes the directory represented by `self`. + /// + /// # Errors + /// + /// This function returns an error if `self` isn't a directory. Errors may also be returned for other + /// reasons such as incorrect permissions or a non-empty directory. + /// + /// # Examples + /// + /// ``` + /// use std::fs; + /// + /// if let Ok(entries) = fs::read_dir(".") { + /// for entry in entries { + /// if let Ok(entry) = entry && entry.path().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 520655aa108a0..78abcd5d31834 100644 --- a/library/std/src/fs/tests.rs +++ b/library/std/src/fs/tests.rs @@ -1,7 +1,7 @@ use rand::RngCore; use super::Dir; -use crate::fs::{self, File, FileTimes, OpenOptions, TryLockError, exists}; +use crate::fs::{self, File, FileTimes, OpenOptions, TryLockError, exists, read_dir}; use crate::io::prelude::*; use crate::io::{BorrowedBuf, ErrorKind, SeekFrom}; use crate::mem::MaybeUninit; @@ -2722,7 +2722,7 @@ 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.write_all(b"bar")); check!(f.flush()); drop(f); let mut f = check!(File::open(tmpdir.join("foo.txt"))); @@ -2735,7 +2735,7 @@ fn test_dir_write_file() { 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.write_all(b"bar")); check!(f.flush()); drop(f); let dir = check!(Dir::open(tmpdir.path())); @@ -2782,7 +2782,7 @@ fn test_dir_open_dir() { let dir2 = check!(Dir::open(tmpdir.path().join("foo"))); let mut f = check!(dir2.open_file_with("bar.txt", &OpenOptions::new().create(true).write(true))); - check!(f.write(b"baz")); + check!(f.write_all(b"baz")); check!(f.flush()); drop(f); let dir3 = check!(dir1.open_dir("foo")); @@ -2810,3 +2810,53 @@ fn test_dir_symlink() { check!(f.read_exact(&mut buf)); assert_eq!(b"quux", &buf); } + +#[test] +fn test_dir_direntry_open() { + let tmpdir = tmpdir(); + let mut file1 = check!(File::create(tmpdir.path().join("foo.txt"))); + let mut file2 = check!(File::create(tmpdir.path().join("bar.txt"))); + check!(file1.write_all(b"baz")); + check!(file2.write_all(b"baz")); + + for dirent in check!(read_dir(tmpdir.path())) { + let mut file = check!(check!(dirent).open()); + let mut buf = [0u8; 3]; + check!(file.read_exact(&mut buf)); + assert_eq!(b"baz", &buf); + } +} + +#[test] +fn test_dir_direntry_open_with() { + let tmpdir = tmpdir(); + check!(File::create(tmpdir.path().join("foo.txt"))); + + for dirent in check!(read_dir(tmpdir.path())) { + let dirent = check!(dirent); + let mut file = check!(dirent.open_with(&OpenOptions::new().read(true).write(true))); + check!(file.write_all(b"baz")); + let contents = check!(fs::read_to_string(dirent.path())); + assert_eq!("baz", contents); + } +} + +#[test] +fn test_dir_direntry_remove() { + let tmpdir = tmpdir(); + check!(File::create(tmpdir.path().join("foo.txt"))); + check!(File::create(tmpdir.path().join("bar.txt"))); + check!(fs::create_dir(tmpdir.path().join("baz"))); + + for dirent in check!(read_dir(tmpdir.path())) { + let dirent = check!(dirent); + if dirent.path().is_file() { + check!(dirent.remove_file()); + } + if dirent.path().is_dir() { + check!(dirent.remove_dir()); + } + } + + assert!(fs::read_dir(tmpdir.path()).is_ok_and(|i| i.count() == 0)) +} diff --git a/library/std/src/sys/fs/mod.rs b/library/std/src/sys/fs/mod.rs index b2666eb2a3da9..eb90ad354867c 100644 --- a/library/std/src/sys/fs/mod.rs +++ b/library/std/src/sys/fs/mod.rs @@ -153,3 +153,18 @@ pub fn set_times(path: &Path, times: FileTimes) -> io::Result<()> { pub fn set_times_nofollow(path: &Path, times: FileTimes) -> io::Result<()> { with_native_path(path, &|path| imp::set_times_nofollow(path, times.clone())) } + +#[cfg(not(any(target_family = "unix", target_os = "wasi")))] +impl DirEntry { + pub fn open_with(&self, opts: &OpenOptions) -> io::Result { + File::open(&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()) + } +} diff --git a/library/std/src/sys/fs/unix.rs b/library/std/src/sys/fs/unix.rs index b33ebadebe4ad..0626c28f8ae28 100644 --- a/library/std/src/sys/fs/unix.rs +++ b/library/std/src/sys/fs/unix.rs @@ -1108,6 +1108,27 @@ impl DirEntry { pub fn file_name_os_str(&self) -> &OsStr { OsStr::from_bytes(self.name.as_bytes()) } + + pub fn open_with(&self, opts: &OpenOptions) -> io::Result { + let dir = unsafe { + mem::ManuallyDrop::new(Dir(OwnedFd::from_raw_fd(cvt(dirfd(self.dir.dirp.0))?))) + }; + dir.open_file_c(&self.name, opts, 0).map(FileDesc::from_inner).map(File) + } + + pub fn remove_file(&self) -> io::Result<()> { + let dir = unsafe { + mem::ManuallyDrop::new(Dir(OwnedFd::from_raw_fd(cvt(dirfd(self.dir.dirp.0))?))) + }; + dir.remove_c(&self.name, false) + } + + pub fn remove_dir(&self) -> io::Result<()> { + let dir = unsafe { + mem::ManuallyDrop::new(Dir(OwnedFd::from_raw_fd(cvt(dirfd(self.dir.dirp.0))?))) + }; + dir.remove_c(&self.name, true) + } } impl OpenOptions { diff --git a/library/std/src/sys/fs/unix/dir.rs b/library/std/src/sys/fs/unix/dir.rs index e0c088025ad10..e20eb67a93c8f 100644 --- a/library/std/src/sys/fs/unix/dir.rs +++ b/library/std/src/sys/fs/unix/dir.rs @@ -36,7 +36,7 @@ const TRAVERSE_DIRECTORY: i32 = _ => libc::O_RDONLY, }; -pub struct Dir(OwnedFd); +pub struct Dir(pub OwnedFd); impl Dir { pub fn open(path: &Path, opts: &OpenOptions) -> io::Result { @@ -107,7 +107,7 @@ impl Dir { Ok(Self(unsafe { OwnedFd::from_raw_fd(fd) })) } - fn open_file_c( + pub fn open_file_c( &self, path: &CStr, opts: &OpenOptions, @@ -124,7 +124,7 @@ impl Dir { Ok(unsafe { OwnedFd::from_raw_fd(fd) }) } - fn remove_c(&self, path: &CStr, remove_dir: bool) -> io::Result<()> { + pub fn remove_c(&self, path: &CStr, remove_dir: bool) -> io::Result<()> { cvt(unsafe { unlinkat( self.0.as_raw_fd(),