use alloc::{
borrow::Cow,
string::{String, ToString},
vec::Vec,
};
use core::fmt::Write;
#[cfg(feature = "std")]
use std::path::is_separator;
use miden_debug_types::Uri;
#[cfg(not(feature = "std"))]
fn is_separator(c: char) -> bool {
matches!(c, '/')
}
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
#[error("{}", self.format_error())]
pub struct Error {
glob: Option<String>,
kind: ErrorKind,
}
impl Error {
pub fn glob(&self) -> Option<&str> {
self.glob.as_deref()
}
pub fn kind(&self) -> &ErrorKind {
&self.kind
}
fn format_error(&self) -> String {
if let Some(glob) = self.glob() {
format!("error parsing glob '{glob}': {}", self.kind)
} else {
format!("{}", self.kind)
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum ErrorKind {
#[error("unclosed character class; missing ']'")]
UnclosedClass,
#[error("unclosed character range")]
InvalidRange(char, char),
#[error("unopened alternate group; missing '{{' (maybe escape '}}' with '[}}]'?)")]
UnopenedAlternates,
#[error("unclosed alternate group; missing '}}' (maybe escape '{{' with '[{{]'?)")]
UnclosedAlternates,
#[error("dangling '\\'")]
DanglingEscape,
#[error("{0}")]
Regex(String),
}
#[derive(Clone, Eq)]
pub struct Glob {
glob: String,
re: String,
opts: GlobOptions,
tokens: Tokens,
}
impl AsRef<Glob> for Glob {
fn as_ref(&self) -> &Glob {
self
}
}
impl PartialEq for Glob {
fn eq(&self, other: &Glob) -> bool {
self.glob == other.glob && self.opts == other.opts
}
}
#[cfg(feature = "std")]
impl std::hash::Hash for Glob {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.glob.hash(state);
self.opts.hash(state);
}
}
impl core::fmt::Debug for Glob {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
if f.alternate() {
f.debug_struct("Glob")
.field("glob", &self.glob)
.field("re", &self.re)
.field("opts", &self.opts)
.field("tokens", &self.tokens)
.finish()
} else {
f.debug_tuple("Glob").field(&self.glob).finish()
}
}
}
impl core::fmt::Display for Glob {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
self.glob.fmt(f)
}
}
impl core::str::FromStr for Glob {
type Err = Error;
fn from_str(glob: &str) -> Result<Self, Self::Err> {
Self::new(glob)
}
}
#[derive(Clone, Debug)]
pub struct GlobMatcher {
pat: Glob,
re: regex::bytes::Regex,
}
impl Eq for GlobMatcher {}
impl PartialEq for GlobMatcher {
fn eq(&self, other: &Self) -> bool {
self.pat == other.pat
}
}
impl GlobMatcher {
pub fn is_match(&self, path: &Uri) -> bool {
self.is_match_candidate(&Candidate::new(path))
}
pub fn is_match_candidate(&self, path: &Candidate<'_>) -> bool {
self.re.is_match(path.path.as_bytes())
}
pub fn glob(&self) -> &Glob {
&self.pat
}
}
#[derive(Debug, Clone)]
pub struct Candidate<'a> {
path: Cow<'a, str>,
}
impl<'a> Candidate<'a> {
pub fn new(uri: &'a Uri) -> Candidate<'a> {
let path = normalize_path(uri);
Candidate { path }
}
}
#[cfg(unix)]
pub(crate) fn normalize_path(uri: &Uri) -> Cow<'_, str> {
Cow::Borrowed(match uri.scheme() {
Some("file") => uri.as_str().strip_prefix("file://").unwrap(),
Some("stdin") => match uri.as_str().strip_prefix("stdin://").unwrap() {
"" => "stdin",
other => other,
},
Some(_) => uri.as_str().split_once("://").unwrap().1,
None => uri.as_str(),
})
}
#[cfg(not(unix))]
pub(crate) fn normalize_path(uri: &Uri) -> Cow<'_, str> {
let path = match uri.scheme() {
Some("stdin") => {
return Cow::Borrowed(match uri.as_str().strip_prefix("stdin://").unwrap() {
"" => "stdin",
other => other,
});
}
Some(scheme) if scheme == "file" || scheme.chars().count() == 1 => {
uri.as_str().split_once("://").unwrap().1
}
Some(_) => return Cow::Borrowed(uri.as_str().split_once("://").unwrap().1),
None => uri.as_str(),
};
let mut output = String::with_capacity(path.len());
for c in path.chars() {
if matches!(c, '/') || !is_separator(c) {
output.push(c);
continue;
}
output.push('/');
}
Cow::Owned(output)
}
#[derive(Clone, Debug)]
pub struct GlobBuilder<'a> {
glob: &'a str,
opts: GlobOptions,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
struct GlobOptions {
case_insensitive: bool,
literal_separator: bool,
backslash_escape: bool,
empty_alternates: bool,
allow_unclosed_class: bool,
}
impl GlobOptions {
fn default() -> GlobOptions {
GlobOptions {
case_insensitive: false,
literal_separator: false,
backslash_escape: !is_separator('\\'),
empty_alternates: false,
allow_unclosed_class: false,
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
struct Tokens(Vec<Token>);
impl core::ops::Deref for Tokens {
type Target = Vec<Token>;
fn deref(&self) -> &Vec<Token> {
&self.0
}
}
impl core::ops::DerefMut for Tokens {
fn deref_mut(&mut self) -> &mut Vec<Token> {
&mut self.0
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum Token {
Literal(char),
Any,
ZeroOrMore,
RecursivePrefix,
RecursiveSuffix,
RecursiveZeroOrMore,
Class {
negated: bool,
ranges: Vec<(char, char)>,
},
Alternates(Vec<Tokens>),
}
impl Glob {
pub fn new(glob: &str) -> Result<Glob, Error> {
GlobBuilder::new(glob).build()
}
pub fn compile_matcher(&self) -> GlobMatcher {
let mut re = regex::bytes::RegexBuilder::new(&self.re);
re.unicode(false).dot_matches_new_line(true);
let re = re.build().expect("regex compilation shouldn't fail");
GlobMatcher {
pat: self.clone(),
re,
}
}
pub fn glob(&self) -> &str {
&self.glob
}
pub fn regex(&self) -> &str {
&self.re
}
}
impl<'a> GlobBuilder<'a> {
pub fn new(glob: &'a str) -> GlobBuilder<'a> {
GlobBuilder {
glob,
opts: GlobOptions::default(),
}
}
pub fn build(&self) -> Result<Glob, Error> {
let mut p = Parser {
glob: self.glob,
alternates_stack: Vec::new(),
branches: vec![Tokens::default()],
chars: self.glob.chars().peekable(),
prev: None,
cur: None,
found_unclosed_class: false,
opts: &self.opts,
};
p.parse()?;
if p.branches.is_empty() {
unreachable!()
} else if p.branches.len() > 1 {
Err(Error {
glob: Some(self.glob.to_string()),
kind: ErrorKind::UnclosedAlternates,
})
} else {
let tokens = p.branches.pop().unwrap();
Ok(Glob {
glob: self.glob.to_string(),
re: tokens.to_regex_with(&self.opts),
opts: self.opts,
tokens,
})
}
}
pub fn case_insensitive(&mut self, yes: bool) -> &mut GlobBuilder<'a> {
self.opts.case_insensitive = yes;
self
}
pub fn literal_separator(&mut self, yes: bool) -> &mut GlobBuilder<'a> {
self.opts.literal_separator = yes;
self
}
pub fn backslash_escape(&mut self, yes: bool) -> &mut GlobBuilder<'a> {
self.opts.backslash_escape = yes;
self
}
pub fn empty_alternates(&mut self, yes: bool) -> &mut GlobBuilder<'a> {
self.opts.empty_alternates = yes;
self
}
pub fn allow_unclosed_class(&mut self, yes: bool) -> &mut GlobBuilder<'a> {
self.opts.allow_unclosed_class = yes;
self
}
}
impl Tokens {
fn to_regex_with(&self, options: &GlobOptions) -> String {
let mut re = String::new();
re.push_str("(?-u)");
if options.case_insensitive {
re.push_str("(?i)");
}
re.push('^');
if self.len() == 1 && self[0] == Token::RecursivePrefix {
re.push_str(".*");
re.push('$');
return re;
}
self.tokens_to_regex(options, self, &mut re);
re.push('$');
re
}
fn tokens_to_regex(&self, options: &GlobOptions, tokens: &[Token], re: &mut String) {
for tok in tokens.iter() {
match *tok {
Token::Literal(c) => {
re.push_str(&char_to_escaped_literal(c));
}
Token::Any => {
if options.literal_separator {
re.push_str("[^/]");
} else {
re.push('.');
}
}
Token::ZeroOrMore => {
if options.literal_separator {
re.push_str("[^/]*");
} else {
re.push_str(".*");
}
}
Token::RecursivePrefix => {
re.push_str("(?:/?|.*/)");
}
Token::RecursiveSuffix => {
re.push_str("/.*");
}
Token::RecursiveZeroOrMore => {
re.push_str("(?:/|/.*/)");
}
Token::Class {
negated,
ref ranges,
} => {
re.push('[');
if negated {
re.push('^');
}
for r in ranges {
if r.0 == r.1 {
re.push_str(&char_to_escaped_literal(r.0));
} else {
re.push_str(&char_to_escaped_literal(r.0));
re.push('-');
re.push_str(&char_to_escaped_literal(r.1));
}
}
re.push(']');
}
Token::Alternates(ref patterns) => {
let mut parts = vec![];
for pat in patterns {
let mut altre = String::new();
self.tokens_to_regex(options, pat, &mut altre);
if !altre.is_empty() || options.empty_alternates {
parts.push(altre);
}
}
if !parts.is_empty() {
re.push_str("(?:");
re.push_str(&parts.join("|"));
re.push(')');
}
}
}
}
}
}
fn char_to_escaped_literal(c: char) -> String {
let mut buf = [0; 4];
let bytes = c.encode_utf8(&mut buf).as_bytes();
bytes_to_escaped_literal(bytes)
}
fn bytes_to_escaped_literal(bs: &[u8]) -> String {
let mut s = String::with_capacity(bs.len());
for &b in bs {
if b <= 0x7f {
regex_syntax::escape_into(char::from(b).encode_utf8(&mut [0; 4]), &mut s);
} else {
write!(&mut s, "\\x{:02x}", b).unwrap();
}
}
s
}
struct Parser<'a> {
glob: &'a str,
alternates_stack: Vec<usize>,
branches: Vec<Tokens>,
chars: core::iter::Peekable<core::str::Chars<'a>>,
prev: Option<char>,
cur: Option<char>,
found_unclosed_class: bool,
opts: &'a GlobOptions,
}
impl<'a> Parser<'a> {
fn error(&self, kind: ErrorKind) -> Error {
Error {
glob: Some(self.glob.to_string()),
kind,
}
}
fn parse(&mut self) -> Result<(), Error> {
while let Some(c) = self.bump() {
match c {
'?' => self.push_token(Token::Any)?,
'*' => self.parse_star()?,
'[' if !self.found_unclosed_class => self.parse_class()?,
'{' => self.push_alternate()?,
'}' => self.pop_alternate()?,
',' => self.parse_comma()?,
'\\' => self.parse_backslash()?,
c => self.push_token(Token::Literal(c))?,
}
}
Ok(())
}
fn push_alternate(&mut self) -> Result<(), Error> {
self.alternates_stack.push(self.branches.len());
self.branches.push(Tokens::default());
Ok(())
}
fn pop_alternate(&mut self) -> Result<(), Error> {
let Some(start) = self.alternates_stack.pop() else {
return Err(self.error(ErrorKind::UnopenedAlternates));
};
assert!(start <= self.branches.len());
let alts = Token::Alternates(self.branches.drain(start..).collect());
self.push_token(alts)?;
Ok(())
}
fn push_token(&mut self, tok: Token) -> Result<(), Error> {
if let Some(ref mut pat) = self.branches.last_mut() {
pat.push(tok);
return Ok(());
}
Err(self.error(ErrorKind::UnopenedAlternates))
}
fn pop_token(&mut self) -> Result<Token, Error> {
if let Some(ref mut pat) = self.branches.last_mut() {
return Ok(pat.pop().unwrap());
}
Err(self.error(ErrorKind::UnopenedAlternates))
}
fn have_tokens(&self) -> Result<bool, Error> {
match self.branches.last() {
None => Err(self.error(ErrorKind::UnopenedAlternates)),
Some(pat) => Ok(!pat.is_empty()),
}
}
fn parse_comma(&mut self) -> Result<(), Error> {
if self.alternates_stack.is_empty() {
self.push_token(Token::Literal(','))
} else {
self.branches.push(Tokens::default());
Ok(())
}
}
fn parse_backslash(&mut self) -> Result<(), Error> {
if self.opts.backslash_escape {
match self.bump() {
None => Err(self.error(ErrorKind::DanglingEscape)),
Some(c) => self.push_token(Token::Literal(c)),
}
} else if is_separator('\\') {
self.push_token(Token::Literal('/'))
} else {
self.push_token(Token::Literal('\\'))
}
}
fn parse_star(&mut self) -> Result<(), Error> {
let prev = self.prev;
if self.peek() != Some('*') {
self.push_token(Token::ZeroOrMore)?;
return Ok(());
}
assert!(self.bump() == Some('*'));
if !self.have_tokens()? {
if !self.peek().is_none_or(is_separator) {
self.push_token(Token::ZeroOrMore)?;
self.push_token(Token::ZeroOrMore)?;
} else {
self.push_token(Token::RecursivePrefix)?;
assert!(self.bump().is_none_or(is_separator));
}
return Ok(());
}
if !prev.map(is_separator).unwrap_or(false)
&& (self.branches.len() <= 1 || (prev != Some(',') && prev != Some('{')))
{
self.push_token(Token::ZeroOrMore)?;
self.push_token(Token::ZeroOrMore)?;
return Ok(());
}
let is_suffix = match self.peek() {
None => {
assert!(self.bump().is_none());
true
}
Some(',') | Some('}') if self.branches.len() >= 2 => true,
Some(c) if is_separator(c) => {
assert!(self.bump().map(is_separator).unwrap_or(false));
false
}
_ => {
self.push_token(Token::ZeroOrMore)?;
self.push_token(Token::ZeroOrMore)?;
return Ok(());
}
};
match self.pop_token()? {
Token::RecursivePrefix => {
self.push_token(Token::RecursivePrefix)?;
}
Token::RecursiveSuffix => {
self.push_token(Token::RecursiveSuffix)?;
}
_ => {
if is_suffix {
self.push_token(Token::RecursiveSuffix)?;
} else {
self.push_token(Token::RecursiveZeroOrMore)?;
}
}
}
Ok(())
}
fn parse_class(&mut self) -> Result<(), Error> {
let saved_chars = self.chars.clone();
let saved_prev = self.prev;
let saved_cur = self.cur;
fn add_to_last_range(glob: &str, r: &mut (char, char), add: char) -> Result<(), Error> {
r.1 = add;
if r.1 < r.0 {
Err(Error {
glob: Some(glob.to_string()),
kind: ErrorKind::InvalidRange(r.0, r.1),
})
} else {
Ok(())
}
}
let mut ranges = vec![];
let negated = match self.chars.peek() {
Some(&'!') | Some(&'^') => {
let bump = self.bump();
assert!(bump == Some('!') || bump == Some('^'));
true
}
_ => false,
};
let mut first = true;
let mut in_range = false;
loop {
let Some(c) = self.bump() else {
return if self.opts.allow_unclosed_class {
self.chars = saved_chars;
self.cur = saved_cur;
self.prev = saved_prev;
self.found_unclosed_class = true;
self.push_token(Token::Literal('['))
} else {
Err(self.error(ErrorKind::UnclosedClass))
};
};
match c {
']' => {
if first {
ranges.push((']', ']'));
} else {
break;
}
}
'-' => {
if first {
ranges.push(('-', '-'));
} else if in_range {
let r = ranges.last_mut().unwrap();
add_to_last_range(self.glob, r, '-')?;
in_range = false;
} else {
assert!(!ranges.is_empty());
in_range = true;
}
}
c => {
if in_range {
add_to_last_range(self.glob, ranges.last_mut().unwrap(), c)?;
} else {
ranges.push((c, c));
}
in_range = false;
}
}
first = false;
}
if in_range {
ranges.push(('-', '-'));
}
self.push_token(Token::Class { negated, ranges })
}
fn bump(&mut self) -> Option<char> {
self.prev = self.cur;
self.cur = self.chars.next();
self.cur
}
fn peek(&mut self) -> Option<char> {
self.chars.peek().copied()
}
}
#[cfg(test)]
mod tests;