1use crate::config::Config;
4use std::path::{Path, PathBuf};
5use walkdir::WalkDir;
6
7pub struct Scanner {
8 config: Config,
9 project_root: PathBuf,
10}
11
12#[derive(Debug)]
13pub enum SkipReason {
14 ExcludedByDir(String),
15 ExcludedByPattern(String),
16 NoMatchingFormat,
17 NotUtf8,
18 SymbolicLink,
19}
20
21impl Scanner {
22 pub fn new(config: Config, project_root: PathBuf) -> Self {
23 Scanner {
24 config,
25 project_root,
26 }
27 }
28
29 pub fn collect_files(&self, target_path: &Path) -> Vec<PathBuf> {
31 let mut files = Vec::new();
32
33 if target_path.is_file() {
34 files.push(target_path.to_path_buf());
36 } else if target_path.is_dir() {
37 for entry in WalkDir::new(target_path).follow_links(false) {
39 if let Ok(entry) = entry {
40 if entry.file_type().is_file() {
41 files.push(entry.path().to_path_buf());
42 }
43 }
44 }
45 }
46
47 files
48 }
49
50 pub fn should_process(&self, file_path: &Path) -> Result<(), SkipReason> {
52 if file_path.is_symlink() {
54 return Err(SkipReason::SymbolicLink);
55 }
56
57 if !self.has_matching_format(file_path) {
60 return Err(SkipReason::NoMatchingFormat);
61 }
62
63 if let Some(reason) = self.check_exclude_rules(file_path) {
66 return Err(reason);
67 }
68
69 if let Ok(content) = std::fs::read(file_path) {
71 if std::str::from_utf8(&content).is_err() {
72 return Err(SkipReason::NotUtf8);
73 }
74 }
75
76 Ok(())
77 }
78
79 fn check_exclude_rules(&self, file_path: &Path) -> Option<SkipReason> {
81 let exclude = &self.config.path_comment.exclude;
82
83 let rel_path = if let Ok(rel) = file_path.strip_prefix(&self.project_root) {
85 rel
86 } else {
87 file_path
88 };
89
90 for excluded_dir in &exclude.dirs {
92 if self.path_contains_dir(rel_path, excluded_dir) {
93 return Some(SkipReason::ExcludedByDir(excluded_dir.clone()));
94 }
95 }
96
97 for pattern in &exclude.patterns {
99 if self.matches_pattern(file_path, pattern) {
100 return Some(SkipReason::ExcludedByPattern(pattern.clone()));
101 }
102 }
103
104 None
105 }
106
107 fn path_contains_dir(&self, path: &Path, dir_name: &str) -> bool {
109 for component in path.components() {
110 if let Some(name) = component.as_os_str().to_str() {
111 if name == dir_name {
112 return true;
113 }
114 }
115 }
116 false
117 }
118
119 fn matches_pattern(&self, file_path: &Path, pattern: &str) -> bool {
121 if let Some(file_name) = file_path.file_name() {
122 if let Some(file_name_str) = file_name.to_str() {
123 if let Ok(glob_pattern) = glob::Pattern::new(pattern) {
124 return glob_pattern.matches(file_name_str);
125 }
126 }
127 }
128 false
129 }
130
131 fn has_matching_format(&self, file_path: &Path) -> bool {
133 if let Some(ext) = file_path.extension() {
134 let ext_with_dot = format!(".{}", ext.to_string_lossy());
135 self.config.path_comment.formats.contains_key(&ext_with_dot)
136 } else {
137 false
138 }
139 }
140
141 pub fn get_format(&self, file_path: &Path) -> Option<&str> {
143 if let Some(ext) = file_path.extension() {
144 let ext_with_dot = format!(".{}", ext.to_string_lossy());
145 self.config
146 .path_comment
147 .formats
148 .get(&ext_with_dot)
149 .map(|s| s.as_str())
150 } else {
151 None
152 }
153 }
154}