1#![allow(clippy::needless_doctest_main)]
85#![warn(missing_docs)]
86
87use ignore::overrides::{Override, OverrideBuilder};
88use ignore::Match;
89use std::cmp::Ordering;
90use std::path::Path;
91use std::path::PathBuf;
92use walkdir::WalkDir;
93
94#[derive(Debug)]
96pub struct GlobError(ignore::Error);
97
98pub type WalkError = walkdir::Error;
100pub type DirEntry = walkdir::DirEntry;
104
105impl From<std::io::Error> for GlobError {
106 fn from(e: std::io::Error) -> Self {
107 GlobError(e.into())
108 }
109}
110
111impl From<GlobError> for std::io::Error {
112 fn from(e: GlobError) -> Self {
113 if let ignore::Error::Io(e) = e.0 {
114 e
115 } else {
116 std::io::ErrorKind::Other.into()
117 }
118 }
119}
120
121impl std::fmt::Display for GlobError {
122 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
123 self.0.fmt(f)
124 }
125}
126
127impl std::error::Error for GlobError {}
128
129bitflags::bitflags! {
130 pub struct FileType: u32 {
136 #[allow(missing_docs)] const FILE = 0b001;
137 #[allow(missing_docs)] const DIR = 0b010;
138 #[allow(missing_docs)] const SYMLINK = 0b100;
139 }
140}
141
142pub struct GlobWalkerBuilder {
146 root: PathBuf,
147 patterns: Vec<String>,
148 walker: WalkDir,
149 case_insensitive: bool,
150 file_type: Option<FileType>,
151}
152
153impl GlobWalkerBuilder {
154 pub fn new<P, S>(base: P, pattern: S) -> Self
159 where
160 P: AsRef<Path>,
161 S: AsRef<str>,
162 {
163 GlobWalkerBuilder::from_patterns(base, &[pattern])
164 }
165
166 pub fn from_patterns<P, S>(base: P, patterns: &[S]) -> Self
171 where
172 P: AsRef<Path>,
173 S: AsRef<str>,
174 {
175 fn normalize_pattern<S: AsRef<str>>(pattern: S) -> String {
176 if pattern.as_ref() == "*" {
179 String::from("/*")
180 } else {
181 pattern.as_ref().to_owned()
182 }
183 }
184 GlobWalkerBuilder {
185 root: base.as_ref().into(),
186 patterns: patterns.iter().map(normalize_pattern).collect::<_>(),
187 walker: WalkDir::new(base),
188 case_insensitive: false,
189 file_type: None,
190 }
191 }
192
193 pub fn min_depth(mut self, depth: usize) -> Self {
199 self.walker = self.walker.min_depth(depth);
200 self
201 }
202
203 pub fn max_depth(mut self, depth: usize) -> Self {
213 self.walker = self.walker.max_depth(depth);
214 self
215 }
216
217 pub fn follow_links(mut self, yes: bool) -> Self {
229 self.walker = self.walker.follow_links(yes);
230 self
231 }
232
233 pub fn max_open(mut self, n: usize) -> Self {
259 self.walker = self.walker.max_open(n);
260 self
261 }
262
263 pub fn sort_by<F>(mut self, cmp: F) -> Self
269 where
270 F: FnMut(&DirEntry, &DirEntry) -> Ordering + Send + Sync + 'static,
271 {
272 self.walker = self.walker.sort_by(cmp);
273 self
274 }
275
276 pub fn contents_first(mut self, yes: bool) -> Self {
287 self.walker = self.walker.contents_first(yes);
288 self
289 }
290
291 pub fn case_insensitive(mut self, yes: bool) -> Self {
295 self.case_insensitive = yes;
296 self
297 }
298
299 pub fn file_type(mut self, file_type: FileType) -> Self {
304 self.file_type = Some(file_type);
305 self
306 }
307
308 pub fn build(self) -> Result<GlobWalker, GlobError> {
310 let mut builder = OverrideBuilder::new(self.root);
311
312 builder
313 .case_insensitive(self.case_insensitive)
314 .map_err(GlobError)?;
315
316 for pattern in self.patterns {
317 builder.add(pattern.as_ref()).map_err(GlobError)?;
318 }
319
320 Ok(GlobWalker {
321 ignore: builder.build().map_err(GlobError)?,
322 walker: self.walker.into_iter(),
323 file_type_filter: self.file_type,
324 })
325 }
326}
327
328pub struct GlobWalker {
336 ignore: Override,
337 walker: walkdir::IntoIter,
338 file_type_filter: Option<FileType>,
339}
340
341impl Iterator for GlobWalker {
342 type Item = Result<DirEntry, WalkError>;
343
344 fn next(&mut self) -> Option<Self::Item> {
346 let mut skip_dir = false;
347
348 'skipper: loop {
351 if skip_dir {
352 self.walker.skip_current_dir();
353 }
354
355 for entry in &mut self.walker {
357 match entry {
358 Ok(e) => {
359 let is_dir = e.file_type().is_dir();
360
361 let file_type = if e.file_type().is_dir() {
362 Some(FileType::DIR)
363 } else if e.file_type().is_file() {
364 Some(FileType::FILE)
365 } else if e.file_type().is_symlink() {
366 Some(FileType::SYMLINK)
367 } else {
368 None
369 };
370
371 let file_type_matches = match (self.file_type_filter.as_ref(), file_type) {
372 (None, _) => true,
373 (Some(_), None) => false,
374 (Some(filter), Some(actual)) => filter.contains(actual),
375 };
376
377 let path = e.path().strip_prefix(self.ignore.path()).unwrap();
382
383 if path.as_os_str().is_empty() {
385 continue 'skipper;
386 }
387
388 match self.ignore.matched(path, is_dir) {
389 Match::Whitelist(_) if file_type_matches => return Some(Ok(e)),
390 Match::Ignore(_) if is_dir => {
393 skip_dir = true;
394 continue 'skipper;
395 }
396 _ => {}
397 }
398 }
399 Err(e) => {
400 return Some(Err(e));
401 }
402 }
403 }
404 break;
405 }
406
407 None
408 }
409}
410
411pub fn glob_builder<S: AsRef<str>>(pattern: S) -> GlobWalkerBuilder {
416 let path_pattern: PathBuf = pattern.as_ref().into();
418 if path_pattern.is_absolute() {
419 let mut base = PathBuf::new();
421 let mut pattern = PathBuf::new();
422 let mut globbing = false;
423
424 for c in path_pattern.components() {
426 let os = c.as_os_str().to_str().unwrap();
427 for c in &["*", "{", "}"][..] {
428 if os.contains(c) {
429 globbing = true;
430 break;
431 }
432 }
433
434 if globbing {
435 pattern.push(c);
436 } else {
437 base.push(c);
438 }
439 }
440
441 let pat = pattern.to_str().unwrap();
442 if cfg!(windows) {
443 GlobWalkerBuilder::new(base.to_str().unwrap(), pat.replace('\\', "/"))
444 } else {
445 GlobWalkerBuilder::new(base.to_str().unwrap(), pat)
446 }
447 } else {
448 GlobWalkerBuilder::new(".", pattern)
450 }
451}
452
453pub fn glob<S: AsRef<str>>(pattern: S) -> Result<GlobWalker, GlobError> {
458 glob_builder(pattern).build()
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464 use std::fs::{create_dir_all, File};
465 use tempfile::TempDir;
466
467 fn touch(dir: &TempDir, names: &[&str]) {
468 for name in names {
469 let name = normalize_path_sep(name);
470 File::create(dir.path().join(name)).expect("Failed to create a test file");
471 }
472 }
473
474 fn normalize_path_sep<S: AsRef<str>>(s: S) -> String {
475 s.as_ref()
476 .replace("[/]", if cfg!(windows) { "\\" } else { "/" })
477 }
478
479 fn equate_to_expected(g: GlobWalker, mut expected: Vec<String>, dir_path: &Path) {
480 for matched_file in g.into_iter().filter_map(Result::ok) {
481 let path = matched_file
482 .path()
483 .strip_prefix(dir_path)
484 .unwrap()
485 .to_str()
486 .unwrap();
487 let path = normalize_path_sep(path);
488
489 let del_idx = if let Some(idx) = expected.iter().position(|n| &path == n) {
490 idx
491 } else {
492 panic!("Iterated file is unexpected: {}", path);
493 };
494
495 expected.remove(del_idx);
496 }
497
498 let empty: &[&str] = &[][..];
501 assert_eq!(expected, empty);
502 }
503
504 #[test]
505 fn test_absolute_path() {
506 let dir = TempDir::new().expect("Failed to create temporary folder");
507 let dir_path = dir.path().canonicalize().unwrap();
508
509 touch(&dir, &["a.rs", "a.jpg", "a.png", "b.docx"][..]);
510
511 let expected = ["a.jpg", "a.png"].iter().map(ToString::to_string).collect();
512 let mut cwd = dir_path.clone();
513 cwd.push("*.{png,jpg,gif}");
514
515 let glob = glob(cwd.to_str().unwrap().to_owned()).unwrap();
516 equate_to_expected(glob, expected, &dir_path);
517 }
518
519 #[test]
520 fn test_new() {
521 let dir = TempDir::new().expect("Failed to create temporary folder");
522 let dir_path = dir.path();
523
524 touch(&dir, &["a.rs", "a.jpg", "a.png", "b.docx"][..]);
525
526 let expected = ["a.jpg", "a.png"].iter().map(ToString::to_string).collect();
527
528 let g = GlobWalkerBuilder::new(dir_path, "*.{png,jpg,gif}")
529 .build()
530 .unwrap();
531
532 equate_to_expected(g, expected, dir_path);
533 }
534
535 #[test]
536 fn test_from_patterns() {
537 let dir = TempDir::new().expect("Failed to create temporary folder");
538 let dir_path = dir.path();
539 create_dir_all(dir_path.join("src/some_mod")).expect("");
540 create_dir_all(dir_path.join("tests")).expect("");
541 create_dir_all(dir_path.join("contrib")).expect("");
542
543 touch(
544 &dir,
545 &[
546 "a.rs",
547 "b.rs",
548 "avocado.rs",
549 "lib.c",
550 "src[/]hello.rs",
551 "src[/]world.rs",
552 "src[/]some_mod[/]unexpected.rs",
553 "src[/]cruel.txt",
554 "contrib[/]README.md",
555 "contrib[/]README.rst",
556 "contrib[/]lib.rs",
557 ][..],
558 );
559
560 let expected: Vec<_> = [
561 "src[/]some_mod[/]unexpected.rs",
562 "src[/]world.rs",
563 "src[/]hello.rs",
564 "lib.c",
565 "contrib[/]lib.rs",
566 "contrib[/]README.md",
567 "contrib[/]README.rst",
568 ]
569 .iter()
570 .map(normalize_path_sep)
571 .collect();
572
573 let patterns = ["src/**/*.rs", "*.c", "**/lib.rs", "**/*.{md,rst}"];
574 let glob = GlobWalkerBuilder::from_patterns(dir_path, &patterns)
575 .build()
576 .unwrap();
577
578 equate_to_expected(glob, expected, dir_path);
579 }
580
581 #[test]
582 fn test_case_insensitive_matching() {
583 let dir = TempDir::new().expect("Failed to create temporary folder");
584 let dir_path = dir.path();
585 create_dir_all(dir_path.join("src/some_mod")).expect("");
586 create_dir_all(dir_path.join("tests")).expect("");
587 create_dir_all(dir_path.join("contrib")).expect("");
588
589 touch(
590 &dir,
591 &[
592 "a.rs",
593 "b.rs",
594 "avocado.RS",
595 "lib.c",
596 "src[/]hello.RS",
597 "src[/]world.RS",
598 "src[/]some_mod[/]unexpected.rs",
599 "src[/]cruel.txt",
600 "contrib[/]README.md",
601 "contrib[/]README.rst",
602 "contrib[/]lib.rs",
603 ][..],
604 );
605
606 let expected: Vec<_> = [
607 "src[/]some_mod[/]unexpected.rs",
608 "src[/]hello.RS",
609 "src[/]world.RS",
610 "lib.c",
611 "contrib[/]lib.rs",
612 "contrib[/]README.md",
613 "contrib[/]README.rst",
614 ]
615 .iter()
616 .map(normalize_path_sep)
617 .collect();
618
619 let patterns = ["src/**/*.rs", "*.c", "**/lib.rs", "**/*.{md,rst}"];
620 let glob = GlobWalkerBuilder::from_patterns(dir_path, &patterns)
621 .case_insensitive(true)
622 .build()
623 .unwrap();
624
625 equate_to_expected(glob, expected, dir_path);
626 }
627
628 #[test]
629 fn test_match_dir() {
630 let dir = TempDir::new().expect("Failed to create temporary folder");
631 let dir_path = dir.path();
632 create_dir_all(dir_path.join("mod")).expect("");
633
634 touch(
635 &dir,
636 &[
637 "a.png",
638 "b.png",
639 "c.png",
640 "mod[/]a.png",
641 "mod[/]b.png",
642 "mod[/]c.png",
643 ][..],
644 );
645
646 let expected: Vec<_> = ["mod"].iter().map(normalize_path_sep).collect();
647 let glob = GlobWalkerBuilder::new(dir_path, "mod").build().unwrap();
648
649 equate_to_expected(glob, expected, dir_path);
650 }
651
652 #[test]
653 fn test_blacklist() {
654 let dir = TempDir::new().expect("Failed to create temporary folder");
655 let dir_path = dir.path();
656 create_dir_all(dir_path.join("src/some_mod")).expect("");
657 create_dir_all(dir_path.join("tests")).expect("");
658 create_dir_all(dir_path.join("contrib")).expect("");
659
660 touch(
661 &dir,
662 &[
663 "a.rs",
664 "b.rs",
665 "avocado.rs",
666 "lib.c",
667 "src[/]hello.rs",
668 "src[/]world.rs",
669 "src[/]some_mod[/]unexpected.rs",
670 "src[/]cruel.txt",
671 "contrib[/]README.md",
672 "contrib[/]README.rst",
673 "contrib[/]lib.rs",
674 ][..],
675 );
676
677 let expected: Vec<_> = [
678 "src[/]some_mod[/]unexpected.rs",
679 "src[/]hello.rs",
680 "lib.c",
681 "contrib[/]lib.rs",
682 "contrib[/]README.md",
683 "contrib[/]README.rst",
684 ]
685 .iter()
686 .map(normalize_path_sep)
687 .collect();
688
689 let patterns = [
690 "src/**/*.rs",
691 "*.c",
692 "**/lib.rs",
693 "**/*.{md,rst}",
694 "!world.rs",
695 ];
696
697 let glob = GlobWalkerBuilder::from_patterns(dir_path, &patterns)
698 .build()
699 .unwrap();
700
701 equate_to_expected(glob, expected, dir_path);
702 }
703
704 #[test]
705 fn test_blacklist_dir() {
706 let dir = TempDir::new().expect("Failed to create temporary folder");
707 let dir_path = dir.path();
708 create_dir_all(dir_path.join("Pictures")).expect("");
709
710 touch(
711 &dir,
712 &[
713 "a.png",
714 "b.png",
715 "c.png",
716 "Pictures[/]a.png",
717 "Pictures[/]b.png",
718 "Pictures[/]c.png",
719 ][..],
720 );
721
722 let expected: Vec<_> = ["a.png", "b.png", "c.png"]
723 .iter()
724 .map(normalize_path_sep)
725 .collect();
726
727 let patterns = ["*.{png,jpg,gif}", "!Pictures"];
728 let glob = GlobWalkerBuilder::from_patterns(dir_path, &patterns)
729 .build()
730 .unwrap();
731
732 equate_to_expected(glob, expected, dir_path);
733 }
734
735 #[test]
736 fn test_glob_with_double_star_pattern() {
737 let dir = TempDir::new().expect("Failed to create temporary folder");
738 let dir_path = dir.path().canonicalize().unwrap();
739
740 touch(&dir, &["a.rs", "a.jpg", "a.png", "b.docx"][..]);
741
742 let expected = ["a.jpg", "a.png"].iter().map(ToString::to_string).collect();
743 let mut cwd = dir_path.clone();
744 cwd.push("**");
745 cwd.push("*.{png,jpg,gif}");
746 let glob = glob(cwd.to_str().unwrap().to_owned()).unwrap();
747 equate_to_expected(glob, expected, &dir_path);
748 }
749
750 #[test]
751 fn test_glob_single_star() {
752 let dir = TempDir::new().expect("Failed to create temporary folder");
753 let dir_path = dir.path();
754 create_dir_all(dir_path.join("Pictures")).expect("");
755 create_dir_all(dir_path.join("Pictures").join("b")).expect("");
756
757 touch(
758 &dir,
759 &[
760 "a.png",
761 "b.png",
762 "c.png",
763 "Pictures[/]a.png",
764 "Pictures[/]b.png",
765 "Pictures[/]c.png",
766 "Pictures[/]b[/]c.png",
767 "Pictures[/]b[/]c.png",
768 "Pictures[/]b[/]c.png",
769 ][..],
770 );
771
772 let glob = GlobWalkerBuilder::new(dir_path, "*")
773 .sort_by(|a, b| a.path().cmp(b.path()))
774 .build()
775 .unwrap();
776 let expected = ["Pictures", "a.png", "b.png", "c.png"]
777 .iter()
778 .map(ToString::to_string)
779 .collect();
780 equate_to_expected(glob, expected, dir_path);
781 }
782
783 #[test]
784 fn test_file_type() {
785 let dir = TempDir::new().expect("Failed to create temporary folder");
786 let dir_path = dir.path();
787 create_dir_all(dir_path.join("Pictures")).expect("");
788 create_dir_all(dir_path.join("Pictures").join("b")).expect("");
789
790 touch(
791 &dir,
792 &[
793 "a.png",
794 "b.png",
795 "c.png",
796 "Pictures[/]a.png",
797 "Pictures[/]b.png",
798 "Pictures[/]c.png",
799 "Pictures[/]b[/]c.png",
800 "Pictures[/]b[/]c.png",
801 "Pictures[/]b[/]c.png",
802 ][..],
803 );
804
805 let glob = GlobWalkerBuilder::new(dir_path, "*")
806 .sort_by(|a, b| a.path().cmp(b.path()))
807 .file_type(FileType::DIR)
808 .build()
809 .unwrap();
810 let expected = ["Pictures"].iter().map(ToString::to_string).collect();
811 equate_to_expected(glob, expected, dir_path);
812
813 let glob = GlobWalkerBuilder::new(dir_path, "*")
814 .sort_by(|a, b| a.path().cmp(b.path()))
815 .file_type(FileType::FILE)
816 .build()
817 .unwrap();
818 let expected = ["a.png", "b.png", "c.png"]
819 .iter()
820 .map(ToString::to_string)
821 .collect();
822 equate_to_expected(glob, expected, dir_path);
823 }
824}