1use path_dedot::{CWD, ParseDot};
2use std::borrow::Cow;
3use std::collections::HashSet;
4use std::ffi::OsStr;
5use std::fmt::Debug;
6use std::io;
7use std::ops::Range;
8use std::path::{MAIN_SEPARATOR, Path, PathBuf};
9use std::sync::Arc;
10use wildmatch::WildMatch;
11
12pub(crate) struct PathInner {
13 pathbase: Arc<String>,
14 range: Range<usize>,
15}
16
17impl PathInner {
18 pub(crate) fn as_str(&self) -> &str {
19 &self.pathbase[self.range.clone()]
20 }
21}
22
23impl Debug for PathInner {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 write!(f, "{:?}", self.as_ref())
26 }
27}
28
29impl AsRef<Path> for PathInner {
30 fn as_ref(&self) -> &Path {
31 Path::new(&self.pathbase[self.range.clone()])
32 }
33}
34
35impl Clone for PathInner {
36 fn clone(&self) -> Self {
37 Self {
38 pathbase: Arc::clone(&self.pathbase),
39 range: self.range.clone(),
40 }
41 }
42}
43
44#[allow(dead_code)]
45#[derive(Debug, Clone)]
46pub(crate) enum SegmentMatcher {
47 AnyPath(PathInner),
48 WildMatch { pattern: String, matcher: WildMatch },
49 Descend,
50}
51
52#[derive(Debug, Clone)]
53struct CompiledRule {
54 rule_index: usize,
55 is_exclude: bool,
56 is_absolute: bool,
57 segments: Vec<SegmentMatcher>,
58}
59
60#[allow(dead_code)]
61#[derive(Debug, Default, Clone)]
62struct RuleTerminal {
63 rule_index: usize,
64 is_exclude: bool,
65}
66
67type NodeId = usize;
68
69#[derive(Debug, Clone, Eq, PartialEq)]
70enum WildEdgeKind {
71 Suffix(String),
72 Prefix(String),
73 SingleChar,
74 General,
75}
76
77#[derive(Debug, Default, Clone)]
78struct TrieNode {
79 literal_edges: hashbrown::HashMap<String, NodeId>,
80 wild_edges_general: Vec<(String, WildMatch, NodeId)>,
81 wild_edges_suffix: hashbrown::HashMap<String, Vec<NodeId>>,
82 wild_edges_prefix: hashbrown::HashMap<String, Vec<NodeId>>,
83 wild_edges_exact1: Vec<NodeId>,
84 descend_edge: Option<NodeId>,
85 terminals: Vec<RuleTerminal>,
86}
87
88#[derive(Debug, Clone)]
89struct GlobTrie {
90 nodes: Vec<TrieNode>,
91}
92
93impl GlobTrie {
94 fn new() -> Self {
95 Self {
96 nodes: vec![TrieNode::default()],
97 }
98 }
99
100 fn add_node(&mut self) -> NodeId {
101 self.nodes.push(TrieNode::default());
102 self.nodes.len() - 1
103 }
104
105 fn insert_rule(&mut self, rule: &CompiledRule) {
106 fn any_path_parts(text: &str) -> impl Iterator<Item = &str> {
107 text.split(MAIN_SEPARATOR).filter(|s| !s.is_empty())
108 }
109
110 let mut node = 0usize;
111 for segment in &rule.segments {
112 match segment {
113 SegmentMatcher::AnyPath(inner) => {
114 for part in any_path_parts(inner.as_str()) {
115 if part.is_empty() {
116 continue;
117 }
118 let next = if let Some(existing) = self.nodes[node].literal_edges.get(part)
119 {
120 *existing
121 } else {
122 let created = self.add_node();
123 self.nodes[node]
124 .literal_edges
125 .insert(part.to_string(), created);
126 created
127 };
128 node = next;
129 }
130 }
131 SegmentMatcher::WildMatch {
132 pattern,
133 matcher: _,
134 } => {
135 let next = self.insert_wild_edge(node, pattern);
136 node = next;
137 }
138 SegmentMatcher::Descend => {
139 let next = if let Some(existing) = self.nodes[node].descend_edge {
140 existing
141 } else {
142 let created = self.add_node();
143 self.nodes[node].descend_edge = Some(created);
144 created
145 };
146 node = next;
147 }
148 }
149 }
150 self.nodes[node].terminals.push(RuleTerminal {
151 rule_index: rule.rule_index,
152 is_exclude: rule.is_exclude,
153 });
154 }
155
156 fn insert_wild_edge(&mut self, node: NodeId, pattern: &str) -> NodeId {
157 match classify_wild_edge(pattern) {
158 WildEdgeKind::Suffix(suffix) => {
159 if let Some(existing) = self.nodes[node]
160 .wild_edges_suffix
161 .get(&suffix)
162 .and_then(|ids| ids.first())
163 {
164 return *existing;
165 }
166 let created = self.add_node();
167 self.nodes[node]
168 .wild_edges_suffix
169 .entry(suffix)
170 .or_default()
171 .push(created);
172 created
173 }
174 WildEdgeKind::Prefix(prefix) => {
175 if let Some(existing) = self.nodes[node]
176 .wild_edges_prefix
177 .get(&prefix)
178 .and_then(|ids| ids.first())
179 {
180 return *existing;
181 }
182 let created = self.add_node();
183 self.nodes[node]
184 .wild_edges_prefix
185 .entry(prefix)
186 .or_default()
187 .push(created);
188 created
189 }
190 WildEdgeKind::SingleChar => {
191 if let Some(existing) = self.nodes[node].wild_edges_exact1.first() {
192 return *existing;
193 }
194 let created = self.add_node();
195 self.nodes[node].wild_edges_exact1.push(created);
196 created
197 }
198 WildEdgeKind::General => {
199 for (existing, _, node_id) in &self.nodes[node].wild_edges_general {
200 if existing == pattern {
201 return *node_id;
202 }
203 }
204 let created = self.add_node();
205 self.nodes[node].wild_edges_general.push((
206 pattern.to_string(),
207 WildMatch::new(pattern),
208 created,
209 ));
210 created
211 }
212 }
213 }
214}
215
216#[derive(Debug, Clone)]
217pub struct CompiledGlob {
218 ordered_rules: Vec<CompiledRule>,
219 trie: GlobTrie,
220 epsilon_closures: Vec<Vec<usize>>,
221 node_can_scan: Vec<bool>,
222 node_best_terminal: Vec<Option<(usize, bool)>>,
223}
224
225impl CompiledGlob {
226 pub fn new(pattern: &str) -> io::Result<Self> {
228 if pattern.is_empty() {
229 return Err(io::Error::new(
230 io::ErrorKind::InvalidInput,
231 "pattern must not be empty",
232 ));
233 }
234 let is_exclude = pattern.starts_with('!');
235 let pattern_body = if is_exclude { &pattern[1..] } else { pattern };
236 if pattern_body.is_empty() {
237 return Err(io::Error::new(
238 io::ErrorKind::InvalidInput,
239 "exclude pattern must include body after '!'",
240 ));
241 }
242
243 let parsed = Path::new(pattern_body).parse_dot()?;
244 let is_absolute = parsed.is_absolute();
245 let pattern = parsed.to_str().unwrap().to_string();
246 let pattern = Arc::new(pattern);
247 let mut segments = Vec::new();
248
249 let mut seg_start = 0usize;
250 fn push_segment_range(
251 segments: &mut Vec<SegmentMatcher>,
252 pattern: &Arc<String>,
253 range: Range<usize>,
254 ) {
255 if range.is_empty() {
256 return;
257 }
258 let seg = &pattern[range.clone()];
259 if let Some(rel_pos) = seg.find("**") {
260 if seg == "**" {
261 segments.push(SegmentMatcher::Descend);
262 return;
263 }
264 let abs_pos = range.start + rel_pos;
265 let pre = range.start..abs_pos;
266 let post = (abs_pos + 2)..range.end;
267 if pre.is_empty() && !post.is_empty() {
268 segments.push(SegmentMatcher::Descend);
269 let mut tail = String::from("*");
270 tail.push_str(&pattern[post]);
271 segments.push(SegmentMatcher::WildMatch {
272 pattern: tail.clone(),
273 matcher: WildMatch::new(&tail),
274 });
275 return;
276 }
277 if !pre.is_empty() && post.is_empty() {
278 let mut head = pattern[pre].to_string();
279 head.push('*');
280 segments.push(SegmentMatcher::WildMatch {
281 pattern: head.clone(),
282 matcher: WildMatch::new(&head),
283 });
284 segments.push(SegmentMatcher::Descend);
285 return;
286 }
287 if !pre.is_empty() && !post.is_empty() {
288 let mut head = pattern[pre].to_string();
289 head.push('*');
290 segments.push(SegmentMatcher::WildMatch {
291 pattern: head.clone(),
292 matcher: WildMatch::new(&head),
293 });
294 segments.push(SegmentMatcher::Descend);
295 let mut tail = String::from("*");
296 tail.push_str(&pattern[post]);
297 segments.push(SegmentMatcher::WildMatch {
298 pattern: tail.clone(),
299 matcher: WildMatch::new(&tail),
300 });
301 return;
302 }
303 return;
304 }
305
306 let has_wild = seg.chars().any(|ch| matches!(ch, '*' | '?'));
307 if has_wild {
308 segments.push(SegmentMatcher::WildMatch {
309 pattern: seg.to_string(),
310 matcher: WildMatch::new(seg),
311 });
312 } else if let Some(SegmentMatcher::AnyPath(last)) = segments.last_mut() {
313 last.range.end = range.end;
314 } else {
315 segments.push(SegmentMatcher::AnyPath(PathInner {
316 pathbase: pattern.clone(),
317 range,
318 }));
319 }
320 }
321
322 for (idx, ch) in pattern.char_indices() {
323 if ch == MAIN_SEPARATOR {
324 push_segment_range(&mut segments, &pattern, seg_start..idx);
325 seg_start = idx + ch.len_utf8();
326 }
327 }
328 push_segment_range(&mut segments, &pattern, seg_start..pattern.len());
329
330 if !is_absolute {
331 let pathbase = Arc::new(CWD.to_str().unwrap().to_string());
332 let range = 0..pathbase.len();
333 segments.insert(
334 0,
336 SegmentMatcher::AnyPath(PathInner { pathbase, range }),
337 );
338 }
339 let mut compiled = CompiledGlob {
340 ordered_rules: Vec::new(),
341 trie: GlobTrie::new(),
342 epsilon_closures: Vec::new(),
343 node_can_scan: Vec::new(),
344 node_best_terminal: Vec::new(),
345 };
346 compiled.push_rule(segments, is_exclude, is_absolute);
347 Ok(compiled)
348 }
349
350 pub fn merge(mut self, other: CompiledGlob) -> CompiledGlob {
351 let base = self.ordered_rules.len();
352 for (offset, mut rule) in other.ordered_rules.into_iter().enumerate() {
353 rule.rule_index = base + offset;
354 self.trie.insert_rule(&rule);
355 self.ordered_rules.push(rule);
356 }
357 self.rebuild_epsilon_closure_cache();
358 self
359 }
360
361 pub fn merge_many(globs: impl IntoIterator<Item = CompiledGlob>) -> io::Result<CompiledGlob> {
362 let mut iter = globs.into_iter();
363 let Some(mut merged) = iter.next() else {
364 return Err(io::Error::new(
365 io::ErrorKind::InvalidInput,
366 "merge_many requires at least one compiled glob",
367 ));
368 };
369 for glob in iter {
370 merged = merged.merge(glob);
371 }
372 Ok(merged)
373 }
374
375 pub(crate) fn initial_states(&self) -> Vec<usize> {
376 self.expand_epsilon_nodes([0usize].as_ref())
377 }
378
379 pub(crate) fn states_for_path(&self, path: &Path) -> Vec<usize> {
380 let mut states = self.initial_states();
381 for part in path
382 .to_string_lossy()
383 .split(MAIN_SEPARATOR)
384 .filter(|s| !s.is_empty())
385 {
386 states = self.advance_states(&states, part);
387 if states.is_empty() {
388 break;
389 }
390 }
391 states
392 }
393
394 pub(crate) fn start_paths(&self) -> Vec<PathBuf> {
395 let mut out = Vec::new();
396 let mut seen = hashbrown::HashSet::new();
397
398 for rule in &self.ordered_rules {
399 if rule.is_exclude {
400 continue;
401 }
402
403 let mut prefix = PathBuf::new();
404 for seg in &rule.segments {
405 match seg {
406 SegmentMatcher::AnyPath(part) => {
407 prefix.push(part.as_ref());
408 }
409 SegmentMatcher::WildMatch { .. } | SegmentMatcher::Descend => break,
410 }
411 }
412
413 let mut candidate = if prefix.as_os_str().is_empty() {
414 PathBuf::from(MAIN_SEPARATOR.to_string())
415 } else {
416 prefix
417 };
418 if rule.is_absolute && !candidate.is_absolute() {
419 candidate = PathBuf::from(MAIN_SEPARATOR.to_string()).join(candidate);
420 }
421
422 if seen.insert(candidate.clone()) {
423 out.push(candidate);
424 }
425 }
426
427 if out.is_empty() {
428 out.push(PathBuf::from(MAIN_SEPARATOR.to_string()));
429 }
430
431 out
432 }
433
434 pub(crate) fn advance_states(&self, current: &[usize], part: &str) -> Vec<usize> {
435 let mut out = Vec::new();
436 self.advance_states_into(current, part, &mut out);
437 out
438 }
439
440 pub(crate) fn advance_states_into(&self, current: &[usize], part: &str, out: &mut Vec<usize>) {
441 let mut scratch_a = Vec::new();
443 let mut scratch_b = Vec::new();
444 self.expand_epsilon_nodes_into(current, &mut scratch_a);
445 out.clear();
446 let single_char = is_single_char(part);
447 let mut overflow_seen: Option<HashSet<usize>> = None;
448 for node_idx in &scratch_a {
449 let node = &self.trie.nodes[*node_idx];
450 if let Some(next_idx) = node.literal_edges.get(part) {
451 push_unique_state(out, &mut overflow_seen, *next_idx);
452 }
453
454 if !node.wild_edges_exact1.is_empty() && single_char {
455 for next_idx in &node.wild_edges_exact1 {
456 push_unique_state(out, &mut overflow_seen, *next_idx);
457 }
458 }
459
460 for (suffix, ids) in &node.wild_edges_suffix {
461 if part.ends_with(suffix) {
462 for next_idx in ids {
463 push_unique_state(out, &mut overflow_seen, *next_idx);
464 }
465 }
466 }
467
468 for (prefix, ids) in &node.wild_edges_prefix {
469 if part.starts_with(prefix) {
470 for next_idx in ids {
471 push_unique_state(out, &mut overflow_seen, *next_idx);
472 }
473 }
474 }
475
476 for (_, matcher, next_idx) in &node.wild_edges_general {
477 if matcher.matches(part) {
478 push_unique_state(out, &mut overflow_seen, *next_idx);
479 }
480 }
481 if node.descend_edge.is_some() {
482 push_unique_state(out, &mut overflow_seen, *node_idx);
483 }
484 }
485 self.expand_epsilon_nodes_into(out, &mut scratch_b);
487 std::mem::swap(out, &mut scratch_b);
488 }
489
490 pub(crate) fn is_match_state(&self, current: &[usize]) -> bool {
491 matches!(self.match_decision(current), Some(true))
492 }
493
494 #[allow(dead_code)]
495 pub(crate) fn literal_candidates(&self, current: &[usize]) -> Vec<String> {
496 let expanded = self.expand_epsilon_nodes_borrowed(current);
497 let mut out = hashbrown::HashSet::new();
498 for node_idx in expanded.iter() {
499 out.extend(self.trie.nodes[*node_idx].literal_edges.keys().cloned());
500 }
501 let mut out = out.into_iter().collect::<Vec<_>>();
502 out.sort_unstable();
503 out
504 }
505
506 pub(crate) fn needs_directory_scan(&self, current: &[usize]) -> bool {
507 self.expand_epsilon_nodes_borrowed(current)
508 .iter()
509 .any(|node_idx| self.node_can_scan.get(*node_idx).copied().unwrap_or(false))
510 }
511
512 fn push_rule(&mut self, segments: Vec<SegmentMatcher>, is_exclude: bool, is_absolute: bool) {
513 let rule = CompiledRule {
514 rule_index: self.ordered_rules.len(),
515 is_exclude,
516 is_absolute,
517 segments,
518 };
519 self.trie.insert_rule(&rule);
520 self.ordered_rules.push(rule);
521 self.rebuild_epsilon_closure_cache();
522 }
523
524 fn expand_epsilon_nodes(&self, current: &[usize]) -> Vec<usize> {
525 let mut out = Vec::new();
526 self.expand_epsilon_nodes_into(current, &mut out);
527 out
528 }
529
530 fn expand_epsilon_nodes_into(&self, current: &[usize], out: &mut Vec<usize>) {
533 out.clear();
534 if current.is_empty() {
535 return;
536 }
537 if current.len() == 1 {
538 if let Some(cached) = self.epsilon_closures.get(current[0]) {
539 out.extend_from_slice(cached);
540 }
541 return;
542 }
543
544 let mut overflow_seen: Option<HashSet<usize>> = None;
545 for node_idx in current {
546 let Some(cached) = self.epsilon_closures.get(*node_idx) else {
547 continue;
548 };
549 for value in cached {
550 push_unique_state(out, &mut overflow_seen, *value);
551 }
552 }
553 out.sort_unstable();
554 }
555
556 fn expand_epsilon_nodes_borrowed<'a>(&'a self, current: &[usize]) -> Cow<'a, [usize]> {
560 match current {
561 [] => Cow::Borrowed(&[]),
562 [one] => match self.epsilon_closures.get(*one) {
563 Some(c) => Cow::Borrowed(c.as_slice()),
564 None => Cow::Borrowed(&[]),
565 },
566 _ => {
567 let mut out = Vec::new();
568 self.expand_epsilon_nodes_into(current, &mut out);
569 Cow::Owned(out)
570 }
571 }
572 }
573
574 fn rebuild_epsilon_closure_cache(&mut self) {
575 let node_count = self.trie.nodes.len();
576 self.epsilon_closures = vec![Vec::new(); node_count];
577 self.node_can_scan = vec![false; node_count];
578 self.node_best_terminal = vec![None; node_count];
579 for node_idx in 0..node_count {
580 let mut closure = Vec::new();
581 let mut cursor = Some(node_idx);
582 while let Some(idx) = cursor {
583 closure.push(idx);
584 cursor = self.trie.nodes[idx].descend_edge;
585 }
586 closure.sort_unstable();
587 closure.dedup();
588 self.epsilon_closures[node_idx] = closure;
589
590 let node = &self.trie.nodes[node_idx];
591 self.node_can_scan[node_idx] = !node.wild_edges_general.is_empty()
592 || !node.wild_edges_suffix.is_empty()
593 || !node.wild_edges_prefix.is_empty()
594 || !node.wild_edges_exact1.is_empty()
595 || node.descend_edge.is_some();
596
597 let mut selected: Option<(usize, bool)> = None;
598 for terminal in &node.terminals {
599 if selected
600 .as_ref()
601 .is_none_or(|(idx, _)| terminal.rule_index >= *idx)
602 {
603 selected = Some((terminal.rule_index, !terminal.is_exclude));
604 }
605 }
606 self.node_best_terminal[node_idx] = selected;
607 }
608 }
609
610 fn match_decision(&self, current: &[usize]) -> Option<bool> {
611 let expanded = self.expand_epsilon_nodes_borrowed(current);
612 let mut selected: Option<(usize, bool)> = None;
613 for node_idx in expanded.iter() {
614 if let Some((rule_index, include)) =
615 self.node_best_terminal.get(*node_idx).copied().flatten()
616 && selected.as_ref().is_none_or(|(idx, _)| rule_index >= *idx)
617 {
618 selected = Some((rule_index, include));
619 }
620 }
621 selected.map(|(_, include)| include)
622 }
623
624 pub fn r#match(&self, path: &OsStr) -> bool {
626 let normalized = match Path::new(path).parse_dot() {
627 Ok(v) => v,
628 Err(_) => return false,
629 };
630 let normalized = match normalized.to_str() {
631 Some(v) => v,
632 None => return false,
633 };
634 let mut states = self.initial_states();
635 for part in normalized.split(MAIN_SEPARATOR).filter(|s| !s.is_empty()) {
636 states = self.advance_states(&states, part);
637 if states.is_empty() {
638 return false;
639 }
640 }
641 self.match_decision(&states).unwrap_or(false)
642 }
643
644 #[allow(dead_code)]
645 pub(crate) fn segments(&self) -> &[SegmentMatcher] {
646 assert!(
647 self.ordered_rules.len() <= 1,
648 "segments() is only available for single-rule CompiledGlob"
649 );
650 self.ordered_rules
651 .first()
652 .map(|rule| rule.segments.as_slice())
653 .unwrap_or(&[])
654 }
655
656 #[allow(dead_code)]
657 pub(crate) fn ordered_rules_segments(&self) -> Vec<&[SegmentMatcher]> {
658 self.ordered_rules
659 .iter()
660 .map(|rule| rule.segments.as_slice())
661 .collect()
662 }
663}
664
665const INLINE_STATE_DEDUP_LIMIT: usize = 16;
666
667fn push_unique_state(
668 out: &mut Vec<usize>,
669 overflow_seen: &mut Option<HashSet<usize>>,
670 value: usize,
671) {
672 if let Some(seen) = overflow_seen.as_mut() {
673 if seen.insert(value) {
674 out.push(value);
675 }
676 return;
677 }
678
679 if out.contains(&value) {
680 return;
681 }
682 out.push(value);
683
684 if out.len() > INLINE_STATE_DEDUP_LIMIT {
685 let mut seen = HashSet::with_capacity(out.len() * 2);
686 for existing in out.iter().copied() {
687 seen.insert(existing);
688 }
689 *overflow_seen = Some(seen);
690 }
691}
692
693#[inline]
696fn is_single_char(s: &str) -> bool {
697 let mut it = s.chars();
698 it.next().is_some() && it.next().is_none()
699}
700
701fn classify_wild_edge(pattern: &str) -> WildEdgeKind {
702 if pattern == "?" {
703 return WildEdgeKind::SingleChar;
704 }
705
706 if pattern.starts_with('*')
707 && !pattern[1..].is_empty()
708 && !pattern[1..].contains('*')
709 && !pattern[1..].contains('?')
710 {
711 return WildEdgeKind::Suffix(pattern[1..].to_string());
712 }
713
714 if pattern.ends_with('*')
715 && !pattern[..pattern.len() - 1].is_empty()
716 && !pattern[..pattern.len() - 1].contains('*')
717 && !pattern[..pattern.len() - 1].contains('?')
718 {
719 return WildEdgeKind::Prefix(pattern[..pattern.len() - 1].to_string());
720 }
721
722 WildEdgeKind::General
723}
724
725#[cfg(test)]
726mod tests {
727 use super::{CompiledGlob, SegmentMatcher};
728 use path_dedot::CWD;
729 use std::io;
730 use std::path::Path;
731
732 #[test]
733 fn expands_leading_double_star_in_segment() {
734 let glob = CompiledGlob::new("/tmp/**.rs").expect("glob must parse");
735 assert!(glob.r#match("/tmp/main.rs".as_ref()));
736 assert!(glob.r#match("/tmp/src/lib.rs".as_ref()));
737 assert!(!glob.r#match("/tmp/src/lib.ts".as_ref()));
738 }
739
740 #[test]
741 fn expands_trailing_double_star_in_segment() {
742 let glob = CompiledGlob::new("/tmp/tag-**").expect("glob must parse");
743 assert!(glob.r#match("/tmp/tag-a".as_ref()));
744 assert!(glob.r#match("/tmp/tag-a/b".as_ref()));
745 assert!(!glob.r#match("/tmp/taga".as_ref()));
746 }
747
748 #[test]
749 fn prepends_cwd_when_first_segment_is_not_anypath() {
750 let glob = CompiledGlob::new("*.rs").expect("glob must parse");
751 assert!(matches!(
752 glob.segments().first(),
753 Some(SegmentMatcher::AnyPath(_))
754 ));
755 }
756
757 #[test]
758 fn does_not_prepend_cwd_for_absolute_descend_glob() {
759 let glob = CompiledGlob::new("/**").expect("glob must parse");
760 assert!(!matches!(
761 glob.segments().first(),
762 Some(SegmentMatcher::AnyPath(_))
763 ));
764 }
765
766 #[test]
767 fn does_not_prepend_cwd_for_absolute_wildcard_glob() {
768 let glob = CompiledGlob::new("/*/").expect("glob must parse");
769 assert!(!matches!(
770 glob.segments().first(),
771 Some(SegmentMatcher::AnyPath(_))
772 ));
773 }
774
775 #[test]
776 fn prepends_cwd_for_relative_literal_prefix() {
777 let glob = CompiledGlob::new("target/*").expect("glob must parse");
778 let cwd = CWD.to_str().expect("cwd must be valid utf-8");
779 assert!(matches!(
780 glob.segments().first(),
781 Some(SegmentMatcher::AnyPath(inner)) if inner.as_str() == cwd
782 ));
783
784 let ok = format!("{}/target/file.txt", CWD.display());
785 assert!(glob.r#match(Path::new(&ok).as_os_str()));
786 assert!(!glob.r#match(Path::new("/target/file.txt").as_os_str()));
787 }
788
789 #[test]
790 fn merge_many_or_union_matches() {
791 let one = CompiledGlob::new("/tmp/**/*.rs").expect("glob must parse");
792 let two = CompiledGlob::new("/tmp/**/*.txt").expect("glob must parse");
793 let merged = CompiledGlob::merge_many(vec![one, two]).expect("must merge");
794 assert!(merged.r#match("/tmp/a/b/main.rs".as_ref()));
795 assert!(merged.r#match("/tmp/a/b/readme.txt".as_ref()));
796 assert!(!merged.r#match("/tmp/a/b/readme.md".as_ref()));
797 }
798
799 #[test]
800 fn merge_preserves_rule_order() {
801 let one = CompiledGlob::new("/tmp/**/*.rs").expect("glob must parse");
802 let two = CompiledGlob::new("/tmp/**/*.txt").expect("glob must parse");
803 let merged = one.merge(two);
804 assert_eq!(merged.ordered_rules.len(), 2);
805 assert_eq!(merged.ordered_rules[0].rule_index, 0);
806 assert_eq!(merged.ordered_rules[1].rule_index, 1);
807 }
808
809 #[test]
810 fn descend_dedup_equivalence_under_merge() {
811 let one = CompiledGlob::new("a/**/**/b").expect("glob must parse");
812 let two = CompiledGlob::new("a/**/b").expect("glob must parse");
813 let merged = one.merge(two);
814 let canonical = CompiledGlob::new("a/**/b").expect("glob must parse");
815 for path in ["a/b", "a/x/b", "a/x/y/b", "a/x/y/c"] {
816 assert_eq!(
817 merged.r#match(path.as_ref()),
818 canonical.r#match(path.as_ref())
819 );
820 }
821 }
822
823 #[test]
824 fn empty_merge_many_is_invalid_input() {
825 let merged = CompiledGlob::merge_many(Vec::new());
826 assert!(matches!(
827 merged,
828 Err(err) if err.kind() == io::ErrorKind::InvalidInput
829 ));
830 }
831
832 #[test]
833 fn supports_exclude_with_last_match_wins() {
834 let include = CompiledGlob::new("/tmp/**/*.txt").expect("glob must parse");
835 let exclude = CompiledGlob::new("!/tmp/**/ignore.txt").expect("glob must parse");
836 let reinclude = CompiledGlob::new("/tmp/**/ignore.txt").expect("glob must parse");
837
838 let merged =
839 CompiledGlob::merge_many(vec![include, exclude, reinclude]).expect("must merge");
840 assert!(merged.r#match("/tmp/a/keep.txt".as_ref()));
841 assert!(merged.r#match("/tmp/a/ignore.txt".as_ref()));
842 }
843
844 #[test]
845 fn exclude_can_remove_previous_include() {
846 let include = CompiledGlob::new("/tmp/**/*.txt").expect("glob must parse");
847 let exclude = CompiledGlob::new("!/tmp/**/ignore.txt").expect("glob must parse");
848 let merged = CompiledGlob::merge_many(vec![include, exclude]).expect("must merge");
849 assert!(merged.r#match("/tmp/a/keep.txt".as_ref()));
850 assert!(!merged.r#match("/tmp/a/ignore.txt".as_ref()));
851 }
852
853 #[test]
854 fn reject_empty_pattern_and_bare_exclude() {
855 assert!(matches!(
856 CompiledGlob::new(""),
857 Err(err) if err.kind() == io::ErrorKind::InvalidInput
858 ));
859 assert!(matches!(
860 CompiledGlob::new("!"),
861 Err(err) if err.kind() == io::ErrorKind::InvalidInput
862 ));
863 }
864
865 #[test]
866 fn start_paths_include_static_prefix_of_include_rules() {
867 let glob = CompiledGlob::new("/tmp/root/**.rs").expect("glob must parse");
868 let starts = glob.start_paths();
869 assert!(starts.iter().any(|p| p == Path::new("/tmp/root")));
870 }
871
872 #[test]
873 fn states_for_path_keeps_descend_capability() {
874 let glob = CompiledGlob::new("/tmp/root/**.rs").expect("glob must parse");
875 let states = glob.states_for_path(Path::new("/tmp/root"));
876 assert!(!states.is_empty());
877 let leaf_states = glob.advance_states(&states, "main.rs");
878 assert!(glob.is_match_state(&leaf_states));
879 }
880
881 #[test]
882 fn suffix_lane_matches_extension_pattern() {
883 let glob = CompiledGlob::new("/tmp/**/*.rs").expect("glob must parse");
884 assert!(glob.r#match("/tmp/a/main.rs".as_ref()));
885 assert!(!glob.r#match("/tmp/a/main.ts".as_ref()));
886 }
887
888 #[test]
889 fn prefix_lane_matches_prefix_pattern() {
890 let glob = CompiledGlob::new("/tmp/foo*/bar.txt").expect("glob must parse");
891 assert!(glob.r#match("/tmp/foobar/bar.txt".as_ref()));
892 assert!(glob.r#match("/tmp/foo/bar.txt".as_ref()));
893 assert!(!glob.r#match("/tmp/barfoo/bar.txt".as_ref()));
894 }
895
896 #[test]
897 fn complex_wildcard_falls_back_to_general_lane() {
898 let glob = CompiledGlob::new("/tmp/a*?b/file.txt").expect("glob must parse");
899 assert!(glob.r#match("/tmp/axxb/file.txt".as_ref()));
900 assert!(!glob.r#match("/tmp/ab/file.txt".as_ref()));
901 }
902}