dirscent 0.1.0

Directory descent
Documentation
use std::io::ErrorKind;
use std::path::Path;

use crate::dir_entry::DirEntry;
use crate::error::Error;

type Result<T> = std::result::Result<T, Error>;

/// Iterator over the entries of a directory, and, recursively, over the entries of all
/// subdirectories.
///
/// The iterator implements the [builder lite] pattern.  Set the options on the iterator value
/// itself, there is no separate `build()` step.
///
/// The iterator can [follow symlinks], traverse filesystem in [post-order] and [skip permission
/// denied] errors.  By default, these options are set to `false`.  `with_`-functions are provided
/// to set options to explicit values.
///
/// The iterator can return directory entries of specific [minimal] (default: `0`) and [maximal]
/// (default: `std::usize::MAX`) depth.  Moreover, it will not descend past the maximal depth.
/// Depth of `0` corresponds to the path passed to [`dirscent`].  Its direct descendants have depth
/// of `1`, their direct descendants have depth of `2` and so on.
///
/// The iteration order is unspecified, except that each directory entry is visited only once.
///
/// [builder lite]: https://matklad.github.io/2022/05/29/builder-lite.html
///
/// [follow symlinks]: Dirscent::follow_symlinks
/// [post-order]: Dirscent::postorder
/// [skip permission denied]: Dirscent::skip_permission_denied
///
/// [minimal]: Dirscent::with_min_depth
/// [maximal]: Dirscent::with_max_depth
///
/// [`dirscent`]: crate::dirscent
///
/// # Example
///
/// Print directory entries with the depth between `1` and `3` in post-order and maybe skip
/// permission denied errors.
///
/// ```no_run
/// use dirscent::dirscent;
///
/// for it in dirscent(path)
///     .unwrap()
///     .with_min_depth(1)
///     .with_max_depth(3)
///     .postorder()
///     .with_skip_permission_denied(cli.ignore_perm_errors)
/// {
///     match it {
///         Ok(entry) => println!("{}", entry.path().display()),
///         Err(err) => eprintln!("{err:?}"),
///     }
/// }
/// ```
pub struct Dirscent {
    stack: Vec<Result<Vec<Result<DirEntry>>>>,
    deferred_dirs: Vec<Result<DirEntry>>,

    min_depth: usize,
    max_depth: usize,

    follow_symlinks: bool,
    postorder: bool,
    skip_permission_denied: bool,
}

impl Dirscent {
    /// Creates an iterator that starts at the directory entry under the path.
    ///
    /// Returns an error if fails to read the metadata.
    pub(crate) fn new(path: impl AsRef<Path>) -> Result<Self> {
        let entry = path.as_ref().to_path_buf().try_into()?;
        let iter = Dirscent {
            stack: vec![Ok(vec![Ok(entry)])],
            deferred_dirs: Vec::new(),

            min_depth: std::usize::MIN,
            max_depth: std::usize::MAX,

            follow_symlinks: false,
            postorder: false,
            skip_permission_denied: false,
        };
        Ok(iter)
    }

    /// Sets the iterator to follow symlinks.
    pub fn follow_symlinks(self) -> Self {
        self.with_follow_symlinks(true)
    }

    /// Sets whether the iterator follows symlinks.
    ///
    /// If the argument is `true`, symlinks are followed.
    pub fn with_follow_symlinks(mut self, x: bool) -> Self {
        self.follow_symlinks = x;
        self
    }

    /// Sets the minimum depth of directory entries returned by the iterator.
    pub fn with_min_depth(mut self, x: usize) -> Self {
        self.min_depth = x;
        self
    }

    /// Sets the maximum depth of directory entries returned by the iterator.
    pub fn with_max_depth(mut self, x: usize) -> Self {
        self.max_depth = x;
        self
    }

    /// Sets the iterator to return entries of directories _after_ entries of their descendants.
    pub fn postorder(self) -> Self {
        self.with_postorder(true)
    }

    /// Sets whether the iterator returns entries of directories _after_ entries of their
    /// descendants.
    ///
    /// If the argument is `true`, directories come after their descendants.
    pub fn with_postorder(mut self, x: bool) -> Self {
        self.postorder = x;
        self
    }

    /// Sets the iterator to skip directory entries that would otherwise cause permission denied
    /// errors.
    pub fn skip_permission_denied(self) -> Self {
        self.with_skip_permission_denied(true)
    }

    /// Sets whether the iterator skips directory entries that would otherwise cause permission
    /// denied errors.
    ///
    /// If the argument is `true`, permission denied errors are skipped.
    pub fn with_skip_permission_denied(mut self, x: bool) -> Self {
        self.skip_permission_denied = x;
        self
    }
}

impl Iterator for Dirscent {
    type Item = Result<DirEntry>;

    fn next(&mut self) -> Option<Self::Item> {
        match self.stack.pop() {
            Some(Ok(mut entry_vec)) => {
                let depth = self.stack.len();
                let item = entry_vec.pop();
                match &item {
                    Some(entry_res) => {
                        // The directory iterator is not exhausted.
                        self.stack.push(Ok(entry_vec));

                        if let Ok(entry) = entry_res {
                            let file_type = entry.file_type();
                            if depth < self.max_depth
                                && (!file_type.is_symlink() || self.follow_symlinks)
                                && file_type.is_dir()
                            {
                                self.stack
                                    .push(read_dir(entry.path(), self.follow_symlinks));
                            }
                        }
                        if depth < self.min_depth {
                            return self.next();
                        }
                        // If the stack has grown, the current item is a directory.
                        if self.stack.len() - depth > 1 && self.postorder {
                            self.deferred_dirs.push(item.unwrap());
                            return self.next();
                        }
                        item
                    }
                    // The directory iterator is exhausted.
                    None => self.deferred_dirs.pop().or_else(|| self.next()),
                }
            }
            // Couldn't read the directory.
            Some(Err(err))
                if err.io_error().kind() == ErrorKind::PermissionDenied
                    && self.skip_permission_denied =>
            {
                self.next()
            }
            Some(Err(err)) => Some(Err(err)),
            // The iteration is complete.
            None => None,
        }
    }
}

/// [`std::fs::read_dir`] that returns a vector.
///
/// `follow_symlinks` is passed to each directory entry.
///
/// Errors are transformed into [`Error`].
fn read_dir(path: &Path, follow_symlinks: bool) -> Result<Vec<Result<DirEntry>>> {
    path.read_dir()
        .map(|it| {
            it.map(|entry_res| {
                entry_res
                    .map_err(|err| Error::new(err, path.to_path_buf()))
                    .and_then(DirEntry::try_from)
                    .map(|entry| entry.with_follow_symlinks(follow_symlinks))
            })
            .collect()
        })
        .map_err(|err| Error::new(err, path.to_path_buf()))
}