1use regex::Regex;
2use std::collections::HashSet;
3use std::fs;
4use std::path::{Path, PathBuf};
5
6#[derive(Debug, Clone)]
7struct Rule {
8 regex: Regex,
9 only_dir: bool,
10 }
13
14fn glob_to_regex(pattern: &str) -> Rule {
15 let raw = pattern.trim();
16 let mut p = raw.replace('\\', "/");
17
18 if p.starts_with("./") {
21 p = p[2..].to_string();
22 }
23
24 let only_dir = p.ends_with('/');
26 if only_dir {
27 p.pop();
28 }
29 let mut regex_str = regex::escape(&p);
32 regex_str = regex_str.replace(r"\*\*", ".*");
33 regex_str = regex_str.replace(r"\*", "[^/]*");
34 regex_str = regex_str.replace(r"\?", "[^/]");
35 regex_str = regex_str.replace(r"\[!", "[^");
36
37 if regex_str.starts_with('/') {
38 regex_str = format!("^{}", ®ex_str[1..]);
39 } else if regex_str.starts_with(".*") {
40 regex_str = format!("^{}", regex_str);
41 } else {
42 regex_str = format!("(?:^|/){}", regex_str);
43 }
44
45 if only_dir {
46 regex_str.push_str("(?:/.*)?$");
47 } else {
48 regex_str.push('$');
49 }
50
51 let final_regex = format!("(?i){}", regex_str);
52
53 Rule {
54 regex: Regex::new(&final_regex).unwrap_or_else(|_| Regex::new("(?i)$.^").unwrap()),
55 only_dir,
56 }
59}
60
61pub struct Task<'a> {
64 pub path_location: &'a str,
65 pub path_exclude: Vec<&'a str>,
66 pub path_include_only: Vec<&'a str>,
67 pub filter_files: Vec<&'a str>,
68 pub output_type: &'a str, }
70
71impl<'a> Default for Task<'a> {
73 fn default() -> Self {
74 Self {
75 path_location: ".",
76 path_exclude: vec![],
77 path_include_only: vec![],
78 filter_files: vec![],
79 output_type: "dirs_and_files",
80 }
81 }
82}
83
84pub fn filespath(tasks: &[Task]) -> Vec<PathBuf> {
85 let mut all_results = HashSet::new();
86
87 for task in tasks {
88 let root_path = Path::new(task.path_location);
89 let canonical_root =
90 fs::canonicalize(root_path).unwrap_or_else(|_| root_path.to_path_buf());
91
92 let mut exclude_rules = Vec::new();
94 for p in &task.path_exclude {
95 if !p.trim().is_empty() {
96 exclude_rules.push(glob_to_regex(p));
97 }
98 }
99
100 let mut include_only_rules = Vec::new();
101 for p in &task.path_include_only {
102 if !p.trim().is_empty() {
103 include_only_rules.push(glob_to_regex(p));
104 }
105 }
106
107 let mut filter_files_rules = Vec::new();
108 for p in &task.filter_files {
109 if !p.trim().is_empty() {
110 filter_files_rules.push(glob_to_regex(p));
111 }
112 }
113
114 let mut scanned_paths = Vec::new();
118 scan_step1(
119 &canonical_root,
120 &canonical_root,
121 &exclude_rules,
122 &mut scanned_paths,
123 );
124
125 for path in scanned_paths {
129 let rel_path = path
130 .strip_prefix(&canonical_root)
131 .unwrap()
132 .to_string_lossy()
133 .replace('\\', "/");
134 let path_slash = format!("{}/", rel_path);
135
136 if !include_only_rules.is_empty() {
137 let mut matches = false;
138 for rule in &include_only_rules {
139 if rule.only_dir {
140 if path.is_dir() && rule.regex.is_match(&path_slash) {
142 matches = true;
143 break;
144 }
145 } else {
146 if rule.regex.is_match(&rel_path) || rule.regex.is_match(&path_slash) {
148 matches = true;
149 break;
150 }
151 }
152 }
153 if !matches {
154 continue;
155 }
156 }
157
158 if path.is_dir() {
159 if task.output_type != "files" {
162 all_results.insert(path);
163 }
164 } else {
165 if task.output_type == "dirs" {
167 continue;
168 }
169
170 let mut is_file_matched = false;
172 if filter_files_rules.is_empty() {
173 is_file_matched = true;
174 } else {
175 for rule in &filter_files_rules {
176 if rule.only_dir {
177 continue;
178 }
179 if rule.regex.is_match(&rel_path) {
180 is_file_matched = true;
181 break;
182 }
183 }
184 }
185
186 if is_file_matched {
187 all_results.insert(path.clone());
188
189 if task.output_type == "files" {
193 let mut current_parent = path.parent();
194 while let Some(p) = current_parent {
195 all_results.insert(p.to_path_buf());
196 if p == canonical_root {
197 break;
198 }
199 current_parent = p.parent();
200 }
201 }
202 }
203 }
204 }
205 }
206
207 let result: Vec<PathBuf> = all_results.into_iter().collect();
208 result
209}
210
211fn scan_step1(
213 root_path: &Path,
214 current_path: &Path,
215 exclude_rules: &[Rule],
216 scanned_paths: &mut Vec<PathBuf>,
217) {
218 let read_dir = match fs::read_dir(current_path) {
219 Ok(rd) => rd,
220 Err(_) => return,
221 };
222
223 for entry in read_dir.filter_map(|e| e.ok()) {
224 let path = entry.path();
225 let is_dir = path.is_dir();
226
227 let rel_path = match path.strip_prefix(root_path) {
228 Ok(p) => p.to_string_lossy().replace('\\', "/"),
229 Err(_) => continue,
230 };
231
232 if rel_path.is_empty() {
233 continue;
234 }
235
236 let path_slash = format!("{}/", rel_path);
237
238 let mut is_excluded = false;
240 for rule in exclude_rules {
241 if rule.only_dir && !is_dir {
242 continue;
243 }
244 if rule.regex.is_match(&rel_path) || (is_dir && rule.regex.is_match(&path_slash)) {
245 is_excluded = true;
246 break;
247 }
248 }
249
250 if is_excluded {
252 continue;
253 }
254
255 scanned_paths.push(path.clone());
257
258 if is_dir {
260 scan_step1(root_path, &path, exclude_rules, scanned_paths);
261 }
262 }
263}