use std::fmt::{Debug, Display};
use crate::case_sensitivity::{casefold, casefold_char};
#[derive(PartialEq)]
enum GlobToken {
Literal(char),
AnyChar,
AnySequence,
}
#[derive(PartialEq)]
pub struct Glob {
pattern: String,
tokens: Vec<GlobToken>,
case_sensitive: bool,
}
impl Glob {
pub fn compile(pattern: &str) -> Self {
Self::compile_with(pattern, false)
}
pub fn compile_cs(pattern: &str) -> Self {
Self::compile_with(pattern, true)
}
fn compile_with(pattern: &str, case_sensitive: bool) -> Self {
let mut tokens = Vec::new();
let mut chars = pattern.chars();
let literal = |tokens: &mut Vec<GlobToken>, c: char| {
if case_sensitive {
tokens.push(GlobToken::Literal(c));
} else {
tokens.extend(casefold_char(c).map(GlobToken::Literal));
}
};
while let Some(c) = chars.next() {
match c {
'*' => {
if tokens.last() != Some(&GlobToken::AnySequence) {
tokens.push(GlobToken::AnySequence);
}
}
'?' => tokens.push(GlobToken::AnyChar),
'\\' => literal(&mut tokens, chars.next().unwrap_or('\\')),
c => literal(&mut tokens, c),
}
}
Self {
pattern: pattern.to_string(),
tokens,
case_sensitive,
}
}
pub fn pattern(&self) -> &str {
&self.pattern
}
pub fn is_case_sensitive(&self) -> bool {
self.case_sensitive
}
pub fn is_match(&self, input: &str) -> bool {
if self.case_sensitive {
self.matches_stream(input.chars().peekable())
} else {
self.matches_stream(casefold(input).peekable())
}
}
fn matches_stream<I: Iterator<Item = char> + Clone>(
&self,
mut chars: std::iter::Peekable<I>,
) -> bool {
let tokens = &self.tokens;
let mut t = 0;
let mut star = None;
loop {
let progressed = match tokens.get(t) {
Some(GlobToken::AnySequence) => {
t += 1;
star = Some((t, chars.clone()));
true
}
Some(GlobToken::AnyChar) => {
t += 1;
chars.next().is_some()
}
Some(GlobToken::Literal(p)) if chars.peek() == Some(p) => {
t += 1;
chars.next();
true
}
Some(GlobToken::Literal(..)) => false,
None if chars.peek().is_none() => return true,
None => false,
};
if !progressed {
match star.take() {
Some((star_t, mut star_chars)) => {
if star_chars.next().is_none() {
return false;
}
t = star_t;
chars = star_chars.clone();
star = Some((star_t, star_chars));
}
None => return false,
}
}
}
}
}
impl Display for Glob {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"\"{}\"",
self.pattern().replace('\\', "\\\\").replace('"', "\\\"")
)
}
}
impl Debug for Glob {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self)
}
}
#[cfg(feature = "regex")]
pub struct CompiledRegex(regex::Regex);
#[cfg(feature = "regex")]
impl CompiledRegex {
pub fn compile(pattern: &str) -> Result<Self, regex::Error> {
regex::Regex::new(pattern).map(Self)
}
pub fn pattern(&self) -> &str {
self.0.as_str()
}
pub fn is_match(&self, input: &str) -> bool {
self.0.is_match(input)
}
}
#[cfg(feature = "regex")]
impl PartialEq for CompiledRegex {
fn eq(&self, other: &Self) -> bool {
self.pattern() == other.pattern()
}
}
#[cfg(feature = "regex")]
impl Display for CompiledRegex {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"\"{}\"",
self.pattern().replace('\\', "\\\\").replace('"', "\\\"")
)
}
}
#[cfg(feature = "regex")]
impl Debug for CompiledRegex {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self)
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case("feat/*", "feat/login", true)]
#[case("feat/*", "feat/", true)]
#[case("feat/*", "fix/login", false)]
#[case("feat/*", "feat", false)]
#[case("*fix", "hotfix", true)]
#[case("*fix", "fixes", false)]
#[case("*mid*", "amidst", true)]
#[case("*mid*", "mid", true)]
#[case("*mid*", "madam", false)]
#[case("?", "a", true)]
#[case("?", "", false)]
#[case("?", "ab", false)]
#[case("v?.?", "v1.2", true)]
#[case("v?.?", "v1.23", false)]
#[case("*", "", true)]
#[case("*", "anything at all", true)]
#[case("", "", true)]
#[case("", "a", false)]
#[case("a*", "", false)]
#[case("main", "main", true)]
#[case("main", "Main", true)]
#[case("main", "maine", false)]
#[case("main", "remain", false)]
#[case("FEAT/*", "feat/login", true)]
#[case("feat/*", "FEAT/LOGIN", true)]
#[case("*a*ab", "aaab", true)]
#[case("*a*ab", "aaba", false)]
#[case("*ab*ba*", "abba", true)]
#[case("*ab*ba*", "abxba", true)]
#[case("*ab*ba*", "aba", false)]
#[case("a**b", "ab", true)]
#[case("a**b", "axxb", true)]
#[case("*a*a*a*", "aaa", true)]
#[case("*a*a*a*a*", "aaa", false)]
#[case("?", "é", true)]
#[case("??", "hé", true)]
#[case("???", "hé", false)]
#[case("h?llo", "héllo", true)]
#[case("*ö*", "schön", true)]
#[case("über*", "ÜBERMUT", true)]
#[case("grüße*", "GRÜSSE", true)]
#[case("GRÜSSE", "grüße", true)]
#[case("*ss", "groß", true)]
#[case("stra*e", "STRASSE", true)]
#[case("gro?", "groß", false)]
#[case("gro??", "groß", true)]
#[case("λογος", "ΛΟΓΟΣ", true)]
#[case("ΛΟΓΟΣ", "λογος", true)]
#[case("*ς", "ΛΟΓΟΣ", true)]
#[case("*σ", "λογος", true)]
#[case("σ", "ς", true)]
#[case("a\\*b", "a*b", true)]
#[case("a\\*b", "axb", false)]
#[case("a\\?b", "a?b", true)]
#[case("a\\?b", "axb", false)]
#[case("a\\\\b", "a\\b", true)]
#[case("a\\xb", "axb", true)]
#[case("trailing\\", "trailing\\", true)]
fn glob_matching(#[case] pattern: &str, #[case] input: &str, #[case] expected: bool) {
let glob = Glob::compile(pattern);
assert_eq!(
glob.is_match(input),
expected,
"expected '{pattern}' matching '{input}' to be {expected}"
);
}
#[rstest]
#[case("feat/*", "feat/login", true)]
#[case("feat/*", "fix/login", false)]
#[case("v?.?", "v1.2", true)]
#[case("Main", "Main", true)]
#[case("Main", "main", false)]
#[case("FEAT/*", "feat/login", false)]
#[case("*Fix", "hotFix", true)]
#[case("*Fix", "hotfix", false)]
#[case("gro?", "groß", true)]
#[case("groß", "groß", true)]
#[case("gross", "groß", false)]
#[case("stra*e", "STRASSE", false)]
#[case("a\\*b", "a*b", true)]
#[case("a\\*b", "axb", false)]
fn case_sensitive_glob_matching(
#[case] pattern: &str,
#[case] input: &str,
#[case] expected: bool,
) {
let glob = Glob::compile_cs(pattern);
assert_eq!(
glob.is_match(input),
expected,
"expected '{pattern}' matching '{input}' case-sensitively to be {expected}"
);
}
#[test]
fn glob_exposes_its_pattern() {
let glob = Glob::compile("feat/*");
assert_eq!(glob.pattern(), "feat/*");
assert!(!glob.is_case_sensitive());
let glob = Glob::compile_cs("feat/*");
assert_eq!(glob.pattern(), "feat/*");
assert!(glob.is_case_sensitive());
}
#[test]
fn glob_display_quotes_and_escapes() {
let glob = Glob::compile("a\\*\"b");
assert_eq!(glob.to_string(), "\"a\\\\*\\\"b\"");
assert_eq!(format!("{glob:?}"), "\"a\\\\*\\\"b\"");
}
#[test]
fn glob_equality_is_based_on_the_pattern_and_sensitivity() {
assert_eq!(Glob::compile("a*"), Glob::compile("a*"));
assert_ne!(Glob::compile("a*"), Glob::compile("b*"));
assert_ne!(Glob::compile("a*"), Glob::compile_cs("a*"));
}
#[cfg(feature = "regex")]
mod regex_tests {
use super::*;
#[test]
fn regex_compilation_reports_errors() {
assert!(CompiledRegex::compile("(unclosed").is_err());
assert!(CompiledRegex::compile("^release/v\\d+$").is_ok());
}
#[rstest]
#[case("^release/v\\d+(\\.\\d+){2}$", "release/v1.2.3", true)]
#[case("^release/v\\d+(\\.\\d+){2}$", "release/v1.2", false)]
#[case("ell", "hello", true)] #[case("(?i)hello", "HELLO", true)]
#[case("hello", "HELLO", false)] fn regex_matching(#[case] pattern: &str, #[case] input: &str, #[case] expected: bool) {
let regex = CompiledRegex::compile(pattern).expect("compile the pattern");
assert_eq!(regex.is_match(input), expected);
}
#[test]
fn regex_exposes_its_pattern() {
let regex = CompiledRegex::compile("^a$").expect("compile the pattern");
assert_eq!(regex.pattern(), "^a$");
}
#[test]
fn regex_equality_is_based_on_the_pattern() {
let a = CompiledRegex::compile("^a$").expect("compile the pattern");
let b = CompiledRegex::compile("^a$").expect("compile the pattern");
let c = CompiledRegex::compile("^c$").expect("compile the pattern");
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn regex_display_quotes_and_escapes() {
let regex = CompiledRegex::compile("^v\\d+$").expect("compile the pattern");
assert_eq!(regex.to_string(), "\"^v\\\\d+$\"");
assert_eq!(format!("{regex:?}"), "\"^v\\\\d+$\"");
}
}
}