fs_walk/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2//! `fs_walk` is a Rust crate for efficiently and flexibly walking the filesystem.
3//! It provides a simple, ergonomic API for recursively traversing directories with fine-grained control over filtering, sorting, and symlink handling.
4//!
5//! ## Why Use `fs_walk`?
6//! - **Flexible filtering**: Filter by file extension, name, regex, or custom predicates.
7//! - **Batch processing**: Process files in chunks for memory efficiency.
8//! - **Symlink handling**: Safely follow symlinks with loop protection.
9//! - **Ergonomic API**: Chainable methods for intuitive configuration.
10//!
11//! ## Features
12//! | Feature               | Description                                                                                     |
13//! |-----------------------|-------------------------------------------------------------------------------------------------|
14//! | **Depth control**     | Limit traversal depth to avoid unnecessary work.                                               |
15//! | **Result chunking**   | Process results in batches for memory efficiency.                                              |
16//! | **Filtering**         | Filter by extension, name, regex, or custom predicates.                                        |
17//! | **Symlink support**   | Optionally follow symlinks with loop protection.                                               |
18//! | **Sorting**           | Sort directory entries for consistent results.                                                 |
19//! | **Regex matching**    | Enable regex-based filtering (requires the `regex` feature).                                   |
20//!
21//! ## Installation
22//! Add `fs_walk` to your `Cargo.toml`:
23//! ```toml
24//! [dependencies]
25//! fs_walk = "0.2"
26//! ```
27//!
28//! ### Cargo Features
29//! - **`regex`**: Enables regex matching for file and directory names, using the `regex` crate
30//!   Enable with:
31//!   ```toml
32//!   [dependencies]
33//!   fs_walk = { version = "0.2", features = ["regex"] }
34//!   ```
35//!
36//! ## Usage
37//!
38//! ### Basic Usage
39//! Walk all files and directories in the current directory:
40//! ```rust
41//! use fs_walk::WalkOptions;
42//!
43//! let walker = WalkOptions::new().walk(".");
44//! for path in walker.flatten() {
45//!     println!("Found: {:?}", path);
46//! }
47//! ```
48//!
49//! ### Filtering Files
50//! Walk only Rust files (`.rs` extension):
51//! ```rust
52//! use fs_walk::WalkOptions;
53//!
54//! let walker = WalkOptions::new()
55//!     .files()
56//!     .extension("rs")
57//!     .walk(".");
58//! for path in walker.flatten() {
59//!     println!("Found Rust file: {:?}", path);
60//! }
61//! ```
62//!
63//! ### Chunking Results
64//! Process files in chunks of 10 for batch operations:
65//! ```rust
66//! use fs_walk::WalkOptions;
67//!
68//! let walker = WalkOptions::new()
69//!     .files()
70//!     .extension("o")
71//!     .walk(".")
72//!     .chunks(10);
73//! for chunk in walker {
74//!     for path in chunk.iter().flatten() {
75//!         println!("Processing: {:?}", path);
76//!     }
77//! }
78//! ```
79//!
80//! ### Regex Matching
81//! Walk files matching a regex pattern (requires the `regex` feature):
82//! ```rust
83//! use fs_walk::WalkOptions;
84//!
85//! let walker = WalkOptions::new()
86//!     .name_regex(r#"^.*\.rs$"#)
87//!     .unwrap()
88//!     .walk(".");
89//! for path in walker.flatten() {
90//!     println!("Found matching file: {:?}", path);
91//! }
92//! ```
93//!
94//! ### Following Symlinks
95//! Walk directories while following symlinks (with loop protection):
96//! ```rust
97//! use fs_walk::WalkOptions;
98//!
99//! let walker = WalkOptions::new()
100//!     .dirs()
101//!     .follow_symlink()
102//!     .walk(".");
103//! for path in walker.flatten() {
104//!     println!("Found directory: {:?}", path);
105//! }
106//! ```
107//!
108//! ## Contributing
109//! Contributions are welcome! If you’d like to report a bug, suggest a feature, or submit a PR.
110//!
111//! ## License
112//! This project is licensed under the [GPL-3 License](./LICENSE).
113#![deny(unused_imports)]
114
115use std::{
116    collections::{HashSet, VecDeque},
117    fs, io,
118    path::{Path, PathBuf},
119};
120
121#[cfg(feature = "regex")]
122use regex::Regex;
123
124/// Structure encoding the desired walking
125/// options
126///
127/// # Example
128///
129/// ```
130/// use fs_walk;
131///
132/// let w = fs_walk::WalkOptions::new()
133///     // we want to walk only files
134///     .files()
135///     // we want files with .o extension
136///     .extension("o")
137///     .walk("./");
138///
139/// assert!(w.count() > 0);
140/// ```
141#[derive(Debug, Default, Clone)]
142pub struct WalkOptions {
143    sort: bool,
144    dirs_only: bool,
145    files_only: bool,
146    follow_symlink: bool,
147    max_depth: Option<u64>,
148    extensions: HashSet<String>,
149    ends_with: Vec<String>,
150    names: HashSet<String>,
151    #[cfg(feature = "regex")]
152    regex: Vec<Regex>,
153}
154
155impl WalkOptions {
156    /// Create default walking options. The default
157    /// behaviour is to return both files and directories.
158    ///
159    /// # Example
160    ///
161    /// ```
162    /// use fs_walk;
163    /// use std::path::PathBuf;
164    ///
165    /// let o = fs_walk::WalkOptions::new();
166    ///
167    /// let paths: Vec<PathBuf> = o.walk("./").flatten().collect();
168    ///
169    /// assert!(paths.iter().any(|p| p.is_dir()));
170    /// assert!(paths.iter().any(|p| p.is_file()));
171    /// ```
172    #[inline(always)]
173    pub fn new() -> Self {
174        Self::default()
175    }
176
177    /// Configure walking option to return only
178    /// directories
179    ///
180    /// # Example
181    ///
182    /// ```
183    /// use fs_walk::WalkOptions;
184    ///
185    /// for p in WalkOptions::new().dirs().walk("./").flatten() {
186    ///     assert!(p.is_dir());
187    /// }
188    /// ```
189    #[inline(always)]
190    pub fn dirs(&mut self) -> &mut Self {
191        self.dirs_only = true;
192        self
193    }
194
195    /// Configure walking option to return only
196    /// files
197    ///
198    /// # Example
199    ///
200    /// ```
201    /// use fs_walk::WalkOptions;
202    ///
203    /// for p in WalkOptions::new().files().walk("./").flatten() {
204    ///     assert!(p.is_file());
205    /// }
206    /// ```
207    #[inline(always)]
208    pub fn files(&mut self) -> &mut Self {
209        self.files_only = true;
210        self
211    }
212
213    /// Configures the walker to follow symbolic links during traversal.
214    ///
215    /// By default, the walker does **not** follow symbolic links.
216    /// When this option is enabled, the walker will recursively traverse
217    /// into directories pointed to by symbolic links, as if they were real directories.
218    ///
219    /// # Symlink Loop Protection
220    /// The walker is protected against infinite loops caused by cyclic symlinks.
221    /// It uses the canonical path and a hash set of visited directories (via BLAKE3 hashing)
222    /// to ensure each directory is only visited once, even if it is linked multiple times.
223    ///
224    /// # Example
225    ///
226    /// ```rust
227    /// use fs_walk::WalkOptions;
228    ///
229    /// // Create a walker that follows symlinks
230    /// let mut options = WalkOptions::new();
231    /// options.follow_symlink();
232    ///
233    /// // Now symlinks to directories will be traversed
234    /// for entry in options.walk("./").flatten() {
235    ///     println!("{:?}", entry);
236    /// }
237    /// ```
238    ///
239    /// # Safety
240    /// While the walker is protected against symlink loops, be cautious when enabling this option
241    /// in untrusted directories, as it may still expose your program to other symlink-based attacks.
242    pub fn follow_symlink(&mut self) -> &mut Self {
243        self.follow_symlink = true;
244        self
245    }
246
247    /// Configure a maximum depth for the walker. If no depth
248    /// is specified the walker will walk through all directories
249    /// in a BFS way.
250    ///
251    /// # Example
252    ///
253    /// ```
254    /// use fs_walk::WalkOptions;
255    /// use std::path::Path;
256    ///
257    /// for p in WalkOptions::new().max_depth(0).walk("./").flatten() {
258    ///     assert_eq!(p.parent(), Some(Path::new(".")));
259    /// }
260    ///
261    /// ```
262    #[inline(always)]
263    pub fn max_depth(&mut self, depth: u64) -> &mut Self {
264        self.max_depth = Some(depth);
265        self
266    }
267
268    /// Configure walker to return only files matching file extension.
269    /// For any file, if [Path::extension] is not [None] it will be
270    /// checked against `ext`. This function can be called several
271    /// times to return files matching one of the desired extension.
272    ///
273    /// See [Path::extension] for the correct way to specify `ext`.
274    ///
275    /// # Example
276    /// ```
277    /// use fs_walk;
278    /// use std::path::PathBuf;
279    /// use std::ffi::OsStr;
280    ///
281    /// let wk = fs_walk::WalkOptions::new()
282    ///     .files()
283    ///     .extension("o")
284    ///     .extension("rs")
285    ///     .walk("./");
286    ///
287    /// let paths: Vec<PathBuf> = wk.flatten().collect();
288    ///
289    /// assert!(paths.iter().any(|p| p.extension() == Some(OsStr::new("o"))));
290    /// assert!(paths.iter().any(|p| p.extension() == Some(OsStr::new("rs"))));
291    /// assert!(!paths.iter().any(|p| p.extension() == Some(OsStr::new("toml"))));
292    /// assert!(!paths.iter().any(|p| p.extension() == Some(OsStr::new("lock"))));
293    /// ```
294    #[inline(always)]
295    pub fn extension<S: AsRef<str>>(&mut self, ext: S) -> &mut Self {
296        self.extensions.insert(ext.as_ref().to_string());
297        self
298    }
299
300    /// Configure walker to return files ending with pattern `pat`
301    /// For any file, if [Path::to_string_lossy] **ends with** pattern
302    /// `pat` it is going to be returned.
303    ///
304    /// This method can be used to match path with double extensions (i.e. `.txt.gz`)
305    /// without having to do manual pattern matching on walker's output.
306    ///
307    /// See [str::ends_with] for more detail about matching
308    ///
309    /// # Example
310    /// ```
311    /// use fs_walk;
312    /// use std::path::PathBuf;
313    /// use std::ffi::OsStr;
314    ///
315    /// let wk = fs_walk::WalkOptions::new()
316    ///     .files()
317    ///     .extension("o")
318    ///     // we can put . here not in extension
319    ///     // can be used to match path with double extensions
320    ///     .ends_with(".rs")
321    ///     .walk("./");
322    ///
323    /// let paths: Vec<PathBuf> = wk.flatten().collect();
324    ///
325    /// assert!(paths.iter().any(|p| p.extension() == Some(OsStr::new("o"))));
326    /// assert!(paths.iter().any(|p| p.extension() == Some(OsStr::new("rs"))));
327    /// assert!(!paths.iter().any(|p| p.extension() == Some(OsStr::new("toml"))));
328    /// assert!(!paths.iter().any(|p| p.extension() == Some(OsStr::new("lock"))));
329    /// ```
330    #[inline(always)]
331    pub fn ends_with<S: AsRef<str>>(&mut self, pat: S) -> &mut Self {
332        self.ends_with.push(pat.as_ref().to_string());
333        self
334    }
335
336    /// Configure walker to return only paths with [Path::file_name] matching `name`
337    ///
338    /// # Example
339    ///
340    /// ```
341    /// use fs_walk;
342    /// use std::path::PathBuf;
343    /// use std::ffi::OsStr;
344    ///
345    /// let wk = fs_walk::WalkOptions::new()
346    ///     .name("lib.rs")
347    ///     .walk("./");
348    ///
349    /// let paths: Vec<PathBuf> = wk.flatten().collect();
350    ///
351    /// assert!(paths.iter().all(|p| p.file_name() == Some(OsStr::new("lib.rs"))));
352    /// assert!(paths.iter().all(|p| p.is_file()));
353    /// ```
354    #[inline(always)]
355    pub fn name<S: AsRef<str>>(&mut self, name: S) -> &mut Self {
356        self.names.insert(name.as_ref().to_string());
357        self
358    }
359
360    /// Configure walker to return only paths with [Path::file_name] matching `regex`
361    ///
362    /// # Example
363    ///
364    /// ```
365    /// use fs_walk;
366    /// use std::path::PathBuf;
367    /// use std::ffi::OsStr;
368    ///
369    /// let w = fs_walk::WalkOptions::new()
370    ///     .name_regex(r#"^(.*\.rs|src|target)$"#)
371    ///     .unwrap()
372    ///     .walk("./");
373    /// let paths: Vec<PathBuf> = w.flatten().collect();
374    /// assert!(paths.iter().any(|p| p.is_dir()));
375    /// assert!(paths.iter().any(|p| p.is_file()));
376    /// ```
377    #[inline(always)]
378    #[cfg(feature = "regex")]
379    #[cfg_attr(docsrs, doc(cfg(feature = "regex")))]
380    pub fn name_regex<S: AsRef<str>>(&mut self, regex: S) -> Result<&mut Self, regex::Error> {
381        self.regex.push(Regex::new(regex.as_ref())?);
382        Ok(self)
383    }
384
385    #[inline(always)]
386    fn regex_is_empty(&self) -> bool {
387        #[cfg(feature = "regex")]
388        return self.regex.is_empty();
389        #[cfg(not(feature = "regex"))]
390        true
391    }
392
393    #[inline(always)]
394    fn path_match<P: AsRef<Path>>(&self, p: P) -> bool {
395        let p = p.as_ref();
396
397        if self.extensions.is_empty()
398            && self.ends_with.is_empty()
399            && self.names.is_empty()
400            && self.regex_is_empty()
401        {
402            return true;
403        }
404
405        // test filename
406        if !self.names.is_empty()
407            && p.file_name()
408                .and_then(|file_name| file_name.to_str())
409                .map(|file_name| self.names.contains(file_name))
410                .unwrap_or_default()
411        {
412            return true;
413        }
414
415        // we check for extension
416        if !self.extensions.is_empty()
417            && p.extension()
418                .and_then(|ext| ext.to_str())
419                .map(|ext| self.extensions.contains(ext))
420                .unwrap_or_default()
421        {
422            return true;
423        }
424
425        // we check for paths ending with pattern
426        for trail in self.ends_with.iter() {
427            if p.to_string_lossy().ends_with(trail) {
428                return true;
429            }
430        }
431
432        // we check regex
433        #[cfg(feature = "regex")]
434        if let Some(file_name) = p.file_name().and_then(|n| n.to_str()) {
435            for re in self.regex.iter() {
436                if re.is_match(file_name) {
437                    return true;
438                }
439            }
440        }
441
442        false
443    }
444
445    /// Sorts entries at every directory listing
446    #[inline(always)]
447    pub fn sort(&mut self, value: bool) -> &mut Self {
448        self.sort = value;
449        self
450    }
451
452    /// Turns [self] into a [Walker]
453    #[inline(always)]
454    pub fn walk<P: AsRef<Path>>(&self, p: P) -> Walker {
455        Walker::from_path(p).with_options(self.clone())
456    }
457}
458
459pub struct Chunks {
460    it: Walker,
461    capacity: usize,
462}
463
464impl Iterator for Chunks {
465    type Item = Vec<Result<PathBuf, io::Error>>;
466
467    #[inline]
468    fn next(&mut self) -> Option<Self::Item> {
469        let out: Self::Item = self.it.by_ref().take(self.capacity).collect();
470        if out.is_empty() {
471            return None;
472        }
473        Some(out)
474    }
475}
476
477#[derive(Debug)]
478struct PathIterator {
479    depth: u64,
480    path: Option<PathBuf>,
481    items: VecDeque<Result<PathBuf, io::Error>>,
482    init: bool,
483    sort: bool,
484}
485
486impl PathIterator {
487    fn new<P: AsRef<Path>>(depth: u64, path: P, sort: bool) -> Self {
488        Self {
489            depth,
490            path: Some(path.as_ref().to_path_buf()),
491            items: VecDeque::new(),
492            init: false,
493            sort,
494        }
495    }
496}
497
498impl Iterator for PathIterator {
499    type Item = Result<PathBuf, io::Error>;
500    fn next(&mut self) -> Option<Self::Item> {
501        if !self.init {
502            self.init = true;
503            // guarantee to exist at init
504            let path = self.path.as_ref().unwrap();
505
506            if path.is_file() {
507                match self.path.take() {
508                    Some(p) => return Some(Ok(p)),
509                    None => return None,
510                }
511            } else {
512                match fs::read_dir(path) {
513                    Ok(rd) => {
514                        let mut tmp: Vec<Result<PathBuf, io::Error>> =
515                            rd.map(|r| r.map(|de| de.path())).collect();
516
517                        if self.sort {
518                            tmp.sort_by(|res1, res2| {
519                                match (res1, res2) {
520                                    (Ok(path1), Ok(path2)) => path1.cmp(path2), // Compare paths if both are Ok
521                                    (Err(_), Ok(_)) => std::cmp::Ordering::Greater, // Err comes after Ok
522                                    (Ok(_), Err(_)) => std::cmp::Ordering::Less, // Ok comes before Err
523                                    (Err(e1), Err(e2)) => e1.to_string().cmp(&e2.to_string()), // Compare errors by message
524                                }
525                            });
526                        }
527
528                        self.items.extend(tmp);
529                    }
530                    Err(e) => self.items.push_back(Err(e)),
531                };
532            }
533        }
534
535        self.items.pop_front()
536    }
537}
538
539#[derive(Debug, Default)]
540pub struct Walker {
541    init: bool,
542    root: PathBuf,
543    options: WalkOptions,
544    queue: VecDeque<PathIterator>,
545    current: Option<PathIterator>,
546    marked: HashSet<[u8; 32]>,
547}
548
549impl Walker {
550    /// Creates a [Walker] with default [WalkOptions] configured
551    /// to walk path `p`.
552    ///
553    /// If `p` is a file, only that file will be returned when
554    /// iterating over the [Walker]
555    ///
556    /// # Example
557    ///
558    /// ```
559    /// use fs_walk::Walker;
560    ///
561    /// assert!(Walker::from_path("./").count() > 0)
562    /// ```
563    #[inline(always)]
564    pub fn from_path<P: AsRef<Path>>(p: P) -> Self {
565        Self {
566            root: p.as_ref().to_path_buf(),
567            ..Default::default()
568        }
569    }
570
571    /// Creates a [Walker] with a [WalkOptions] configured
572    /// to walk path `p`.
573    ///
574    /// If `p` is a file, only that file will be returned when
575    /// iterating over the [Walker]
576    ///
577    /// # Example
578    ///
579    /// ```
580    /// use fs_walk::{Walker, WalkOptions};
581    ///
582    /// let mut o = WalkOptions::new();
583    /// o.files();
584    ///
585    /// assert!(Walker::from_path("./").with_options(o.clone()).flatten().any(|p| p.is_file()));
586    /// assert!(!Walker::from_path("./").with_options(o).flatten().any(|p| p.is_dir()));
587    /// ```
588    #[inline(always)]
589    pub fn with_options(mut self, o: WalkOptions) -> Self {
590        self.options = o;
591        self
592    }
593
594    /// Returns a [Chunks] iterator allowing to process [Walker] data
595    /// in chunks of desired `size`.
596    ///
597    /// # Example
598    ///
599    /// ```
600    /// use fs_walk::{Walker, WalkOptions};
601    ///
602    ///
603    /// for chunk in Walker::from_path("./").chunks(1) {
604    ///     assert_eq!(chunk.len(), 1);
605    ///     for p in chunk.iter().flatten() {
606    ///         assert!(p.is_dir() || p.is_file());
607    ///     }
608    /// }
609    /// ```
610    #[inline(always)]
611    pub fn chunks(self, size: usize) -> Chunks {
612        Chunks {
613            it: self,
614            capacity: size,
615        }
616    }
617
618    #[inline(always)]
619    fn initialize(&mut self) {
620        if let Ok(can) = self.root.canonicalize() {
621            let h = blake3::hash(can.to_string_lossy().as_bytes());
622            self.current = Some(PathIterator::new(0, &self.root, self.options.sort));
623            self.marked.insert(h.into());
624        }
625        self.init = true
626    }
627
628    #[inline(always)]
629    fn _next(&mut self) -> Option<Result<PathBuf, io::Error>> {
630        if !self.init {
631            self.initialize();
632        }
633
634        let Some(pi) = self.current.as_mut() else {
635            if self.queue.is_empty() {
636                return None;
637            } else {
638                self.current = self.queue.pop_back();
639                return self._next();
640            }
641        };
642
643        let depth = pi.depth;
644        let ni = pi.next();
645
646        match ni {
647            Some(Ok(p)) => {
648                if p.is_file() {
649                    Some(Ok(p))
650                } else {
651                    let next_depth = pi.depth + 1;
652                    if let Some(max_depth) = self.options.max_depth {
653                        if next_depth > max_depth {
654                            return Some(Ok(p));
655                        }
656                    }
657
658                    // we use canonical path for marking directories
659                    if let Ok(can) = p.canonicalize() {
660                        let mut must_walk = false;
661
662                        if p.is_symlink() && self.options.follow_symlink {
663                            let h = blake3::hash(can.to_string_lossy().as_bytes());
664
665                            if !self.marked.contains(h.as_bytes()) {
666                                must_walk |= true;
667                                self.marked.insert(h.into());
668                            }
669                        }
670
671                        if must_walk || !p.is_symlink() {
672                            // current cannot be null here
673                            let pi = self.current.take().unwrap();
674                            // we push our ongoing iterator to the queue
675                            // to process dfs
676                            self.queue.push_back(pi);
677                            self.current = Some(PathIterator::new(depth + 1, &p, self.options.sort))
678                        }
679                    }
680
681                    Some(Ok(p))
682                }
683            }
684            Some(Err(e)) => Some(Err(e)),
685            None => {
686                self.current = self.queue.pop_back();
687                self._next()
688            }
689        }
690    }
691}
692
693impl Iterator for Walker {
694    type Item = Result<PathBuf, io::Error>;
695
696    #[inline]
697    fn next(&mut self) -> Option<Self::Item> {
698        while let Some(item) = self._next() {
699            if item.is_err() {
700                return Some(item);
701            }
702            match item {
703                Ok(p) => {
704                    if p.is_dir()
705                        && (!self.options.files_only || self.options.dirs_only)
706                        && self.options.path_match(&p)
707                    {
708                        return Some(Ok(p));
709                    }
710
711                    if p.is_file()
712                        && (!self.options.dirs_only || self.options.files_only)
713                        && self.options.path_match(&p)
714                    {
715                        return Some(Ok(p));
716                    }
717                }
718                Err(e) => return Some(Err(e)),
719            }
720        }
721        None
722    }
723}
724
725#[cfg(test)]
726mod tests {
727
728    use std::ffi::OsStr;
729
730    use super::*;
731
732    #[test]
733    fn test_walker() {
734        let w = Walker::from_path("./");
735        for e in w.flatten() {
736            println!("{e:?}")
737        }
738    }
739
740    #[test]
741    fn test_walker_on_file() {
742        // walking on a file doesn't return any error instead
743        // it returns only the file
744        let w = WalkOptions::new().walk("./src/lib.rs");
745        let v = w.flatten().collect::<Vec<PathBuf>>();
746
747        assert_eq!(v.len(), 1)
748    }
749
750    #[test]
751    fn test_walker_only_files() {
752        let mut o = WalkOptions::new();
753        o.files();
754        let w = o.walk("./");
755
756        for p in w.flatten() {
757            assert!(p.is_file())
758        }
759    }
760
761    #[test]
762    fn test_files_by_extension() {
763        let mut o = WalkOptions::new();
764        o.files().extension("o");
765
766        let w = o.walk("./");
767
768        let mut c = 0;
769        for p in w.flatten() {
770            assert_eq!(p.extension(), Some(OsStr::new("o")));
771            c += 1;
772        }
773        assert!(c > 0);
774    }
775
776    #[test]
777    fn test_files_ends_with() {
778        let mut o = WalkOptions::new();
779        o.ends_with(".o");
780        let w = o.walk("./");
781
782        let mut c = 0;
783        for p in w.flatten() {
784            assert_eq!(p.extension(), Some(OsStr::new("o")));
785            c += 1;
786        }
787        assert!(c > 0);
788    }
789
790    #[test]
791    fn test_dirs_ends_with() {
792        let mut o = WalkOptions::new();
793        o.ends_with("src").ends_with(".git");
794        let v = o.walk("./").flatten().collect::<Vec<PathBuf>>();
795
796        assert!(v.len() >= 2);
797        for p in v.iter() {
798            assert!(p.is_dir());
799        }
800    }
801
802    #[test]
803    fn test_files_by_chunks_and_extension() {
804        let mut o = WalkOptions::new();
805        o.files().extension("o");
806        let w = o.walk("./");
807
808        let mut c = 0;
809        for chunk in w.chunks(1) {
810            assert_eq!(chunk.len(), 1);
811            for p in chunk.iter().flatten() {
812                assert_eq!(p.extension(), Some(OsStr::new("o")));
813                c += 1;
814            }
815        }
816        assert!(c > 0);
817    }
818
819    #[test]
820    fn test_walker_only_dirs() {
821        let mut o = WalkOptions::new();
822        o.dirs();
823
824        let w = o.walk("./");
825
826        for p in w.flatten() {
827            assert!(p.is_dir());
828        }
829    }
830
831    #[test]
832    fn test_walker_dirs_and_files() {
833        let mut o = WalkOptions::new();
834        o.dirs().files();
835        let w = o.walk("./");
836
837        for p in w.flatten() {
838            assert!(p.is_dir() || p.is_file());
839        }
840    }
841
842    #[test]
843    fn test_max_depth() {
844        let d0 = WalkOptions::new().max_depth(0).walk("./").count();
845        let d1 = WalkOptions::new().max_depth(1).walk("./").count();
846
847        println!("d0={d0} d1={d1}");
848        // we verify that at depth 0 we have less items than at depth 1
849        assert!(d1 > d0);
850    }
851
852    #[test]
853    fn test_sort() {
854        let w = WalkOptions::new().max_depth(0).sort(true).walk("./");
855        let ns = WalkOptions::new().max_depth(0).sort(false).walk("./");
856
857        let sorted = w.flatten().collect::<Vec<PathBuf>>();
858        let mut unsorted = ns.flatten().collect::<Vec<PathBuf>>();
859
860        assert!(sorted.len() > 1);
861        assert_ne!(sorted, unsorted);
862        unsorted.sort();
863        assert_eq!(sorted, unsorted);
864    }
865
866    #[test]
867    fn test_name() {
868        let w = WalkOptions::new().name("lib.rs").name("src").walk("./");
869
870        let v = w.flatten().collect::<Vec<PathBuf>>();
871        assert!(v.len() > 1);
872        for p in v.iter() {
873            if p.file_name().unwrap() == "lib.rs" {
874                assert!(p.is_file())
875            }
876            if p.file_name().unwrap() == "src" {
877                assert!(p.is_dir())
878            }
879        }
880    }
881
882    #[test]
883    #[cfg(feature = "regex")]
884    fn test_name_regex() {
885        let mut w = WalkOptions::new();
886
887        w.name_regex(r#"^(.*\.rs|src|target)$"#)
888            .unwrap()
889            .name_regex(r#".*\.md"#)
890            .unwrap();
891
892        assert!(w.clone().dirs().walk("./").count() > 0);
893        assert!(w.clone().files().walk("./").count() > 0);
894    }
895
896    #[test]
897    fn test_walker_follow_symlink() {
898        use std::os::unix::fs::symlink;
899        use tempfile::{tempdir, Builder};
900
901        // Create a temporary directory and a file inside it
902        let dir = tempdir().unwrap();
903        let test_dir_path = dir.path().join("test_dir");
904        fs::create_dir(&test_dir_path).unwrap();
905        let file_path = test_dir_path.join("test_file.txt");
906        fs::File::create(&file_path).unwrap();
907
908        // Create a symlink to the temp directory
909        let symlink_path = Builder::new().prefix("symlink_test").tempdir().unwrap();
910        symlink(&dir, symlink_path.path().join("symlink")).unwrap();
911        symlink(&symlink_path, symlink_path.path().join("loop")).unwrap();
912
913        // Test with follow_symlink=true
914        let paths = WalkOptions::new()
915            .follow_symlink()
916            .files()
917            .walk(&symlink_path)
918            .flatten()
919            .collect::<Vec<PathBuf>>();
920        // Should find the file inside the symlink's target
921        assert_eq!(paths.len(), 1);
922        assert!(paths[0].ends_with("test_file.txt"));
923
924        // Test with follow_symlink=false (default)
925        let paths = WalkOptions::new()
926            .files()
927            .walk(&symlink_path)
928            .flatten()
929            .collect::<Vec<PathBuf>>();
930        // Should NOT find the file inside the symlink's target
931        assert!(paths.is_empty());
932
933        // Test dir with follow_symlink=false (default)
934        let paths = WalkOptions::new()
935            .dirs()
936            .walk(&symlink_path)
937            .flatten()
938            .collect::<Vec<PathBuf>>();
939        assert!(paths.iter().any(|p| p.ends_with("loop")));
940        assert!(paths.iter().any(|p| p.ends_with("symlink")));
941        assert!(!paths.iter().any(|p| p == &test_dir_path));
942
943        // Test dirs with follow_symlink=true
944        let paths = WalkOptions::new()
945            .dirs()
946            .follow_symlink()
947            .walk(&symlink_path)
948            .flatten()
949            .collect::<Vec<PathBuf>>();
950        println!("{paths:#?}");
951        println!("{test_dir_path:?}");
952        assert!(paths.iter().any(|p| p.ends_with("loop")));
953        assert!(paths.iter().any(|p| p.ends_with("symlink")));
954        assert!(paths
955            .iter()
956            .any(|p| p.canonicalize().unwrap() == test_dir_path));
957    }
958}