recv_dir/
lib.rs

1//! # recv-dir
2//!
3//! Simple recursive directory traversal.
4//!
5//! ## Usage
6//!
7//! **recv-dir** implements a simple directory traversal, which supports selective directory visit.
8//!
9//! Since it is a simple iterator, you can use regular iterator functions like `filter` and `map` to
10//! ignore individual files or directories, but it does not prevent them from being visited.
11//!
12//! To ignore directories from being visited, you will need to use a filtered iterator,
13//! like the examples below.
14//!
15//! ## Example
16//!
17//! ### Visit all directories
18//!
19//! ```rust
20//! use recv_dir::RecursiveDirIterator;
21//! let dir = RecursiveDirIterator::from_root("test_dir").unwrap();
22//!
23//! for entry in dir {
24//!    println!("{:?}", entry);
25//! }
26//! ```
27//!
28//! ### Ignore symlinks
29//!
30//! ```rust
31//! use std::path::Path;
32//! use recv_dir::RecursiveDirIterator;
33//! let dir = RecursiveDirIterator::with_closure_filter("test_dir", |dir: &Path| !dir.is_symlink()).unwrap();
34//!
35//! for entry in dir {
36//!    println!("{:?}", entry);
37//! }
38//! ```
39//!
40//! ### Control depth
41//!
42//! Visits only the files in `test_dir`:
43//!
44//! ```rust
45//! use std::path::{Path, PathBuf};
46//! use recv_dir::RecursiveDirIterator;
47//! let root = PathBuf::from("test_dir");
48//! let ancestors = root.ancestors().count();
49//! let dir = RecursiveDirIterator::with_closure_filter(root, |dir: &Path| dir.ancestors().count() - ancestors <= 1).unwrap();
50//!
51//! for entry in dir {
52//!    println!("{:?}", entry);
53//! }
54//! ```
55//!
56//! Visits the files in `test_dir` and in first-level sub directories:
57//!
58//! ```rust
59//! use std::path::{Path, PathBuf};
60//! use recv_dir::RecursiveDirIterator;
61//! let root = PathBuf::from("test_dir");
62//! let ancestors = root.ancestors().count();
63//! let dir = RecursiveDirIterator::with_closure_filter(root, |dir: &Path| dir.ancestors().count() - ancestors <= 2).unwrap();
64//!
65//! for entry in dir {
66//!    println!("{:?}", entry);
67//! }
68//! ```
69//!
70//! You can also compose the filters:
71//!
72//! ```rust
73//! use std::num::NonZeroUsize;
74//! use std::path::{Path, PathBuf};
75//! use recv_dir::{Filter, MaxDepth, NoSymlink, RecursiveDirIterator};
76//! let root = PathBuf::from("test_dir");
77//! let dir = RecursiveDirIterator::with_filter(&root, NoSymlink.and(MaxDepth::new(&root, NonZeroUsize::new(2).unwrap()))).unwrap();
78//!
79//! for entry in dir {
80//!    println!("{:?}", entry);
81//! }
82//! ```
83//!
84//! ## Order
85//!
86//! There is no guarantee on the order of the traversal, it may be OS-dependent, File System-dependent or completely
87//! arbitrary.
88//!
89//! But it does not means that there is no guarantees at all, this crate relies on [`std::fs::read_dir`],
90//! and it is expected that, at least, the same directory is not visited twice. Although it is extremely
91//! unlikely to happen, it is still possible, mainly when considering symlink recursions.
92//!
93//! Some filesystems has a very consistent order of traversal, and others don't, directories are commonly
94//! stored in B-trees (or B+ trees), and commonly respects their order, but it may vary.
95//!
96//! **recv-dir** does not guarantee that either directories or files are visited first, but it is
97//! guaranteed that they are visited at some point if accessible, even if directories were already visited.
98//!
99//! ## Failures
100//!
101//! Walking the directory tree may fail and at the moment, **recv-dir** will only fail if the base directory
102//! cannot be visited, but it will silently ignore the sub directories that are not accessible
103//! or fails to open.
104//!
105//! ## Recursion
106//!
107//! **recv-dir** is stack-based, which means that it will never crash because of recursion limits,
108//! however, recursion limits are good to prevent the tree-walk going too deep (and they are also
109//! the worse way to handle this and have an extremely negative performance impact).
110//!
111//! Because it never crashes, if you do visit symlinks (which is the default), you can end up
112//! in an infinite symlink recursion, which will cause high-cpu usage and can also cause the
113//! stack to grow until it consumes all the available memory, which in a good scenario, means
114//! that the program will crash, but in a bad scenario (and if the stack do not grow) it will
115//! just run indefinitely.
116//!
117//! To avoid this, you can either configure a maximum depth or never visit symlinks, since, at the moment,
118//! **recv-dir** does not provide a protection against infinite symlink recursion.
119//!
120//! ## Experiment
121//!
122//! **This project is part of the [Kores/rust-experiments](https://gitlab.com/Kores/rust-experiments).**
123//!
124
125mod filter;
126
127pub use crate::filter::{Accept, Closure, Extension, Filter, MaxDepth, NoSymlink, This};
128#[cfg(not(feature = "tokio"))]
129use std::fs::{read_dir, ReadDir};
130use std::num::NonZeroUsize;
131use std::path::{Path, PathBuf};
132#[cfg(feature = "tokio")]
133use tokio::fs::{read_dir, ReadDir};
134
135/// Reads the directory recursively without any filter.
136///
137/// Beware the risks of traversing a directory without any filter, like
138/// infinite symlink recursion.
139#[cfg(not(feature = "tokio"))]
140pub fn recursive_read_dir<P: AsRef<Path>>(
141    dir: P,
142) -> std::io::Result<impl Iterator<Item = PathBuf>> {
143    RecursiveDirIterator::from_root(dir)
144}
145
146/// Reads the directory recursively without any filter.
147///
148/// Beware the risks of traversing a directory without any filter, like
149/// infinite symlink recursion.
150#[cfg(feature = "tokio")]
151pub async fn recursive_read_dir<P: AsRef<Path>>(dir: P) -> std::io::Result<RecursiveDirIterator> {
152    RecursiveDirIterator::from_root(dir).await
153}
154
155/// Holds the state of the directory recursion.
156pub struct RecursiveDirIterator<F = Accept> {
157    base_dir: PathBuf,
158    current_iter: Option<ReadDir>,
159    dirs: Vec<PathBuf>,
160    visit_filter: F,
161}
162
163impl RecursiveDirIterator {
164    /// Creates an iterator that starts right from the provided directory.
165    ///
166    /// The iterator will visit all directories, regardless if they are symlinks or not.
167    #[cfg(not(feature = "tokio"))]
168    pub fn from_root(path: impl AsRef<Path>) -> std::io::Result<Self> {
169        let base_dir = path.as_ref().to_path_buf();
170        let r = read_dir(path)?;
171
172        Ok(RecursiveDirIterator {
173            base_dir,
174            current_iter: Some(r),
175            dirs: vec![],
176            visit_filter: Accept,
177        })
178    }
179
180    /// Creates an iterator that starts right from the provided directory.
181    ///
182    /// The iterator will visit all directories, regardless if they are symlinks or not.
183    #[cfg(feature = "tokio")]
184    pub async fn from_root(path: impl AsRef<Path>) -> std::io::Result<Self> {
185        let base_dir = path.as_ref().to_path_buf();
186        let r = read_dir(path).await?;
187
188        Ok(RecursiveDirIterator {
189            base_dir,
190            current_iter: Some(r),
191            dirs: vec![],
192            visit_filter: Accept,
193        })
194    }
195}
196
197impl RecursiveDirIterator<NoSymlink> {
198    /// Creates an iterator that starts right from the provided directory.
199    ///
200    /// The iterator will visit all directories but will ignore any symlinks.
201    #[cfg(not(feature = "tokio"))]
202    pub fn from_root_no_symlinks(path: impl AsRef<Path>) -> std::io::Result<Self> {
203        let base_dir = path.as_ref().to_path_buf();
204        let r = read_dir(path)?;
205
206        Ok(RecursiveDirIterator {
207            base_dir,
208            current_iter: Some(r),
209            dirs: vec![],
210            visit_filter: NoSymlink,
211        })
212    }
213
214    /// Creates an iterator that starts right from the provided directory.
215    ///
216    /// The iterator will visit all directories but will ignore any symlinks.
217    #[cfg(feature = "tokio")]
218    pub async fn from_root_no_symlinks(path: impl AsRef<Path>) -> std::io::Result<Self> {
219        let base_dir = path.as_ref().to_path_buf();
220        let r = read_dir(path).await?;
221
222        Ok(RecursiveDirIterator {
223            base_dir,
224            current_iter: Some(r),
225            dirs: vec![],
226            visit_filter: NoSymlink,
227        })
228    }
229}
230
231impl RecursiveDirIterator<MaxDepth> {
232    /// Creates an iterator that starts right from the provided directory up to a max depth.
233    #[cfg(not(feature = "tokio"))]
234    pub fn from_root_max_depth(
235        path: impl AsRef<Path>,
236        max_depth: NonZeroUsize,
237    ) -> std::io::Result<Self> {
238        let base_dir = path.as_ref().to_path_buf();
239        let r = read_dir(path)?;
240
241        Ok(RecursiveDirIterator {
242            base_dir,
243            current_iter: Some(r),
244            dirs: vec![],
245            visit_filter: MaxDepth(max_depth),
246        })
247    }
248
249    /// Creates an iterator that starts right from the provided directory up to a max depth.
250    #[cfg(not(feature = "tokio"))]
251    pub fn from_root_single_depth(path: impl AsRef<Path>) -> std::io::Result<Self> {
252        let base_dir = path.as_ref().to_path_buf();
253        let r = read_dir(path)?;
254
255        Ok(RecursiveDirIterator {
256            base_dir,
257            current_iter: Some(r),
258            dirs: vec![],
259            visit_filter: MaxDepth::single_depth(),
260        })
261    }
262
263    /// Creates an iterator that starts right from the provided directory up to a max depth.
264    #[cfg(feature = "tokio")]
265    pub async fn from_root_max_depth(
266        path: impl AsRef<Path>,
267        max_depth: NonZeroUsize,
268    ) -> std::io::Result<Self> {
269        let base_dir = path.as_ref().to_path_buf();
270        let r = read_dir(path).await?;
271
272        Ok(RecursiveDirIterator {
273            base_dir,
274            current_iter: Some(r),
275            dirs: vec![],
276            visit_filter: MaxDepth(max_depth),
277        })
278    }
279
280    /// Creates an iterator that starts right from the provided directory up to a max depth.
281    #[cfg(feature = "tokio")]
282    pub async fn from_root_single_depth(path: impl AsRef<Path>) -> std::io::Result<Self> {
283        let base_dir = path.as_ref().to_path_buf();
284        let r = read_dir(path).await?;
285
286        Ok(RecursiveDirIterator {
287            base_dir,
288            current_iter: Some(r),
289            dirs: vec![],
290            visit_filter: MaxDepth::single_depth(),
291        })
292    }
293}
294
295impl<F> RecursiveDirIterator<Closure<F>> {
296    /// Creates an iterator that starts right from the provided directory with a specific filter
297    /// to decide which directories to visit.
298    #[cfg(not(feature = "tokio"))]
299    pub fn with_closure_filter(path: impl AsRef<Path>, filter: F) -> std::io::Result<Self>
300    where
301        F: Into<Closure<F>>,
302    {
303        let base_dir = path.as_ref().to_path_buf();
304        let r = read_dir(path)?;
305
306        Ok(RecursiveDirIterator {
307            base_dir,
308            current_iter: Some(r),
309            dirs: vec![],
310            visit_filter: filter.into(),
311        })
312    }
313
314    #[cfg(feature = "tokio")]
315    pub async fn with_closure_filter(path: impl AsRef<Path>, filter: F) -> std::io::Result<Self>
316    where
317        F: Into<Closure<F>>,
318    {
319        let base_dir = path.as_ref().to_path_buf();
320        let r = read_dir(path).await?;
321
322        Ok(RecursiveDirIterator {
323            base_dir,
324            current_iter: Some(r),
325            dirs: vec![],
326            visit_filter: filter.into(),
327        })
328    }
329}
330
331impl<F> RecursiveDirIterator<This<F>> {
332    /// Creates an iterator that starts right from the provided directory with a specific filter
333    /// to decide which directories to visit.
334    #[cfg(not(feature = "tokio"))]
335    pub fn with_filter(path: impl AsRef<Path>, filter: F) -> std::io::Result<Self>
336    where
337        F: Filter,
338    {
339        let base_dir = path.as_ref().to_path_buf();
340        let r = read_dir(path)?;
341
342        Ok(RecursiveDirIterator {
343            base_dir,
344            current_iter: Some(r),
345            dirs: vec![],
346            visit_filter: This::new(filter),
347        })
348    }
349
350    /// Creates an iterator that starts right from the provided directory with a specific filter
351    /// to decide which directories to visit.
352    #[cfg(feature = "tokio")]
353    pub async fn with_filter(path: impl AsRef<Path>, filter: F) -> std::io::Result<Self>
354    where
355        F: Filter,
356    {
357        let base_dir = path.as_ref().to_path_buf();
358        let r = read_dir(path).await?;
359
360        Ok(RecursiveDirIterator {
361            base_dir,
362            current_iter: Some(r),
363            dirs: vec![],
364            visit_filter: This::new(filter),
365        })
366    }
367}
368
369impl<S> RecursiveDirIterator<Extension<S>> {
370    /// Creates an iterator that only emits the paths with the provided extension.
371    ///
372    /// Read more at [`Extension`] documentation.
373    #[cfg(not(feature = "tokio"))]
374    pub fn with_extension(path: impl AsRef<Path>, extension: S) -> std::io::Result<Self> {
375        let base_dir = path.as_ref().to_path_buf();
376        let r = read_dir(path)?;
377
378        Ok(RecursiveDirIterator {
379            base_dir,
380            current_iter: Some(r),
381            dirs: vec![],
382            visit_filter: Extension::new(extension),
383        })
384    }
385
386    /// Creates an iterator that only emits the paths with the provided extension.
387    ///
388    /// Read more at [`Extension`] documentation.
389    #[cfg(feature = "tokio")]
390    pub async fn with_extension(path: impl AsRef<Path>, extension: S) -> std::io::Result<Self> {
391        let base_dir = path.as_ref().to_path_buf();
392        let r = read_dir(path).await?;
393
394        Ok(RecursiveDirIterator {
395            base_dir,
396            current_iter: Some(r),
397            dirs: vec![],
398            visit_filter: Extension::new(extension),
399        })
400    }
401}
402
403#[cfg(not(feature = "tokio"))]
404impl<F> Iterator for RecursiveDirIterator<F>
405where
406    F: Filter,
407{
408    type Item = PathBuf;
409
410    fn next(&mut self) -> Option<Self::Item> {
411        fn compute_next_iter<VF>(iter: &mut RecursiveDirIterator<VF>) -> Option<ReadDir> {
412            while let Some(ref p) = iter.dirs.pop() {
413                if let Ok(d) = read_dir(p) {
414                    return Some(d);
415                }
416            }
417            None
418        }
419
420        if self.current_iter.is_none() {
421            self.current_iter = compute_next_iter(self);
422        }
423
424        while let Some(ref mut iter) = self.current_iter {
425            for n in iter.flatten().by_ref() {
426                let path = n.path();
427
428                if path.is_dir() && self.visit_filter.filter(&self.base_dir, &path) {
429                    self.dirs.push(path.clone());
430                }
431
432                if self.visit_filter.should_emit(&path) {
433                    return Some(path.clone());
434                }
435            }
436
437            self.current_iter = compute_next_iter(self);
438        }
439
440        None
441    }
442}
443
444#[cfg(feature = "tokio")]
445impl<F> RecursiveDirIterator<F>
446where
447    F: Filter,
448{
449    pub async fn next(&mut self) -> Option<PathBuf> {
450        async fn compute_next_iter<VF>(iter: &mut RecursiveDirIterator<VF>) -> Option<ReadDir> {
451            while let Some(ref p) = iter.dirs.pop() {
452                if let Ok(d) = read_dir(p).await {
453                    return Some(d);
454                }
455            }
456            None
457        }
458
459        if let None = self.current_iter {
460            self.current_iter = compute_next_iter(self).await;
461        }
462
463        while let Some(ref mut iter) = self.current_iter {
464            loop {
465                let next_entry = iter.next_entry().await;
466                match next_entry {
467                    Ok(Some(n)) => {
468                        let path = n.path();
469
470                        if path.is_dir() {
471                            if self.visit_filter.filter(&self.base_dir, &path) {
472                                self.dirs.push(path.clone());
473                            }
474                        }
475
476                        if self.visit_filter.should_emit(&path) {
477                            return Some(path.clone());
478                        }
479                    }
480                    Ok(None) => {
481                        break;
482                    }
483                    Err(_) => {
484                        // TODO: handle this properly.
485                    }
486                }
487            }
488
489            self.current_iter = compute_next_iter(self).await;
490        }
491
492        None
493    }
494}
495
496#[cfg(test)]
497#[cfg(not(feature = "tokio"))]
498mod tests {
499    use crate::filter::Extension;
500    use crate::{Filter, MaxDepth, NoSymlink, RecursiveDirIterator};
501    use std::collections::HashSet;
502    use std::num::NonZeroUsize;
503    use std::path::{Path, PathBuf};
504
505    #[test]
506    fn all_files() {
507        let mut dirs = HashSet::new();
508        let from_test_dir = RecursiveDirIterator::from_root("test_dir").unwrap();
509
510        for x in from_test_dir {
511            dirs.insert(x.to_string_lossy().to_string());
512        }
513
514        assert_eq!(dirs.len(), 18);
515        assert!(dirs.contains("test_dir/foo"));
516        assert!(dirs.contains("test_dir/bar"));
517        assert!(dirs.contains("test_dir/baz"));
518        assert!(dirs.contains("test_dir/fus"));
519        assert!(dirs.contains("test_dir/baz/meow"));
520        assert!(dirs.contains("test_dir/baz/meow/oof"));
521        assert!(dirs.contains("test_dir/baz/meow/uuf"));
522        assert!(dirs.contains("test_dir/baz/barbaz"));
523        assert!(dirs.contains("test_dir/baz/foobaz"));
524        assert!(dirs.contains("test_dir/fus/dragonborn"));
525        assert!(dirs.contains("test_dir/fus/dragonborn/witchcraft"));
526        assert!(dirs.contains("test_dir/fus/ro"));
527        assert!(dirs.contains("test_dir/fus/ro/dah"));
528        assert!(dirs.contains("test_dir/fus/ro/dah/dovahkiin"));
529        assert!(dirs.contains("test_dir/one_more_weird_dir"));
530        assert!(dirs.contains("test_dir/one_more_weird_dir/file.i"));
531        assert!(dirs.contains("test_dir/one_more_weird_dir/another_weird_dir.f"));
532        assert!(dirs.contains("test_dir/one_more_weird_dir/another_weird_dir.f/foa"));
533    }
534
535    #[test]
536    fn no_symlinks() {
537        let mut dirs = HashSet::new();
538        let from_test_dir =
539            RecursiveDirIterator::with_closure_filter("test_dir", |_: &Path, p: &Path| {
540                !p.is_symlink()
541            })
542            .unwrap();
543
544        for x in from_test_dir {
545            dirs.insert(x.to_string_lossy().to_string());
546        }
547
548        assert_eq!(dirs.len(), 18);
549        assert!(dirs.contains("test_dir/foo"));
550        assert!(dirs.contains("test_dir/bar"));
551        assert!(dirs.contains("test_dir/baz"));
552        assert!(dirs.contains("test_dir/fus"));
553        assert!(dirs.contains("test_dir/baz/meow"));
554        assert!(dirs.contains("test_dir/baz/meow/oof"));
555        assert!(dirs.contains("test_dir/baz/meow/uuf"));
556        assert!(dirs.contains("test_dir/baz/barbaz"));
557        assert!(dirs.contains("test_dir/baz/foobaz"));
558        assert!(dirs.contains("test_dir/fus/dragonborn"));
559        assert!(dirs.contains("test_dir/fus/dragonborn/witchcraft"));
560        assert!(dirs.contains("test_dir/fus/ro"));
561        assert!(dirs.contains("test_dir/fus/ro/dah"));
562        assert!(dirs.contains("test_dir/fus/ro/dah/dovahkiin"));
563        assert!(dirs.contains("test_dir/one_more_weird_dir"));
564        assert!(dirs.contains("test_dir/one_more_weird_dir/file.i"));
565        assert!(dirs.contains("test_dir/one_more_weird_dir/another_weird_dir.f"));
566        assert!(dirs.contains("test_dir/one_more_weird_dir/another_weird_dir.f/foa"));
567    }
568
569    #[test]
570    fn depth_1() {
571        let mut dirs = HashSet::new();
572
573        let root = PathBuf::from("test_dir");
574        let ancestors = root.ancestors().count();
575
576        let from_test_dir =
577            RecursiveDirIterator::with_closure_filter(&root, |_: &Path, p: &Path| {
578                p.ancestors().count() - ancestors == 0
579            })
580            .unwrap();
581
582        for x in from_test_dir {
583            dirs.insert(x.to_string_lossy().to_string());
584        }
585
586        assert_eq!(dirs.len(), 5);
587        assert!(dirs.contains("test_dir/foo"));
588        assert!(dirs.contains("test_dir/bar"));
589        assert!(dirs.contains("test_dir/baz"));
590        assert!(dirs.contains("test_dir/fus"));
591        assert!(dirs.contains("test_dir/one_more_weird_dir"));
592    }
593
594    #[test]
595    fn depth_2() {
596        let mut dirs = HashSet::new();
597
598        let root = PathBuf::from("test_dir");
599        let ancestors = root.ancestors().count();
600
601        let from_test_dir =
602            RecursiveDirIterator::with_closure_filter(&root, |_: &Path, p: &Path| {
603                p.ancestors().count() - ancestors <= 1
604            })
605            .unwrap();
606
607        for x in from_test_dir {
608            dirs.insert(x.to_string_lossy().to_string());
609        }
610
611        assert_eq!(dirs.len(), 12);
612        assert!(dirs.contains("test_dir/foo"));
613        assert!(dirs.contains("test_dir/bar"));
614        assert!(dirs.contains("test_dir/baz"));
615        assert!(dirs.contains("test_dir/fus"));
616        assert!(dirs.contains("test_dir/baz/meow"));
617        assert!(dirs.contains("test_dir/baz/barbaz"));
618        assert!(dirs.contains("test_dir/baz/foobaz"));
619        assert!(dirs.contains("test_dir/fus/dragonborn"));
620        assert!(dirs.contains("test_dir/fus/ro"));
621        assert!(dirs.contains("test_dir/one_more_weird_dir"));
622        assert!(dirs.contains("test_dir/one_more_weird_dir/file.i"));
623    }
624
625    #[test]
626    fn no_symlink_depth_2() {
627        let mut dirs = HashSet::new();
628
629        let root = PathBuf::from("test_dir");
630        let _ancestors = root.ancestors().count();
631
632        let from_test_dir = RecursiveDirIterator::with_filter(
633            &root,
634            NoSymlink.and(MaxDepth::new(NonZeroUsize::new(2).unwrap())),
635        )
636        .unwrap();
637
638        for x in from_test_dir {
639            dirs.insert(x.to_string_lossy().to_string());
640        }
641
642        assert_eq!(dirs.len(), 12);
643        assert!(dirs.contains("test_dir/foo"));
644        assert!(dirs.contains("test_dir/bar"));
645        assert!(dirs.contains("test_dir/baz"));
646        assert!(dirs.contains("test_dir/fus"));
647        assert!(dirs.contains("test_dir/baz/meow"));
648        assert!(dirs.contains("test_dir/baz/barbaz"));
649        assert!(dirs.contains("test_dir/baz/foobaz"));
650        assert!(dirs.contains("test_dir/fus/dragonborn"));
651        assert!(dirs.contains("test_dir/fus/ro"));
652        assert!(dirs.contains("test_dir/one_more_weird_dir"));
653        assert!(dirs.contains("test_dir/one_more_weird_dir/file.i"));
654    }
655
656    #[test]
657    fn file_with_extension() {
658        let mut dirs = Vec::new();
659
660        let root = PathBuf::from("test_dir");
661
662        let from_test_dir = RecursiveDirIterator::with_filter(&root, Extension::new("i")).unwrap();
663
664        for x in from_test_dir {
665            dirs.push(x.to_string_lossy().to_string());
666        }
667
668        dirs.sort();
669
670        assert_eq!(dirs.len(), 1);
671        assert_eq!(
672            dirs,
673            vec![String::from("test_dir/one_more_weird_dir/file.i")]
674        );
675    }
676
677    #[test]
678    fn path_with_extension() {
679        let mut dirs = Vec::new();
680
681        let root = PathBuf::from("test_dir");
682
683        let from_test_dir = RecursiveDirIterator::with_filter(&root, Extension::new("f")).unwrap();
684
685        for x in from_test_dir {
686            dirs.push(x.to_string_lossy().to_string());
687        }
688
689        dirs.sort();
690
691        assert_eq!(dirs.len(), 1);
692        assert_eq!(
693            dirs,
694            vec![String::from(
695                "test_dir/one_more_weird_dir/another_weird_dir.f"
696            )]
697        );
698    }
699}
700
701#[cfg(test)]
702#[cfg(feature = "tokio")]
703mod tests {
704    use crate::filter::Extension;
705    use crate::{Filter, MaxDepth, NoSymlink, RecursiveDirIterator};
706    use std::collections::HashSet;
707    use std::num::NonZeroUsize;
708    use std::path::{Path, PathBuf};
709
710    #[tokio::test]
711    async fn tokio_all_files() {
712        let mut dirs = HashSet::new();
713        let mut from_test_dir = RecursiveDirIterator::from_root("test_dir").await.unwrap();
714
715        while let Some(x) = from_test_dir.next().await {
716            dirs.insert(x.to_string_lossy().to_string());
717        }
718
719        assert_eq!(dirs.len(), 18);
720        assert!(dirs.contains("test_dir/foo"));
721        assert!(dirs.contains("test_dir/bar"));
722        assert!(dirs.contains("test_dir/baz"));
723        assert!(dirs.contains("test_dir/fus"));
724        assert!(dirs.contains("test_dir/baz/meow"));
725        assert!(dirs.contains("test_dir/baz/meow/oof"));
726        assert!(dirs.contains("test_dir/baz/meow/uuf"));
727        assert!(dirs.contains("test_dir/baz/barbaz"));
728        assert!(dirs.contains("test_dir/baz/foobaz"));
729        assert!(dirs.contains("test_dir/fus/dragonborn"));
730        assert!(dirs.contains("test_dir/fus/dragonborn/witchcraft"));
731        assert!(dirs.contains("test_dir/fus/ro"));
732        assert!(dirs.contains("test_dir/fus/ro/dah"));
733        assert!(dirs.contains("test_dir/fus/ro/dah/dovahkiin"));
734        assert!(dirs.contains("test_dir/one_more_weird_dir"));
735        assert!(dirs.contains("test_dir/one_more_weird_dir/file.i"));
736        assert!(dirs.contains("test_dir/one_more_weird_dir/another_weird_dir.f"));
737        assert!(dirs.contains("test_dir/one_more_weird_dir/another_weird_dir.f/foa"));
738    }
739
740    #[tokio::test]
741    async fn tokio_no_symlinks() {
742        let mut dirs = HashSet::new();
743        let mut from_test_dir =
744            RecursiveDirIterator::with_closure_filter("test_dir", |_: &Path, p: &Path| {
745                !p.is_symlink()
746            })
747            .await
748            .unwrap();
749
750        while let Some(x) = from_test_dir.next().await {
751            dirs.insert(x.to_string_lossy().to_string());
752        }
753
754        assert_eq!(dirs.len(), 18);
755        assert!(dirs.contains("test_dir/foo"));
756        assert!(dirs.contains("test_dir/bar"));
757        assert!(dirs.contains("test_dir/baz"));
758        assert!(dirs.contains("test_dir/fus"));
759        assert!(dirs.contains("test_dir/baz/meow"));
760        assert!(dirs.contains("test_dir/baz/meow/oof"));
761        assert!(dirs.contains("test_dir/baz/meow/uuf"));
762        assert!(dirs.contains("test_dir/baz/barbaz"));
763        assert!(dirs.contains("test_dir/baz/foobaz"));
764        assert!(dirs.contains("test_dir/fus/dragonborn"));
765        assert!(dirs.contains("test_dir/fus/dragonborn/witchcraft"));
766        assert!(dirs.contains("test_dir/fus/ro"));
767        assert!(dirs.contains("test_dir/fus/ro/dah"));
768        assert!(dirs.contains("test_dir/fus/ro/dah/dovahkiin"));
769        assert!(dirs.contains("test_dir/one_more_weird_dir"));
770        assert!(dirs.contains("test_dir/one_more_weird_dir/file.i"));
771        assert!(dirs.contains("test_dir/one_more_weird_dir/another_weird_dir.f"));
772        assert!(dirs.contains("test_dir/one_more_weird_dir/another_weird_dir.f/foa"));
773    }
774
775    #[tokio::test]
776    async fn tokio_depth_1() {
777        let mut dirs = HashSet::new();
778
779        let root = PathBuf::from("test_dir");
780        let ancestors = root.ancestors().count();
781
782        let mut from_test_dir =
783            RecursiveDirIterator::with_closure_filter(&root, |_: &Path, p: &Path| {
784                p.ancestors().count() - ancestors <= 0
785            })
786            .await
787            .unwrap();
788
789        while let Some(x) = from_test_dir.next().await {
790            dirs.insert(x.to_string_lossy().to_string());
791        }
792
793        assert_eq!(dirs.len(), 5);
794        assert!(dirs.contains("test_dir/foo"));
795        assert!(dirs.contains("test_dir/bar"));
796        assert!(dirs.contains("test_dir/baz"));
797        assert!(dirs.contains("test_dir/fus"));
798        assert!(dirs.contains("test_dir/one_more_weird_dir"));
799    }
800
801    #[tokio::test]
802    async fn tokio_depth_2() {
803        let mut dirs = HashSet::new();
804
805        let root = PathBuf::from("test_dir");
806        let ancestors = root.ancestors().count();
807
808        let mut from_test_dir =
809            RecursiveDirIterator::with_closure_filter(&root, |_: &Path, p: &Path| {
810                p.ancestors().count() - ancestors <= 1
811            })
812            .await
813            .unwrap();
814
815        while let Some(x) = from_test_dir.next().await {
816            dirs.insert(x.to_string_lossy().to_string());
817        }
818
819        assert_eq!(dirs.len(), 12);
820        assert!(dirs.contains("test_dir/foo"));
821        assert!(dirs.contains("test_dir/bar"));
822        assert!(dirs.contains("test_dir/baz"));
823        assert!(dirs.contains("test_dir/fus"));
824        assert!(dirs.contains("test_dir/baz/meow"));
825        assert!(dirs.contains("test_dir/baz/barbaz"));
826        assert!(dirs.contains("test_dir/baz/foobaz"));
827        assert!(dirs.contains("test_dir/fus/dragonborn"));
828        assert!(dirs.contains("test_dir/fus/ro"));
829        assert!(dirs.contains("test_dir/one_more_weird_dir"));
830        assert!(dirs.contains("test_dir/one_more_weird_dir/file.i"));
831    }
832
833    #[tokio::test]
834    async fn tokio_no_symlink_depth_2() {
835        let mut dirs = HashSet::new();
836
837        let root = PathBuf::from("test_dir");
838        let _ancestors = root.ancestors().count();
839
840        let mut from_test_dir = RecursiveDirIterator::with_filter(
841            &root,
842            NoSymlink.and(MaxDepth::new(NonZeroUsize::new(2).unwrap())),
843        )
844        .await
845        .unwrap();
846
847        while let Some(x) = from_test_dir.next().await {
848            dirs.insert(x.to_string_lossy().to_string());
849        }
850
851        assert_eq!(dirs.len(), 12);
852        assert!(dirs.contains("test_dir/foo"));
853        assert!(dirs.contains("test_dir/bar"));
854        assert!(dirs.contains("test_dir/baz"));
855        assert!(dirs.contains("test_dir/fus"));
856        assert!(dirs.contains("test_dir/baz/meow"));
857        assert!(dirs.contains("test_dir/baz/barbaz"));
858        assert!(dirs.contains("test_dir/baz/foobaz"));
859        assert!(dirs.contains("test_dir/fus/dragonborn"));
860        assert!(dirs.contains("test_dir/fus/ro"));
861        assert!(dirs.contains("test_dir/one_more_weird_dir"));
862        assert!(dirs.contains("test_dir/one_more_weird_dir/file.i"));
863    }
864
865    #[tokio::test]
866    async fn tokio_file_with_extension() {
867        let mut dirs = Vec::new();
868
869        let root = PathBuf::from("test_dir");
870
871        let mut from_test_dir = RecursiveDirIterator::with_filter(&root, Extension::new("i"))
872            .await
873            .unwrap();
874
875        while let Some(x) = from_test_dir.next().await {
876            dirs.push(x.to_string_lossy().to_string());
877        }
878
879        dirs.sort();
880
881        assert_eq!(dirs.len(), 1);
882        assert_eq!(
883            dirs,
884            vec![String::from("test_dir/one_more_weird_dir/file.i")]
885        );
886    }
887
888    #[tokio::test]
889    async fn tokio_path_with_extension() {
890        let mut dirs = Vec::new();
891
892        let root = PathBuf::from("test_dir");
893
894        let mut from_test_dir = RecursiveDirIterator::with_filter(&root, Extension::new("f"))
895            .await
896            .unwrap();
897
898        while let Some(x) = from_test_dir.next().await {
899            dirs.push(x.to_string_lossy().to_string());
900        }
901
902        dirs.sort();
903
904        assert_eq!(dirs.len(), 1);
905        assert_eq!(
906            dirs,
907            vec![String::from(
908                "test_dir/one_more_weird_dir/another_weird_dir.f"
909            )]
910        );
911    }
912}