Skip to content

dirfd: initial quick and dirty implementation for unix #139514

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions library/std/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,29 @@ pub enum TryLockError {
WouldBlock,
}

#[unstable(feature = "dirfd", issue = "120426")]
/// An object providing access to a directory on the filesystem.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you elaborate the documentation here a bit? At least make it clear that this is a handle, and what happens on drop (similar to File).

///
/// 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
/// use std::fs::Dir;
///
/// fn main() -> std::io::Result<()> {
/// let dir = Dir::new("/home/foo")?;
/// let file = dir.open("bar.txt")?;
/// Ok(())
/// }
/// ```
pub struct Dir {
inner: fs_imp::Dir,
}

/// Metadata information about a file.
///
/// This structure is returned from the [`metadata`] or
Expand Down Expand Up @@ -1402,6 +1425,33 @@ impl Seek for Arc<File> {
}
}

impl Dir {
/// Opens a file relative to this directory.
///
/// # Examples
/// ```no_run
/// use std::fs::Dir;
///
/// let dir = Dir::new("foo")?;
/// ```
#[unstable(feature = "dirfd", issue = "120426")]
pub fn open<P: AsRef<Path>>(&self, path: P) -> io::Result<File> {
self.inner.open(path).map(|f| File { inner: f })
}
/// Opens a file relative to this directory with the specified options.
#[unstable(feature = "dirfd", issue = "120426")]
pub fn open_with<P: AsRef<Path>>(&self, path: P, opts: &OpenOptions) -> io::Result<File> {
self.inner.open_with(path, &opts.0).map(|f| File { inner: f })
}
}

#[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.
///
Expand Down
2 changes: 2 additions & 0 deletions library/std/src/sys/fs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ pub fn with_native_path<T>(path: &Path, f: &dyn Fn(&Path) -> io::Result<T>) -> i
f(path)
}

#[cfg(target_family = "unix")]
pub use imp::Dir;
pub use imp::{
DirBuilder, DirEntry, File, FileAttr, FilePermissions, FileTimes, FileType, OpenOptions,
ReadDir,
Expand Down
259 changes: 181 additions & 78 deletions library/std/src/sys/fs/unix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ use libc::{c_int, mode_t};
#[cfg(target_os = "android")]
use libc::{
dirent as dirent64, fstat as fstat64, fstatat as fstatat64, ftruncate64, lseek64,
lstat as lstat64, off64_t, open as open64, stat as stat64,
lstat as lstat64, off64_t, open as open64, openat as openat64, stat as stat64,
};
#[cfg(not(any(
all(target_os = "linux", not(target_env = "musl")),
Expand All @@ -64,14 +64,14 @@ use libc::{
)))]
use libc::{
dirent as dirent64, fstat as fstat64, ftruncate as ftruncate64, lseek as lseek64,
lstat as lstat64, off_t as off64_t, open as open64, stat as stat64,
lstat as lstat64, off_t as off64_t, open as open64, openat as openat64, stat as stat64,
};
#[cfg(any(
all(target_os = "linux", not(target_env = "musl")),
target_os = "l4re",
target_os = "hurd"
))]
use libc::{dirent64, fstat64, ftruncate64, lseek64, lstat64, off64_t, open64, stat64};
use libc::{dirent64, fstat64, ftruncate64, lseek64, lstat64, off64_t, open64, openat64, stat64};

use crate::ffi::{CStr, OsStr, OsString};
use crate::fmt::{self, Write as _};
Expand Down Expand Up @@ -264,7 +264,183 @@ impl ReadDir {
}
}

struct Dir(*mut libc::DIR);
pub struct Dir(*mut libc::DIR);

// dirfd isn't supported everywhere
#[cfg(not(any(
miri,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we handle such missing platforms by returning errors instead of not having the methods exist at all

target_os = "redox",
target_os = "nto",
target_os = "vita",
target_os = "hurd",
target_os = "espidf",
target_os = "horizon",
target_os = "vxworks",
target_os = "rtems",
target_os = "nuttx",
)))]
impl Dir {
pub fn open<P: AsRef<Path>>(&self, path: P) -> io::Result<File> {
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<P: AsRef<Path>>(&self, path: P, opts: &OpenOptions) -> io::Result<File> {
run_path_with_cstr(path.as_ref(), &|path| self.open_c(path, opts))
}

pub fn open_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(libc::dirfd(self.0), path.as_ptr(), flags, opts.mode as c_int)
})?;
Ok(File(unsafe { FileDesc::from_raw_fd(fd) }))
}
}

#[cfg(any(
miri,
target_os = "redox",
target_os = "nto",
target_os = "vita",
target_os = "hurd",
target_os = "espidf",
target_os = "horizon",
target_os = "vxworks",
target_os = "rtems",
target_os = "nuttx",
))]
impl Dir {
pub fn open<P: AsRef<Path>>(&self, path: P) -> io::Result<File> {
Err(io::const_error!(
io::ErrorKind::Unsupported,
"directory handles are not available for this platform",
))
}

pub fn open_with<P: AsRef<Path>>(&self, path: P, opts: &OpenOptions) -> io::Result<File> {
Err(io::const_error!(
io::ErrorKind::Unsupported,
"directory handles are not available for this platform",
))
}

pub fn open_c(&self, path: &CStr, opts: &OpenOptions) -> io::Result<File> {
Err(io::const_error!(
io::ErrorKind::Unsupported,
"directory handles are not available for this platform",
))
}
}

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_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<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)
}

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 = unsafe { dirfd(self.0) };
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 {}
Expand Down Expand Up @@ -1653,79 +1829,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<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_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<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 {
Expand All @@ -1742,7 +1845,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) {
Expand Down
Loading