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 = fs::canonicalize(root_path).unwrap_or_else(|_| root_path.to_path_buf());
90
91 let mut exclude_rules = Vec::new();
93 for p in &task.path_exclude {
94 if !p.trim().is_empty() { exclude_rules.push(glob_to_regex(p)); }
95 }
96
97 let mut include_only_rules = Vec::new();
98 for p in &task.path_include_only {
99 if !p.trim().is_empty() { include_only_rules.push(glob_to_regex(p)); }
100 }
101
102 let mut filter_files_rules = Vec::new();
103 for p in &task.filter_files {
104 if !p.trim().is_empty() { filter_files_rules.push(glob_to_regex(p)); }
105 }
106
107 let mut scanned_paths = Vec::new();
111 scan_step1(&canonical_root, &canonical_root, &exclude_rules, &mut scanned_paths);
112
113 for path in scanned_paths {
117 let rel_path = path.strip_prefix(&canonical_root)
118 .unwrap()
119 .to_string_lossy()
120 .replace('\\', "/");
121 let path_slash = format!("{}/", rel_path);
122
123 if !include_only_rules.is_empty() {
124 let mut matches = false;
125 for rule in &include_only_rules {
126 if rule.only_dir {
127 if path.is_dir() && rule.regex.is_match(&path_slash) {
129 matches = true;
130 break;
131 }
132 } else {
133 if rule.regex.is_match(&rel_path) || rule.regex.is_match(&path_slash) {
135 matches = true;
136 break;
137 }
138 }
139 }
140 if !matches {
141 continue;
142 }
143 }
144
145 if path.is_dir() {
146 if task.output_type != "files" {
149 all_results.insert(path);
150 }
151 } else {
152 if task.output_type == "dirs" {
154 continue;
155 }
156
157 let mut is_file_matched = false;
159 if filter_files_rules.is_empty() {
160 is_file_matched = true;
161 } else {
162 for rule in &filter_files_rules {
163 if rule.only_dir { continue; }
164 if rule.regex.is_match(&rel_path) {
165 is_file_matched = true;
166 break;
167 }
168 }
169 }
170
171 if is_file_matched {
172 all_results.insert(path.clone());
173
174 if task.output_type == "files" {
178 let mut current_parent = path.parent();
179 while let Some(p) = current_parent {
180 all_results.insert(p.to_path_buf());
181 if p == canonical_root { break; }
182 current_parent = p.parent();
183 }
184 }
185 }
186 }
187 }
188 }
189
190 let result: Vec<PathBuf> = all_results.into_iter().collect();
191 result
192}
193
194fn scan_step1(
196 root_path: &Path,
197 current_path: &Path,
198 exclude_rules: &[Rule],
199 scanned_paths: &mut Vec<PathBuf>
200) {
201 let read_dir = match fs::read_dir(current_path) {
202 Ok(rd) => rd,
203 Err(_) => return,
204 };
205
206 for entry in read_dir.filter_map(|e| e.ok()) {
207 let path = entry.path();
208 let is_dir = path.is_dir();
209
210 let rel_path = match path.strip_prefix(root_path) {
211 Ok(p) => p.to_string_lossy().replace('\\', "/"),
212 Err(_) => continue,
213 };
214
215 if rel_path.is_empty() {
216 continue;
217 }
218
219 let path_slash = format!("{}/", rel_path);
220
221 let mut is_excluded = false;
223 for rule in exclude_rules {
224 if rule.only_dir && !is_dir {
225 continue;
226 }
227 if rule.regex.is_match(&rel_path) || (is_dir && rule.regex.is_match(&path_slash)) {
228 is_excluded = true;
229 break;
230 }
231 }
232
233 if is_excluded {
235 continue;
236 }
237
238 scanned_paths.push(path.clone());
240
241 if is_dir {
243 scan_step1(root_path, &path, exclude_rules, scanned_paths);
244 }
245 }
246}