summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md2
-rw-r--r--src/dir.rs210
-rw-r--r--src/lib.rs1
-rw-r--r--test/test.rs1
-rw-r--r--test/test_dir.rs46
5 files changed, 260 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9480cd5c..e85d2303 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
([#921](https://github.com/nix-rust/nix/pull/921))
- Added support for `SCM_CREDENTIALS`, allowing to send process credentials over Unix sockets.
([#923](https://github.com/nix-rust/nix/pull/923))
+- Added a `dir` module for reading directories (wraps `fdopendir`, `readdir`, and `rewinddir`).
+ ([#916](https://github.com/nix-rust/nix/pull/916))
### Changed
- Increased required Rust version to 1.22.1/
diff --git a/src/dir.rs b/src/dir.rs
new file mode 100644
index 00000000..814e9e0c
--- /dev/null
+++ b/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()
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 07e84c12..ed96a8e1 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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;
diff --git a/test/test.rs b/test/test.rs
index da55235a..a72dc44a 100644
--- a/test/test.rs
+++ b/test/test.rs
@@ -10,6 +10,7 @@ extern crate rand;
extern crate tempfile;
mod sys;
+mod test_dir;
mod test_fcntl;
#[cfg(any(target_os = "dragonfly",
target_os = "freebsd",
diff --git a/test/test_dir.rs b/test/test_dir.rs
new file mode 100644
index 00000000..c42fbcd1
--- /dev/null
+++ b/test/test_dir.rs
@@ -0,0 +1,46 @@
+extern crate nix;
+extern crate tempfile;
+
+use nix::dir::{Dir, Type};
+use nix::fcntl::OFlag;
+use nix::sys::stat::Mode;
+use std::fs::File;
+use self::tempfile::tempdir;
+
+#[test]
+fn read() {
+ let tmp = tempdir().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().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));
+}