Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/windirstat/walkdir.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndrew Gallant <jamslam@gmail.com>2019-05-08 01:15:29 +0300
committerAndrew Gallant <jamslam@gmail.com>2020-01-12 15:33:48 +0300
commit6b1442a1597fbb6260acdca4b56476ca5be4c7c4 (patch)
tree1f7cfb5ee93049f4450d3394403fc17926e0d325
parentbab4066b218dc20a625d405e02433d882237d59c (diff)
platform: add platform specific directory traversals
This commit performs the unenviable task of implementing directory traversal for Windows, Unix-like platforms and Linux (where Linux is supported via the generic Unix implementation, but also has its own specialized implementation that uses getdents64 directly). The purpose of doing this is largely to control our costs more explicitly, and to fix platform specific bugs. For costs, this largely boils down to amortizing allocation. That is, instead of allocating a fresh OsString for every entry, we can now read directory entries into a previously used entry. Anecdotally, this leads to a 20-30% performance improvement on listing a single large directory for *both* Windows and Linux. As for bugs, these mostly center around long file paths. For example, on Unix, the typical maximum path length is 4096 bytes. There is no way to avoid this using std's APIs, so we need to instead roll our own based on file descriptors. There's also Windows, which has a maximum file path length of only 260 chars, although this is trickier to fix. There are basically two downsides to doing things this way: 1. This increases maintenance burden quite a bit, and this change will almost certainly introduce bugs on less well tested platforms. 2. It's not clear yet whether we will *also* need to hand-roll our own `Metadata` implementation. std provides no way to build the `Metadata` type from raw inputs. The key factor here is that std's `DirEntry::metadata` method benefits from using `fstatat` internally. If we don't re-roll our own `Metadata`, then we'll have to use `std::fs::metadata`, which does a normal `stat` call on the file path. `fstatat` should generally be faster. On the other hand, std's implementation strategy also means that file descriptors aren't closed until the last `DirEntry` is dropped, which is a bit sneaky. (And is also indicative of a bug in walkdir, since this means its "maximum opened fds" feature doesn't actually work.) Unfortunate, but everything gets sacrificed at the alter of performance.
-rw-r--r--BREADCRUMBS17
-rw-r--r--Cargo.toml11
-rw-r--r--build.rs9
-rw-r--r--src/lib.rs3
-rw-r--r--src/os/linux/dirent.rs80
-rw-r--r--src/os/linux/mod.rs315
-rw-r--r--src/os/mod.rs10
-rw-r--r--src/os/unix/dirent.rs203
-rw-r--r--src/os/unix/errno-dragonfly.c5
-rw-r--r--src/os/unix/errno.rs48
-rw-r--r--src/os/unix/mod.rs624
-rw-r--r--src/os/windows/mod.rs519
-rw-r--r--src/tests/linux.rs201
-rw-r--r--src/tests/mod.rs6
-rw-r--r--src/tests/recursive.rs28
-rw-r--r--src/tests/unix.rs200
-rw-r--r--src/tests/util.rs315
-rw-r--r--src/tests/windows.rs147
-rw-r--r--walkdir-list/main.rs528
19 files changed, 3159 insertions, 110 deletions
diff --git a/BREADCRUMBS b/BREADCRUMBS
new file mode 100644
index 0000000..4eca10f
--- /dev/null
+++ b/BREADCRUMBS
@@ -0,0 +1,17 @@
+For the recursive directory iterator, there's no way around storing the full
+joined file path for each entry. So our only salvation is to amortize the
+allocation necessary for storing that full path. There's two pieces to this:
+
+First, part of it requires the caller to provide scratch space for a dir entry.
+We return the next dir entry via this mechanism.
+
+Second, we need to keep copies of all paths up the current stack, BUT, these
+paths can be reused as we push and pop things from the stack. So we should be
+able to use a memory pool for these.
+
+Other note: getting openat to work correctly with the file descriptor limit is
+tricky. It looks like we're going to have to only use openat when we still have
+the open file descriptor, but otherwise also support using plain open when the
+file descriptor has been closed (and its stream read into memory). This seems
+fine, since in practice, the file descriptor limit is big enough to handle most
+directory trees without running out.
diff --git a/Cargo.toml b/Cargo.toml
index 94fc562..1b1f691 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -23,12 +23,21 @@ members = ["walkdir-list"]
[dependencies]
same-file = "1.0.1"
+[target.'cfg(unix)'.dependencies.libc]
+version = "0.2.54"
+
+[target.'cfg(target_os = "dragonfly")'.build-dependencies]
+cc = "1.0.36"
+
[target.'cfg(windows)'.dependencies.winapi]
version = "0.3"
-features = ["std", "winnt"]
+features = ["std", "impl-debug", "fileapi", "handleapi", "winnt"]
[target.'cfg(windows)'.dependencies.winapi-util]
version = "0.1.1"
[dev-dependencies]
doc-comment = "0.3"
+
+[profile.release]
+debug = true
diff --git a/build.rs b/build.rs
new file mode 100644
index 0000000..9f26302
--- /dev/null
+++ b/build.rs
@@ -0,0 +1,9 @@
+#[cfg(not(target_os = "dragonfly"))]
+fn main() {}
+
+#[cfg(target_os = "dragonfly")]
+fn main() {
+ cc::Build::new()
+ .file("src/os/unix/errno-dragonfly.c")
+ .compile("errno-dragonfly");
+}
diff --git a/src/lib.rs b/src/lib.rs
index 5132dd5..24c4cec 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -105,10 +105,13 @@ for entry in walker.filter_entry(|e| !is_hidden(e)) {
#![deny(missing_docs)]
#![allow(unknown_lints)]
+#![allow(warnings)]
#[cfg(test)]
doc_comment::doctest!("../README.md");
+pub mod os;
+
use std::cmp::{min, Ordering};
use std::fmt;
use std::fs::{self, ReadDir};
diff --git a/src/os/linux/dirent.rs b/src/os/linux/dirent.rs
new file mode 100644
index 0000000..18ae8ce
--- /dev/null
+++ b/src/os/linux/dirent.rs
@@ -0,0 +1,80 @@
+use std::ffi::CStr;
+use std::fmt;
+
+use libc::c_char;
+
+use crate::os::unix::FileType;
+
+/// A raw directory entry used to read entries from Linux's getdents64 syscall.
+///
+/// Note that this type is very much not safe to use because `d_name` is a
+/// flexible array member. That is, the *size* of values of this type are
+/// usually larger than size_of::<RawDirEntry>, since the file name will extend
+/// beyond the end of the struct. Therefore, values of this type should only be
+/// read when they exist in their original buffer.
+///
+/// We expose this by making it not safe to ask for the name in this
+/// entry, since its `NUL` terminator scan could result in a buffer overrun
+/// when used incorrectly.
+#[derive(Clone)]
+#[repr(C)]
+pub struct RawDirEntry {
+ d_ino: u64,
+ d_off: u64,
+ d_reclen: u16,
+ d_type: u8,
+ d_name: [u8; 0],
+}
+
+impl fmt::Debug for RawDirEntry {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.debug_struct("RawDirEntry")
+ .field("d_ino", &self.ino())
+ .field("d_off", &self.d_off)
+ .field("d_reclen", &self.d_reclen)
+ .field("d_type", &self.file_type())
+ // Reading the name is not safe, and we can't guarantee its
+ // safety in the context of this Debug impl unfortunately. We
+ // *could* use a small fixed size array instead of [u8; 0] as the
+ // representation, which would at least let us safely read a prefix
+ // to show here, but it's not clear what cost that would have
+ // (probably none?) or whether it's worth it.
+ .field("d_name", &"<N/A>")
+ .finish()
+ }
+}
+
+impl RawDirEntry {
+ /// Return the file name in this directory entry as a C string.
+ ///
+ /// This computes the length of the name in this entry by scanning for a
+ /// `NUL` terminator.
+ ///
+ /// # Safety
+ ///
+ /// This is not safe because callers who call this function must guarantee
+ /// that the `RawDirEntry` is still within its original buffer. Otherwise,
+ /// it's possible for a buffer overrun to occur.
+ pub unsafe fn file_name(&self) -> &CStr {
+ CStr::from_ptr(self.d_name.as_ptr() as *const c_char)
+ }
+
+ /// Return the file type of this directory entry, if one exists.
+ ///
+ /// A file type may not exist if the underlying file system reports an
+ /// unknown file type in the directory entry.
+ pub fn file_type(&self) -> Option<FileType> {
+ FileType::from_dirent_type(self.d_type)
+ }
+
+ /// Returns the underlying file serial number for this directory entry.
+ pub fn ino(&self) -> u64 {
+ self.d_ino
+ }
+
+ /// Returns the total length (including padding), in bytes, of this
+ /// directory entry.
+ pub fn record_len(&self) -> usize {
+ self.d_reclen as usize
+ }
+}
diff --git a/src/os/linux/mod.rs b/src/os/linux/mod.rs
new file mode 100644
index 0000000..b6255ad
--- /dev/null
+++ b/src/os/linux/mod.rs
@@ -0,0 +1,315 @@
+/*!
+Low level Linux specific APIs for reading directory entries via `getdents64`.
+*/
+
+use std::alloc::{alloc_zeroed, dealloc, handle_alloc_error, Layout};
+use std::ffi::{CStr, CString, OsStr};
+use std::fmt;
+use std::io;
+use std::mem;
+use std::os::unix::ffi::{OsStrExt, OsStringExt};
+use std::os::unix::io::{AsRawFd, RawFd};
+use std::path::PathBuf;
+use std::ptr::NonNull;
+
+use libc::{syscall, SYS_getdents64};
+
+use crate::os::linux::dirent::RawDirEntry;
+use crate::os::unix::{
+ errno, escaped_bytes, DirEntry as UnixDirEntry, DirFd, FileType,
+};
+
+mod dirent;
+
+/// A safe function for calling Linux's `getdents64` API.
+///
+/// The basic idea of `getdents` is that it executes a single syscall but
+/// returns potentially many directory entries in a single buffer. This can
+/// provide a small speed boost when compared with the typical `readdir` POSIX
+/// API, depending on your platform's implementation.
+///
+/// This routine will read directory entries from the given file descriptor
+/// into the given cursor. The cursor can then be used to cheaply and safely
+/// iterate over the directory entries that were read.
+///
+/// When all directory entries have been read from the given file descriptor,
+/// then this function will return `false`. Otherwise, it returns `true`.
+///
+/// If there was a problem calling the underlying `getdents64` syscall, then
+/// an error is returned.
+pub fn getdents(fd: RawFd, cursor: &mut DirEntryCursor) -> io::Result<bool> {
+ cursor.clear();
+ let res = unsafe {
+ syscall(
+ SYS_getdents64,
+ fd,
+ cursor.raw.as_ptr() as *mut RawDirEntry,
+ cursor.capacity,
+ )
+ };
+ match res {
+ -1 => Err(io::Error::last_os_error()),
+ 0 => Ok(false),
+ nwritten => {
+ cursor.len = nwritten as usize;
+ Ok(true)
+ }
+ }
+}
+
+/// A Linux specific directory entry.
+///
+/// This directory entry is just like the Unix `DirEntry`, except its file
+/// name is borrowed from a `DirEntryCursor`'s internal buffer. This makes
+/// it possible to iterate over directory entries on Linux by reusing the
+/// cursor's internal buffer with no additional allocations or copying.
+///
+/// In practice, if one needs an owned directory entry, then convert it to a
+/// Unix `DirEntry` either via the Unix methods on this `DirEntry`, or by
+/// simply reading a Unix `DirEntry` directly from `DirEntryCursor`.
+#[derive(Clone)]
+pub struct DirEntry<'a> {
+ /// A borrowed version of the `d_name` field found in the raw directory
+ /// entry. This field is the only reason why this type exists, otherwise
+ /// we'd just expose `RawDirEntry` directly to users. The issue with
+ /// exposing the raw directory entry is that its size isn't correct (since
+ /// the file name may extend beyond the end of the struct).
+ ///
+ /// This borrow ties this entry to the `DirEntryBuffer`.
+ file_name: &'a CStr,
+ /// The file type, as is, from the raw dirent.
+ file_type: Option<FileType>,
+ /// The file serial number, as is, from the raw dirent.
+ ino: u64,
+}
+
+impl<'a> fmt::Debug for DirEntry<'a> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ use crate::os::unix::escaped_bytes;
+
+ f.debug_struct("DirEntry")
+ .field("file_name", &escaped_bytes(self.file_name_bytes()))
+ .field("file_type", &self.file_type)
+ .field("ino", &self.ino)
+ .finish()
+ }
+}
+
+impl<'a> DirEntry<'a> {
+ /// Return the file name in this directory entry as a C string.
+ #[inline]
+ pub fn file_name(&self) -> &CStr {
+ self.file_name
+ }
+
+ /// Return the file name in this directory entry as raw bytes without
+ /// a `NUL` terminator.
+ #[inline]
+ pub fn file_name_bytes(&self) -> &[u8] {
+ self.file_name.to_bytes()
+ }
+
+ /// Return the file name in this directory entry as an OS string without
+ /// a `NUL` terminator.
+ #[inline]
+ pub fn file_name_os(&self) -> &OsStr {
+ OsStr::from_bytes(self.file_name_bytes())
+ }
+
+ /// Return the file type of this directory entry, if one exists.
+ ///
+ /// A file type may not exist if the underlying file system reports an
+ /// unknown file type in the directory entry.
+ #[inline]
+ pub fn file_type(&self) -> Option<FileType> {
+ self.file_type
+ }
+
+ /// Returns the underlying file serial number for this directory entry.
+ #[inline]
+ pub fn ino(&self) -> u64 {
+ self.ino
+ }
+
+ /// Convert this directory entry into an owned Unix `DirEntry`. If you
+ /// want to be able to reuse allocations, then use `write_to_unix` instead.
+ #[inline]
+ pub fn to_unix(&self) -> UnixDirEntry {
+ let mut ent = UnixDirEntry::empty();
+ self.write_to_unix(&mut ent);
+ ent
+ }
+
+ /// Write this directory entry into the given Unix `DirEntry`. This makes
+ /// it possible to amortize allocation.
+ #[inline]
+ pub fn write_to_unix(&self, unix_dirent: &mut UnixDirEntry) {
+ unix_dirent.from_linux_raw(self)
+ }
+}
+
+/// A cursor for reading directory entries from a `getdents` buffer.
+///
+/// This cursor allocates space internally for storing one or more Linux
+/// directory entries, and exposes an API for cheaply iterating over those
+/// directory entries.
+///
+/// A cursor can and should be reused across multiple calls to `getdents`. A
+/// cursor is not tied to any one particular directory.
+#[derive(Clone, Debug)]
+pub struct DirEntryCursor {
+ /// Spiritually, this is a *mut RawDirEntry. Unfortunately, this doesn't
+ /// quite make sense since a value with type `RawDirEntry` does not
+ /// actually have a size of `size_of::<RawDirEntry>()` due to the way in
+ /// which the entry's name is stored in a flexible array member.
+ ///
+ /// With that said, we do transmute bytes in this buffer to a
+ /// `RawDirEntry`, which lets us read the members of the struct (including
+ /// the flexible array member) correctly. However, because of that, we need
+ /// to make sure our memory has the correct alignment. Hence, this is why
+ /// we use a raw `*mut u8` created by the std::alloc APIs. If there was an
+ /// easy way to control alignment with a `Vec<u8>`, then we could use that
+ /// instead. (It is indeed possible, but seems fragile.)
+ ///
+ /// Since a `RawDirEntry` is inherently unsafe to use because of its
+ /// flexible array member, it is converted to a `DirEntry` (cheaply,
+ /// without allocation) before being exposed to the caller.
+ raw: NonNull<u8>,
+ /// The lenth, in bytes, of all valid entries in `raw`.
+ len: usize,
+ /// The lenth, in bytes, of `raw`.
+ capacity: usize,
+ /// The current position of this buffer as a pointer into `raw`.
+ cursor: NonNull<u8>,
+}
+
+impl Drop for DirEntryCursor {
+ fn drop(&mut self) {
+ unsafe {
+ dealloc(self.raw.as_ptr(), layout(self.capacity));
+ }
+ }
+}
+
+/// Returns the allocation layout used for constructing the getdents buffer
+/// with the given capacity (in bytes).
+///
+/// This panics if the given length isn't a multiple of the alignment of
+/// `RawDirEntry` or is `0`.
+fn layout(capacity: usize) -> Layout {
+ let align = mem::align_of::<RawDirEntry>();
+ assert!(capacity > 0, "capacity must be greater than 0");
+ assert!(capacity % align == 0, "capacity must be a multiple of alignment");
+ Layout::from_size_align(capacity, align).expect("failed to create Layout")
+}
+
+impl DirEntryCursor {
+ /// Create a new cursor for reading directory entries.
+ ///
+ /// It is beneficial to reuse a cursor in multiple calls to `getdents`. A
+ /// cursor can be used with any number of directories.
+ pub fn new() -> DirEntryCursor {
+ DirEntryCursor::with_capacity(32 * (1 << 10))
+ }
+
+ /// Create a new cursor with the specified capacity. The capacity given
+ /// should be in bytes, and must be a multiple of the alignment of a raw
+ /// directory entry.
+ fn with_capacity(capacity: usize) -> DirEntryCursor {
+ // TODO: It would be nice to expose a way to control the capacity to
+ // the caller, but we'd really like the capacity to be a multiple of
+ // the alignment. (Technically, the only restriction is that
+ // the capacity and the alignment have a least common multiple that
+ // doesn't overflow `usize::MAX`. But requiring the size to be a
+ // multiple of alignment just seems like good sense in this case.)
+ //
+ // Anyway, exposing raw capacity to the caller is weird, because they
+ // shouldn't need to care about the alignment of an internal type.
+ // We *could* expose capacity in "units" of `RawDirEntry` itself, but
+ // even this is somewhat incorrect because the size of `RawDirEntry`
+ // is smaller than what it typically is, since the size doesn't account
+ // for file names. We could just pick a fixed approximate size for
+ // file names and add that to the size of `RawDirEntry`. But let's wait
+ // for a more concrete use case to emerge before exposing anything.
+ let lay = layout(capacity);
+ let raw = match NonNull::new(unsafe { alloc_zeroed(lay) }) {
+ Some(raw) => raw,
+ None => handle_alloc_error(lay),
+ };
+ DirEntryCursor { raw, len: 0, capacity, cursor: raw }
+ }
+
+ /// Read the next directory entry from this cursor. If the cursor has been
+ /// exhausted, then return `None`.
+ ///
+ /// The returned directory entry contains a file name that is borrowed from
+ /// this cursor's internal buffer. In particular, no allocation is
+ /// performed by this routine. If you need an owned directory entry, then
+ /// use `read_unix` or `read_unix_into`.
+ ///
+ /// Note that no filtering of entries (such as `.` and `..`) is performed.
+ pub fn read<'a>(&'a mut self) -> Option<DirEntry<'a>> {
+ if self.cursor.as_ptr() >= self.raw.as_ptr().wrapping_add(self.len) {
+ return None;
+ }
+ // SAFETY: This is safe by the contract of getdents64. Namely, that it
+ // writes structures of type `RawDirEntry` to `raw`. The lifetime of
+ // this raw dirent is also tied to this buffer via the type signature
+ // of this method, which prevents use-after-free. Moreover, our
+ // allocation layout guarantees that the cursor is correctly aligned
+ // for RawDirEntry.
+ let raw_dirent =
+ unsafe { &*(self.cursor.as_ptr() as *const RawDirEntry) };
+ let ent = DirEntry {
+ // SAFETY: This is safe since we are asking for the file name on a
+ // `RawDirEntry` that resides in its original buffer.
+ file_name: unsafe { raw_dirent.file_name() },
+ file_type: raw_dirent.file_type(),
+ ino: raw_dirent.ino(),
+ };
+ // SAFETY: This is safe by the assumption that `d_reclen` on the raw
+ // dirent is correct.
+ self.cursor = unsafe {
+ let next = self.cursor.as_ptr().add(raw_dirent.record_len());
+ NonNull::new_unchecked(next)
+ };
+ Some(ent)
+ }
+
+ /// Read the next directory entry from this cursor as an owned Unix
+ /// `DirEntry`. If the cursor has been exhausted, then return `None`.
+ ///
+ /// This will allocate new space to store the file name in the directory
+ /// entry. To reuse a previous allocation, use `read_unix_into` instead.
+ ///
+ /// Note that no filtering of entries (such as `.` and `..`) is performed.
+ pub fn read_unix(&mut self) -> Option<UnixDirEntry> {
+ self.read().map(|ent| ent.to_unix())
+ }
+
+ /// Read the next directory entry from this cursor into the given Unix
+ /// `DirEntry`. If the cursor has been exhausted, then return `false`.
+ /// Otherwise return `true`.
+ ///
+ /// Note that no filtering of entries (such as `.` and `..`) is performed.
+ pub fn read_unix_into(&mut self, unix_dirent: &mut UnixDirEntry) -> bool {
+ match self.read() {
+ None => false,
+ Some(dent) => {
+ dent.write_to_unix(unix_dirent);
+ true
+ }
+ }
+ }
+
+ /// Rewind this cursor such that it points to the first directory entry.
+ pub fn rewind(&mut self) {
+ self.cursor = self.raw;
+ }
+
+ /// Clear this cursor such that it has no entries.
+ fn clear(&mut self) {
+ self.cursor = self.raw;
+ self.len = 0;
+ }
+}
diff --git a/src/os/mod.rs b/src/os/mod.rs
new file mode 100644
index 0000000..b7a2835
--- /dev/null
+++ b/src/os/mod.rs
@@ -0,0 +1,10 @@
+/*!
+Low level platform specific APIs for reading directory entries.
+*/
+
+#[cfg(target_os = "linux")]
+pub mod linux;
+#[cfg(unix)]
+pub mod unix;
+#[cfg(windows)]
+pub mod windows;
diff --git a/src/os/unix/dirent.rs b/src/os/unix/dirent.rs
new file mode 100644
index 0000000..7c6d14f
--- /dev/null
+++ b/src/os/unix/dirent.rs
@@ -0,0 +1,203 @@
+// It turns out that `dirent` is a complete mess across different platforms.
+// All POSIX defines is a "struct containing the fields d_ino and d_name" where
+// d_name has an unspecified size. And not even that minimal subset is actually
+// portable. For example, DragonflyBSD, FreeBSD, NetBSD and OpenBSD all use
+// `d_fileno` instead of `d_ino`.
+//
+// Some platforms (macOS) have a `d_namlen` field indicating the number of
+// bytes in `d_name`, while other platforms (Linux) only have a `d_reclen`
+// field indicating the total size of the entry.
+//
+// Finally, not every platform (Solaris) has a `d_type` field, which is insane,
+// because that means you need an extra stat call for every directory entry
+// in order to do recursive directory traversal.
+//
+// Rebuilding all this goop that's already done in std really sucks, but if we
+// want to specialize even one platform (e.g., using getdents64 on Linux), then
+// we wind up needing to specialize ALL of them because std::fs::FileType is
+// impossible to either cheaply construct or convert, so we wind up needing to
+// roll our own.
+//
+// So basically what we do here is define a very thin unix-specific but
+// platform independent layer on top of all the different dirent formulations
+// that we support. We try to avoid costs (e.g., NUL-byte scanning) wherever
+// possible.
+
+use std::ffi::CStr;
+use std::fmt;
+use std::ptr::NonNull;
+use std::slice;
+
+use libc::c_char;
+#[cfg(any(
+ target_os = "dragonfly",
+ target_os = "freebsd",
+ target_os = "haiku",
+ target_os = "hermit",
+ target_os = "macos",
+ target_os = "netbsd",
+ target_os = "newlib",
+ target_os = "openbsd",
+ target_os = "solaris",
+))]
+use libc::dirent;
+#[cfg(any(
+ target_os = "android",
+ target_os = "emscripten",
+ target_os = "fuchsia",
+ target_os = "linux",
+))]
+use libc::dirent64 as dirent;
+
+use crate::os::unix::{escaped_bytes, FileType};
+
+/// A low-level Unix-specific but otherwise platform independent API to the
+/// underlying platform's `dirent` struct.
+#[derive(Clone)]
+pub struct RawDirEntry(NonNull<dirent>);
+
+impl fmt::Debug for RawDirEntry {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.debug_struct("RawDirEntry")
+ .field("d_name", &escaped_bytes(self.file_name().to_bytes()))
+ .field("d_type", &self.file_type())
+ .field("d_ino", &self.ino())
+ .finish()
+ }
+}
+
+impl RawDirEntry {
+ /// Create a new raw directory entry from the given dirent structure.
+ ///
+ /// If the given entry is null, then this returns `None`.
+ pub fn new(ent: *const dirent) -> Option<RawDirEntry> {
+ NonNull::new(ent as *mut _).map(RawDirEntry)
+ }
+
+ /// Return the underlying dirent.
+ pub fn dirent(&self) -> &dirent {
+ // SAFETY: This is safe since we tie the lifetime of the returned
+ // dirent to self.
+ unsafe { self.0.as_ref() }
+ }
+
+ /// Return the file name in this directory entry as a C string.
+ pub fn file_name(&self) -> &CStr {
+ // This implementation uses namlen to determine the size of file name.
+ #[cfg(any(
+ target_os = "dragonfly",
+ target_os = "freebsd",
+ target_os = "macos",
+ target_os = "netbsd",
+ target_os = "openbsd",
+ ))]
+ fn imp(ent: &RawDirEntry) -> &CStr {
+ // SAFETY: This is safe given the guarantee that `d_namlen` is the
+ // total number of bytes in the file name, minus the NUL
+ // terminator. This is also only safe given the guarantee that
+ // `d_name` contains a NUL terminator.
+ unsafe {
+ let bytes = slice::from_raw_parts(
+ ent.dirent().d_name.as_ptr() as *const u8,
+ ent.dirent().d_namlen as usize + 1, // +1 for NUL
+ );
+ CStr::from_bytes_with_nul_unchecked(bytes)
+ }
+ }
+
+ // This implementation uses strlen to determine the size of the file
+ // name, since these platforms don't have a `d_namlen` field. Some of
+ // them *do* have a `d_reclen` field, which seems like it could help
+ // us, but there is no clear documentation on how to use it properly.
+ // In particular, it can include the padding between the directory
+ // entries, and it's not clear how to account for that.
+ #[cfg(any(
+ target_os = "android",
+ target_os = "emscripten",
+ target_os = "fuchsia",
+ target_os = "haiku",
+ target_os = "hermit",
+ target_os = "linux",
+ target_os = "solaris",
+ ))]
+ fn imp(ent: &RawDirEntry) -> &CStr {
+ // SAFETY: This is safe since `d_name` is guaranteed to be valid
+ // and guaranteed to contain a NUL terminator.
+ unsafe {
+ CStr::from_ptr(ent.dirent().d_name.as_ptr() as *const c_char)
+ }
+ }
+
+ imp(self)
+ }
+
+ /// Return the file type embedded in this directory entry, if one exists.
+ ///
+ /// If this platform doesn't support reporting the file type in the
+ /// directory entry, or if the file type was reported as unknown, then
+ /// this returns `None`.
+ pub fn file_type(&self) -> Option<FileType> {
+ // This implementation uses the `d_type` field.
+ //
+ // Note that this can still return None if the value of this field
+ // is DT_UNKNOWN.
+ #[cfg(any(
+ target_os = "android",
+ target_os = "emscripten",
+ target_os = "dragonfly",
+ target_os = "freebsd",
+ target_os = "fuchsia",
+ target_os = "linux",
+ target_os = "macos",
+ target_os = "netbsd",
+ target_os = "openbsd",
+ ))]
+ fn imp(ent: &RawDirEntry) -> Option<FileType> {
+ FileType::from_dirent_type(ent.dirent().d_type)
+ }
+
+ // No `d_type` field is available here, so always return None.
+ #[cfg(any(
+ target_os = "haiku",
+ target_os = "hermit",
+ target_os = "solaris",
+ ))]
+ fn imp(ent: &RawDirEntry) -> Option<FileType> {
+ None
+ }
+
+ imp(self)
+ }
+
+ /// Return the file serial number for this directory entry.
+ pub fn ino(&self) -> u64 {
+ // This implementation uses the d_fileno field.
+ #[cfg(any(
+ target_os = "dragonfly",
+ target_os = "freebsd",
+ target_os = "netbsd",
+ target_os = "openbsd",
+ ))]
+ fn imp(ent: &RawDirEntry) -> u64 {
+ ent.dirent().d_fileno as u64
+ }
+
+ // This implementation uses the d_ino field.
+ #[cfg(any(
+ target_os = "android",
+ target_os = "emscripten",
+ target_os = "fuchsia",
+ target_os = "macos",
+ target_os = "haiku",
+ target_os = "hermit",
+ target_os = "linux",
+ target_os = "solaris",
+ target_os = "dragonfly",
+ ))]
+ fn imp(ent: &RawDirEntry) -> u64 {
+ ent.dirent().d_ino as u64
+ }
+
+ imp(self)
+ }
+}
diff --git a/src/os/unix/errno-dragonfly.c b/src/os/unix/errno-dragonfly.c
new file mode 100644
index 0000000..dfbe9e5
--- /dev/null
+++ b/src/os/unix/errno-dragonfly.c
@@ -0,0 +1,5 @@
+#include <errno.h>
+
+int *errno_location() {
+ return &errno;
+}
diff --git a/src/os/unix/errno.rs b/src/os/unix/errno.rs
new file mode 100644
index 0000000..6e163f3
--- /dev/null
+++ b/src/os/unix/errno.rs
@@ -0,0 +1,48 @@
+// This was mostly lifted from the standard library's sys module. The main
+// difference is that we need to use a C shim to get access to errno on
+// DragonflyBSD, since the #[thread_local] attribute isn't stable (as of Rust
+// 1.34).
+
+use libc::c_int;
+
+extern "C" {
+ #[cfg_attr(
+ any(
+ target_os = "linux",
+ target_os = "emscripten",
+ target_os = "fuchsia",
+ target_os = "l4re",
+ ),
+ link_name = "__errno_location"
+ )]
+ #[cfg_attr(
+ any(
+ target_os = "bitrig",
+ target_os = "netbsd",
+ target_os = "openbsd",
+ target_os = "android",
+ target_os = "hermit",
+ ),
+ link_name = "__errno"
+ )]
+ #[cfg_attr(target_os = "dragonfly", link_name = "errno_location")]
+ #[cfg_attr(target_os = "solaris", link_name = "___errno")]
+ #[cfg_attr(
+ any(target_os = "macos", target_os = "ios", target_os = "freebsd",),
+ link_name = "__error"
+ )]
+ #[cfg_attr(target_os = "haiku", link_name = "_errnop")]
+ fn errno_location() -> *mut c_int;
+}
+
+/// Returns the platform-specific value of errno.
+pub fn errno() -> i32 {
+ unsafe { (*errno_location()) as i32 }
+}
+
+/// Clears the platform-specific value of errno to 0.
+pub fn clear() {
+ unsafe {
+ *errno_location() = 0;
+ }
+}
diff --git a/src/os/unix/mod.rs b/src/os/unix/mod.rs
new file mode 100644
index 0000000..694f5ac
--- /dev/null
+++ b/src/os/unix/mod.rs
@@ -0,0 +1,624 @@
+/*!
+Low level Unix specific APIs for reading directory entries via `readdir`.
+*/
+
+use std::ffi::{CStr, CString, OsStr, OsString};
+use std::fmt;
+use std::fs::File;
+use std::io;
+use std::mem;
+use std::os::unix::ffi::{OsStrExt, OsStringExt};
+use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd, RawFd};
+use std::path::PathBuf;
+use std::ptr::NonNull;
+
+use libc;
+#[cfg(any(
+ target_os = "dragonfly",
+ target_os = "freebsd",
+ target_os = "haiku",
+ target_os = "hermit",
+ target_os = "macos",
+ target_os = "netbsd",
+ target_os = "newlib",
+ target_os = "openbsd",
+ target_os = "solaris",
+))]
+use libc::readdir;
+#[cfg(any(
+ target_os = "android",
+ target_os = "emscripten",
+ target_os = "fuchsia",
+ target_os = "linux",
+))]
+use libc::readdir64 as readdir;
+
+#[cfg(target_os = "linux")]
+use crate::os::linux::DirEntry as LinuxDirEntry;
+use crate::os::unix::dirent::RawDirEntry;
+
+mod dirent;
+pub(crate) mod errno;
+
+/// A low-level Unix specific directory entry.
+///
+/// This type corresponds as closely as possible to the `dirent` structure
+/// found on Unix-like platforms. It exposes the underlying file name, file
+/// serial number, and, on platforms that support it, the file type.
+///
+/// All methods on this directory entry have zero cost. That is, no allocations
+/// or syscalls are performed.
+#[derive(Clone)]
+pub struct DirEntry {
+ /// A copy of the file name contents from the raw dirent, represented as a
+ /// NUL terminated C string. We use a Vec<u8> here instead of a `CString`
+ /// because it makes it easier to correctly amortize allocation, and keep
+ /// track of the correct length of the string without needing to recompute
+ /// it.
+ ///
+ /// Note that this has to be a copy since the lifetime of `d_name` from
+ /// `struct dirent *` is not guaranteed to last beyond the next call to
+ /// `readdir`.
+ file_name: Vec<u8>,
+ /// The file type, as is, from the raw dirent.
+ file_type: Option<FileType>,
+ /// The file serial number, as is, from the raw dirent.
+ ino: u64,
+}
+
+impl fmt::Debug for DirEntry {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.debug_struct("DirEntry")
+ .field("file_name", &escaped_bytes(self.file_name_bytes()))
+ .field("file_type", &self.file_type)
+ .field("ino", &self.ino)
+ .finish()
+ }
+}
+
+impl DirEntry {
+ /// Read the contents of the given raw Unix/POSIX directory entry into this
+ /// entry.
+ #[inline]
+ fn from_unix_raw(&mut self, raw: &RawDirEntry) {
+ self.file_type = raw.file_type();
+ self.ino = raw.ino();
+
+ let bytes = raw.file_name().to_bytes_with_nul();
+ self.file_name.resize(bytes.len(), 0);
+ self.file_name.copy_from_slice(bytes);
+ }
+
+ /// Read the contents of the given raw Unix/POSIX directory entry into this
+ /// entry.
+ #[cfg(target_os = "linux")]
+ #[inline]
+ pub(crate) fn from_linux_raw(&mut self, raw: &LinuxDirEntry) {
+ self.file_type = raw.file_type();
+ self.ino = raw.ino();
+
+ let bytes = raw.file_name().to_bytes_with_nul();
+ self.file_name.resize(bytes.len(), 0);
+ self.file_name.copy_from_slice(bytes);
+ }
+
+ /// Create a new empty directory entry.
+ ///
+ /// For an empty directory entry, the file name is empty, the file type is
+ /// `None` and the inode number is `0`.
+ ///
+ /// This is useful for creating space for using `Dir::read_into`.
+ #[inline]
+ pub fn empty() -> DirEntry {
+ DirEntry { file_name: vec![0], file_type: None, ino: 0 }
+ }
+
+ /// Return the file name in this directory entry as a C string.
+ #[inline]
+ pub fn file_name(&self) -> &CStr {
+ // SAFETY: file_name is always a normal NUL terminated C string.
+ // We just represent it as a Vec<u8> to make amortizing allocation
+ // easier.
+ unsafe { CStr::from_bytes_with_nul_unchecked(&self.file_name) }
+ }
+
+ /// Consume this directory entry and return the underlying C string.
+ #[inline]
+ pub fn into_file_name(mut self) -> CString {
+ // SAFETY: file_name is always a normal NUL terminated C string.
+ // We just represent it as a Vec<u8> to make amortizing allocation
+ // easier.
+ unsafe {
+ // There's no way to build a CString from a Vec with zero overhead.
+ // Namely, from_vec_unchecked actually adds a NUL byte. Since we
+ // already have one, pop it.
+ //
+ // FIXME: CString really should have a from_vec_with_nul_unchecked
+ // routine like CStr.
+ self.file_name.pop().expect("a NUL byte");
+ CString::from_vec_unchecked(self.file_name)
+ }
+ }
+
+ /// Return the file name in this directory entry as raw bytes without
+ /// a `NUL` terminator.
+ #[inline]
+ pub fn file_name_bytes(&self) -> &[u8] {
+ &self.file_name[..self.file_name.len() - 1]
+ }
+
+ /// Consume this directory entry and return the underlying bytes without
+ /// a `NUL` terminator.
+ #[inline]
+ pub fn into_file_name_bytes(mut self) -> Vec<u8> {
+ self.file_name.pop().expect("a NUL terminator");
+ self.file_name
+ }
+
+ /// Return the file name in this directory entry as an OS string. The
+ /// string returned does not contain a `NUL` terminator.
+ #[inline]
+ pub fn file_name_os(&self) -> &OsStr {
+ OsStr::from_bytes(self.file_name_bytes())
+ }
+
+ /// Consume this directory entry and return its file name as an OS string
+ /// without a `NUL` terminator.
+ #[inline]
+ pub fn into_file_name_os(self) -> OsString {
+ OsString::from_vec(self.into_file_name_bytes())
+ }
+
+ /// Return the file type of this directory entry, if one exists.
+ ///
+ /// A file type may not exist if the underlying file system reports an
+ /// unknown file type in the directory entry, or if the platform does not
+ /// support reporting the file type in the directory entry at all.
+ #[inline]
+ pub fn file_type(&self) -> Option<FileType> {
+ self.file_type
+ }
+
+ /// Returns the underlying file serial number for this directory entry.
+ #[inline]
+ pub fn ino(&self) -> u64 {
+ self.ino
+ }
+}
+
+/// A file descriptor opened as a directory.
+///
+/// The file descriptor is automatically closed when it's dropped.
+#[derive(Debug)]
+pub struct DirFd(RawFd);
+
+unsafe impl Send for DirFd {}
+
+impl Drop for DirFd {
+ fn drop(&mut self) {
+ unsafe {
+ // Explicitly ignore the error here if one occurs. To get an error
+ // when closing, use DirFd::close.
+ libc::close(self.0);
+ }
+ }
+}
+
+impl AsRawFd for DirFd {
+ fn as_raw_fd(&self) -> RawFd {
+ self.0
+ }
+}
+
+impl IntoRawFd for DirFd {
+ fn into_raw_fd(self) -> RawFd {
+ let fd = self.0;
+ mem::forget(self);
+ fd
+ }
+}
+
+impl FromRawFd for DirFd {
+ unsafe fn from_raw_fd(fd: RawFd) -> DirFd {
+ DirFd(fd)
+ }
+}
+
+impl io::Seek for DirFd {
+ fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
+ let mut file = unsafe { File::from_raw_fd(self.0) };
+ let res = file.seek(pos);
+ file.into_raw_fd();
+ res
+ }
+}
+
+impl DirFd {
+ /// Open a file descriptor for the given directory path.
+ ///
+ /// If there was a problem opening the directory, or if the given path
+ /// contains a `NUL` byte, then an error is returned.
+ ///
+ /// If possible, prefer using `openat` since it is generally faster.
+ pub fn open<P: Into<PathBuf>>(dir_path: P) -> io::Result<DirFd> {
+ let bytes = dir_path.into().into_os_string().into_vec();
+ DirFd::open_c(&CString::new(bytes)?)
+ }
+
+ /// Open a file descriptor for the given directory path.
+ ///
+ /// This is just like `DirFd::open`, except it accepts a pre-made C string.
+ /// As such, this only returns an error when opening the directory fails.
+ pub fn open_c(dir_path: &CStr) -> io::Result<DirFd> {
+ let flags = libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC;
+ // SAFETY: This is safe since we've guaranteed that cstr has no
+ // interior NUL bytes and is terminated by a NUL.
+ let fd = unsafe { libc::open(dir_path.as_ptr(), flags) };
+ if fd < 0 {
+ Err(io::Error::last_os_error())
+ } else {
+ Ok(DirFd(fd))
+ }
+ }
+
+ /// Open a file descriptor for the given directory name, where the given
+ /// file descriptor (`parent_dirfd`) corresponds to the parent directory
+ /// of the given name.
+ ///
+ /// One should prefer using this in lieu of `open` when possible, since it
+ /// should generally be faster (but does of course require having an open
+ /// file descriptor to the parent directory).
+ ///
+ /// If there was a problem opening the directory, or if the given path
+ /// contains a `NUL` byte, then an error is returned.
+ pub fn openat<D: Into<OsString>>(
+ parent_dirfd: RawFd,
+ dir_name: D,
+ ) -> io::Result<DirFd> {
+ DirFd::openat_c(
+ parent_dirfd,
+ &CString::new(dir_name.into().into_vec())?,
+ )
+ }
+
+ /// Open a file descriptor for the given directory name, where the given
+ /// file descriptor (`parent_dirfd`) corresponds to the parent directory
+ /// of the given name.
+ ///
+ /// This is just like `DirFd::openat`, except it accepts a pre-made C
+ /// string. As such, this only returns an error when opening the directory
+ /// fails.
+ pub fn openat_c(
+ parent_dirfd: RawFd,
+ dir_name: &CStr,
+ ) -> io::Result<DirFd> {
+ let flags = libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC;
+ // SAFETY: This is safe since we've guaranteed that cstr has no
+ // interior NUL bytes and is terminated by a NUL.
+ let fd =
+ unsafe { libc::openat(parent_dirfd, dir_name.as_ptr(), flags) };
+ if fd < 0 {
+ Err(io::Error::last_os_error())
+ } else {
+ Ok(DirFd(fd))
+ }
+ }
+
+ /// Close this directory file descriptor and return an error if closing
+ /// failed.
+ ///
+ /// Note that this does not need to be called explicitly. This directory
+ /// file descriptor will be closed automatically when it is dropped (and
+ /// if an error occurs, it is ignored). This routine is only useful if you
+ /// want to explicitly close the directory file descriptor and check the
+ /// error.
+ pub fn close(self) -> io::Result<()> {
+ let res = if unsafe { libc::close(self.0) } < 0 {
+ Err(io::Error::last_os_error())
+ } else {
+ Ok(())
+ };
+ // Don't drop DirFd after we've explicitly closed the dir stream to
+ // avoid running close again.
+ mem::forget(self);
+ res
+ }
+}
+
+/// A handle to a directory stream.
+///
+/// The handle is automatically closed when it's dropped.
+#[derive(Debug)]
+pub struct Dir(NonNull<libc::DIR>);
+
+unsafe impl Send for Dir {}
+
+impl Drop for Dir {
+ fn drop(&mut self) {
+ unsafe {
+ // Explicitly ignore the error here if one occurs. To get an error
+ // when closing, use Dir::close.
+ libc::closedir(self.0.as_ptr());
+ }
+ }
+}
+
+impl AsRawFd for Dir {
+ fn as_raw_fd(&self) -> RawFd {
+ // It's possible for this to return an error according to POSIX, but I
+ // guess we just ignore it. In particular, it looks like common
+ // implementations (e.g., Linux) do not actually ever return an error.
+ unsafe { libc::dirfd(self.0.as_ptr()) }
+ }
+}
+
+impl IntoRawFd for Dir {
+ fn into_raw_fd(self) -> RawFd {
+ let fd = self.as_raw_fd();
+ mem::forget(self);
+ fd
+ }
+}
+
+impl FromRawFd for Dir {
+ unsafe fn from_raw_fd(fd: RawFd) -> Dir {
+ match NonNull::new(unsafe { libc::fdopendir(fd) }) {
+ Some(dir) => Dir(dir),
+ None => panic!(
+ "failed to create libc::DIR from file descriptor: {}",
+ io::Error::last_os_error()
+ ),
+ }
+ }
+}
+
+impl Dir {
+ /// Open a handle to a directory stream for the given directory path.
+ ///
+ /// If there was a problem opening the directory stream, or if the given
+ /// path contains a `NUL` byte, then an error is returned.
+ ///
+ /// If possible, prefer using `openat` since it is generally faster.
+ pub fn open<P: Into<PathBuf>>(dir_path: P) -> io::Result<Dir> {
+ let bytes = dir_path.into().into_os_string().into_vec();
+ Dir::open_c(&CString::new(bytes)?)
+ }
+
+ /// Open a handle to a directory stream for the given directory path.
+ ///
+ /// This is just like `Dir::open`, except it accepts a pre-made C string.
+ /// As such, this only returns an error when opening the directory stream
+ /// fails.
+ pub fn open_c(dir_path: &CStr) -> io::Result<Dir> {
+ // SAFETY: This is safe since we've guaranteed that cstr has no
+ // interior NUL bytes and is terminated by a NUL.
+ match NonNull::new(unsafe { libc::opendir(dir_path.as_ptr()) }) {
+ None => Err(io::Error::last_os_error()),
+ Some(dir) => Ok(Dir(dir)),
+ }
+ }
+
+ /// Open a handle to a directory stream for the given directory name, where
+ /// the file descriptor corresponds to the parent directory of the given
+ /// name.
+ ///
+ /// One should prefer using this in lieu of `open` when possible, since it
+ /// should generally be faster (but does of course require having an open
+ /// file descriptor to the parent directory).
+ ///
+ /// If there was a problem opening the directory stream, or if the given
+ /// path contains a `NUL` byte, then an error is returned.
+ pub fn openat<D: Into<OsString>>(
+ parent_dirfd: RawFd,
+ dir_name: D,
+ ) -> io::Result<Dir> {
+ Dir::openat_c(parent_dirfd, &CString::new(dir_name.into().into_vec())?)
+ }
+
+ /// Open a handle to a directory stream for the given directory name, where
+ /// the file descriptor corresponds to the parent directory of the given
+ /// name.
+ ///
+ /// This is just like `Dir::openat`, except it accepts a pre-made C string
+ /// for the directory name. As such, this only returns an error when
+ /// opening the directory stream fails.
+ pub fn openat_c(parent_dirfd: RawFd, dir_name: &CStr) -> io::Result<Dir> {
+ let dirfd = DirFd::openat_c(parent_dirfd, dir_name)?;
+ // SAFETY: fd is a valid file descriptor, per the above check.
+ match NonNull::new(unsafe { libc::fdopendir(dirfd.into_raw_fd()) }) {
+ None => Err(io::Error::last_os_error()),
+ Some(dir) => Ok(Dir(dir)),
+ }
+ }
+
+ /// Read the next directory entry from this stream.
+ ///
+ /// This returns `None` when no more directory entries could be read.
+ ///
+ /// If there was a problem reading the next directory entry, then an error
+ /// is returned. When an error occurs, callers can still continue to read
+ /// subsequent directory entries.
+ ///
+ /// Note that no filtering of entries (such as `.` and `..`) is performed.
+ pub fn read(&mut self) -> Option<io::Result<DirEntry>> {
+ let mut ent = DirEntry::empty();
+ match self.read_into(&mut ent) {
+ Ok(true) => Some(Ok(ent)),
+ Ok(false) => None,
+ Err(err) => Some(Err(err)),
+ }
+ }
+
+ /// Read the next directory entry from this stream into the given space.
+ ///
+ /// This returns false when no more directory entries could be read.
+ ///
+ /// If there was a problem reading the next directory entry, then an error
+ /// is returned. When an error occurs, callers can still continue to read
+ /// subsequent directory entries.
+ ///
+ /// The contents of `ent` when the end of the stream has been reached or
+ /// when an error occurs are unspecified.
+ ///
+ /// Note that no filtering of entries (such as `.` and `..`) is performed.
+ pub fn read_into(&mut self, ent: &mut DirEntry) -> io::Result<bool> {
+ // We need to clear the errno because it's the only way to
+ // differentiate errors and end-of-stream. (Since both return a NULL
+ // dirent.)
+ //
+ // TODO: It might be worth experimenting with readdir_r, but note that
+ // it is deprecated on Linux, and is presumably going to be deprecated
+ // in POSIX. The idea is that readdir is supposed to be reentrant these
+ // days. readdir_r does have some of its own interesting problems
+ // associated with it. See readdir_r(3) on Linux.
+ errno::clear();
+ match RawDirEntry::new(unsafe { readdir(self.0.as_ptr()) }) {
+ Some(rawent) => {
+ ent.from_unix_raw(&rawent);
+ Ok(true)
+ }
+ None => {
+ if errno::errno() != 0 {
+ Err(io::Error::last_os_error())
+ } else {
+ Ok(false)
+ }
+ }
+ }
+ }
+
+ /// Rewind this directory stream such that it restarts back at the
+ /// beginning of the directory.
+ pub fn rewind(&mut self) {
+ unsafe {
+ libc::rewinddir(self.0.as_ptr());
+ }
+ }
+
+ /// Close this directory stream and return an error if closing failed.
+ ///
+ /// Note that this does not need to be called explicitly. This directory
+ /// stream will be closed automatically when it is dropped (and if an error
+ /// occurs, it is ignored). This routine is only useful if you want to
+ /// explicitly close the directory stream and check the error.
+ pub fn close(self) -> io::Result<()> {
+ let res = if unsafe { libc::closedir(self.0.as_ptr()) } < 0 {
+ Err(io::Error::last_os_error())
+ } else {
+ Ok(())
+ };
+ // Don't drop Dir after we've explicitly closed the dir stream to
+ // avoid running close again.
+ mem::forget(self);
+ res
+ }
+}
+
+/// One of seven possible file types on Unix.
+#[derive(Clone, Copy)]
+pub struct FileType(libc::mode_t);
+
+impl fmt::Debug for FileType {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let human = if self.is_file() {
+ "File"
+ } else if self.is_dir() {
+ "Directory"
+ } else if self.is_symlink() {
+ "Symbolic Link"
+ } else if self.is_block_device() {
+ "Block Device"
+ } else if self.is_char_device() {
+ "Char Device"
+ } else if self.is_fifo() {
+ "FIFO"
+ } else if self.is_socket() {
+ "Socket"
+ } else {
+ "Unknown"
+ };
+ write!(f, "FileType({})", human)
+ }
+}
+
+impl FileType {
+ /// Create a new file type from a directory entry's type field.
+ ///
+ /// If the given type is not recognized or is `DT_UNKNOWN`, then `None`
+ /// is returned.
+ pub fn from_dirent_type(d_type: u8) -> Option<FileType> {
+ Some(FileType(match d_type {
+ libc::DT_REG => libc::S_IFREG,
+ libc::DT_DIR => libc::S_IFDIR,
+ libc::DT_LNK => libc::S_IFLNK,
+ libc::DT_BLK => libc::S_IFBLK,
+ libc::DT_CHR => libc::S_IFCHR,
+ libc::DT_FIFO => libc::S_IFIFO,
+ libc::DT_SOCK => libc::S_IFSOCK,
+ libc::DT_UNKNOWN => return None,
+ _ => return None, // wat?
+ }))
+ }
+
+ /// Create a new file type from a stat's `st_mode` field.
+ pub fn from_stat_mode(st_mode: u64) -> FileType {
+ FileType(st_mode as libc::mode_t)
+ }
+
+ /// Returns true if this file type is a regular file.
+ ///
+ /// This corresponds to the `S_IFREG` value on Unix.
+ pub fn is_file(&self) -> bool {
+ self.0 & libc::S_IFMT == libc::S_IFREG
+ }
+
+ /// Returns true if this file type is a directory.
+ ///
+ /// This corresponds to the `S_IFDIR` value on Unix.
+ pub fn is_dir(&self) -> bool {
+ self.0 & libc::S_IFMT == libc::S_IFDIR
+ }
+
+ /// Returns true if this file type is a symbolic link.
+ ///
+ /// This corresponds to the `S_IFLNK` value on Unix.
+ pub fn is_symlink(&self) -> bool {
+ self.0 & libc::S_IFMT == libc::S_IFLNK
+ }
+
+ /// Returns true if this file type is a block device.
+ ///
+ /// This corresponds to the `S_IFBLK` value on Unix.
+ pub fn is_block_device(&self) -> bool {
+ self.0 & libc::S_IFMT == libc::S_IFBLK
+ }
+
+ /// Returns true if this file type is a character device.
+ ///
+ /// This corresponds to the `S_IFCHR` value on Unix.
+ pub fn is_char_device(&self) -> bool {
+ self.0 & libc::S_IFMT == libc::S_IFCHR
+ }
+
+ /// Returns true if this file type is a FIFO.
+ ///
+ /// This corresponds to the `S_IFIFO` value on Unix.
+ pub fn is_fifo(&self) -> bool {
+ self.0 & libc::S_IFMT == libc::S_IFIFO
+ }
+
+ /// Returns true if this file type is a socket.
+ ///
+ /// This corresponds to the `S_IFSOCK` value on Unix.
+ pub fn is_socket(&self) -> bool {
+ self.0 & libc::S_IFMT == libc::S_IFSOCK
+ }
+}
+
+/// Return a convenience ASCII-only debug representation of the given bytes.
+/// In essence, non-ASCII and non-printable bytes are escaped.
+pub(crate) fn escaped_bytes(bytes: &[u8]) -> String {
+ use std::ascii::escape_default;
+
+ bytes.iter().cloned().flat_map(escape_default).map(|b| b as char).collect()
+}
diff --git a/src/os/windows/mod.rs b/src/os/windows/mod.rs
new file mode 100644
index 0000000..d144f98
--- /dev/null
+++ b/src/os/windows/mod.rs
@@ -0,0 +1,519 @@
+/*!
+Low level Windows specific APIs for reading directory entries via
+`FindNextFile`.
+*/
+
+use std::char;
+use std::ffi::{OsStr, OsString};
+use std::fmt;
+use std::io;
+use std::mem;
+use std::os::windows::ffi::{OsStrExt, OsStringExt};
+use std::path::Path;
+
+use winapi::shared::minwindef::{DWORD, FILETIME};
+use winapi::shared::winerror::ERROR_NO_MORE_FILES;
+use winapi::um::errhandlingapi::GetLastError;
+use winapi::um::fileapi::{FindClose, FindFirstFileW, FindNextFileW};
+use winapi::um::handleapi::INVALID_HANDLE_VALUE;
+use winapi::um::minwinbase::WIN32_FIND_DATAW;
+use winapi::um::winnt::HANDLE;
+
+/// A low-level Windows specific directory entry.
+///
+/// This type corresponds as closely as possible to the `WIN32_FIND_DATA`
+/// structure found on Windows platforms. It exposes the underlying file name,
+/// raw file attributions, time information and file size. Notably, this is
+/// quite a bit more information than Unix APIs, which typically only expose
+/// the file name, file serial number, and in most cases, the file type.
+///
+/// All methods on this directory entry have zero cost. That is, no allocations
+/// or syscalls are performed.
+#[derive(Clone, Debug)]
+pub struct DirEntry {
+ attr: DWORD,
+ creation_time: u64,
+ last_access_time: u64,
+ last_write_time: u64,
+ file_size: u64,
+ file_type: FileType,
+ /// The file name converted to an OsString (using WTF-8 internally).
+ file_name: OsString,
+ /// The raw 16-bit code units that make up a file name in Windows. This
+ /// does not include the NUL terminator.
+ file_name_u16: Vec<u16>,
+}
+
+impl DirEntry {
+ #[inline]
+ fn from_find_data(&mut self, fd: &FindData) {
+ self.attr = fd.0.dwFileAttributes;
+ self.creation_time = fd.creation_time();
+ self.last_access_time = fd.last_access_time();
+ self.last_write_time = fd.last_write_time();
+ self.file_size = fd.file_size();
+ self.file_type = FileType::from_attr(self.attr, fd.0.dwReserved0);
+
+ self.file_name.clear();
+ self.file_name_u16.clear();
+ fd.decode_file_names_into(
+ &mut self.file_name,
+ &mut self.file_name_u16,
+ );
+ }
+
+ /// Create a new empty directory entry.
+ ///
+ /// For an empty directory entry, the file name is empty, the file
+ /// type returns `true` for `is_file` and `false` for all other public
+ /// predicates, and the rest of the public API methods on a `DirEntry`
+ /// return `0`.
+ ///
+ /// This is useful for creating for using `FindHandle::read_into`.
+ #[inline]
+ pub fn empty() -> DirEntry {
+ DirEntry {
+ attr: 0,
+ creation_time: 0,
+ last_access_time: 0,
+ last_write_time: 0,
+ file_size: 0,
+ file_type: FileType::from_attr(0, 0),
+ file_name: OsString::new(),
+ file_name_u16: vec![],
+ }
+ }
+
+ /// Return the raw file attributes reported in this directory entry.
+ ///
+ /// The value returned directly corresponds to the `dwFileAttributes`
+ /// member of the `WIN32_FIND_DATA` structure.
+ #[inline]
+ pub fn file_attributes(&self) -> u32 {
+ self.attr
+ }
+
+ /// Return a 64-bit representation of the creation time of the underlying
+ /// file.
+ ///
+ /// The 64-bit value returned is equivalent to winapi's `FILETIME`
+ /// structure, which represents the number of 100-nanosecond intervals
+ /// since January 1, 1601 (UTC).
+ ///
+ /// If the underlying file system does not support creation time, then
+ /// `0` is returned.
+ #[inline]
+ pub fn creation_time(&self) -> u64 {
+ self.creation_time
+ }
+
+ /// Return a 64-bit representation of the last access time of the
+ /// underlying file.
+ ///
+ /// The 64-bit value returned is equivalent to winapi's `FILETIME`
+ /// structure, which represents the number of 100-nanosecond intervals
+ /// since January 1, 1601 (UTC).
+ ///
+ /// If the underlying file system does not support last access time, then
+ /// `0` is returned.
+ #[inline]
+ pub fn last_access_time(&self) -> u64 {
+ self.last_access_time
+ }
+
+ /// Return a 64-bit representation of the last write time of the
+ /// underlying file.
+ ///
+ /// The 64-bit value returned is equivalent to winapi's `FILETIME`
+ /// structure, which represents the number of 100-nanosecond intervals
+ /// since January 1, 1601 (UTC).
+ ///
+ /// If the underlying file system does not support last write time, then
+ /// `0` is returned.
+ #[inline]
+ pub fn last_write_time(&self) -> u64 {
+ self.last_write_time
+ }
+
+ /// Return the file size, in bytes, of the corresponding file.
+ ///
+ /// This value has no meaning if this entry corresponds to a directory.
+ #[inline]
+ pub fn file_size(&self) -> u64 {
+ self.file_size
+ }
+
+ /// Return the file type of this directory entry.
+ #[inline]
+ pub fn file_type(&self) -> FileType {
+ self.file_type
+ }
+
+ /// Return the file name in this directory entry as an OS string.
+ #[inline]
+ pub fn file_name_os(&self) -> &OsStr {
+ &self.file_name
+ }
+
+ /// Return the file name in this directory entry in its original form as
+ /// a sequence of 16-bit code units.
+ ///
+ /// The sequence returned is not guaranteed to be valid UTF-16.
+ #[inline]
+ pub fn file_name_u16(&self) -> &[u16] {
+ &self.file_name_u16
+ }
+
+ /// Consume this directory entry and return its file name as an OS string.
+ #[inline]
+ pub fn into_file_name_os(self) -> OsString {
+ self.file_name
+ }
+
+ /// Consume this directory entry and return its file name in its original
+ /// form as a sequence of 16-bit code units.
+ ///
+ /// The sequence returned is not guaranteed to be valid UTF-16.
+ #[inline]
+ pub fn into_file_name_u16(self) -> Vec<u16> {
+ self.file_name_u16
+ }
+}
+
+/// File type information discoverable from the `FindNextFile` winapi routines.
+///
+/// Note that this does not include all possible file types on Windows.
+/// Instead, this only differentiates between directories, regular files and
+/// symlinks. Additional file type information (such as whether a file handle
+/// is a socket) can only be retrieved via the `GetFileType` winapi routines.
+/// A safe wrapper for it is
+/// [available in the `winapi-util` crate](https://docs.rs/winapi-util/*/x86_64-pc-windows-msvc/winapi_util/file/fn.typ.html).
+#[derive(Clone, Copy)]
+pub struct FileType {
+ attr: DWORD,
+ reparse_tag: DWORD,
+}
+
+impl fmt::Debug for FileType {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let human = if self.is_file() {
+ "File"
+ } else if self.is_dir() {
+ "Directory"
+ } else if self.is_symlink_file() {
+ "Symbolic Link (File)"
+ } else if self.is_symlink_dir() {
+ "Symbolic Link (Directory)"
+ } else {
+ "Unknown"
+ };
+ write!(f, "FileType({})", human)
+ }
+}
+
+impl FileType {
+ /// Create a file type from its raw winapi components.
+ ///
+ /// `attr` should be a file attribute bitset, corresponding to the
+ /// `dwFileAttributes` member of file information structs.
+ ///
+ /// `reparse_tag` should be a valid reparse tag value when the
+ /// `FILE_ATTRIBUTE_REPARSE_POINT` bit is set in `attr`. If the bit isn't
+ /// set or if the tag is not available, then the tag can be any value.
+ pub fn from_attr(attr: u32, reparse_tag: u32) -> FileType {
+ FileType { attr: attr, reparse_tag: reparse_tag }
+ }
+
+ /// Returns true if this file type is a regular file.
+ ///
+ /// This corresponds to any file that is neither a symlink nor a directory.
+ pub fn is_file(&self) -> bool {
+ !self.is_dir() && !self.is_symlink()
+ }
+
+ /// Returns true if this file type is a directory.
+ ///
+ /// This corresponds to any file that has the `FILE_ATTRIBUTE_DIRECTORY`
+ /// attribute and is not a symlink.
+ pub fn is_dir(&self) -> bool {
+ use winapi::um::winnt::FILE_ATTRIBUTE_DIRECTORY;
+
+ self.attr & FILE_ATTRIBUTE_DIRECTORY != 0 && !self.is_symlink()
+ }
+
+ /// Returns true if this file type is a symlink. This could be a symlink
+ /// to a directory or to a file. To distinguish between them, use
+ /// `is_symlink_file` and `is_symlink_dir`.
+ ///
+ /// This corresponds to any file that has a surrogate reparse point.
+ pub fn is_symlink(&self) -> bool {
+ use winapi::um::winnt::IsReparseTagNameSurrogate;
+
+ self.reparse_tag().map_or(false, IsReparseTagNameSurrogate)
+ }
+
+ /// Returns true if this file type is a symlink to a file.
+ ///
+ /// This corresponds to any file that has a surrogate reparse point and
+ /// is not a symlink to a directory.
+ pub fn is_symlink_file(&self) -> bool {
+ !self.is_symlink_dir() && self.is_symlink()
+ }
+
+ /// Returns true if this file type is a symlink to a file.
+ ///
+ /// This corresponds to any file that has a surrogate reparse point and has
+ /// the `FILE_ATTRIBUTE_DIRECTORY` attribute.
+ pub fn is_symlink_dir(&self) -> bool {
+ use winapi::um::winnt::FILE_ATTRIBUTE_DIRECTORY;
+
+ self.attr & FILE_ATTRIBUTE_DIRECTORY != 0 && self.is_symlink()
+ }
+
+ fn reparse_tag(&self) -> Option<DWORD> {
+ use winapi::um::winnt::FILE_ATTRIBUTE_REPARSE_POINT;
+
+ if self.attr & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
+ Some(self.reparse_tag)
+ } else {
+ None
+ }
+ }
+}
+
+/// A handle to a directory stream.
+///
+/// The handle is automatically closed when it's dropped.
+#[derive(Debug)]
+pub struct FindHandle {
+ handle: HANDLE,
+ first: Option<FindData>,
+}
+
+unsafe impl Send for FindHandle {}
+
+impl Drop for FindHandle {
+ fn drop(&mut self) {
+ unsafe {
+ // Explicitly ignore the error here if one occurs. To get an error
+ // when closing, use FindHandle::close.
+ FindClose(self.handle);
+ }
+ }
+}
+
+impl FindHandle {
+ /// Open a handle for listing files in the given directory.
+ ///
+ /// If there was a problem opening the handle, then an error is returned.
+ pub fn open<P: AsRef<Path>>(dir_path: P) -> io::Result<FindHandle> {
+ let dir_path = dir_path.as_ref();
+ let mut buffer = Vec::with_capacity(dir_path.as_os_str().len() / 2);
+ FindHandle::open_buffer(dir_path, &mut buffer)
+ }
+
+ /// Open a handle for listing files in the given directory.
+ ///
+ /// This is like `open`, except it permits the caller to provide a buffer
+ /// that's used for converting the given directory path to UTF-16, as
+ /// required by the underlying Windows API.
+ pub fn open_buffer<P: AsRef<Path>>(
+ dir_path: P,
+ buffer: &mut Vec<u16>,
+ ) -> io::Result<FindHandle> {
+ let dir_path = dir_path.as_ref();
+
+ // Convert the given path to UTF-16, and then add a wild-card to the
+ // end of it. Yes, this is how we list files in a directory on Windows.
+ // Canonical example:
+ // https://docs.microsoft.com/en-us/windows/desktop/FileIO/listing-the-files-in-a-directory
+ buffer.clear();
+ to_utf16(dir_path, buffer)?;
+ if !buffer.ends_with(&['\\' as u16]) {
+ buffer.push('\\' as u16);
+ }
+ buffer.push('*' as u16);
+ buffer.push(0);
+
+ let mut first: WIN32_FIND_DATAW = unsafe { mem::zeroed() };
+ let handle = unsafe { FindFirstFileW(buffer.as_ptr(), &mut first) };
+ if handle == INVALID_HANDLE_VALUE {
+ Err(io::Error::last_os_error())
+ } else {
+ Ok(FindHandle { handle, first: Some(FindData(first)) })
+ }
+ }
+
+ /// Read the next directory entry from this handle.
+ ///
+ /// This returns `None` when no more directory entries could be read.
+ ///
+ /// If there was a problem reading the next directory entry, then an error
+ /// is returned. When an error occurs, callers can still continue to read
+ /// subsequent directory entries.
+ ///
+ /// Note that no filtering of entries (such as `.` and `..`) is performed.
+ pub fn read(&mut self) -> Option<io::Result<DirEntry>> {
+ let mut ent = DirEntry::empty();
+ match self.read_into(&mut ent) {
+ Ok(true) => Some(Ok(ent)),
+ Ok(false) => None,
+ Err(err) => Some(Err(err)),
+ }
+ }
+
+ /// Read the next directory entry from this handle into the given space.
+ ///
+ /// This returns false when no more directory entries could be read.
+ ///
+ /// If there was a problem reading the next directory entry, then an error
+ /// is returned. When an error occurs, callers can still continue to read
+ /// subsequent directory entries.
+ ///
+ /// The contents of `ent` when the end of the stream has been reached or
+ /// when an error occurs are unspecified.
+ ///
+ /// Note that no filtering of entries (such as `.` and `..`) is performed.
+ pub fn read_into(&mut self, ent: &mut DirEntry) -> io::Result<bool> {
+ if let Some(first) = self.first.take() {
+ ent.from_find_data(&first);
+ return Ok(true);
+ }
+ let mut data: WIN32_FIND_DATAW = unsafe { mem::zeroed() };
+ let res = unsafe { FindNextFileW(self.handle, &mut data) };
+ if res == 0 {
+ return if unsafe { GetLastError() } == ERROR_NO_MORE_FILES {
+ Ok(false)
+ } else {
+ Err(io::Error::last_os_error())
+ };
+ }
+ ent.from_find_data(&FindData(data));
+ Ok(true)
+ }
+
+ /// Close this find handle and return an error if closing failed.
+ ///
+ /// Note that this does not need to be called explicitly. This directory
+ /// stream will be closed automatically when it is dropped (and if an error
+ /// occurs, it is ignored). This routine is only useful if you want to
+ /// explicitly close the directory stream and check the error.
+ pub fn close(self) -> io::Result<()> {
+ let res = if unsafe { FindClose(self.handle) } == 0 {
+ Err(io::Error::last_os_error())
+ } else {
+ Ok(())
+ };
+ // Don't drop FindHandle after we've explicitly closed the dir stream
+ // to avoid running close again.
+ mem::forget(self);
+ res
+ }
+}
+
+struct FindData(WIN32_FIND_DATAW);
+
+impl fmt::Debug for FindData {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ f.debug_struct("FindData")
+ .field("dwFileAttributes", &self.0.dwFileAttributes)
+ .field("ftCreationTime", &self.0.ftCreationTime)
+ .field("ftLastAccessTime", &self.0.ftLastAccessTime)
+ .field("ftLastWriteTime", &self.0.ftLastWriteTime)
+ .field("nFileSizeHigh", &self.0.nFileSizeHigh)
+ .field("nFileSizeLow", &self.0.nFileSizeLow)
+ .field("dwReserved0", &self.0.dwReserved0)
+ .field("dwReserved1", &self.0.dwReserved1)
+ .field("cFileName", &self.file_name())
+ .field(
+ "cAlternateFileName",
+ &OsString::from_wide(&truncate_utf16(
+ &self.0.cAlternateFileName,
+ )),
+ )
+ .finish()
+ }
+}
+
+impl FindData {
+ fn creation_time(&self) -> u64 {
+ time_as_u64(&self.0.ftCreationTime)
+ }
+
+ fn last_access_time(&self) -> u64 {
+ time_as_u64(&self.0.ftLastAccessTime)
+ }
+
+ fn last_write_time(&self) -> u64 {
+ time_as_u64(&self.0.ftLastWriteTime)
+ }
+
+ fn file_size(&self) -> u64 {
+ (self.0.nFileSizeHigh as u64) << 32 | self.0.nFileSizeLow as u64
+ }
+
+ /// Return an owned copy of the underlying file name as an OS string.
+ fn file_name(&self) -> OsString {
+ let file_name = truncate_utf16(&self.0.cFileName);
+ OsString::from_wide(file_name)
+ }
+
+ /// Read the contents of the underlying file name into the given OS string.
+ /// If the allocation can be reused, then it will be, otherwise it will be
+ /// overwritten with a fresh OsString.
+ ///
+ /// The second buffer provided will have the raw 16-bit code units of the
+ /// file name pushed to it.
+ fn decode_file_names_into(
+ &self,
+ dst_os: &mut OsString,
+ dst_16: &mut Vec<u16>,
+ ) {
+ // This implementation is a bit weird, but basically, there is no way
+ // to amortize OsString allocations in the general case, since the only
+ // API to build an OsString from a &[u16] is OsStringExt::from_wide,
+ // which returns an OsString.
+ //
+ // However, in the vast majority of cases, the underlying file name
+ // will be valid UTF-16, which we can transcode to UTF-8 and then
+ // push to a pre-existing OsString. It's not the best solution, but
+ // it permits reusing allocations!
+ let file_name = truncate_utf16(&self.0.cFileName);
+ dst_16.extend_from_slice(file_name);
+ for result in char::decode_utf16(file_name.iter().cloned()) {
+ match result {
+ Ok(c) => {
+ dst_os.push(c.encode_utf8(&mut [0; 4]));
+ }
+ Err(_) => {
+ *dst_os = OsString::from_wide(file_name);
+ return;
+ }
+ }
+ }
+ }
+}
+
+fn time_as_u64(time: &FILETIME) -> u64 {
+ (time.dwHighDateTime as u64) << 32 | time.dwLowDateTime as u64
+}
+
+fn to_utf16<T: AsRef<OsStr>>(t: T, buf: &mut Vec<u16>) -> io::Result<()> {
+ for cu16 in t.as_ref().encode_wide() {
+ if cu16 == 0 {
+ return Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "file paths on Windows cannot contain NUL bytes",
+ ));
+ }
+ buf.push(cu16);
+ }
+ Ok(())
+}
+
+fn truncate_utf16(slice: &[u16]) -> &[u16] {
+ match slice.iter().position(|c| *c == 0) {
+ Some(i) => &slice[..i],
+ None => slice,
+ }
+}
diff --git a/src/tests/linux.rs b/src/tests/linux.rs
new file mode 100644
index 0000000..6db9533
--- /dev/null
+++ b/src/tests/linux.rs
@@ -0,0 +1,201 @@
+use std::ffi::OsString;
+use std::fs;
+use std::io::{self, Seek};
+use std::os::unix::io::AsRawFd;
+use std::path::PathBuf;
+
+use crate::os::unix;
+use crate::tests::util::Dir;
+
+#[test]
+fn empty() {
+ let dir = Dir::tmp();
+
+ let mut dirfd = unix::DirFd::open(dir.path()).unwrap();
+ let r = dir.run_linux(&mut dirfd);
+ r.assert_no_errors();
+
+ let ents = r.sorted_ents();
+ assert_eq!(2, ents.len());
+ assert_eq!(".", ents[0].file_name_os());
+ assert_eq!("..", ents[1].file_name_os());
+ assert!(ents[0].file_type().unwrap().is_dir());
+ assert!(ents[1].file_type().unwrap().is_dir());
+}
+
+#[test]
+fn one_dir() {
+ let dir = Dir::tmp();
+ dir.mkdirp("a");
+
+ let mut dirfd = unix::DirFd::open(dir.path()).unwrap();
+ let r = dir.run_linux(&mut dirfd);
+ r.assert_no_errors();
+
+ let ents = r.sorted_ents();
+ assert_eq!(3, ents.len());
+ assert_eq!("a", ents[2].file_name_os());
+ assert_ne!(0, ents[2].ino());
+ assert!(ents[2].file_type().unwrap().is_dir());
+}
+
+#[test]
+fn one_file() {
+ let dir = Dir::tmp();
+ dir.touch("a");
+
+ let mut dirfd = unix::DirFd::open(dir.path()).unwrap();
+ let r = dir.run_linux(&mut dirfd);
+ r.assert_no_errors();
+
+ let ents = r.sorted_ents();
+ assert_eq!(3, ents.len());
+ assert_eq!("a", ents[2].file_name_os());
+ assert_ne!(0, ents[2].ino());
+ assert!(ents[2].file_type().unwrap().is_file());
+}
+
+#[test]
+fn one_dir_file() {
+ let dir = Dir::tmp();
+ dir.mkdirp("foo");
+ dir.touch("foo/a");
+
+ let mut dirfd = unix::DirFd::open(dir.path()).unwrap();
+ let r = dir.run_linux(&mut dirfd);
+ r.assert_no_errors();
+ let expected =
+ vec![OsString::from("."), OsString::from(".."), OsString::from("foo")];
+ assert_eq!(expected, r.sorted_file_names());
+
+ let mut dirfd = unix::DirFd::open(dir.path().join("foo")).unwrap();
+ let r = dir.run_linux(&mut dirfd);
+ r.assert_no_errors();
+ let expected =
+ vec![OsString::from("."), OsString::from(".."), OsString::from("a")];
+ assert_eq!(expected, r.sorted_file_names());
+}
+
+#[test]
+fn many_files() {
+ let dir = Dir::tmp();
+ dir.touch_all(&["a", "b", "c", "d"]);
+
+ let mut dirfd = unix::DirFd::open(dir.path()).unwrap();
+ let r = dir.run_linux(&mut dirfd);
+ r.assert_no_errors();
+
+ let expected = vec![
+ OsString::from("."),
+ OsString::from(".."),
+ OsString::from("a"),
+ OsString::from("b"),
+ OsString::from("c"),
+ OsString::from("d"),
+ ];
+ assert_eq!(expected, r.sorted_file_names());
+}
+
+#[test]
+fn many_mixed() {
+ let dir = Dir::tmp();
+ dir.mkdirp("b");
+ dir.mkdirp("d");
+ dir.touch_all(&["a", "c"]);
+
+ let mut dirfd = unix::DirFd::open(dir.path()).unwrap();
+ let r = dir.run_linux(&mut dirfd);
+ r.assert_no_errors();
+
+ let expected = vec![
+ OsString::from("."),
+ OsString::from(".."),
+ OsString::from("a"),
+ OsString::from("b"),
+ OsString::from("c"),
+ OsString::from("d"),
+ ];
+ assert_eq!(expected, r.sorted_file_names());
+
+ let ents = r.sorted_ents();
+ assert!(ents[2].file_type().unwrap().is_file());
+ assert!(ents[3].file_type().unwrap().is_dir());
+ assert!(ents[4].file_type().unwrap().is_file());
+ assert!(ents[5].file_type().unwrap().is_dir());
+}
+
+#[test]
+fn symlink() {
+ let dir = Dir::tmp();
+ dir.touch("a");
+ dir.symlink_file("a", "a-link");
+
+ let mut dirfd = unix::DirFd::open(dir.path()).unwrap();
+ let r = dir.run_linux(&mut dirfd);
+ r.assert_no_errors();
+
+ let expected = vec![
+ OsString::from("."),
+ OsString::from(".."),
+ OsString::from("a"),
+ OsString::from("a-link"),
+ ];
+ assert_eq!(expected, r.sorted_file_names());
+
+ let ents = r.sorted_ents();
+ assert!(ents[2].file_type().unwrap().is_file());
+ assert!(ents[3].file_type().unwrap().is_symlink());
+}
+
+#[test]
+fn openat() {
+ let dir = Dir::tmp();
+ dir.mkdirp("foo");
+ dir.touch("foo/a");
+
+ let mut root = unix::DirFd::open(dir.path()).unwrap();
+ let mut foo = unix::DirFd::openat(root.as_raw_fd(), "foo").unwrap();
+ let r = dir.run_linux(&mut foo);
+ r.assert_no_errors();
+
+ let expected =
+ vec![OsString::from("."), OsString::from(".."), OsString::from("a")];
+ assert_eq!(expected, r.sorted_file_names());
+}
+
+#[test]
+fn rewind() {
+ let dir = Dir::tmp();
+ dir.touch_all(&["a", "b", "c", "d"]);
+
+ let mut dirfd = unix::DirFd::open(dir.path()).unwrap();
+
+ let r = dir.run_linux(&mut dirfd);
+ r.assert_no_errors();
+ let expected = vec![
+ OsString::from("."),
+ OsString::from(".."),
+ OsString::from("a"),
+ OsString::from("b"),
+ OsString::from("c"),
+ OsString::from("d"),
+ ];
+ assert_eq!(expected, r.sorted_file_names());
+
+ let r = dir.run_linux(&mut dirfd);
+ r.assert_no_errors();
+ assert_eq!(0, r.ents().len());
+
+ dirfd.seek(io::SeekFrom::Start(0)).unwrap();
+ let r = dir.run_linux(&mut dirfd);
+ r.assert_no_errors();
+ let expected = vec![
+ OsString::from("."),
+ OsString::from(".."),
+ OsString::from("a"),
+ OsString::from("b"),
+ OsString::from("c"),
+ OsString::from("d"),
+ ];
+ assert_eq!(expected, r.sorted_file_names());
+}
diff --git a/src/tests/mod.rs b/src/tests/mod.rs
index ebf952d..4d95cfa 100644
--- a/src/tests/mod.rs
+++ b/src/tests/mod.rs
@@ -2,3 +2,9 @@
mod util;
mod recursive;
+#[cfg(target_os = "linux")]
+mod linux;
+#[cfg(unix)]
+mod unix;
+#[cfg(windows)]
+mod windows;
diff --git a/src/tests/recursive.rs b/src/tests/recursive.rs
index bbb1ce1..063deb6 100644
--- a/src/tests/recursive.rs
+++ b/src/tests/recursive.rs
@@ -1,7 +1,7 @@
use std::fs;
use std::path::PathBuf;
-use crate::tests::util::Dir;
+use crate::tests::util::{self, Dir};
use crate::WalkDir;
#[test]
@@ -323,6 +323,8 @@ fn siblings() {
#[test]
fn sym_root_file_nofollow() {
+ skip_if_no_symlinks!();
+
let dir = Dir::tmp();
dir.touch("a");
dir.symlink_file("a", "a-link");
@@ -354,6 +356,8 @@ fn sym_root_file_nofollow() {
#[test]
fn sym_root_file_follow() {
+ skip_if_no_symlinks!();
+
let dir = Dir::tmp();
dir.touch("a");
dir.symlink_file("a", "a-link");
@@ -384,6 +388,8 @@ fn sym_root_file_follow() {
#[test]
fn sym_root_dir_nofollow() {
+ skip_if_no_symlinks!();
+
let dir = Dir::tmp();
dir.mkdirp("a");
dir.symlink_dir("a", "a-link");
@@ -420,6 +426,8 @@ fn sym_root_dir_nofollow() {
#[test]
fn sym_root_dir_follow() {
+ skip_if_no_symlinks!();
+
let dir = Dir::tmp();
dir.mkdirp("a");
dir.symlink_dir("a", "a-link");
@@ -456,6 +464,8 @@ fn sym_root_dir_follow() {
#[test]
fn sym_file_nofollow() {
+ skip_if_no_symlinks!();
+
let dir = Dir::tmp();
dir.touch("a");
dir.symlink_file("a", "a-link");
@@ -492,6 +502,8 @@ fn sym_file_nofollow() {
#[test]
fn sym_file_follow() {
+ skip_if_no_symlinks!();
+
let dir = Dir::tmp();
dir.touch("a");
dir.symlink_file("a", "a-link");
@@ -528,6 +540,8 @@ fn sym_file_follow() {
#[test]
fn sym_dir_nofollow() {
+ skip_if_no_symlinks!();
+
let dir = Dir::tmp();
dir.mkdirp("a");
dir.symlink_dir("a", "a-link");
@@ -565,6 +579,8 @@ fn sym_dir_nofollow() {
#[test]
fn sym_dir_follow() {
+ skip_if_no_symlinks!();
+
let dir = Dir::tmp();
dir.mkdirp("a");
dir.symlink_dir("a", "a-link");
@@ -608,6 +624,8 @@ fn sym_dir_follow() {
#[test]
fn sym_noloop() {
+ skip_if_no_symlinks!();
+
let dir = Dir::tmp();
dir.mkdirp("a/b/c");
dir.symlink_dir("a", "a/b/c/a-link");
@@ -622,6 +640,8 @@ fn sym_noloop() {
#[test]
fn sym_loop_detect() {
+ skip_if_no_symlinks!();
+
let dir = Dir::tmp();
dir.mkdirp("a/b/c");
dir.symlink_dir("a", "a/b/c/a-link");
@@ -647,6 +667,8 @@ fn sym_loop_detect() {
#[test]
fn sym_self_loop_no_error() {
+ skip_if_no_symlinks!();
+
let dir = Dir::tmp();
dir.symlink_file("a", "a");
@@ -672,6 +694,8 @@ fn sym_self_loop_no_error() {
#[test]
fn sym_file_self_loop_io_error() {
+ skip_if_no_symlinks!();
+
let dir = Dir::tmp();
dir.symlink_file("a", "a");
@@ -693,6 +717,8 @@ fn sym_file_self_loop_io_error() {
#[test]
fn sym_dir_self_loop_io_error() {
+ skip_if_no_symlinks!();
+
let dir = Dir::tmp();
dir.symlink_dir("a", "a");
diff --git a/src/tests/unix.rs b/src/tests/unix.rs
new file mode 100644
index 0000000..1427097
--- /dev/null
+++ b/src/tests/unix.rs
@@ -0,0 +1,200 @@
+use std::ffi::OsString;
+use std::fs;
+use std::os::unix::io::AsRawFd;
+use std::path::PathBuf;
+
+use crate::os::unix;
+use crate::tests::util::Dir;
+
+#[test]
+fn empty() {
+ let dir = Dir::tmp();
+
+ let mut udir = unix::Dir::open(dir.path()).unwrap();
+ let r = dir.run_unix(&mut udir);
+ r.assert_no_errors();
+
+ let ents = r.sorted_ents();
+ assert_eq!(2, ents.len());
+ assert_eq!(".", ents[0].file_name_os());
+ assert_eq!("..", ents[1].file_name_os());
+ assert!(ents[0].file_type().unwrap().is_dir());
+ assert!(ents[1].file_type().unwrap().is_dir());
+}
+
+#[test]
+fn one_dir() {
+ let dir = Dir::tmp();
+ dir.mkdirp("a");
+
+ let mut udir = unix::Dir::open(dir.path()).unwrap();
+ let r = dir.run_unix(&mut udir);
+ r.assert_no_errors();
+
+ let ents = r.sorted_ents();
+ assert_eq!(3, ents.len());
+ assert_eq!("a", ents[2].file_name_os());
+ assert_ne!(0, ents[2].ino());
+ assert!(ents[2].file_type().unwrap().is_dir());
+}
+
+#[test]
+fn one_file() {
+ let dir = Dir::tmp();
+ dir.touch("a");
+
+ let mut udir = unix::Dir::open(dir.path()).unwrap();
+ let r = dir.run_unix(&mut udir);
+ r.assert_no_errors();
+
+ let ents = r.sorted_ents();
+ assert_eq!(3, ents.len());
+ assert_eq!("a", ents[2].file_name_os());
+ assert_ne!(0, ents[2].ino());
+ assert!(ents[2].file_type().unwrap().is_file());
+}
+
+#[test]
+fn one_dir_file() {
+ let dir = Dir::tmp();
+ dir.mkdirp("foo");
+ dir.touch("foo/a");
+
+ let mut udir = unix::Dir::open(dir.path()).unwrap();
+ let r = dir.run_unix(&mut udir);
+ r.assert_no_errors();
+ let expected =
+ vec![OsString::from("."), OsString::from(".."), OsString::from("foo")];
+ assert_eq!(expected, r.sorted_file_names());
+
+ let mut udir = unix::Dir::open(dir.path().join("foo")).unwrap();
+ let r = dir.run_unix(&mut udir);
+ r.assert_no_errors();
+ let expected =
+ vec![OsString::from("."), OsString::from(".."), OsString::from("a")];
+ assert_eq!(expected, r.sorted_file_names());
+}
+
+#[test]
+fn many_files() {
+ let dir = Dir::tmp();
+ dir.touch_all(&["a", "b", "c", "d"]);
+
+ let mut udir = unix::Dir::open(dir.path()).unwrap();
+ let r = dir.run_unix(&mut udir);
+ r.assert_no_errors();
+
+ let expected = vec![
+ OsString::from("."),
+ OsString::from(".."),
+ OsString::from("a"),
+ OsString::from("b"),
+ OsString::from("c"),
+ OsString::from("d"),
+ ];
+ assert_eq!(expected, r.sorted_file_names());
+}
+
+#[test]
+fn many_mixed() {
+ let dir = Dir::tmp();
+ dir.mkdirp("b");
+ dir.mkdirp("d");
+ dir.touch_all(&["a", "c"]);
+
+ let mut udir = unix::Dir::open(dir.path()).unwrap();
+ let r = dir.run_unix(&mut udir);
+ r.assert_no_errors();
+
+ let expected = vec![
+ OsString::from("."),
+ OsString::from(".."),
+ OsString::from("a"),
+ OsString::from("b"),
+ OsString::from("c"),
+ OsString::from("d"),
+ ];
+ assert_eq!(expected, r.sorted_file_names());
+
+ let ents = r.sorted_ents();
+ assert!(ents[2].file_type().unwrap().is_file());
+ assert!(ents[3].file_type().unwrap().is_dir());
+ assert!(ents[4].file_type().unwrap().is_file());
+ assert!(ents[5].file_type().unwrap().is_dir());
+}
+
+#[test]
+fn symlink() {
+ let dir = Dir::tmp();
+ dir.touch("a");
+ dir.symlink_file("a", "a-link");
+
+ let mut udir = unix::Dir::open(dir.path()).unwrap();
+ let r = dir.run_unix(&mut udir);
+ r.assert_no_errors();
+
+ let expected = vec![
+ OsString::from("."),
+ OsString::from(".."),
+ OsString::from("a"),
+ OsString::from("a-link"),
+ ];
+ assert_eq!(expected, r.sorted_file_names());
+
+ let ents = r.sorted_ents();
+ assert!(ents[2].file_type().unwrap().is_file());
+ assert!(ents[3].file_type().unwrap().is_symlink());
+}
+
+#[test]
+fn openat() {
+ let dir = Dir::tmp();
+ dir.mkdirp("foo");
+ dir.touch("foo/a");
+
+ let mut root = unix::Dir::open(dir.path()).unwrap();
+ let mut foo = unix::Dir::openat(root.as_raw_fd(), "foo").unwrap();
+ let r = dir.run_unix(&mut foo);
+ r.assert_no_errors();
+
+ let expected =
+ vec![OsString::from("."), OsString::from(".."), OsString::from("a")];
+ assert_eq!(expected, r.sorted_file_names());
+}
+
+#[test]
+fn rewind() {
+ let dir = Dir::tmp();
+ dir.touch_all(&["a", "b", "c", "d"]);
+
+ let mut udir = unix::Dir::open(dir.path()).unwrap();
+
+ let r = dir.run_unix(&mut udir);
+ r.assert_no_errors();
+ let expected = vec![
+ OsString::from("."),
+ OsString::from(".."),
+ OsString::from("a"),
+ OsString::from("b"),
+ OsString::from("c"),
+ OsString::from("d"),
+ ];
+ assert_eq!(expected, r.sorted_file_names());
+
+ let r = dir.run_unix(&mut udir);
+ r.assert_no_errors();
+ assert_eq!(0, r.ents().len());
+
+ udir.rewind();
+ let r = dir.run_unix(&mut udir);
+ r.assert_no_errors();
+ let expected = vec![
+ OsString::from("."),
+ OsString::from(".."),
+ OsString::from("a"),
+ OsString::from("b"),
+ OsString::from("c"),
+ OsString::from("d"),
+ ];
+ assert_eq!(expected, r.sorted_file_names());
+}
diff --git a/src/tests/util.rs b/src/tests/util.rs
index fdf06f5..f1b36d9 100644
--- a/src/tests/util.rs
+++ b/src/tests/util.rs
@@ -1,12 +1,29 @@
use std::env;
use std::error;
+#[cfg(any(unix, windows))]
+use std::ffi::OsString;
use std::fs::{self, File};
use std::io;
use std::path::{Path, PathBuf};
use std::result;
+#[cfg(unix)]
+use crate::os::unix;
+#[cfg(windows)]
+use crate::os::windows;
use crate::{DirEntry, Error};
+/// Skip the current test if the current environment doesn't support symlinks.
+#[macro_export]
+macro_rules! skip_if_no_symlinks {
+ () => {
+ if !$crate::tests::util::symlink_file_works() {
+ eprintln!("skipping test because symlinks don't work");
+ return;
+ }
+ };
+}
+
/// Create an error from a format!-like syntax.
#[macro_export]
macro_rules! err {
@@ -70,6 +87,116 @@ impl RecursiveResults {
}
}
+/// The result of running a Unix directory iterator on a single directory.
+#[cfg(unix)]
+#[derive(Debug)]
+pub struct UnixResults {
+ ents: Vec<unix::DirEntry>,
+ errs: Vec<io::Error>,
+}
+
+#[cfg(unix)]
+impl UnixResults {
+ /// Return all of the errors encountered during traversal.
+ pub fn errs(&self) -> &[io::Error] {
+ &self.errs
+ }
+
+ /// Assert that no errors have occurred.
+ pub fn assert_no_errors(&self) {
+ assert!(
+ self.errs.is_empty(),
+ "expected to find no errors, but found: {:?}",
+ self.errs
+ );
+ }
+
+ /// Return all the successfully retrieved directory entries in the order
+ /// in which they were retrieved.
+ pub fn ents(&self) -> &[unix::DirEntry] {
+ &self.ents
+ }
+
+ /// Return all file names from all successfully retrieved directory
+ /// entries.
+ ///
+ /// This does not include file names that correspond to an error.
+ pub fn file_names(&self) -> Vec<OsString> {
+ self.ents.iter().map(|d| d.file_name_os().to_os_string()).collect()
+ }
+
+ /// Return all the successfully retrieved directory entries, sorted
+ /// lexicographically by their file name.
+ pub fn sorted_ents(&self) -> Vec<unix::DirEntry> {
+ let mut ents = self.ents.clone();
+ ents.sort_by(|e1, e2| e1.file_name_bytes().cmp(e2.file_name_bytes()));
+ ents
+ }
+
+ /// Return all file names from all successfully retrieved directory
+ /// entries, sorted lexicographically.
+ ///
+ /// This does not include file names that correspond to an error.
+ pub fn sorted_file_names(&self) -> Vec<OsString> {
+ self.sorted_ents().into_iter().map(|d| d.into_file_name_os()).collect()
+ }
+}
+
+/// The result of running a Windows directory iterator on a single directory.
+#[cfg(windows)]
+#[derive(Debug)]
+pub struct WindowsResults {
+ ents: Vec<windows::DirEntry>,
+ errs: Vec<io::Error>,
+}
+
+#[cfg(windows)]
+impl WindowsResults {
+ /// Return all of the errors encountered during traversal.
+ pub fn errs(&self) -> &[io::Error] {
+ &self.errs
+ }
+
+ /// Assert that no errors have occurred.
+ pub fn assert_no_errors(&self) {
+ assert!(
+ self.errs.is_empty(),
+ "expected to find no errors, but found: {:?}",
+ self.errs
+ );
+ }
+
+ /// Return all the successfully retrieved directory entries in the order
+ /// in which they were retrieved.
+ pub fn ents(&self) -> &[windows::DirEntry] {
+ &self.ents
+ }
+
+ /// Return all file names from all successfully retrieved directory
+ /// entries.
+ ///
+ /// This does not include file names that correspond to an error.
+ pub fn file_names(&self) -> Vec<OsString> {
+ self.ents.iter().map(|d| d.file_name_os().to_os_string()).collect()
+ }
+
+ /// Return all the successfully retrieved directory entries, sorted
+ /// lexicographically by their file name.
+ pub fn sorted_ents(&self) -> Vec<windows::DirEntry> {
+ let mut ents = self.ents.clone();
+ ents.sort_by(|e1, e2| e1.file_name_u16().cmp(e2.file_name_u16()));
+ ents
+ }
+
+ /// Return all file names from all successfully retrieved directory
+ /// entries, sorted lexicographically.
+ ///
+ /// This does not include file names that correspond to an error.
+ pub fn sorted_file_names(&self) -> Vec<OsString> {
+ self.sorted_ents().into_iter().map(|d| d.into_file_name_os()).collect()
+ }
+}
+
/// A helper for managing a directory in which to run tests.
///
/// When manipulating paths within this directory, paths are interpreted
@@ -112,6 +239,56 @@ impl Dir {
results
}
+ #[cfg(unix)]
+ pub fn run_unix(&self, udir: &mut unix::Dir) -> UnixResults {
+ let mut results = UnixResults { ents: vec![], errs: vec![] };
+ while let Some(result) = udir.read() {
+ match result {
+ Ok(ent) => results.ents.push(ent),
+ Err(err) => results.errs.push(err),
+ }
+ }
+ results
+ }
+
+ #[cfg(target_os = "linux")]
+ pub fn run_linux(&self, dirfd: &mut unix::DirFd) -> UnixResults {
+ use crate::os::linux::{getdents, DirEntryCursor};
+ use std::os::unix::io::AsRawFd;
+
+ let mut results = UnixResults { ents: vec![], errs: vec![] };
+ let mut cursor = DirEntryCursor::new();
+ loop {
+ match getdents(dirfd.as_raw_fd(), &mut cursor) {
+ Err(err) => {
+ results.errs.push(err);
+ break;
+ }
+ Ok(false) => {
+ break;
+ }
+ Ok(true) => {
+ while let Some(ent) = cursor.read_unix() {
+ results.ents.push(ent);
+ }
+ }
+ }
+ }
+ results
+ }
+
+ #[cfg(windows)]
+ pub fn run_windows(&self, h: &mut windows::FindHandle) -> WindowsResults {
+ let mut results = WindowsResults { ents: vec![], errs: vec![] };
+ while let Some(result) = h.read() {
+ match result {
+ Ok(ent) => results.ents.push(ent),
+ Err(err) => results.errs.push(err),
+ }
+ }
+ results
+ }
+
/// Create a directory at the given path, while creating all intermediate
/// directories as needed.
pub fn mkdirp<P: AsRef<Path>>(&self, path: P) {
@@ -148,29 +325,7 @@ impl Dir {
src: P1,
link_name: P2,
) {
- #[cfg(windows)]
- fn imp(src: &Path, link_name: &Path) -> io::Result<()> {
- use std::os::windows::fs::symlink_file;
- symlink_file(src, link_name)
- }
-
- #[cfg(unix)]
- fn imp(src: &Path, link_name: &Path) -> io::Result<()> {
- use std::os::unix::fs::symlink;
- symlink(src, link_name)
- }
-
- let (src, link_name) = (self.join(src), self.join(link_name));
- imp(&src, &link_name)
- .map_err(|e| {
- err!(
- "failed to symlink file {} with target {}: {}",
- src.display(),
- link_name.display(),
- e
- )
- })
- .unwrap()
+ symlink_file(self.join(src), self.join(link_name)).unwrap()
}
/// Create a directory symlink to the given src with the given link name.
@@ -179,29 +334,7 @@ impl Dir {
src: P1,
link_name: P2,
) {
- #[cfg(windows)]
- fn imp(src: &Path, link_name: &Path) -> io::Result<()> {
- use std::os::windows::fs::symlink_dir;
- symlink_dir(src, link_name)
- }
-
- #[cfg(unix)]
- fn imp(src: &Path, link_name: &Path) -> io::Result<()> {
- use std::os::unix::fs::symlink;
- symlink(src, link_name)
- }
-
- let (src, link_name) = (self.join(src), self.join(link_name));
- imp(&src, &link_name)
- .map_err(|e| {
- err!(
- "failed to symlink directory {} with target {}: {}",
- src.display(),
- link_name.display(),
- e
- )
- })
- .unwrap()
+ symlink_dir(self.join(src), self.join(link_name)).unwrap()
}
}
@@ -250,3 +383,93 @@ impl TempDir {
&self.0
}
}
+
+/// Test whether file symlinks are believed to work on in this environment.
+///
+/// If they work, then return true, otherwise return false.
+pub fn symlink_file_works() -> bool {
+ use std::sync::atomic::{AtomicUsize, Ordering};
+
+ // 0 = untried
+ // 1 = works
+ // 2 = does not work
+ static WORKS: AtomicUsize = AtomicUsize::new(0);
+
+ let status = WORKS.load(Ordering::SeqCst);
+ if status != 0 {
+ return status == 1;
+ }
+
+ let tmp = TempDir::new().unwrap();
+ let foo = tmp.path().join("foo");
+ let foolink = tmp.path().join("foo-link");
+ File::create(&foo)
+ .map_err(|e| {
+ err!("error creating file {} for link test: {}", foo.display(), e)
+ })
+ .unwrap();
+ if let Err(_) = symlink_file(&foo, &foolink) {
+ WORKS.store(2, Ordering::SeqCst);
+ return false;
+ }
+ if let Err(_) = fs::read(&foolink) {
+ WORKS.store(2, Ordering::SeqCst);
+ return false;
+ }
+ WORKS.store(1, Ordering::SeqCst);
+ true
+}
+
+/// Create a file symlink to the given src with the given link name.
+fn symlink_file<P1: AsRef<Path>, P2: AsRef<Path>>(
+ src: P1,
+ link_name: P2,
+) -> Result<()> {
+ #[cfg(windows)]
+ fn imp(src: &Path, link_name: &Path) -> io::Result<()> {
+ use std::os::windows::fs::symlink_file;
+ symlink_file(src, link_name)
+ }
+
+ #[cfg(unix)]
+ fn imp(src: &Path, link_name: &Path) -> io::Result<()> {
+ use std::os::unix::fs::symlink;
+ symlink(src, link_name)
+ }
+
+ imp(src.as_ref(), link_name.as_ref()).map_err(|e| {
+ err!(
+ "failed to symlink file {} with target {}: {}",
+ src.as_ref().display(),
+ link_name.as_ref().display(),
+ e
+ )
+ })
+}
+
+/// Create a directory symlink to the given src with the given link name.
+fn symlink_dir<P1: AsRef<Path>, P2: AsRef<Path>>(
+ src: P1,
+ link_name: P2,
+) -> Result<()> {
+ #[cfg(windows)]
+ fn imp(src: &Path, link_name: &Path) -> io::Result<()> {
+ use std::os::windows::fs::symlink_dir;
+ symlink_dir(src, link_name)
+ }
+
+ #[cfg(unix)]
+ fn imp(src: &Path, link_name: &Path) -> io::Result<()> {
+ use std::os::unix::fs::symlink;
+ symlink(src, link_name)
+ }
+
+ imp(src.as_ref(), link_name.as_ref()).map_err(|e| {
+ err!(
+ "failed to symlink directory {} with target {}: {}",
+ src.as_ref().display(),
+ link_name.as_ref().display(),
+ e
+ )
+ })
+}
diff --git a/src/tests/windows.rs b/src/tests/windows.rs
new file mode 100644
index 0000000..218e07f
--- /dev/null
+++ b/src/tests/windows.rs
@@ -0,0 +1,147 @@
+use std::ffi::OsString;
+
+use crate::os::windows::FindHandle;
+use crate::tests::util::Dir;
+
+#[test]
+fn empty() {
+ let dir = Dir::tmp();
+
+ let mut handle = FindHandle::open(dir.path()).unwrap();
+ let r = dir.run_windows(&mut handle);
+ r.assert_no_errors();
+
+ let ents = r.sorted_ents();
+ assert_eq!(2, ents.len());
+ assert_eq!(".", ents[0].file_name_os());
+ assert_eq!("..", ents[1].file_name_os());
+ assert!(ents[0].file_type().is_dir());
+ assert!(ents[1].file_type().is_dir());
+}
+
+#[test]
+fn one_dir() {
+ let dir = Dir::tmp();
+ dir.mkdirp("a");
+
+ let mut handle = FindHandle::open(dir.path()).unwrap();
+ let r = dir.run_windows(&mut handle);
+ r.assert_no_errors();
+
+ let ents = r.sorted_ents();
+ assert_eq!(3, ents.len());
+ assert_eq!("a", ents[2].file_name_os());
+ assert!(ents[2].file_type().is_dir());
+}
+
+#[test]
+fn one_file() {
+ let dir = Dir::tmp();
+ dir.touch("a");
+
+ let mut handle = FindHandle::open(dir.path()).unwrap();
+ let r = dir.run_windows(&mut handle);
+ r.assert_no_errors();
+
+ let ents = r.sorted_ents();
+ assert_eq!(3, ents.len());
+ assert_eq!("a", ents[2].file_name_os());
+ assert!(ents[2].file_type().is_file());
+}
+
+#[test]
+fn one_dir_file() {
+ let dir = Dir::tmp();
+ dir.mkdirp("foo");
+ dir.touch("foo/a");
+
+ let mut handle = FindHandle::open(dir.path()).unwrap();
+ let r = dir.run_windows(&mut handle);
+ r.assert_no_errors();
+ let expected =
+ vec![OsString::from("."), OsString::from(".."), OsString::from("foo")];
+ assert_eq!(expected, r.sorted_file_names());
+
+ let mut handle = FindHandle::open(dir.path().join("foo")).unwrap();
+ let r = dir.run_windows(&mut handle);
+ r.assert_no_errors();
+ let expected =
+ vec![OsString::from("."), OsString::from(".."), OsString::from("a")];
+ assert_eq!(expected, r.sorted_file_names());
+}
+
+#[test]
+fn many_files() {
+ let dir = Dir::tmp();
+ dir.touch_all(&["a", "b", "c", "d"]);
+
+ let mut handle = FindHandle::open(dir.path()).unwrap();
+ let r = dir.run_windows(&mut handle);
+ r.assert_no_errors();
+
+ let expected = vec![
+ OsString::from("."),
+ OsString::from(".."),
+ OsString::from("a"),
+ OsString::from("b"),
+ OsString::from("c"),
+ OsString::from("d"),
+ ];
+ assert_eq!(expected, r.sorted_file_names());
+}
+
+#[test]
+fn many_mixed() {
+ let dir = Dir::tmp();
+ dir.mkdirp("b");
+ dir.mkdirp("d");
+ dir.touch_all(&["a", "c"]);
+
+ let mut handle = FindHandle::open(dir.path()).unwrap();
+ let r = dir.run_windows(&mut handle);
+ r.assert_no_errors();
+
+ let expected = vec![
+ OsString::from("."),
+ OsString::from(".."),
+ OsString::from("a"),
+ OsString::from("b"),
+ OsString::from("c"),
+ OsString::from("d"),
+ ];
+ assert_eq!(expected, r.sorted_file_names());
+
+ let ents = r.sorted_ents();
+ assert!(ents[2].file_type().is_file());
+ assert!(ents[3].file_type().is_dir());
+ assert!(ents[4].file_type().is_file());
+ assert!(ents[5].file_type().is_dir());
+}
+
+#[test]
+fn symlink() {
+ skip_if_no_symlinks!();
+
+ let dir = Dir::tmp();
+ dir.touch("a");
+ dir.symlink_file("a", "a-link");
+
+ let mut handle = FindHandle::open(dir.path()).unwrap();
+ let r = dir.run_windows(&mut handle);
+ r.assert_no_errors();
+
+ let expected = vec![
+ OsString::from("."),
+ OsString::from(".."),
+ OsString::from("a"),
+ OsString::from("a-link"),
+ ];
+ assert_eq!(expected, r.sorted_file_names());
+
+ let ents = r.sorted_ents();
+ assert!(ents[2].file_type().is_file());
+ assert!(ents[3].file_type().is_symlink());
+ assert!(ents[3].file_type().is_symlink_file());
+ assert!(!ents[3].file_type().is_symlink_dir());
+ assert!(!ents[3].file_type().is_file());
+}
diff --git a/walkdir-list/main.rs b/walkdir-list/main.rs
index 5b7f7fb..b24f98c 100644
--- a/walkdir-list/main.rs
+++ b/walkdir-list/main.rs
@@ -13,6 +13,7 @@
use std::error::Error;
use std::ffi::OsStr;
+use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process;
@@ -63,11 +64,20 @@ where
W1: io::Write,
W2: io::Write,
{
- let mut count: u64 = 0;
- for dir in &args.dirs {
+ fn count_walkdir<W: io::Write>(
+ args: &Args,
+ mut stderr: W,
+ dir: &Path,
+ ) -> Result<CountResult> {
+ let mut res = args.empty_count_result();
for result in args.walkdir(dir) {
match result {
- Ok(_) => count += 1,
+ Ok(dent) => {
+ res.count += 1;
+ if let Some(ref mut size) = res.size {
+ *size = dent.metadata()?.len();
+ }
+ }
Err(err) => {
if !args.ignore_errors {
writeln!(stderr, "ERROR: {}", err)?;
@@ -75,79 +85,396 @@ where
}
}
}
+ Ok(res)
}
- writeln!(stdout, "{}", count)?;
- Ok(())
-}
-fn print_paths<W1, W2>(
- args: &Args,
- mut stdout: W1,
- mut stderr: W2,
-) -> Result<()>
-where
- W1: io::Write,
- W2: io::Write,
-{
+ fn count_std<W: io::Write>(
+ args: &Args,
+ mut stderr: W,
+ dir: &Path,
+ ) -> Result<CountResult> {
+ let mut res = args.empty_count_result();
+ for result in fs::read_dir(dir)? {
+ match result {
+ Ok(dent) => {
+ res.count += 1;
+ if let Some(ref mut size) = res.size {
+ *size = dent.metadata()?.len();
+ }
+ }
+ Err(err) => {
+ if !args.ignore_errors {
+ writeln!(stderr, "ERROR: {}", err)?;
+ }
+ }
+ }
+ }
+ Ok(res)
+ }
+
+ #[cfg(windows)]
+ fn count_windows<W: io::Write>(
+ args: &Args,
+ mut stderr: W,
+ dir: &Path,
+ ) -> Result<CountResult> {
+ use walkdir::os::windows;
+
+ let mut res = args.empty_count_result();
+ let mut handle = windows::FindHandle::open(dir)?;
+ let mut dent = windows::DirEntry::empty();
+ loop {
+ match handle.read_into(&mut dent) {
+ Ok(true) => {
+ res.count += 1;
+ if let Some(ref mut size) = res.size {
+ let md = dir.join(dent.file_name_os()).metadata()?;
+ *size = md.len();
+ }
+ }
+ Ok(false) => break,
+ Err(err) => {
+ if !args.ignore_errors {
+ writeln!(stderr, "ERROR: {}", err)?;
+ }
+ }
+ }
+ }
+ Ok(res)
+ }
+
+ #[cfg(not(windows))]
+ fn count_windows<W: io::Write>(
+ _args: &Args,
+ _stderr: W,
+ _dir: &Path,
+ ) -> Result<CountResult> {
+ err!("cannot use --flat-windows on non-Windows platform")
+ }
+
+ #[cfg(unix)]
+ fn count_unix<W: io::Write>(
+ args: &Args,
+ mut stderr: W,
+ dir: &Path,
+ ) -> Result<CountResult> {
+ use walkdir::os::unix;
+
+ let mut res = args.empty_count_result();
+ let mut udir = unix::Dir::open(dir)?;
+ let mut dent = unix::DirEntry::empty();
+ loop {
+ match udir.read_into(&mut dent) {
+ Ok(true) => {
+ res.count += 1;
+ if let Some(ref mut size) = res.size {
+ let md = dir.join(dent.file_name_os()).metadata()?;
+ *size = md.len();
+ }
+ }
+ Ok(false) => break,
+ Err(err) => {
+ if !args.ignore_errors {
+ writeln!(stderr, "ERROR: {}", err)?;
+ }
+ }
+ }
+ }
+ Ok(res)
+ }
+
+ #[cfg(not(unix))]
+ fn count_unix<W: io::Write>(
+ _args: &Args,
+ _stderr: W,
+ _dir: &Path,
+ ) -> Result<CountResult> {
+ err!("cannot use --flat-unix on non-Unix platform")
+ }
+
+ #[cfg(target_os = "linux")]
+ fn count_linux<W: io::Write>(
+ args: &Args,
+ mut stderr: W,
+ dir: &Path,
+ ) -> Result<CountResult> {
+ use std::os::unix::io::AsRawFd;
+ use walkdir::os::{linux, unix};
+
+ let mut res = args.empty_count_result();
+ let udir = unix::Dir::open(dir)?;
+ let mut cursor = linux::DirEntryCursor::new();
+ loop {
+ match linux::getdents(udir.as_raw_fd(), &mut cursor) {
+ Ok(true) => {
+ while let Some(dent) = cursor.read() {
+ res.count += 1;
+ if let Some(ref mut size) = res.size {
+ let md =
+ dir.join(dent.file_name_os()).metadata()?;
+ *size = md.len();
+ }
+ }
+ }
+ Ok(false) => break,
+ Err(err) => {
+ if !args.ignore_errors {
+ writeln!(stderr, "ERROR: {}", err)?;
+ }
+ }
+ }
+ }
+ Ok(res)
+ }
+
+ #[cfg(not(target_os = "linux"))]
+ fn count_linux<W: io::Write>(
+ _args: &Args,
+ _stderr: W,
+ _dir: &Path,
+ ) -> Result<CountResult> {
+ err!("cannot use --flat-linux on non-Linux platform")
+ }
+
+ let mut res = args.empty_count_result();
for dir in &args.dirs {
- if args.tree {
- print_paths_tree(&args, &mut stdout, &mut stderr, dir)?;
+ if args.flat_std {
+ res = res.add(count_std(args, &mut stderr, &dir)?);
+ } else if args.flat_windows {
+ res = res.add(count_windows(args, &mut stderr, &dir)?);
+ } else if args.flat_unix {
+ res = res.add(count_unix(args, &mut stderr, &dir)?);
+ } else if args.flat_linux {
+ res = res.add(count_linux(args, &mut stderr, &dir)?);
} else {
- print_paths_flat(&args, &mut stdout, &mut stderr, dir)?;
+ res = res.add(count_walkdir(args, &mut stderr, &dir)?);
+ }
+ }
+ match res.size {
+ Some(size) => {
+ writeln!(stdout, "{} (file size: {})", res.count, size)?;
+ }
+ None => {
+ writeln!(stdout, "{}", res.count)?;
}
}
Ok(())
}
-fn print_paths_flat<W1, W2>(
+fn print_paths<W1, W2>(
args: &Args,
mut stdout: W1,
mut stderr: W2,
- dir: &Path,
) -> Result<()>
where
W1: io::Write,
W2: io::Write,
{
- for result in args.walkdir(dir) {
- let dent = match result {
- Ok(dent) => dent,
- Err(err) => {
- if !args.ignore_errors {
- writeln!(stderr, "ERROR: {}", err)?;
+ fn print_walkdir<W1, W2>(
+ args: &Args,
+ mut stdout: W1,
+ mut stderr: W2,
+ dir: &Path,
+ ) -> Result<()>
+ where
+ W1: io::Write,
+ W2: io::Write,
+ {
+ for result in args.walkdir(dir) {
+ let dent = match result {
+ Ok(dent) => dent,
+ Err(err) => {
+ if !args.ignore_errors {
+ writeln!(stderr, "ERROR: {}", err)?;
+ }
+ continue;
+ }
+ };
+ write_path(&mut stdout, dent.path())?;
+ stdout.write_all(b"\n")?;
+ }
+ Ok(())
+ }
+
+ fn print_std<W1, W2>(
+ args: &Args,
+ mut stdout: W1,
+ mut stderr: W2,
+ dir: &Path,
+ ) -> Result<()>
+ where
+ W1: io::Write,
+ W2: io::Write,
+ {
+ for result in fs::read_dir(dir)? {
+ let dent = match result {
+ Ok(dent) => dent,
+ Err(err) => {
+ if !args.ignore_errors {
+ writeln!(stderr, "ERROR: {}", err)?;
+ }
+ continue;
+ }
+ };
+ write_os_str(&mut stdout, &dent.file_name())?;
+ stdout.write_all(b"\n")?;
+ }
+ Ok(())
+ }
+
+ #[cfg(windows)]
+ fn print_windows<W1, W2>(
+ args: &Args,
+ mut stdout: W1,
+ mut stderr: W2,
+ dir: &Path,
+ ) -> Result<()>
+ where
+ W1: io::Write,
+ W2: io::Write,
+ {
+ use walkdir::os::windows;
+
+ let mut handle = windows::FindHandle::open(dir)?;
+ let mut dent = windows::DirEntry::empty();
+ loop {
+ match handle.read_into(&mut dent) {
+ Ok(true) => {
+ write_os_str(&mut stdout, dent.file_name_os())?;
+ stdout.write_all(b"\n")?;
+ }
+ Ok(false) => break,
+ Err(err) => {
+ if !args.ignore_errors {
+ writeln!(stderr, "ERROR: {}", err)?;
+ }
}
- continue;
}
- };
- write_path(&mut stdout, dent.path())?;
- stdout.write_all(b"\n")?;
+ }
+ Ok(())
}
- Ok(())
-}
-fn print_paths_tree<W1, W2>(
- args: &Args,
- mut stdout: W1,
- mut stderr: W2,
- dir: &Path,
-) -> Result<()>
-where
- W1: io::Write,
- W2: io::Write,
-{
- for result in args.walkdir(dir) {
- let dent = match result {
- Ok(dent) => dent,
- Err(err) => {
- if !args.ignore_errors {
- writeln!(stderr, "ERROR: {}", err)?;
+ #[cfg(not(windows))]
+ fn print_windows<W1, W2>(
+ _args: &Args,
+ _stdout: W1,
+ _stderr: W2,
+ _dir: &Path,
+ ) -> Result<u64>
+ where
+ W1: io::Write,
+ W2: io::Write,
+ {
+ err!("cannot use --flat-windows on non-Windows platform")
+ }
+
+ #[cfg(unix)]
+ fn print_unix<W1, W2>(
+ args: &Args,
+ mut stdout: W1,
+ mut stderr: W2,
+ dir: &Path,
+ ) -> Result<()>
+ where
+ W1: io::Write,
+ W2: io::Write,
+ {
+ use walkdir::os::unix;
+
+ let mut dir = unix::Dir::open(dir)?;
+ let mut dent = unix::DirEntry::empty();
+ loop {
+ match dir.read_into(&mut dent) {
+ Ok(true) => {
+ write_os_str(&mut stdout, dent.file_name_os())?;
+ stdout.write_all(b"\n")?;
+ }
+ Ok(false) => break,
+ Err(err) => {
+ if !args.ignore_errors {
+ writeln!(stderr, "ERROR: {}", err)?;
+ }
}
- continue;
}
- };
- stdout.write_all(" ".repeat(dent.depth()).as_bytes())?;
- write_os_str(&mut stdout, dent.file_name())?;
- stdout.write_all(b"\n")?;
+ }
+ Ok(())
+ }
+
+ #[cfg(not(unix))]
+ fn print_unix<W1, W2>(
+ _args: &Args,
+ _stdout: W1,
+ _stderr: W2,
+ _dir: &Path,
+ ) -> Result<()>
+ where
+ W1: io::Write,
+ W2: io::Write,
+ {
+ err!("cannot use --flat-unix on non-Unix platform")
+ }
+
+ #[cfg(target_os = "linux")]
+ fn print_linux<W1, W2>(
+ args: &Args,
+ mut stdout: W1,
+ mut stderr: W2,
+ dir: &Path,
+ ) -> Result<()>
+ where
+ W1: io::Write,
+ W2: io::Write,
+ {
+ use std::os::unix::io::AsRawFd;
+ use walkdir::os::{linux, unix};
+
+ let dir = unix::Dir::open(dir)?;
+ let mut cursor = linux::DirEntryCursor::new();
+ loop {
+ match linux::getdents(dir.as_raw_fd(), &mut cursor) {
+ Ok(true) => {
+ while let Some(dent) = cursor.read() {
+ write_os_str(&mut stdout, dent.file_name_os())?;
+ stdout.write_all(b"\n")?;
+ }
+ }
+ Ok(false) => break,
+ Err(err) => {
+ if !args.ignore_errors {
+ writeln!(stderr, "ERROR: {}", err)?;
+ }
+ }
+ }
+ }
+ Ok(())
+ }
+
+ #[cfg(not(target_os = "linux"))]
+ fn print_linux<W1, W2>(
+ _args: &Args,
+ _stdout: W1,
+ _stderr: W2,
+ _dir: &Path,
+ ) -> Result<()>
+ where
+ W1: io::Write,
+ W2: io::Write,
+ {
+ err!("cannot use --flat-linux on non-Linux platform")
+ }
+
+ for dir in &args.dirs {
+ if args.flat_std {
+ print_std(&args, &mut stdout, &mut stderr, dir)?;
+ } else if args.flat_windows {
+ print_windows(&args, &mut stdout, &mut stderr, dir)?;
+ } else if args.flat_unix {
+ print_unix(&args, &mut stdout, &mut stderr, dir)?;
+ } else if args.flat_linux {
+ print_linux(&args, &mut stdout, &mut stderr, dir)?;
+ } else {
+ print_walkdir(&args, &mut stdout, &mut stderr, dir)?;
+ }
}
Ok(())
}
@@ -159,20 +486,24 @@ struct Args {
min_depth: Option<usize>,
max_depth: Option<usize>,
max_open: Option<usize>,
- tree: bool,
ignore_errors: bool,
sort: bool,
depth_first: bool,
same_file_system: bool,
timeit: bool,
count: bool,
+ file_size: bool,
+ flat_std: bool,
+ flat_windows: bool,
+ flat_unix: bool,
+ flat_linux: bool,
}
impl Args {
fn parse() -> Result<Args> {
use clap::{crate_authors, crate_version, App, Arg};
- let parsed = App::new("List files using walkdir")
+ let mut app = App::new("List files using walkdir")
.author(crate_authors!())
.version(crate_version!())
.max_term_width(100)
@@ -214,11 +545,6 @@ impl Args {
.help("Don't print error messages."),
)
.arg(
- Arg::with_name("sort")
- .long("sort")
- .help("Sort file paths lexicographically."),
- )
- .arg(
Arg::with_name("depth-first").long("depth-first").help(
"Show directory contents before the directory path.",
),
@@ -243,7 +569,59 @@ impl Args {
.short("c")
.help("Print only a total count of all file paths."),
)
- .get_matches();
+ .arg(Arg::with_name("file-size").long("file-size").help(
+ "Print the total file size of all files. This \
+ implies --count.",
+ ))
+ .arg(
+ Arg::with_name("flat-std")
+ .long("flat-std")
+ .conflicts_with("flat-unix")
+ .conflicts_with("flat-linux")
+ .conflicts_with("flat-windows")
+ .help(
+ "Use std::fs::read_dir to list contents of a single \
+ directory. This is NOT recursive.",
+ ),
+ );
+ if cfg!(unix) {
+ app = app.arg(
+ Arg::with_name("flat-unix")
+ .long("flat-unix")
+ .conflicts_with("flat-std")
+ .conflicts_with("flat-linux")
+ .conflicts_with("flat-windows")
+ .help(
+ "Use Unix-specific APIs to list contents of a single \
+ directory. This is NOT recursive.",
+ ),
+ );
+ }
+ if cfg!(target_os = "linux") {
+ app = app.arg(
+ Arg::with_name("flat-linux")
+ .long("flat-linux")
+ .conflicts_with("flat-std")
+ .conflicts_with("flat-unix")
+ .conflicts_with("flat-windows")
+ .help(
+ "Use Linux-specific syscalls (getdents64) to list \
+ contents of a single directory. This is NOT \
+ recursive.",
+ ),
+ );
+ }
+ if cfg!(windows) {
+ app = app
+ .arg(Arg::with_name("flat-windows")
+ .long("flat-windows")
+ .conflicts_with("flat-std")
+ .conflicts_with("flat-unix")
+ .conflicts_with("flat-linux")
+ .help("Use Windows-specific APIs to list contents of a single \
+ directory. This is NOT recursive."));
+ }
+ let parsed = app.get_matches();
let dirs = match parsed.values_of_os("dirs") {
None => vec![PathBuf::from("./")],
@@ -255,13 +633,17 @@ impl Args {
min_depth: parse_usize(&parsed, "min-depth")?,
max_depth: parse_usize(&parsed, "max-depth")?,
max_open: parse_usize(&parsed, "max-open")?,
- tree: parsed.is_present("tree"),
ignore_errors: parsed.is_present("ignore-errors"),
sort: parsed.is_present("sort"),
depth_first: parsed.is_present("depth-first"),
same_file_system: parsed.is_present("same-file-system"),
timeit: parsed.is_present("timeit"),
count: parsed.is_present("count"),
+ file_size: parsed.is_present("file-size"),
+ flat_std: parsed.is_present("flat-std"),
+ flat_windows: parsed.is_present("flat-windows"),
+ flat_unix: parsed.is_present("flat-unix"),
+ flat_linux: parsed.is_present("flat-linux"),
})
}
@@ -284,6 +666,28 @@ impl Args {
}
walkdir
}
+
+ fn empty_count_result(&self) -> CountResult {
+ CountResult {
+ count: 0,
+ size: if self.file_size { Some(0) } else { None },
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug)]
+struct CountResult {
+ count: u64,
+ size: Option<u64>,
+}
+
+impl CountResult {
+ fn add(self, other: CountResult) -> CountResult {
+ CountResult {
+ count: self.count + other.count,
+ size: self.size.and_then(|s1| other.size.map(|s2| s1 + s2)),
+ }
+ }
}
fn parse_usize(