1use std::fmt;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37pub enum Tri {
38 Yes,
40 No,
42 Unknown,
44}
45
46impl Tri {
47 pub fn is_yes(self) -> bool {
49 self == Self::Yes
50 }
51
52 pub fn is_no(self) -> bool {
54 self == Self::No
55 }
56
57 pub fn is_unknown(self) -> bool {
59 self == Self::Unknown
60 }
61
62 pub fn and(self, other: Self) -> Self {
67 match (self, other) {
68 (Self::No, _) | (_, Self::No) => Self::No,
69 (Self::Unknown, _) | (_, Self::Unknown) => Self::Unknown,
70 _ => Self::Yes,
71 }
72 }
73
74 pub fn or(self, other: Self) -> Self {
76 match (self, other) {
77 (Self::Yes, _) | (_, Self::Yes) => Self::Yes,
78 (Self::Unknown, _) | (_, Self::Unknown) => Self::Unknown,
79 _ => Self::No,
80 }
81 }
82
83 pub fn all(items: impl IntoIterator<Item = Self>) -> Self {
85 items.into_iter().fold(Self::Yes, Self::and)
86 }
87
88 pub fn any(items: impl IntoIterator<Item = Self>) -> Self {
90 items.into_iter().fold(Self::No, Self::or)
91 }
92}
93
94impl fmt::Display for Tri {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 f.write_str(match self {
97 Self::Yes => "yes",
98 Self::No => "no",
99 Self::Unknown => "undecidable",
100 })
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
106pub(crate) enum Seg {
107 Literal(String),
109 Single,
111 Recursive,
113 Opaque(String),
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
125pub struct Pattern {
126 segments: Vec<Seg>,
127 decidable: bool,
128}
129
130impl Pattern {
131 pub fn parse(resource: &str) -> Self {
136 if resource.is_empty() {
138 return Self {
139 segments: Vec::new(),
140 decidable: true,
141 };
142 }
143
144 let trimmed = resource.strip_suffix('/').unwrap_or(resource);
147 let raw: Vec<&str> = trimmed.split('/').collect();
148 let last = raw.len().saturating_sub(1);
149
150 let mut decidable = true;
151 let segments = raw
152 .into_iter()
153 .enumerate()
154 .map(|(i, s)| match s {
155 "*" => Seg::Single,
156 "**" if i == last => Seg::Recursive,
159 "**" => {
160 decidable = false;
161 Seg::Opaque(s.to_string())
162 }
163 other if other.contains('{') || other.contains('}') => {
164 decidable = false;
165 Seg::Opaque(other.to_string())
166 }
167 other if other.contains('*') => {
168 decidable = false;
170 Seg::Opaque(other.to_string())
171 }
172 other => Seg::Literal(other.to_string()),
173 })
174 .collect();
175
176 Self {
177 segments,
178 decidable,
179 }
180 }
181
182 pub(crate) fn segments(&self) -> &[Seg] {
184 &self.segments
185 }
186
187 pub fn is_decidable(&self) -> bool {
189 self.decidable
190 }
191
192 pub fn undecidable_reason(&self) -> Option<Undecidable> {
198 self.segments.iter().find_map(|s| match s {
199 Seg::Opaque(raw) if raw.contains('{') || raw.contains('}') => {
200 Some(Undecidable::Template {
201 segment: raw.clone(),
202 })
203 }
204 Seg::Opaque(raw) if raw == "**" => Some(Undecidable::NonFinalRecursive),
205 Seg::Opaque(raw) => Some(Undecidable::PartialGlob {
206 segment: raw.clone(),
207 }),
208 _ => None,
209 })
210 }
211}
212
213#[derive(Debug, Clone, PartialEq, Eq)]
215pub enum Undecidable {
216 Template {
218 segment: String,
220 },
221 PartialGlob {
223 segment: String,
225 },
226 NonFinalRecursive,
228}
229
230impl fmt::Display for Undecidable {
231 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232 match self {
233 Self::Template { segment } => write!(
234 f,
235 "`{segment}` is a template, which stays symbolic until the runtime resolves and clamps it"
236 ),
237 Self::PartialGlob { segment } => write!(
238 f,
239 "`{segment}` globs within a segment; the solver decides whole-segment `*` and trailing `**` only"
240 ),
241 Self::NonFinalRecursive => f.write_str(
242 "`**` appears before the end of the pattern; it is decided only in final position",
243 ),
244 }
245 }
246}
247
248impl Pattern {
249 pub fn canonical(&self) -> String {
252 if self.segments.is_empty() {
253 return String::new();
254 }
255 self.segments
256 .iter()
257 .map(|s| match s {
258 Seg::Literal(l) => l.as_str(),
259 Seg::Single => "*",
260 Seg::Recursive => "**",
261 Seg::Opaque(raw) => raw.as_str(),
262 })
263 .collect::<Vec<_>>()
264 .join("/")
265 }
266}
267
268impl fmt::Display for Pattern {
269 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270 f.write_str(&self.canonical())
271 }
272}
273
274pub fn covers(outer: &str, inner: &str) -> Tri {
279 if outer == inner {
282 return Tri::Yes;
283 }
284
285 let o = Pattern::parse(outer);
286 let i = Pattern::parse(inner);
287
288 if o == i {
290 return Tri::Yes;
291 }
292 if !o.decidable || !i.decidable {
293 return Tri::Unknown;
294 }
295
296 covers_segments(&o.segments, &i.segments)
297}
298
299fn covers_segments(outer: &[Seg], inner: &[Seg]) -> Tri {
300 match (outer.first(), inner.first()) {
301 (None, None) => Tri::Yes,
303 (Some(Seg::Recursive), _) => Tri::Yes,
305 (None, Some(_)) | (Some(_), None) => Tri::No,
307
308 (Some(Seg::Single), Some(inner_seg)) => match inner_seg {
309 Seg::Recursive => Tri::No,
311 Seg::Opaque(_) => Tri::Unknown,
312 _ => covers_segments(&outer[1..], &inner[1..]),
313 },
314
315 (Some(Seg::Literal(a)), Some(inner_seg)) => match inner_seg {
316 Seg::Literal(b) if a == b => covers_segments(&outer[1..], &inner[1..]),
317 Seg::Literal(_) | Seg::Single | Seg::Recursive => Tri::No,
319 Seg::Opaque(_) => Tri::Unknown,
320 },
321
322 (Some(Seg::Opaque(_)), _) => Tri::Unknown,
323 }
324}
325
326pub fn overlaps(a: &str, b: &str) -> Tri {
331 if a == b {
332 return Tri::Yes;
333 }
334
335 let pa = Pattern::parse(a);
336 let pb = Pattern::parse(b);
337
338 if pa == pb {
339 return Tri::Yes;
340 }
341 if !pa.decidable || !pb.decidable {
342 return Tri::Unknown;
343 }
344
345 overlaps_segments(&pa.segments, &pb.segments)
346}
347
348pub fn intersect(a: &str, b: &str) -> Option<String> {
360 let pa = Pattern::parse(a);
361 let pb = Pattern::parse(b);
362
363 if pa == pb {
365 return Some(pa.canonical());
366 }
367 if !pa.decidable || !pb.decidable {
368 return None;
369 }
370
371 intersect_segments(pa.segments(), pb.segments()).map(|segs| {
372 Pattern {
373 segments: segs,
374 decidable: true,
375 }
376 .canonical()
377 })
378}
379
380fn intersect_segments(a: &[Seg], b: &[Seg]) -> Option<Vec<Seg>> {
381 match (a.first(), b.first()) {
382 (None, None) => Some(Vec::new()),
383
384 (Some(Seg::Recursive), _) => Some(b.to_vec()),
388 (_, Some(Seg::Recursive)) => Some(a.to_vec()),
389
390 (None, Some(_)) | (Some(_), None) => None,
392
393 (Some(Seg::Opaque(_)), _) | (_, Some(Seg::Opaque(_))) => None,
394
395 (Some(head_a), Some(head_b)) => {
396 let head = match (head_a, head_b) {
397 (Seg::Literal(x), Seg::Literal(y)) if x == y => Seg::Literal(x.clone()),
398 (Seg::Literal(_), Seg::Literal(_)) => return None,
399 (Seg::Literal(x), Seg::Single) | (Seg::Single, Seg::Literal(x)) => {
401 Seg::Literal(x.clone())
402 }
403 (Seg::Single, Seg::Single) => Seg::Single,
404 _ => return None,
405 };
406 let mut rest = intersect_segments(&a[1..], &b[1..])?;
407 rest.insert(0, head);
408 Some(rest)
409 }
410 }
411}
412
413fn overlaps_segments(a: &[Seg], b: &[Seg]) -> Tri {
414 match (a.first(), b.first()) {
415 (None, None) => Tri::Yes,
416 (Some(Seg::Recursive), _) | (_, Some(Seg::Recursive)) => Tri::Yes,
418 (None, Some(_)) | (Some(_), None) => Tri::No,
420
421 (Some(Seg::Opaque(_)), _) | (_, Some(Seg::Opaque(_))) => Tri::Unknown,
422
423 (Some(Seg::Single), Some(_)) | (Some(_), Some(Seg::Single)) => {
425 overlaps_segments(&a[1..], &b[1..])
426 }
427
428 (Some(Seg::Literal(x)), Some(Seg::Literal(y))) => {
429 if x == y {
430 overlaps_segments(&a[1..], &b[1..])
431 } else {
432 Tri::No
433 }
434 }
435 }
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441
442 #[test]
443 fn tri_conjunction_lets_a_definite_no_win() {
444 assert_eq!(Tri::No.and(Tri::Unknown), Tri::No);
445 assert_eq!(Tri::Unknown.and(Tri::Yes), Tri::Unknown);
446 assert_eq!(Tri::Yes.and(Tri::Yes), Tri::Yes);
447 }
448
449 #[test]
450 fn tri_disjunction_lets_a_definite_yes_win() {
451 assert_eq!(Tri::Yes.or(Tri::Unknown), Tri::Yes);
452 assert_eq!(Tri::Unknown.or(Tri::No), Tri::Unknown);
453 assert_eq!(Tri::No.or(Tri::No), Tri::No);
454 }
455
456 #[test]
457 fn unknown_never_reads_as_true() {
458 assert!(!Tri::Unknown.is_yes());
459 assert!(!Tri::Unknown.is_no());
460 }
461
462 #[test]
465 fn a_pattern_covers_itself() {
466 for p in ["/src/**", "/src", "*", "{path}", "*.rs", "", "a/**/b"] {
467 assert_eq!(covers(p, p), Tri::Yes, "{p} must cover itself");
468 }
469 }
470
471 #[test]
472 fn recursive_glob_covers_everything_beneath_it() {
473 assert_eq!(covers("/src/**", "/src/lib/a.rs"), Tri::Yes);
474 assert_eq!(covers("/src/**", "/src/lib"), Tri::Yes);
475 assert_eq!(covers("/src/**", "/src"), Tri::Yes);
477 assert_eq!(covers("/src/**", "/src/*"), Tri::Yes);
478 }
479
480 #[test]
481 fn a_recursive_glob_is_not_covered_by_anything_narrower() {
482 assert_eq!(covers("/src", "/src/**"), Tri::No);
483 assert_eq!(covers("/src/*", "/src/**"), Tri::No);
484 }
485
486 #[test]
487 fn disjoint_prefixes_do_not_cover_each_other() {
488 assert_eq!(covers("/src/**", "/etc/passwd"), Tri::No);
489 assert_eq!(covers("/etc/passwd", "/src/**"), Tri::No);
490 }
491
492 #[test]
493 fn single_glob_matches_exactly_one_segment() {
494 assert_eq!(covers("/src/*", "/src/lib"), Tri::Yes);
495 assert_eq!(covers("/src/*", "/src/lib/a.rs"), Tri::No);
496 assert_eq!(covers("/src/*", "/src"), Tri::No);
497 }
498
499 #[test]
500 fn a_trailing_separator_does_not_change_meaning() {
501 assert_eq!(covers("/src/", "/src"), Tri::Yes);
502 assert_eq!(covers("/src", "/src/"), Tri::Yes);
503 }
504
505 #[test]
506 fn non_path_resources_compare_as_single_segments() {
507 assert_eq!(
508 covers("api.example.com:443", "api.example.com:443"),
509 Tri::Yes
510 );
511 assert_eq!(
512 covers("api.example.com:443", "evil.example.com:443"),
513 Tri::No
514 );
515 assert_eq!(covers("*", "api.example.com:443"), Tri::Yes);
516 }
517
518 #[test]
519 fn bare_resources_only_cover_each_other() {
520 assert_eq!(covers("", ""), Tri::Yes);
521 assert_eq!(covers("", "/src"), Tri::No);
522 assert_eq!(covers("/src", ""), Tri::No);
523 }
524
525 #[test]
531 fn distinct_undecidable_patterns_are_never_treated_as_equal() {
532 assert_eq!(covers("/src/*.rs", "/src/*.py"), Tri::Unknown);
533 assert_eq!(covers("/src/*.py", "/src/*.rs"), Tri::Unknown);
534 assert_eq!(covers("{path}", "{other}"), Tri::Unknown);
535 assert_eq!(overlaps("/src/*.rs", "/src/*.py"), Tri::Unknown);
536
537 assert_ne!(
538 Pattern::parse("/src/*.rs"),
539 Pattern::parse("/src/*.py"),
540 "opaque segments must carry their text"
541 );
542 }
543
544 #[test]
545 fn undecidable_patterns_report_unknown_rather_than_guessing() {
546 assert_eq!(covers("/src/*.rs", "/src/main.rs"), Tri::Unknown);
548 assert_eq!(covers("/src/**", "{path}"), Tri::Unknown);
550 assert_eq!(covers("{path}", "/src/a"), Tri::Unknown);
551 assert_eq!(covers("/a/**/b", "/a/x/b"), Tri::Unknown);
553 }
554
555 #[test]
558 fn identical_and_nested_patterns_overlap() {
559 assert_eq!(overlaps("/src/**", "/src/**"), Tri::Yes);
560 assert_eq!(overlaps("/src/**", "/src/lib/a.rs"), Tri::Yes);
561 assert_eq!(overlaps("/src/lib/a.rs", "/src/**"), Tri::Yes);
562 }
563
564 #[test]
565 fn sibling_paths_do_not_overlap() {
566 assert_eq!(overlaps("/src/**", "/build/**"), Tri::No);
567 assert_eq!(overlaps("/src/a", "/src/b"), Tri::No);
568 }
569
570 #[test]
571 fn overlap_is_symmetric() {
572 let cases = [
573 ("/src/**", "/src/lib"),
574 ("/src/**", "/build/**"),
575 ("/src/*", "/src/a"),
576 ("/a/b", "/a/b/c"),
577 ];
578 for (x, y) in cases {
579 assert_eq!(overlaps(x, y), overlaps(y, x), "{x} vs {y}");
580 }
581 }
582
583 #[test]
584 fn different_depths_do_not_overlap_without_a_recursive_glob() {
585 assert_eq!(overlaps("/a/b", "/a/b/c"), Tri::No);
586 assert_eq!(overlaps("/a/*", "/a/b/c"), Tri::No);
587 }
588
589 #[test]
590 fn overlap_is_unknown_for_undecidable_patterns() {
591 assert_eq!(overlaps("/src/*.rs", "/src/main.rs"), Tri::Unknown);
592 assert_eq!(overlaps("{path}", "/src/a"), Tri::Unknown);
593 }
594
595 #[test]
596 fn covering_implies_overlapping() {
597 let pairs = [
598 ("/src/**", "/src/lib/a.rs"),
599 ("/src/*", "/src/lib"),
600 ("/src", "/src"),
601 ];
602 for (outer, inner) in pairs {
603 assert_eq!(covers(outer, inner), Tri::Yes);
604 assert_eq!(
605 overlaps(outer, inner),
606 Tri::Yes,
607 "if {outer} covers {inner} they must overlap"
608 );
609 }
610 }
611}