plot/lib/logic/path/
paths_patterns.rs1use regex::Regex;
2
3use crate::lib::logic::PathContext;
4
5pub trait PattEnvIndex {
10 fn has_dir(&self, dir: &str) -> bool;
11 fn has_file_with_prefix(&self, prefix: &str) -> bool;
12 fn any_file_in_dir(&self, dir: &str, check: &mut dyn FnMut(&str) -> bool) -> bool;
13}
14
15#[derive(Debug, Clone)]
21pub struct PattRaw(pub Vec<String>);
22
23#[derive(Debug, Clone)]
25pub struct PattExp(pub Vec<String>);
26
27#[derive(Debug, Clone)]
28pub struct PatternsToApply {
29 pub patterns: PattRaw,
30 pub expanded: PattExp,
31 compiled: Vec<PatternCompiled>,
32}
33
34#[derive(Debug, Clone)]
39struct PatternCompiled {
40 regex: Regex,
41 targets_file: bool,
42 requires_sibling: bool,
43 requires_orphan: bool,
44 is_deep: bool,
45 include_parents: bool,
46 base_name: String,
47 pub is_negated: bool,
48 #[allow(dead_code)]
49 pub original: String,
50}
51
52impl PatternsToApply {
57 pub fn new<I, S>(patterns: I, ignore_case_sensitive: bool) -> Self
58 where
59 I: IntoIterator<Item = S>,
60 S: AsRef<str>, {
61 let mut patterns_vec = Vec::new();
62 let mut expanded_vec = Vec::new();
63
64 for p in patterns {
65 let s = p.as_ref();
66 patterns_vec.push(s.to_string());
67 expanded_vec.extend(Self::expand_braces(s));
68 }
69
70 let compiled = expanded_vec.iter().map(|p| PatternCompiled::new(p, ignore_case_sensitive).unwrap()).collect();
71
72 Self {
73 patterns: PattRaw(patterns_vec),
75 expanded: PattExp(expanded_vec),
76 compiled,
77 }
78 }
79
80 pub fn is_match<E: PattEnvIndex>(&self, path: &str, env: &E) -> bool {
82 let mut has_positive = false;
83 let mut matched = false;
84
85 for c in &self.compiled {
86 if c.is_negated {
87 if c.is_match(path, env) {
88 return false;
89 }
90 } else {
91 has_positive = true;
92 if !matched && c.is_match(path, env) {
93 matched = true;
94 }
95 }
96 }
97
98 if has_positive { matched } else { true }
99 }
100
101 pub fn rules_count(&self) -> usize { self.compiled.len() }
102
103 fn expand_braces(input: &str) -> Vec<String> {
108 if let Some(end) = input.find('}') {
110 if let Some(start) = input[..end].rfind('{') {
112 let prefix = &input[..start];
113 let suffix = &input[end + 1..];
114 let options = &input[start + 1..end];
115
116 let mut result = Vec::new();
117 for opt in options.split(',') {
118 let merged = format!("{}{}{}", prefix, opt, suffix);
119 result.extend(Self::expand_braces(&merged));
121 }
122 return result;
123 }
124 }
125 vec![input.to_string()]
126 }
127}
128
129impl PatternCompiled {
134 pub fn new(pattern: &str, ignore_case_sensitive: bool) -> Result<Self, regex::Error> {
135 let is_negated = pattern.starts_with('!');
136 let p = if is_negated { &pattern[1..] } else { pattern };
137
138 let is_deep = p.ends_with('+');
139 let include_parents = p.ends_with("&/") || p.ends_with("&\\");
140 let requires_sibling = p.contains('@');
141 let requires_orphan = p.contains('$');
142
143 let mut clean = p.replace(['@', '$', '+'], "");
144
145 if include_parents {
146 clean = clean.strip_suffix("&/").or_else(|| clean.strip_suffix("&\\")).unwrap_or(&clean).to_string();
147 }
148
149 let base_name = clean
150 .trim_end_matches('/')
151 .split('/')
152 .next_back()
153 .unwrap_or("")
154 .split('.')
155 .next()
156 .unwrap_or("")
157 .to_string();
158
159 let mut re = String::new();
160
161 if ignore_case_sensitive {
162 re.push_str("(?i)");
163 }
164
165 let mut p_str = clean.as_str();
166 let mut anchored = false;
167
168 let targets_file = !p_str.ends_with('/') && !p_str.ends_with("**");
169
170 if p_str.starts_with("./") {
171 anchored = true;
172 p_str = &p_str[2..];
173 } else if p_str.starts_with("**/") {
174 anchored = true;
175 }
176
177 if anchored {
178 re.push('^');
179 } else {
180 re.push_str("(?:^|/)");
181 }
182
183 let chars: Vec<char> = p_str.chars().collect();
184 let mut i = 0;
185
186 while i < chars.len() {
187 match chars[i] {
188 '\\' => {
189 i += 1;
190 if i < chars.len() {
191 re.push_str(®ex::escape(&chars[i].to_string()));
192 }
193 }
194 '.' => re.push_str("\\."),
195 '/' => re.push('/'),
196 '*' => {
197 if i + 1 < chars.len() && chars[i + 1] == '*' {
198 re.push_str(".+");
199 i += 1;
200 } else {
201 re.push_str("[^/]*");
202 }
203 }
204 '?' => re.push_str("[^/]"),
205 '[' => {
206 re.push('[');
207 if i + 1 < chars.len() && chars[i + 1] == '!' {
208 re.push('^');
209 i += 1;
210 }
211 }
212 ']' | '-' | '^' => re.push(chars[i]),
213 _ => re.push_str(®ex::escape(&chars[i].to_string())),
215 }
216 i += 1;
217 }
218
219 if is_deep {
220 re.push_str("(?:/.*)?$");
221 } else {
222 re.push('$');
223 }
224
225 Ok(Self {
226 regex: Regex::new(&re)?,
227 targets_file,
228 requires_sibling,
229 requires_orphan,
230 is_deep,
231 include_parents,
232 base_name,
233 is_negated,
234 original: pattern.to_string(),
235 })
236 }
237
238 pub fn is_match<E: PattEnvIndex>(&self, path: &str, env: &E) -> bool {
239 let is_dir = path.ends_with('/');
240 let clean = path.strip_prefix("./").unwrap_or(path);
241
242 if self.targets_file && is_dir {
244 if self.include_parents {
245 return env.any_file_in_dir(path, &mut |p| self.regex.is_match(p.strip_prefix("./").unwrap_or(p)));
246 }
247 return false;
248 }
249
250 if !self.regex.is_match(clean) {
251 if is_dir && self.include_parents {
252 return env.any_file_in_dir(path, &mut |p| self.regex.is_match(p.strip_prefix("./").unwrap_or(p)));
253 }
254 return false;
255 }
256
257 if (self.requires_sibling || self.requires_orphan) && !is_dir {
259 if self.is_deep && self.requires_sibling {
260 if !self.check_authorized_root(path, env) {
261 return false;
262 }
263 return true;
264 }
265
266 let ctx = PathContext::from(path);
267 let expected_folder = if ctx.parent.is_empty() {
268 format!("{}/", self.base_name)
269 } else {
270 format!("{}/{}/", ctx.parent, self.base_name)
271 };
272
273 let exists = env.has_dir(&expected_folder);
274
275 if self.requires_sibling && !exists {
276 return false;
277 }
278 if self.requires_orphan && exists {
279 return false;
280 }
281 }
282
283 if (self.requires_sibling || self.requires_orphan) && is_dir {
285 if self.is_deep && self.requires_sibling {
286 if !self.check_authorized_root(path, env) {
287 return false;
288 }
289 } else {
290 let dir_no_slash = path.trim_end_matches('/');
291 let search_prefix = format!("{}.", dir_no_slash);
292 let has_file_sibling = env.has_file_with_prefix(&search_prefix);
293
294 if self.requires_sibling && !has_file_sibling {
295 return false;
296 }
297 if self.requires_orphan && has_file_sibling {
298 return false;
299 }
300 }
301 }
302
303 true
304 }
305
306 fn check_authorized_root<E: PattEnvIndex>(&self, path: &str, env: &E) -> bool {
307 let clean = path.strip_prefix("./").unwrap_or(path);
308 let components: Vec<&str> = clean.split('/').collect();
309
310 for i in 0..components.len() {
311 let comp_core = components[i].split('.').next().unwrap_or("");
312
313 if comp_core == self.base_name {
314 let base_dir = if i == 0 {
315 self.base_name.clone()
316 } else {
317 format!("{}/{}", components[0..i].join("/"), self.base_name)
318 };
319
320 let full_base_dir = if path.starts_with("./") { format!("./{}", base_dir) } else { base_dir };
321 let dir_path = format!("{}/", full_base_dir);
322
323 let has_dir = env.has_dir(&dir_path);
324 let search_prefix = format!("{}.", full_base_dir);
325 let has_file = env.has_file_with_prefix(&search_prefix);
326
327 if has_dir && has_file {
328 return true;
329 }
330 }
331 }
332 false
333 }
334}