Skip to main content

mofu/
walk_dir.rs

1//! Utilities for directory traversal.
2
3use std::fs;
4use std::fs::{Metadata, ReadDir};
5use std::io::{Error, ErrorKind, Result};
6use std::num::NonZero;
7use std::path::{Path, PathBuf};
8
9/// Returns an iterator that recursively traverses the specified directory.
10///
11/// Note that the iterator will skip any entries that produce errors. To
12/// handle errors explicitly, see [`WalkDir`].
13///
14/// # Parameters
15///
16/// - `path`: The directory path to traverse.
17/// - `max_depth`: Maximum depth to traverse:
18///   - `0` means unlimited depth.
19///   - `1` means only traverse top-level entries.
20///
21/// # Errors
22///
23/// This function will return an error in the following situations, but is not
24/// limited to just these cases:
25///
26/// - The provided `path` doesn't exist.
27/// - The process lacks permissions to view the contents.
28/// - The `path` points at a non-directory file.
29pub fn walk_dir<P>(path: P, max_depth: usize) -> Result<impl Iterator<Item = DirEntry>>
30where
31    P: AsRef<Path>,
32{
33    let max_depth = NonZero::new(max_depth);
34    let iter = WalkDir::new(path)?
35        .max_depth(max_depth)
36        .filter_map(Result::ok);
37    Ok(iter)
38}
39
40/// An iterator that recursively traverses the specified directory.
41///
42/// # Examples
43///
44/// ```
45/// use mofu::walk_dir::WalkDir;
46/// use std::num::NonZero;
47///
48/// let max_depth = NonZero::new(3);
49/// let iter = WalkDir::new(".")
50///     .unwrap()
51///     .max_depth(max_depth)
52///     .filter_map(Result::ok);
53/// for entry in iter {
54///     println!("{}", entry.path().display());
55/// }
56/// ```
57#[derive(Debug)]
58pub struct WalkDir {
59    stack: Vec<StackItem>,
60    max_depth: Option<NonZero<usize>>,
61}
62
63impl WalkDir {
64    /// Creates a new [`WalkDir`].
65    ///
66    /// # Errors
67    ///
68    /// This function will return an error in the following situations, but is not
69    /// limited to just these cases:
70    ///
71    /// - The provided `path` doesn't exist.
72    /// - The process lacks permissions to view the contents.
73    /// - The `path` points at a non-directory file.
74    pub fn new<P>(path: P) -> Result<Self>
75    where
76        P: AsRef<Path>,
77    {
78        let depth = unsafe { NonZero::new_unchecked(1) };
79        let iter = fs::read_dir(path)?;
80        let item = StackItem { depth, iter };
81        let stack = vec![item];
82        let max_depth = None;
83        Ok(Self { stack, max_depth })
84    }
85
86    /// Sets the maximum depth for traversal.
87    pub fn max_depth(mut self, max_depth: Option<NonZero<usize>>) -> Self {
88        self.max_depth = max_depth;
89        self
90    }
91}
92
93impl Iterator for WalkDir {
94    type Item = Result<DirEntry>;
95
96    fn next(&mut self) -> Option<Self::Item> {
97        let (depth, entry) = loop {
98            let item = self.stack.last_mut()?;
99            match item.iter.next() {
100                None => self.stack.pop(),
101                Some(Err(error)) => return Some(Err(error)),
102                Some(Ok(entry)) => break (item.depth, entry),
103            };
104        };
105
106        let entry = match DirEntry::try_from(entry) {
107            Err(error) => return Some(Err(error)),
108            Ok(entry) => entry,
109        };
110
111        if entry.metadata.is_dir() && self.max_depth.is_none_or(|max_depth| depth < max_depth) {
112            match fs::read_dir(entry.path()) {
113                // Yes, this branch is still reachable.
114                Err(error) if error.kind() == ErrorKind::NotADirectory => (),
115                Err(error) => return Some(Err(error)),
116                Ok(iter) => {
117                    // Will not overflow because `depth < max_depth`.
118                    let depth = unsafe { NonZero::new_unchecked(depth.get() + 1) };
119                    let item = StackItem { depth, iter };
120                    self.stack.push(item);
121                }
122            }
123        }
124
125        Some(Ok(entry))
126    }
127}
128
129/// A directory entry returned by [`WalkDir`].
130///
131/// Each entry provides a path along with cached metadata to help avoid
132/// redundant system calls. However, due to possible concurrent file access,
133/// the cached metadata may degrade in validity over time.
134///
135/// Note that the metadata does not follow symlinks.
136#[derive(Debug)]
137pub struct DirEntry {
138    path: PathBuf,
139    metadata: Metadata,
140}
141
142impl DirEntry {
143    /// Returns the path.
144    #[inline]
145    pub fn path(&self) -> &Path {
146        &self.path
147    }
148
149    /// Returns the cached metadata.
150    ///
151    /// Due to possible concurrent file access, the cached metadata may degrade in
152    /// validity over time.
153    ///
154    /// Note that the metadata does not follow symbolic links.
155    #[inline]
156    pub fn metadata(&self) -> &Metadata {
157        &self.metadata
158    }
159}
160
161impl TryFrom<fs::DirEntry> for DirEntry {
162    type Error = Error;
163
164    #[inline]
165    fn try_from(value: fs::DirEntry) -> Result<Self> {
166        let path = value.path();
167        let metadata = value.metadata()?;
168        Ok(Self { path, metadata })
169    }
170}
171
172impl TryFrom<PathBuf> for DirEntry {
173    type Error = Error;
174
175    #[inline]
176    fn try_from(value: PathBuf) -> Result<Self> {
177        let path = value;
178        let metadata = path.symlink_metadata()?;
179        Ok(Self { path, metadata })
180    }
181}
182
183impl From<DirEntry> for PathBuf {
184    #[inline]
185    fn from(value: DirEntry) -> Self {
186        value.path
187    }
188}
189
190#[derive(Debug)]
191struct StackItem {
192    depth: NonZero<usize>,
193    iter: ReadDir,
194}