#![doc(hidden)]
#![allow(clippy::derivable_impls)]
#![allow(clippy::min_ident_chars)]
#![allow(clippy::missing_inline_in_public_items)]
#![allow(warnings)]
use core::fmt;
use core::mem;
use std::vec::IntoIter as VecIntoIter;
#[allow(clippy::error_impl_error)]
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
BareEscape,
UnclosedClass,
NotImplemented(Box<str>),
ReversedRange(char, char),
RangeAfterRange(char, char),
UnclosedAlternation,
InvalidRegex(Box<str>),
}
#[allow(clippy::error_impl_error)]
#[allow(clippy::pattern_type_mismatch)] impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::BareEscape => write!(f, "Bare escape at the end of the pattern"),
Self::UnclosedClass => write!(f, "Unclosed character class"),
Self::NotImplemented(s) => write!(f, "Not implemented: {s}"),
Self::ReversedRange(start, end) => {
write!(f, "Reversed range: {start} > {end}")
}
Self::RangeAfterRange(start, end) => {
write!(f, "Range after range: {start}-{end}")
}
Self::UnclosedAlternation => write!(f, "Unclosed alternation"),
Self::InvalidRegex(message) => {
write!(f, "Invalid regex: {message}")
}
}
}
}
#[derive(Debug)]
enum ClassItem {
Char(char),
Range(char, char),
}
#[derive(Debug)]
struct ClassAccumulator {
negated: bool,
items: Vec<ClassItem>,
}
#[derive(Debug)]
enum State {
Start,
End,
Literal,
Escape,
ClassStart,
Class(ClassAccumulator),
ClassRange(ClassAccumulator, char),
ClassRangeDash(ClassAccumulator),
ClassEscape(ClassAccumulator),
Alternate(String, Vec<String>),
AlternateEscape(String, Vec<String>),
}
impl Default for State {
fn default() -> Self {
Self::Start
}
}
fn escape_in_class(chr: char) -> String {
if chr == ']' || chr == '\\' {
format!("\\{chr}")
} else {
chr.to_string()
}
}
fn escape(chr: char) -> String {
if "[{(|^$.*?+\\".contains(chr) {
format!("\\{chr}")
} else {
chr.to_string()
}
}
const fn map_letter_escape(chr: char) -> char {
match chr {
'a' => '\x07',
'b' => '\x08',
'e' => '\x1b',
'f' => '\x0c',
'n' => '\x0a',
'r' => '\x0d',
't' => '\x09',
'v' => '\x0b',
other => other,
}
}
#[allow(clippy::single_call_fn)]
fn escape_special(chr: char) -> String {
escape(map_letter_escape(chr))
}
struct ExcIter<I>
where
I: Iterator<Item = ClassItem>,
{
it: I,
}
impl<I> Iterator for ExcIter<I>
where
I: Iterator<Item = ClassItem>,
{
type Item = VecIntoIter<ClassItem>;
fn next(&mut self) -> Option<Self::Item> {
self.it.next().map(|cls| {
match cls {
ClassItem::Char('/') => vec![],
ClassItem::Char(_) => vec![cls],
ClassItem::Range('.', '/') => vec![ClassItem::Char('.')],
ClassItem::Range(start, '/') => vec![ClassItem::Range(start, '.')],
ClassItem::Range('/', '0') => vec![ClassItem::Char('0')],
ClassItem::Range('/', end) => vec![ClassItem::Range('0', end)],
ClassItem::Range(start, end) if start > '/' || end < '/' => vec![cls],
ClassItem::Range(start, end) => vec![
if start == '.' {
ClassItem::Char('.')
} else {
ClassItem::Range(start, '.')
},
if end == '0' {
ClassItem::Char('0')
} else {
ClassItem::Range('0', end)
},
],
}
.into_iter()
})
}
}
#[allow(clippy::single_call_fn)]
fn handle_slash_exclude(acc: ClassAccumulator) -> ClassAccumulator {
assert!(!acc.negated, "this should not be negated");
ClassAccumulator {
items: ExcIter {
it: acc.items.into_iter(),
}
.flatten()
.collect(),
..acc
}
}
#[allow(clippy::single_call_fn)]
fn handle_slash_include(mut acc: ClassAccumulator) -> ClassAccumulator {
assert!(acc.negated, "this should be negated");
let slash_found = acc.items.iter().any(|item| match *item {
ClassItem::Char('/') => true,
ClassItem::Char(_) => false,
ClassItem::Range(start, end) => start <= '/' && end >= '/',
});
if !slash_found {
acc.items.push(ClassItem::Char('/'));
}
acc
}
#[allow(clippy::single_call_fn)]
fn handle_slash(acc: ClassAccumulator) -> ClassAccumulator {
if acc.negated {
handle_slash_include(acc)
} else {
handle_slash_exclude(acc)
}
}
fn close_class(glob_acc: ClassAccumulator) -> String {
let acc = handle_slash(glob_acc);
let mut chars_vec = Vec::new();
let mut classes_vec = Vec::new();
for item in acc.items {
match item {
ClassItem::Char(chr) => chars_vec.push(chr),
ClassItem::Range(start, end) => classes_vec.push((start, end)),
}
}
let (chars, final_dash) = {
let mut has_dash = false;
let mut chars_filtered = Vec::new();
for chr in chars_vec {
if chr == '-' {
has_dash = true;
} else {
chars_filtered.push(chr);
}
}
chars_filtered.sort_unstable();
chars_filtered.dedup();
let formatted_chars = chars_filtered
.into_iter()
.map(escape_in_class)
.collect::<Vec<_>>();
(formatted_chars, if has_dash { "-" } else { "" })
};
classes_vec.sort_unstable();
classes_vec.dedup();
let classes = classes_vec
.into_iter()
.map(|cls| format!("{}-{}", escape_in_class(cls.0), escape_in_class(cls.1)))
.collect::<Vec<_>>();
let mut result = String::new();
if acc.negated {
result.push('^');
}
for char_str in chars {
result.push_str(&char_str);
}
for class_str in classes {
result.push_str(&class_str);
}
result.push_str(final_dash);
format!("[{result}]")
}
#[allow(clippy::single_call_fn)]
fn close_alternate(gathered: Vec<String>) -> String {
let mut items = gathered
.into_iter()
.map(|item| item.chars().map(escape).collect::<String>())
.collect::<Vec<_>>();
items.sort_unstable();
items.dedup();
let joined = items.join("|");
format!("({joined})")
}
struct GlobIterator<I: Iterator<Item = char>> {
pattern: I,
state: State,
}
type StringResult = Result<Option<String>, Error>;
impl<I> GlobIterator<I>
where
I: Iterator<Item = char>,
{
fn handle_start(&mut self) -> String {
self.state = State::Literal;
"^".to_owned()
}
fn handle_literal(&mut self) -> Option<String> {
match self.pattern.next() {
None => {
self.state = State::End;
Some("$".to_owned())
}
Some(chr) => {
let (new_state, res) = match chr {
'\\' => (State::Escape, None),
'[' => (State::ClassStart, None),
'{' => (State::Alternate(String::new(), Vec::new()), None),
'?' => (State::Literal, Some("[^/]".to_owned())),
'*' => (State::Literal, Some(".*".to_owned())),
']' | '}' | '.' => (State::Literal, Some(format!("\\{chr}"))),
_ => (State::Literal, Some(format!("{chr}"))),
};
self.state = new_state;
res
}
}
}
fn handle_escape(&mut self) -> StringResult {
match self.pattern.next() {
Some(chr) => {
self.state = State::Literal;
Ok(Some(escape_special(chr)))
}
None => Err(Error::BareEscape),
}
}
fn handle_class_start(&mut self) -> StringResult {
match self.pattern.next() {
Some(chr) => {
self.state = match chr {
'!' => State::Class(ClassAccumulator {
negated: true,
items: Vec::new(),
}),
'-' => State::Class(ClassAccumulator {
negated: false,
items: vec![ClassItem::Char('-')],
}),
']' => State::Class(ClassAccumulator {
negated: false,
items: vec![ClassItem::Char(']')],
}),
'\\' => State::ClassEscape(ClassAccumulator {
negated: false,
items: Vec::new(),
}),
other => State::Class(ClassAccumulator {
negated: false,
items: vec![ClassItem::Char(other)],
}),
};
Ok(None)
}
None => Err(Error::UnclosedClass),
}
}
fn handle_class(&mut self, mut acc: ClassAccumulator) -> StringResult {
match self.pattern.next() {
Some(chr) => Ok(match chr {
']' => {
if acc.items.is_empty() {
acc.items.push(ClassItem::Char(']'));
self.state = State::Class(acc);
None
} else {
self.state = State::Literal;
Some(close_class(acc))
}
}
'-' => match acc.items.pop() {
None => {
acc.items.push(ClassItem::Char('-'));
self.state = State::Class(acc);
None
}
Some(ClassItem::Range(start, end)) => {
acc.items.push(ClassItem::Range(start, end));
self.state = State::ClassRangeDash(acc);
None
}
Some(ClassItem::Char(start)) => {
self.state = State::ClassRange(acc, start);
None
}
},
'\\' => {
self.state = State::ClassEscape(acc);
None
}
other => {
acc.items.push(ClassItem::Char(other));
self.state = State::Class(acc);
None
}
}),
None => Err(Error::UnclosedClass),
}
}
fn handle_class_escape(&mut self, mut acc: ClassAccumulator) -> StringResult {
match self.pattern.next() {
Some(chr) => {
acc.items.push(ClassItem::Char(map_letter_escape(chr)));
self.state = State::Class(acc);
Ok(None)
}
None => Err(Error::UnclosedClass),
}
}
fn handle_class_range(&mut self, mut acc: ClassAccumulator, start: char) -> StringResult {
match self.pattern.next() {
Some(chr) => match chr {
'\\' => Err(Error::NotImplemented(
format!("FIXME: handle class range end escape with {acc:?} start {start:?}")
.into(),
)),
']' => {
acc.items.push(ClassItem::Char(start));
acc.items.push(ClassItem::Char('-'));
self.state = State::Literal;
Ok(Some(close_class(acc)))
}
end if start > end => Err(Error::ReversedRange(start, end)),
end if start == end => {
acc.items.push(ClassItem::Char(start));
self.state = State::Class(acc);
Ok(None)
}
end => {
acc.items.push(ClassItem::Range(start, end));
self.state = State::Class(acc);
Ok(None)
}
},
None => Err(Error::UnclosedClass),
}
}
#[allow(clippy::panic_in_result_fn)]
#[allow(clippy::unreachable)]
fn handle_class_range_dash(&mut self, mut acc: ClassAccumulator) -> StringResult {
match self.pattern.next() {
Some(chr) => {
if chr == ']' {
acc.items.push(ClassItem::Char('-'));
self.state = State::Literal;
Ok(Some(close_class(acc)))
} else if let Some(ClassItem::Range(start, end)) = acc.items.pop() {
Err(Error::RangeAfterRange(start, end))
} else {
unreachable!()
}
}
None => Err(Error::UnclosedClass),
}
}
fn handle_alternate(&mut self, mut current: String, mut gathered: Vec<String>) -> StringResult {
match self.pattern.next() {
Some(chr) => match chr {
',' => {
gathered.push(current);
self.state = State::Alternate(String::new(), gathered);
Ok(None)
}
'}' => {
self.state = State::Literal;
if current.is_empty() && gathered.is_empty() {
Ok(Some(r"\{\}".to_owned()))
} else {
gathered.push(current);
Ok(Some(close_alternate(gathered)))
}
}
'\\' => {
self.state = State::AlternateEscape(current, gathered);
Ok(None)
}
'[' => Err(Error::NotImplemented(
"FIXME: alternate character class".into(),
)),
other => {
current.push(other);
self.state = State::Alternate(current, gathered);
Ok(None)
}
},
None => Err(Error::UnclosedAlternation),
}
}
fn handle_alternate_escape(
&mut self,
mut current: String,
gathered: Vec<String>,
) -> StringResult {
match self.pattern.next() {
Some(chr) => {
current.push(map_letter_escape(chr));
self.state = State::Alternate(current, gathered);
Ok(None)
}
None => Err(Error::UnclosedAlternation),
}
}
}
impl<I> Iterator for GlobIterator<I>
where
I: Iterator<Item = char>,
{
type Item = StringResult;
fn next(&mut self) -> Option<Self::Item> {
match mem::take(&mut self.state) {
State::Start => Some(Ok(Some(self.handle_start()))),
State::End => None,
State::Literal => Some(Ok(self.handle_literal())),
State::Escape => Some(self.handle_escape()),
State::ClassStart => Some(self.handle_class_start()),
State::Class(acc) => Some(self.handle_class(acc)),
State::ClassEscape(acc) => Some(self.handle_class_escape(acc)),
State::ClassRange(acc, start) => Some(self.handle_class_range(acc, start)),
State::ClassRangeDash(acc) => Some(self.handle_class_range_dash(acc)),
State::Alternate(current, gathered) => Some(self.handle_alternate(current, gathered)),
State::AlternateEscape(current, gathered) => {
Some(self.handle_alternate_escape(current, gathered))
}
}
}
}
#[allow(clippy::single_call_fn)]
fn flatten_ok<I, T, E>(iter: I) -> impl Iterator<Item = Result<T, E>>
where
I: Iterator<Item = Result<Option<T>, E>>,
{
iter.filter_map(|res| match res {
Ok(Some(item)) => Some(Ok(item)),
Ok(None) => None,
Err(err) => Some(Err(err)),
})
}
#[allow(clippy::missing_inline_in_public_items)]
pub fn glob_to_regex(pattern: &str) -> Result<String, Error> {
let parser = GlobIterator {
pattern: pattern.chars(),
state: State::Start,
};
let mut result = Vec::new();
for item in flatten_ok(parser) {
result.push(item?);
}
Ok(result.join(""))
}