Skip to content

Commit

Permalink
Merge pull request #12 from rbtcollins/readdir
Browse files Browse the repository at this point in the history
Readdir support #11
  • Loading branch information
rbtcollins authored Aug 7, 2022
2 parents c466c22 + 0acc709 commit a224ae5
Show file tree
Hide file tree
Showing 5 changed files with 364 additions and 24 deletions.
7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,18 @@ version = "0.0.1"

[dependencies]
cfg-if = "1.0.0"
cvt = "0.1.1"

[dev-dependencies]
tempfile = "3.3.0"

[target.'cfg(not(windows))'.dependencies]
cvt = "0.1.1"
libc = "0.2.121"
# Saves nontrivial unsafe and platform specific code (Darwin vs other Unixes,
# MAX_PATH and more) : consider it weak and something we can remove if expedient
# later.
nix = { version = "0.24.2", default-features = false, features = ["dir"] }


[target.'cfg(windows)'.dependencies]
ntapi = "0.3.7"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ filesystem code, since otherwise the state of the filesystem path that
operations are executed against can change silently, leading to TOC-TOU race
conditions. For Unix these calls are readily available in the libc crate, but
for Windows some more plumbing is needed. This crate provides a unified
Rust-y interface to these calls.
Rust-y and safe interface to these calls.

## MSRV policy

Expand Down
115 changes: 112 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
//! unified Rust-y interface to these calls.
use std::{
ffi::OsStr,
fs::File,
io::{Error, ErrorKind, Result},
path::Path,
Expand All @@ -23,11 +24,11 @@ cfg_if::cfg_if! {
if #[cfg(windows)] {
mod win;

use win::OpenOptionsImpl;
use win::{OpenOptionsImpl, ReadDirImpl, DirEntryImpl};
} else {
mod unix;

use unix::OpenOptionsImpl;
use unix::{OpenOptionsImpl, ReadDirImpl, DirEntryImpl};
}
}

Expand Down Expand Up @@ -183,6 +184,11 @@ impl OpenOptions {
/// This will honour the options set for creation/append etc, but will only
/// operate relative to d. To open a file with an absolute path, use the
/// stdlib fs::OpenOptions.
///
/// Note: On Windows this uses low level APIs that do not perform path
/// separator translation: if passing a path containing a separator, it must
/// be a platform native one. e.g. `foo\\bar` on Windows, vs `foo/bar` on
/// most other OS's.
pub fn open_at<P: AsRef<Path>>(&self, d: &mut File, p: P) -> Result<File> {
self._impl.open_at(d, OpenOptions::ensure_root(p.as_ref())?)
}
Expand All @@ -198,6 +204,66 @@ impl OpenOptions {
}
}

/// Iterate over the contents of a directory. Created by calling read_dir() on
/// an opened directory. Each item yielded by the iterator is an io::Result to
/// allow communication of io errors as the iterator is advanced.
///
/// To the greatest extent possible the underlying OS semantics are preserved.
/// That means that `.` and `..` entries are exposed, and that no sort order is
/// guaranteed by the iterator.
#[derive(Debug)]
pub struct ReadDir<'a> {
_impl: ReadDirImpl<'a>,
}

impl<'a> ReadDir<'a> {
pub fn new(d: &'a mut File) -> Result<Self> {
Ok(ReadDir {
_impl: ReadDirImpl::new(d)?,
})
}
}

impl Iterator for ReadDir<'_> {
type Item = Result<DirEntry>;

fn next(&mut self) -> Option<Result<DirEntry>> {
self._impl
.next()
.map(|entry| entry.map(|_impl| DirEntry { _impl }))
}
}

/// The returned type for each entry found by [`read_dir`].
///
/// Each entry represents a single entry inside the directory. Platforms that
/// provide rich metadata may in future expose this through methods or extension
/// traits on DirEntry.
///
/// For now however, only the [`name()`] is exposed. This does not imply any
/// additional IO for most workloads: metadata returned from a directory listing
/// is inherently racy: presuming that what was a dir, or symlink etc when the
/// directory was listed, will still be the same when opened is fallible.
/// Instead, use open_at to open the contents, and then process based on the
/// type of content found.
#[derive(Debug)]
pub struct DirEntry {
_impl: DirEntryImpl,
}

impl DirEntry {
pub fn name(&self) -> &OsStr {
self._impl.name()
}
}

/// Read the children of the directory d.
///
/// See [`ReadDir`] and [`DirEntry`] for details.
pub fn read_dir(d: &mut File) -> Result<ReadDir> {
ReadDir::new(d)
}

pub mod os {
cfg_if::cfg_if! {
if #[cfg(windows)] {
Expand All @@ -214,14 +280,15 @@ pub mod testsupport;
#[cfg(test)]
mod tests {
use std::{
ffi::OsStr,
fs::{rename, File},
io::{Error, ErrorKind, Result, Seek, SeekFrom, Write},
path::PathBuf,
};

use tempfile::TempDir;

use crate::{testsupport::open_dir, OpenOptions, OpenOptionsWriteMode};
use crate::{read_dir, testsupport::open_dir, DirEntry, OpenOptions, OpenOptionsWriteMode};

/// Create a directory parent, open it, then rename it to renamed-parent and
/// create another directory in its place. returns the file handle and the
Expand Down Expand Up @@ -464,4 +531,46 @@ mod tests {
}
Ok(())
}

#[test]
fn readdir() -> Result<()> {
let (_tmp, mut parent_dir, _pathname) = setup()?;
assert_eq!(
2, // . and ..
read_dir(&mut parent_dir)?
.collect::<Result<Vec<DirEntry>>>()?
.len()
);
let dir_present =
|children: &Vec<DirEntry>, name: &OsStr| children.iter().any(|e| e.name() == name);

let mut options = OpenOptions::default();
options.create_new(true).write(OpenOptionsWriteMode::Write);
options.open_at(&mut parent_dir, "1")?;
options.open_at(&mut parent_dir, "2")?;
options.open_at(&mut options.mkdir_at(&mut parent_dir, "child")?, "3")?;
let children = read_dir(&mut parent_dir)?.collect::<Result<Vec<_>>>()?;
assert_eq!(
5,
children.len(),
"directory contains 5 entries (., .., 1, 2, child)"
);
assert!(dir_present(&children, OsStr::new("1")), "{:?}", children);
assert!(dir_present(&children, OsStr::new("2")), "{:?}", children);
assert!(
dir_present(&children, OsStr::new("child")),
"{:?}",
children
);

{
let mut child = OpenOptions::default()
.read(true)
.open_at(&mut parent_dir, "child")?;
let children = read_dir(&mut child)?.collect::<Result<Vec<_>>>()?;
assert_eq!(3, children.len(), "{:?}", children);
assert!(dir_present(&children, OsStr::new("3")), "{:?}", children);
}
Ok(())
}
}
107 changes: 106 additions & 1 deletion src/unix.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use std::{
ffi::CString,
ffi::{CString, OsStr, OsString},
fs::File,
io::Result,
marker::PhantomData,
os::unix::prelude::{AsRawFd, FromRawFd, OsStrExt},
path::Path,
ptr,
};

// This will probably take a few iterations to get right. The idea: always use
Expand Down Expand Up @@ -180,6 +182,109 @@ impl OpenOptionsExt for OpenOptions {
}
}

#[derive(Debug)]
pub(crate) struct ReadDirImpl<'a> {
// Since we clone the FD, the original FD is now separate. In theory.
// However for Windows we use the File directly, thus here we need to
// pretend.
_phantom: PhantomData<&'a File>,
// Set to None after we closedir it. Perhaps we should we impl Send and Sync
// because the data referenced is owned by libc ?
dir: Option<ptr::NonNull<libc::DIR>>,
}

impl<'a> ReadDirImpl<'a> {
pub fn new(dir_file: &'a mut File) -> Result<Self> {
// closedir closes the FD; make a new one that we can close when done with.
let new_fd =
cvt_r(|| unsafe { libc::fcntl(dir_file.as_raw_fd(), libc::F_DUPFD_CLOEXEC, 0) })?;
let mut dir = Some(
ptr::NonNull::new(unsafe { libc::fdopendir(new_fd) }).ok_or_else(|| {
let _droppable = unsafe { File::from_raw_fd(new_fd) };
std::io::Error::last_os_error()
})?,
);

// If dir_file has had operations on it - such as open_at - its pointer
// might not be at the start of the dir, and fdopendir is documented
// (e.g. BSD man pages) to not rewind the fd - and our cloned fd
// inherits the pointer.
if let Some(d) = dir.as_mut() {
unsafe { libc::rewinddir(d.as_mut()) };
}

Ok(ReadDirImpl {
_phantom: PhantomData,
dir,
})
}

fn close_dir(&mut self) -> Result<()> {
if let Some(ref mut dir) = self.dir {
let result = unsafe { libc::closedir(dir.as_mut()) };
// call made, clear state
self.dir = None;
cvt_r(|| result)?;
}
Ok(())
}
}

impl Drop for ReadDirImpl<'_> {
fn drop(&mut self) {
// like the stdlib, we eat errors occuring during drop, as there is no
// way to get error handling.
let _ = self.close_dir();
}
}

impl Iterator for ReadDirImpl<'_> {
type Item = Result<DirEntryImpl>;

fn next(&mut self) -> Option<Self::Item> {
let dir = unsafe { self.dir?.as_mut() };
// the readdir result is only guaranteed valid within the same thread
// and until other calls are made on the same dir stream. Thus we
// perform the required work inside next, allowing the next call to
// readdir to be managed by the single mutable borrower rule in Rust.
// readdir requires errno set to zero.
nix::Error::clear();
ptr::NonNull::new(unsafe { libc::readdir(dir) })
.map(|e| {
Ok(DirEntryImpl {
name: unsafe {
// Step one: C pointer to CStr - referenced data, length not known.
let c_str = std::ffi::CStr::from_ptr(e.as_ref().d_name.as_ptr());
// Step two: OsStr: referenced data, length calcu;ated
let os_str = OsStr::from_bytes(c_str.to_bytes());
// Step three: owned copy
os_str.to_os_string()
},
})
})
.or_else(|| {
// NULL result, an error IFF errno has been set.
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(0) {
None
} else {
Some(Err(err))
}
})
}
}

#[derive(Debug)]
pub(crate) struct DirEntryImpl {
name: OsString,
}

impl DirEntryImpl {
pub fn name(&self) -> &OsStr {
&self.name
}
}

#[cfg(test)]
mod tests {
use std::{
Expand Down
Loading

0 comments on commit a224ae5

Please sign in to comment.