1use std::path::Path;
31
32use crate::engine_contract::{Error, Result};
33
34const MAX_ALTERNATIVES: usize = 1_024;
40
41#[derive(Clone, Debug)]
43pub struct Pattern {
44 alternatives: Vec<Alternative>,
46 anchored: bool,
48 source: String,
50}
51
52#[derive(Clone, Debug)]
54struct Alternative {
55 components: Vec<Component>,
56}
57
58#[derive(Clone, Debug)]
60enum Component {
61 AnyComponents,
63 Tokens(Vec<Token>),
65}
66
67#[derive(Clone, Debug)]
69enum Token {
70 Literal(char),
72 AnyChar,
74 AnyRun,
76 Class {
78 negated: bool,
80 ranges: Vec<(char, char)>,
82 },
83}
84
85impl Pattern {
86 pub fn parse(source: &str) -> Result<Self> {
88 if source.is_empty() {
89 return Err(glob_error(source, "expected a pattern, as in `*.rs` or `src/**/*.rs`"));
90 }
91
92 let expanded = expand_braces(source)?;
93 let anchored = source.contains('/');
94 let mut alternatives = Vec::with_capacity(expanded.len());
95 for candidate in expanded {
96 alternatives.push(Alternative { components: parse_components(source, &candidate)? });
97 }
98 Ok(Self { alternatives, anchored, source: source.to_string() })
99 }
100
101 pub fn matches(&self, relative: &Path, name: &str) -> bool {
106 if self.anchored {
107 let parts: Vec<String> = relative
111 .components()
112 .filter_map(|component| match component {
113 std::path::Component::Normal(part) => Some(part.to_string_lossy().into_owned()),
114 _ => None,
115 })
116 .collect();
117 let parts: Vec<&str> = parts.iter().map(String::as_str).collect();
118 self.alternatives.iter().any(|alt| match_components(&alt.components, &parts))
119 } else {
120 self.alternatives.iter().any(|alt| match_components(&alt.components, &[name]))
121 }
122 }
123
124 pub fn source(&self) -> &str {
126 &self.source
127 }
128
129 pub(crate) fn retained_heap_bytes(&self) -> usize {
131 let alternatives = self.alternatives.iter().fold(0_usize, |total, alternative| {
132 let components = alternative.components.iter().fold(0_usize, |total, component| {
133 let nested = match component {
134 Component::AnyComponents => 0,
135 Component::Tokens(tokens) => tokens.iter().fold(
136 tokens.capacity().saturating_mul(std::mem::size_of::<Token>()),
137 |total, token| match token {
138 Token::Class { ranges, .. } => total.saturating_add(
139 ranges
140 .capacity()
141 .saturating_mul(std::mem::size_of::<(char, char)>()),
142 ),
143 Token::Literal(_) | Token::AnyChar | Token::AnyRun => total,
144 },
145 ),
146 };
147 total.saturating_add(nested)
148 });
149 total
150 .saturating_add(
151 alternative
152 .components
153 .capacity()
154 .saturating_mul(std::mem::size_of::<Component>()),
155 )
156 .saturating_add(components)
157 });
158 self.source
159 .capacity()
160 .saturating_add(
161 self.alternatives.capacity().saturating_mul(std::mem::size_of::<Alternative>()),
162 )
163 .saturating_add(alternatives)
164 }
165}
166
167fn expand_braces(source: &str) -> Result<Vec<String>> {
169 let mut pending = vec![String::new()];
170 let mut chars = source.chars().peekable();
171
172 while let Some(ch) = chars.next() {
173 match ch {
174 '\\' => {
175 let escaped = chars
176 .next()
177 .ok_or_else(|| glob_error(source, "pattern ends with a trailing `\\`"))?;
178 for candidate in &mut pending {
179 candidate.push('\\');
180 candidate.push(escaped);
181 }
182 }
183 '{' => {
184 let group = take_group(source, &mut chars)?;
185 let branches = split_branches(&group);
186 let mut grown = Vec::with_capacity(pending.len() * branches.len());
187 for candidate in &pending {
188 for branch in &branches {
189 for nested in expand_braces(branch)? {
192 grown.push(format!("{candidate}{nested}"));
193 }
194 }
195 }
196 if grown.len() > MAX_ALTERNATIVES {
197 return Err(glob_error(
198 source,
199 "pattern expands to too many alternatives; write several patterns instead",
200 ));
201 }
202 pending = grown;
203 }
204 '}' => return Err(glob_error(source, "unmatched `}` in pattern")),
205 _ => {
206 for candidate in &mut pending {
207 candidate.push(ch);
208 }
209 }
210 }
211 }
212
213 Ok(pending)
214}
215
216fn take_group(source: &str, chars: &mut std::iter::Peekable<std::str::Chars>) -> Result<String> {
218 let mut depth = 1usize;
219 let mut group = String::new();
220 for ch in chars.by_ref() {
221 match ch {
222 '{' => {
223 depth += 1;
224 group.push(ch);
225 }
226 '}' => {
227 depth -= 1;
228 if depth == 0 {
229 return Ok(group);
230 }
231 group.push(ch);
232 }
233 _ => group.push(ch),
234 }
235 }
236 Err(glob_error(source, "unmatched `{` in pattern"))
237}
238
239fn split_branches(group: &str) -> Vec<String> {
241 let mut branches = Vec::new();
242 let mut current = String::new();
243 let mut depth = 0usize;
244 let mut chars = group.chars();
245 while let Some(ch) = chars.next() {
246 match ch {
247 '\\' => {
248 current.push(ch);
249 if let Some(escaped) = chars.next() {
250 current.push(escaped);
251 }
252 }
253 '{' => {
254 depth += 1;
255 current.push(ch);
256 }
257 '}' => {
258 depth = depth.saturating_sub(1);
259 current.push(ch);
260 }
261 ',' if depth == 0 => branches.push(std::mem::take(&mut current)),
262 _ => current.push(ch),
263 }
264 }
265 branches.push(current);
266 branches
267}
268
269fn parse_components(source: &str, pattern: &str) -> Result<Vec<Component>> {
271 let mut components = Vec::new();
272 for part in pattern.split('/') {
273 if part.is_empty() {
274 continue;
277 }
278 if part == "**" {
279 components.push(Component::AnyComponents);
280 } else {
281 components.push(Component::Tokens(tokenize(source, part)?));
282 }
283 }
284 Ok(components)
285}
286
287fn tokenize(source: &str, part: &str) -> Result<Vec<Token>> {
289 let mut tokens = Vec::new();
290 let mut chars = part.chars().peekable();
291 while let Some(ch) = chars.next() {
292 match ch {
293 '*' => {
294 while chars.peek() == Some(&'*') {
297 chars.next();
298 }
299 tokens.push(Token::AnyRun);
300 }
301 '?' => tokens.push(Token::AnyChar),
302 '[' => tokens.push(parse_class(source, &mut chars)?),
303 '\\' => {
304 let escaped = chars
305 .next()
306 .ok_or_else(|| glob_error(source, "pattern ends with a trailing `\\`"))?;
307 tokens.push(Token::Literal(escaped));
308 }
309 _ => tokens.push(Token::Literal(ch)),
310 }
311 }
312 Ok(tokens)
313}
314
315fn parse_class(source: &str, chars: &mut std::iter::Peekable<std::str::Chars>) -> Result<Token> {
317 let negated = matches!(chars.peek(), Some('!' | '^'));
318 if negated {
319 chars.next();
320 }
321
322 let mut ranges = Vec::new();
323 let mut first = true;
324 while let Some(ch) = chars.next() {
325 if ch == ']' && !first {
327 if ranges.is_empty() {
328 return Err(glob_error(source, "empty character class in pattern"));
329 }
330 return Ok(Token::Class { negated, ranges });
331 }
332 first = false;
333
334 let start = if ch == '\\' {
335 chars.next().ok_or_else(|| glob_error(source, "pattern ends with a trailing `\\`"))?
336 } else {
337 ch
338 };
339
340 if chars.peek() == Some(&'-') {
341 chars.next();
342 match chars.next() {
343 Some(']') => {
345 ranges.push((start, start));
346 ranges.push(('-', '-'));
347 return Ok(Token::Class { negated, ranges });
348 }
349 Some(end) => ranges.push((start, end)),
350 None => return Err(glob_error(source, "unmatched `[` in pattern")),
351 }
352 } else {
353 ranges.push((start, start));
354 }
355 }
356 Err(glob_error(source, "unmatched `[` in pattern"))
357}
358
359fn match_components(pattern: &[Component], parts: &[&str]) -> bool {
361 match pattern.split_first() {
362 None => parts.is_empty(),
363 Some((Component::AnyComponents, rest)) => {
364 (0..=parts.len()).any(|skip| match_components(rest, &parts[skip..]))
366 }
367 Some((Component::Tokens(tokens), rest)) => match parts.split_first() {
368 Some((part, remaining)) => {
369 match_tokens(tokens, part) && match_components(rest, remaining)
370 }
371 None => false,
372 },
373 }
374}
375
376fn match_tokens(tokens: &[Token], part: &str) -> bool {
378 let chars: Vec<char> = part.chars().collect();
379 match_tokens_at(tokens, &chars)
380}
381
382fn match_tokens_at(tokens: &[Token], text: &[char]) -> bool {
384 match tokens.split_first() {
385 None => text.is_empty(),
386 Some((Token::AnyRun, rest)) => {
387 (0..=text.len()).any(|skip| match_tokens_at(rest, &text[skip..]))
388 }
389 Some((token, rest)) => match text.split_first() {
390 Some((ch, remaining)) => match_one(token, *ch) && match_tokens_at(rest, remaining),
391 None => false,
392 },
393 }
394}
395
396fn match_one(token: &Token, ch: char) -> bool {
398 match token {
399 Token::Literal(expected) => *expected == ch,
400 Token::AnyChar => true,
401 Token::Class { negated, ranges } => {
402 let inside = ranges.iter().any(|(start, end)| *start <= ch && ch <= *end);
403 inside != *negated
404 }
405 Token::AnyRun => false,
407 }
408}
409
410fn glob_error(source: &str, hint: &str) -> Error {
412 Error::InvalidValue { kind: "pattern", value: source.to_string(), hint: hint.to_string() }
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418 use std::path::PathBuf;
419
420 fn matches(pattern: &str, path: &str) -> bool {
421 let compiled = Pattern::parse(pattern).expect("pattern compiles");
422 let relative = PathBuf::from(path);
423 let name = relative
424 .file_name()
425 .map(|name| name.to_string_lossy().into_owned())
426 .unwrap_or_default();
427 compiled.matches(&relative, &name)
428 }
429
430 fn rejection(pattern: &str) -> String {
431 match Pattern::parse(pattern) {
432 Err(Error::InvalidValue { kind: "pattern", hint, .. }) => hint,
433 other => panic!("expected {pattern:?} to be rejected, got {other:?}"),
434 }
435 }
436
437 #[test]
438 fn bare_patterns_match_the_file_name_at_any_depth() {
439 assert!(matches("*.rs", "main.rs"));
441 assert!(matches("*.rs", "src/deep/nested/main.rs"));
442 assert!(!matches("*.rs", "src/main.toml"));
443 assert!(matches("main.rs", "a/b/c/main.rs"));
444 }
445
446 #[test]
447 fn patterns_with_a_separator_match_the_whole_relative_path() {
448 assert!(matches("src/*.rs", "src/main.rs"));
449 assert!(!matches("src/*.rs", "other/main.rs"));
450 assert!(!matches("src/*.rs", "src/deep/main.rs"));
452 }
453
454 #[test]
455 fn double_star_crosses_component_boundaries_including_none() {
456 assert!(matches("src/**/*.rs", "src/main.rs"), "** matches zero components");
457 assert!(matches("src/**/*.rs", "src/a/main.rs"));
458 assert!(matches("src/**/*.rs", "src/a/b/c/main.rs"));
459 assert!(!matches("src/**/*.rs", "other/a/main.rs"));
460 assert!(matches("**/target/**", "a/b/target/c/d"));
461 }
462
463 #[test]
464 fn braces_expand_to_alternatives() {
465 assert!(matches("*.{rs,toml}", "main.rs"));
466 assert!(matches("*.{rs,toml}", "Cargo.toml"));
467 assert!(!matches("*.{rs,toml}", "notes.md"));
468 assert!(matches("{src,tests}/*.{rs,md}", "tests/readme.md"));
470 assert!(!matches("{src,tests}/*.{rs,md}", "docs/readme.md"));
471 }
472
473 #[test]
474 fn character_classes_match_one_character() {
475 assert!(matches("file[0-9].txt", "file7.txt"));
476 assert!(!matches("file[0-9].txt", "filex.txt"));
477 assert!(matches("file[!0-9].txt", "filex.txt"));
478 assert!(!matches("file[!0-9].txt", "file7.txt"));
479 assert!(matches("[abc]at", "cat"));
480 }
481
482 #[test]
483 fn question_mark_matches_exactly_one_character() {
484 assert!(matches("?.rs", "a.rs"));
485 assert!(!matches("?.rs", "ab.rs"));
486 assert!(!matches("?.rs", ".rs"));
487 }
488
489 #[test]
490 fn escapes_make_metacharacters_literal() {
491 assert!(matches(r"\*.rs", "*.rs"));
492 assert!(!matches(r"\*.rs", "main.rs"));
493 assert!(matches(r"a\?b", "a?b"));
494 }
495
496 #[test]
497 fn stars_match_empty_runs() {
498 assert!(matches("*", "anything"));
499 assert!(matches("*.rs", ".rs"));
500 assert!(matches("a*b", "ab"));
501 }
502
503 #[test]
504 fn repeated_stars_inside_a_component_collapse() {
505 assert!(matches("a***b", "axyzb"));
507 assert!(matches("a***b", "ab"));
508 }
509
510 #[test]
511 fn malformed_patterns_are_rejected_with_a_reason() {
512 assert!(rejection("").contains("expected a pattern"));
513 assert!(rejection("{a,b").contains("unmatched `{`"));
514 assert!(rejection("a}b").contains("unmatched `}`"));
515 assert!(rejection("[abc").contains("unmatched `[`"));
516 assert!(rejection("[]").contains("unmatched `[`"));
517 assert!(rejection(r"abc\").contains("trailing `\\`"));
518 }
519
520 #[test]
521 fn runaway_brace_expansion_is_rejected_rather_than_allocated() {
522 let bomb = "{a,b}".repeat(11);
523 assert!(rejection(&bomb).contains("too many alternatives"));
524 }
525
526 #[test]
527 fn anchored_patterns_match_however_the_platform_spells_a_separator() {
528 let relative: PathBuf = ["src", "deep", "main.rs"].iter().collect();
532 let compiled = Pattern::parse("src/**/*.rs").expect("pattern compiles");
533 assert!(compiled.matches(&relative, "main.rs"));
534
535 let shallow: PathBuf = ["src", "main.rs"].iter().collect();
536 assert!(Pattern::parse("src/*.rs").expect("compiles").matches(&shallow, "main.rs"));
537 assert!(!Pattern::parse("other/*.rs").expect("compiles").matches(&shallow, "main.rs"));
538 }
539
540 #[test]
541 fn source_is_retained_for_diagnostics() {
542 assert_eq!(Pattern::parse("*.rs").expect("compiles").source(), "*.rs");
543 }
544}