use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Tri {
Yes,
No,
Unknown,
}
impl Tri {
pub fn is_yes(self) -> bool {
self == Self::Yes
}
pub fn is_no(self) -> bool {
self == Self::No
}
pub fn is_unknown(self) -> bool {
self == Self::Unknown
}
pub fn and(self, other: Self) -> Self {
match (self, other) {
(Self::No, _) | (_, Self::No) => Self::No,
(Self::Unknown, _) | (_, Self::Unknown) => Self::Unknown,
_ => Self::Yes,
}
}
pub fn or(self, other: Self) -> Self {
match (self, other) {
(Self::Yes, _) | (_, Self::Yes) => Self::Yes,
(Self::Unknown, _) | (_, Self::Unknown) => Self::Unknown,
_ => Self::No,
}
}
pub fn all(items: impl IntoIterator<Item = Self>) -> Self {
items.into_iter().fold(Self::Yes, Self::and)
}
pub fn any(items: impl IntoIterator<Item = Self>) -> Self {
items.into_iter().fold(Self::No, Self::or)
}
}
impl fmt::Display for Tri {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Yes => "yes",
Self::No => "no",
Self::Unknown => "undecidable",
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Seg {
Literal(String),
Single,
Recursive,
Opaque(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Pattern {
segments: Vec<Seg>,
decidable: bool,
}
impl Pattern {
pub fn parse(resource: &str) -> Self {
if resource.is_empty() {
return Self {
segments: Vec::new(),
decidable: true,
};
}
let trimmed = resource.strip_suffix('/').unwrap_or(resource);
let raw: Vec<&str> = trimmed.split('/').collect();
let last = raw.len().saturating_sub(1);
let mut decidable = true;
let segments = raw
.into_iter()
.enumerate()
.map(|(i, s)| match s {
"*" => Seg::Single,
"**" if i == last => Seg::Recursive,
"**" => {
decidable = false;
Seg::Opaque(s.to_string())
}
other if other.contains('{') || other.contains('}') => {
decidable = false;
Seg::Opaque(other.to_string())
}
other if other.contains('*') => {
decidable = false;
Seg::Opaque(other.to_string())
}
other => Seg::Literal(other.to_string()),
})
.collect();
Self {
segments,
decidable,
}
}
pub(crate) fn segments(&self) -> &[Seg] {
&self.segments
}
pub fn is_decidable(&self) -> bool {
self.decidable
}
pub fn undecidable_reason(&self) -> Option<Undecidable> {
self.segments.iter().find_map(|s| match s {
Seg::Opaque(raw) if raw.contains('{') || raw.contains('}') => {
Some(Undecidable::Template {
segment: raw.clone(),
})
}
Seg::Opaque(raw) if raw == "**" => Some(Undecidable::NonFinalRecursive),
Seg::Opaque(raw) => Some(Undecidable::PartialGlob {
segment: raw.clone(),
}),
_ => None,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Undecidable {
Template {
segment: String,
},
PartialGlob {
segment: String,
},
NonFinalRecursive,
}
impl fmt::Display for Undecidable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Template { segment } => write!(
f,
"`{segment}` is a template, which stays symbolic until the runtime resolves and clamps it"
),
Self::PartialGlob { segment } => write!(
f,
"`{segment}` globs within a segment; the solver decides whole-segment `*` and trailing `**` only"
),
Self::NonFinalRecursive => f.write_str(
"`**` appears before the end of the pattern; it is decided only in final position",
),
}
}
}
impl Pattern {
pub fn canonical(&self) -> String {
if self.segments.is_empty() {
return String::new();
}
self.segments
.iter()
.map(|s| match s {
Seg::Literal(l) => l.as_str(),
Seg::Single => "*",
Seg::Recursive => "**",
Seg::Opaque(raw) => raw.as_str(),
})
.collect::<Vec<_>>()
.join("/")
}
}
impl fmt::Display for Pattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.canonical())
}
}
pub fn covers(outer: &str, inner: &str) -> Tri {
if outer == inner {
return Tri::Yes;
}
let o = Pattern::parse(outer);
let i = Pattern::parse(inner);
if o == i {
return Tri::Yes;
}
if !o.decidable || !i.decidable {
return Tri::Unknown;
}
covers_segments(&o.segments, &i.segments)
}
fn covers_segments(outer: &[Seg], inner: &[Seg]) -> Tri {
match (outer.first(), inner.first()) {
(None, None) => Tri::Yes,
(Some(Seg::Recursive), _) => Tri::Yes,
(None, Some(_)) | (Some(_), None) => Tri::No,
(Some(Seg::Single), Some(inner_seg)) => match inner_seg {
Seg::Recursive => Tri::No,
Seg::Opaque(_) => Tri::Unknown,
_ => covers_segments(&outer[1..], &inner[1..]),
},
(Some(Seg::Literal(a)), Some(inner_seg)) => match inner_seg {
Seg::Literal(b) if a == b => covers_segments(&outer[1..], &inner[1..]),
Seg::Literal(_) | Seg::Single | Seg::Recursive => Tri::No,
Seg::Opaque(_) => Tri::Unknown,
},
(Some(Seg::Opaque(_)), _) => Tri::Unknown,
}
}
pub fn overlaps(a: &str, b: &str) -> Tri {
if a == b {
return Tri::Yes;
}
let pa = Pattern::parse(a);
let pb = Pattern::parse(b);
if pa == pb {
return Tri::Yes;
}
if !pa.decidable || !pb.decidable {
return Tri::Unknown;
}
overlaps_segments(&pa.segments, &pb.segments)
}
pub fn intersect(a: &str, b: &str) -> Option<String> {
let pa = Pattern::parse(a);
let pb = Pattern::parse(b);
if pa == pb {
return Some(pa.canonical());
}
if !pa.decidable || !pb.decidable {
return None;
}
intersect_segments(pa.segments(), pb.segments()).map(|segs| {
Pattern {
segments: segs,
decidable: true,
}
.canonical()
})
}
fn intersect_segments(a: &[Seg], b: &[Seg]) -> Option<Vec<Seg>> {
match (a.first(), b.first()) {
(None, None) => Some(Vec::new()),
(Some(Seg::Recursive), _) => Some(b.to_vec()),
(_, Some(Seg::Recursive)) => Some(a.to_vec()),
(None, Some(_)) | (Some(_), None) => None,
(Some(Seg::Opaque(_)), _) | (_, Some(Seg::Opaque(_))) => None,
(Some(head_a), Some(head_b)) => {
let head = match (head_a, head_b) {
(Seg::Literal(x), Seg::Literal(y)) if x == y => Seg::Literal(x.clone()),
(Seg::Literal(_), Seg::Literal(_)) => return None,
(Seg::Literal(x), Seg::Single) | (Seg::Single, Seg::Literal(x)) => {
Seg::Literal(x.clone())
}
(Seg::Single, Seg::Single) => Seg::Single,
_ => return None,
};
let mut rest = intersect_segments(&a[1..], &b[1..])?;
rest.insert(0, head);
Some(rest)
}
}
}
fn overlaps_segments(a: &[Seg], b: &[Seg]) -> Tri {
match (a.first(), b.first()) {
(None, None) => Tri::Yes,
(Some(Seg::Recursive), _) | (_, Some(Seg::Recursive)) => Tri::Yes,
(None, Some(_)) | (Some(_), None) => Tri::No,
(Some(Seg::Opaque(_)), _) | (_, Some(Seg::Opaque(_))) => Tri::Unknown,
(Some(Seg::Single), Some(_)) | (Some(_), Some(Seg::Single)) => {
overlaps_segments(&a[1..], &b[1..])
}
(Some(Seg::Literal(x)), Some(Seg::Literal(y))) => {
if x == y {
overlaps_segments(&a[1..], &b[1..])
} else {
Tri::No
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tri_conjunction_lets_a_definite_no_win() {
assert_eq!(Tri::No.and(Tri::Unknown), Tri::No);
assert_eq!(Tri::Unknown.and(Tri::Yes), Tri::Unknown);
assert_eq!(Tri::Yes.and(Tri::Yes), Tri::Yes);
}
#[test]
fn tri_disjunction_lets_a_definite_yes_win() {
assert_eq!(Tri::Yes.or(Tri::Unknown), Tri::Yes);
assert_eq!(Tri::Unknown.or(Tri::No), Tri::Unknown);
assert_eq!(Tri::No.or(Tri::No), Tri::No);
}
#[test]
fn unknown_never_reads_as_true() {
assert!(!Tri::Unknown.is_yes());
assert!(!Tri::Unknown.is_no());
}
#[test]
fn a_pattern_covers_itself() {
for p in ["/src/**", "/src", "*", "{path}", "*.rs", "", "a/**/b"] {
assert_eq!(covers(p, p), Tri::Yes, "{p} must cover itself");
}
}
#[test]
fn recursive_glob_covers_everything_beneath_it() {
assert_eq!(covers("/src/**", "/src/lib/a.rs"), Tri::Yes);
assert_eq!(covers("/src/**", "/src/lib"), Tri::Yes);
assert_eq!(covers("/src/**", "/src"), Tri::Yes);
assert_eq!(covers("/src/**", "/src/*"), Tri::Yes);
}
#[test]
fn a_recursive_glob_is_not_covered_by_anything_narrower() {
assert_eq!(covers("/src", "/src/**"), Tri::No);
assert_eq!(covers("/src/*", "/src/**"), Tri::No);
}
#[test]
fn disjoint_prefixes_do_not_cover_each_other() {
assert_eq!(covers("/src/**", "/etc/passwd"), Tri::No);
assert_eq!(covers("/etc/passwd", "/src/**"), Tri::No);
}
#[test]
fn single_glob_matches_exactly_one_segment() {
assert_eq!(covers("/src/*", "/src/lib"), Tri::Yes);
assert_eq!(covers("/src/*", "/src/lib/a.rs"), Tri::No);
assert_eq!(covers("/src/*", "/src"), Tri::No);
}
#[test]
fn a_trailing_separator_does_not_change_meaning() {
assert_eq!(covers("/src/", "/src"), Tri::Yes);
assert_eq!(covers("/src", "/src/"), Tri::Yes);
}
#[test]
fn non_path_resources_compare_as_single_segments() {
assert_eq!(
covers("api.example.com:443", "api.example.com:443"),
Tri::Yes
);
assert_eq!(
covers("api.example.com:443", "evil.example.com:443"),
Tri::No
);
assert_eq!(covers("*", "api.example.com:443"), Tri::Yes);
}
#[test]
fn bare_resources_only_cover_each_other() {
assert_eq!(covers("", ""), Tri::Yes);
assert_eq!(covers("", "/src"), Tri::No);
assert_eq!(covers("/src", ""), Tri::No);
}
#[test]
fn distinct_undecidable_patterns_are_never_treated_as_equal() {
assert_eq!(covers("/src/*.rs", "/src/*.py"), Tri::Unknown);
assert_eq!(covers("/src/*.py", "/src/*.rs"), Tri::Unknown);
assert_eq!(covers("{path}", "{other}"), Tri::Unknown);
assert_eq!(overlaps("/src/*.rs", "/src/*.py"), Tri::Unknown);
assert_ne!(
Pattern::parse("/src/*.rs"),
Pattern::parse("/src/*.py"),
"opaque segments must carry their text"
);
}
#[test]
fn undecidable_patterns_report_unknown_rather_than_guessing() {
assert_eq!(covers("/src/*.rs", "/src/main.rs"), Tri::Unknown);
assert_eq!(covers("/src/**", "{path}"), Tri::Unknown);
assert_eq!(covers("{path}", "/src/a"), Tri::Unknown);
assert_eq!(covers("/a/**/b", "/a/x/b"), Tri::Unknown);
}
#[test]
fn identical_and_nested_patterns_overlap() {
assert_eq!(overlaps("/src/**", "/src/**"), Tri::Yes);
assert_eq!(overlaps("/src/**", "/src/lib/a.rs"), Tri::Yes);
assert_eq!(overlaps("/src/lib/a.rs", "/src/**"), Tri::Yes);
}
#[test]
fn sibling_paths_do_not_overlap() {
assert_eq!(overlaps("/src/**", "/build/**"), Tri::No);
assert_eq!(overlaps("/src/a", "/src/b"), Tri::No);
}
#[test]
fn overlap_is_symmetric() {
let cases = [
("/src/**", "/src/lib"),
("/src/**", "/build/**"),
("/src/*", "/src/a"),
("/a/b", "/a/b/c"),
];
for (x, y) in cases {
assert_eq!(overlaps(x, y), overlaps(y, x), "{x} vs {y}");
}
}
#[test]
fn different_depths_do_not_overlap_without_a_recursive_glob() {
assert_eq!(overlaps("/a/b", "/a/b/c"), Tri::No);
assert_eq!(overlaps("/a/*", "/a/b/c"), Tri::No);
}
#[test]
fn overlap_is_unknown_for_undecidable_patterns() {
assert_eq!(overlaps("/src/*.rs", "/src/main.rs"), Tri::Unknown);
assert_eq!(overlaps("{path}", "/src/a"), Tri::Unknown);
}
#[test]
fn covering_implies_overlapping() {
let pairs = [
("/src/**", "/src/lib/a.rs"),
("/src/*", "/src/lib"),
("/src", "/src"),
];
for (outer, inner) in pairs {
assert_eq!(covers(outer, inner), Tri::Yes);
assert_eq!(
overlaps(outer, inner),
Tri::Yes,
"if {outer} covers {inner} they must overlap"
);
}
}
}