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