Skip to content

Commit

Permalink
Merge #916
Browse files Browse the repository at this point in the history
916: new dir module r=asomers a=scottlamb

Fixes #915 

This is a lower-level interface than `std::fs::ReadDir`. Notable differences:

   * can be opened from a file descriptor (as returned by `openat`, perhaps
     before knowing if the path represents a file or directory). Uses
     `fdopendir` for this, available on all Unix platforms as of
     rust-lang/libc#1018.

   * implements `AsRawFd`, so it can be passed to `fstat`, `openat`, etc.

   * can be iterated through multiple times without closing and reopening the
     file descriptor. Each iteration rewinds when finished.

   * returns entries for `.` (current directory) and `..` (parent directory).

   * returns entries' names as a `CStr` (no allocation or conversion beyond
     whatever libc does).

Co-authored-by: Scott Lamb <slamb@slamb.org>
  • Loading branch information
bors[bot] and scottlamb committed Aug 31, 2018
2 parents 70cce1a + 6d6e9c5 commit a99300c
Show file tree
Hide file tree
Showing 4 changed files with 258 additions and 0 deletions.
210 changes: 210 additions & 0 deletions src/dir.rs
@@ -0,0 +1,210 @@
use {Error, NixPath, Result};
use errno::Errno;
use fcntl::{self, OFlag};
use libc;
use std::os::unix::io::{AsRawFd, IntoRawFd, RawFd};
use std::{ffi, fmt, ptr};
use sys;

#[cfg(target_os = "linux")]
use libc::{dirent64 as dirent, readdir64_r as readdir_r};

#[cfg(not(target_os = "linux"))]
use libc::{dirent, readdir_r};

/// An open directory.
///
/// This is a lower-level interface than `std::fs::ReadDir`. Notable differences:
/// * can be opened from a file descriptor (as returned by `openat`, perhaps before knowing
/// if the path represents a file or directory).
/// * implements `AsRawFd`, so it can be passed to `fstat`, `openat`, etc.
/// The file descriptor continues to be owned by the `Dir`, so callers must not keep a `RawFd`
/// after the `Dir` is dropped.
/// * can be iterated through multiple times without closing and reopening the file
/// descriptor. Each iteration rewinds when finished.
/// * returns entries for `.` (current directory) and `..` (parent directory).
/// * returns entries' names as a `CStr` (no allocation or conversion beyond whatever libc
/// does).
pub struct Dir(
// This could be ptr::NonNull once nix requires Rust 1.25.
*mut libc::DIR
);

impl Dir {
/// Opens the given path as with `fcntl::open`.
pub fn open<P: ?Sized + NixPath>(path: &P, oflag: OFlag,
mode: sys::stat::Mode) -> Result<Self> {
let fd = fcntl::open(path, oflag, mode)?;
Dir::from_fd(fd)
}

/// Opens the given path as with `fcntl::openat`.
pub fn openat<P: ?Sized + NixPath>(dirfd: RawFd, path: &P, oflag: OFlag,
mode: sys::stat::Mode) -> Result<Self> {
let fd = fcntl::openat(dirfd, path, oflag, mode)?;
Dir::from_fd(fd)
}

/// Converts from a descriptor-based object, closing the descriptor on success or failure.
#[inline]
pub fn from<F: IntoRawFd>(fd: F) -> Result<Self> {
Dir::from_fd(fd.into_raw_fd())
}

/// Converts from a file descriptor, closing it on success or failure.
pub fn from_fd(fd: RawFd) -> Result<Self> {
let d = unsafe { libc::fdopendir(fd) };
if d.is_null() {
let e = Error::last();
unsafe { libc::close(fd) };
return Err(e);
};
Ok(Dir(d))
}

/// Returns an iterator of `Result<Entry>` which rewinds when finished.
pub fn iter(&mut self) -> Iter {
Iter(self)
}
}

// `Dir` is not `Sync`. With the current implementation, it could be, but according to
// https://www.gnu.org/software/libc/manual/html_node/Reading_002fClosing-Directory.html,
// future versions of POSIX are likely to obsolete `readdir_r` and specify that it's unsafe to
// call `readdir` simultaneously from multiple threads.
//
// `Dir` is safe to pass from one thread to another, as it's not reference-counted.
unsafe impl Send for Dir {}

impl AsRawFd for Dir {
fn as_raw_fd(&self) -> RawFd {
unsafe { libc::dirfd(self.0) }
}
}

impl fmt::Debug for Dir {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Dir")
.field("fd", &self.as_raw_fd())
.finish()
}
}

impl Drop for Dir {
fn drop(&mut self) {
unsafe { libc::closedir(self.0) };
}
}

#[derive(Debug)]
pub struct Iter<'d>(&'d mut Dir);

impl<'d> Iterator for Iter<'d> {
type Item = Result<Entry>;

fn next(&mut self) -> Option<Self::Item> {
unsafe {
// Note: POSIX specifies that portable applications should dynamically allocate a
// buffer with room for a `d_name` field of size `pathconf(..., _PC_NAME_MAX)` plus 1
// for the NUL byte. It doesn't look like the std library does this; it just uses
// fixed-sized buffers (and libc's dirent seems to be sized so this is appropriate).
// Probably fine here too then.
let mut ent: Entry = Entry(::std::mem::uninitialized());
let mut result = ptr::null_mut();
if let Err(e) = Errno::result(readdir_r((self.0).0, &mut ent.0, &mut result)) {
return Some(Err(e));
}
if result == ptr::null_mut() {
return None;
}
assert_eq!(result, &mut ent.0 as *mut dirent);
return Some(Ok(ent));
}
}
}

impl<'d> Drop for Iter<'d> {
fn drop(&mut self) {
unsafe { libc::rewinddir((self.0).0) }
}
}

/// A directory entry, similar to `std::fs::DirEntry`.
///
/// Note that unlike the std version, this may represent the `.` or `..` entries.
#[derive(Copy, Clone)]
pub struct Entry(dirent);

#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum Type {
Fifo,
CharacterDevice,
Directory,
BlockDevice,
File,
Symlink,
Socket,
}

impl Entry {
/// Returns the inode number (`d_ino`) of the underlying `dirent`.
#[cfg(any(target_os = "android",
target_os = "emscripten",
target_os = "fuchsia",
target_os = "haiku",
target_os = "ios",
target_os = "l4re",
target_os = "linux",
target_os = "macos",
target_os = "solaris"))]
pub fn ino(&self) -> u64 {
self.0.d_ino as u64
}

/// Returns the inode number (`d_fileno`) of the underlying `dirent`.
#[cfg(not(any(target_os = "android",
target_os = "emscripten",
target_os = "fuchsia",
target_os = "haiku",
target_os = "ios",
target_os = "l4re",
target_os = "linux",
target_os = "macos",
target_os = "solaris")))]
pub fn ino(&self) -> u64 {
self.0.d_fileno as u64
}

/// Returns the bare file name of this directory entry without any other leading path component.
pub fn file_name(&self) -> &ffi::CStr {
unsafe { ::std::ffi::CStr::from_ptr(self.0.d_name.as_ptr()) }
}

/// Returns the type of this directory entry, if known.
///
/// See platform `readdir(3)` or `dirent(5)` manpage for when the file type is known;
/// notably, some Linux filesystems don't implement this. The caller should use `stat` or
/// `fstat` if this returns `None`.
pub fn file_type(&self) -> Option<Type> {
match self.0.d_type {
libc::DT_FIFO => Some(Type::Fifo),
libc::DT_CHR => Some(Type::CharacterDevice),
libc::DT_DIR => Some(Type::Directory),
libc::DT_BLK => Some(Type::BlockDevice),
libc::DT_REG => Some(Type::File),
libc::DT_LNK => Some(Type::Symlink),
libc::DT_SOCK => Some(Type::Socket),
/* libc::DT_UNKNOWN | */ _ => None,
}
}
}

impl fmt::Debug for Entry {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("Entry")
.field("ino", &self.ino())
.field("file_name", &self.file_name())
.field("file_type", &self.file_type())
.finish()
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Expand Up @@ -29,6 +29,7 @@ pub extern crate libc;
#[macro_use] mod macros;

// Public crates
pub mod dir;
pub mod errno;
#[deny(missing_docs)]
pub mod features;
Expand Down
1 change: 1 addition & 0 deletions test/test.rs
Expand Up @@ -11,6 +11,7 @@ extern crate tempdir;
extern crate tempfile;

mod sys;
mod test_dir;
mod test_fcntl;
#[cfg(any(target_os = "dragonfly",
target_os = "freebsd",
Expand Down
46 changes: 46 additions & 0 deletions test/test_dir.rs
@@ -0,0 +1,46 @@
extern crate nix;
extern crate tempdir;

use nix::dir::{Dir, Type};
use nix::fcntl::OFlag;
use nix::sys::stat::Mode;
use std::fs::File;
use self::tempdir::TempDir;

#[test]
fn read() {
let tmp = TempDir::new("nix-dir-read").unwrap();
File::create(&tmp.path().join("foo")).unwrap();
::std::os::unix::fs::symlink("foo", tmp.path().join("bar")).unwrap();
let mut dir = Dir::open(tmp.path(), OFlag::O_DIRECTORY | OFlag::O_RDONLY | OFlag::O_CLOEXEC,
Mode::empty()).unwrap();
let mut entries: Vec<_> = dir.iter().map(|e| e.unwrap()).collect();
entries.sort_by(|a, b| a.file_name().cmp(b.file_name()));
let entry_names: Vec<_> = entries
.iter()
.map(|e| e.file_name().to_str().unwrap().to_owned())
.collect();
assert_eq!(&entry_names[..], &[".", "..", "bar", "foo"]);

// Check file types. The system is allowed to return DT_UNKNOWN (aka None here) but if it does
// return a type, ensure it's correct.
assert!(&[Some(Type::Directory), None].contains(&entries[0].file_type())); // .: dir
assert!(&[Some(Type::Directory), None].contains(&entries[1].file_type())); // ..: dir
assert!(&[Some(Type::Symlink), None].contains(&entries[2].file_type())); // bar: symlink
assert!(&[Some(Type::File), None].contains(&entries[3].file_type())); // foo: regular file
}

#[test]
fn rewind() {
let tmp = TempDir::new("nix-dir-rewind").unwrap();
let mut dir = Dir::open(tmp.path(), OFlag::O_DIRECTORY | OFlag::O_RDONLY | OFlag::O_CLOEXEC,
Mode::empty()).unwrap();
let entries1: Vec<_> = dir.iter().map(|e| e.unwrap().file_name().to_owned()).collect();
let entries2: Vec<_> = dir.iter().map(|e| e.unwrap().file_name().to_owned()).collect();
assert_eq!(entries1, entries2);
}

#[test]
fn ebadf() {
assert_eq!(Dir::from_fd(-1).unwrap_err(), nix::Error::Sys(nix::errno::Errno::EBADF));
}

0 comments on commit a99300c

Please sign in to comment.