use alloc::borrow::ToOwned;
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PatternError {
Empty,
Negation,
CharacterRange,
}
impl fmt::Display for PatternError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::Empty => "empty pattern",
Self::Negation => "`!` negation is not supported in CODEOWNERS",
Self::CharacterRange => "`[ ]` character ranges are not supported in CODEOWNERS",
};
f.write_str(s)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Segment {
AnyDepth,
Literal(String),
Glob(String),
}
impl Segment {
fn new(s: &str) -> Self {
if s == "**" {
Self::AnyDepth
} else if s.contains('*') || s.contains('?') {
Self::Glob(s.to_owned())
} else {
Self::Literal(s.to_owned())
}
}
fn matches(&self, text: &str) -> bool {
match self {
Self::AnyDepth => true,
Self::Literal(l) => l == text,
Self::Glob(g) => glob_match(g, text),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Pattern {
raw: String,
segments: Vec<Segment>,
anchored: bool,
dir_only: bool,
trailing_literal: bool,
}
impl Pattern {
pub fn new(raw: &str) -> Result<Self, PatternError> {
if raw.is_empty() {
return Err(PatternError::Empty);
}
if raw.starts_with('!') {
return Err(PatternError::Negation);
}
if raw.contains('[') || raw.contains(']') {
return Err(PatternError::CharacterRange);
}
let body = raw.strip_suffix('/').unwrap_or(raw);
let anchored = body.contains('/');
let dir_only = raw.ends_with('/');
let segments: Vec<Segment> = body
.trim_start_matches('/')
.split('/')
.filter(|s| !s.is_empty())
.map(Segment::new)
.collect();
if segments.is_empty() {
return Err(PatternError::Empty);
}
let trailing_literal = matches!(segments.last(), Some(Segment::Literal(_)));
Ok(Self {
raw: raw.to_owned(),
segments,
anchored,
dir_only,
trailing_literal,
})
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.raw
}
#[must_use]
pub fn matches(&self, path: &str) -> bool {
let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
if parts.is_empty() {
return false;
}
if self.anchored {
self.matches_at(0, &parts, 0)
} else {
(0..parts.len()).any(|start| self.matches_at(0, &parts, start))
}
}
fn matches_at(&self, pi: usize, parts: &[&str], si: usize) -> bool {
if pi == self.segments.len() {
return self.terminal_ok(si, parts.len());
}
match &self.segments[pi] {
Segment::AnyDepth => (si..=parts.len()).any(|k| self.matches_at(pi + 1, parts, k)),
seg => {
si < parts.len() && seg.matches(parts[si]) && self.matches_at(pi + 1, parts, si + 1)
}
}
}
fn terminal_ok(&self, si: usize, len: usize) -> bool {
if self.dir_only {
si < len
} else if si == len {
true
} else {
self.trailing_literal
}
}
}
fn glob_match(pattern: &str, text: &str) -> bool {
let p: Vec<char> = pattern.chars().collect();
let t: Vec<char> = text.chars().collect();
let (mut pi, mut ti) = (0usize, 0usize);
let (mut star, mut backtrack) = (None, 0usize);
while ti < t.len() {
if pi < p.len() && (p[pi] == '?' || p[pi] == t[ti]) {
pi += 1;
ti += 1;
} else if pi < p.len() && p[pi] == '*' {
star = Some(pi);
backtrack = ti;
pi += 1;
} else if let Some(s) = star {
pi = s + 1;
backtrack += 1;
ti = backtrack;
} else {
return false;
}
}
p[pi..].iter().all(|&c| c == '*')
}