#![warn(missing_docs)]
#![warn(rust_2018_idioms)]
pub mod dfa;
pub mod engine;
pub mod error;
pub mod hir;
pub mod literal;
pub mod nfa;
pub mod parser;
pub mod reference;
pub mod vm;
#[cfg(feature = "jit")]
pub mod jit;
#[cfg(feature = "simd")]
pub mod simd;
pub use error::{Error, Result};
use engine::CompiledRegex;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone, Default)]
pub struct RegexBuilder {
pattern: String,
jit: bool,
optimize_prefixes: bool,
backtrack_limit: u64,
}
impl RegexBuilder {
pub fn new(pattern: &str) -> Self {
Self {
pattern: pattern.to_string(),
jit: false,
optimize_prefixes: false,
backtrack_limit: vm::backtracking::DEFAULT_BACKTRACK_LIMIT,
}
}
pub fn backtrack_limit(mut self, limit: u64) -> Self {
self.backtrack_limit = limit;
self
}
pub fn jit(mut self, enabled: bool) -> Self {
self.jit = enabled;
self
}
pub fn optimize_prefixes(mut self, enabled: bool) -> Self {
self.optimize_prefixes = enabled;
self
}
pub fn build(self) -> Result<Regex> {
let ast = parser::parse(&self.pattern)?;
let mut hir_result = hir::translate(&ast)?;
if self.optimize_prefixes {
hir_result = hir::optimize_prefixes(hir_result);
}
let named_groups = Arc::new(hir_result.props.named_groups.clone());
let inner = if self.jit {
engine::compile_with_jit(&hir_result)?
} else {
engine::compile_from_hir(&hir_result)?
};
Ok(Regex {
inner,
pattern: self.pattern,
named_groups,
backtrack_limit: self.backtrack_limit,
})
}
}
pub fn escape(text: &str) -> String {
let mut out = String::with_capacity(text.len());
for c in text.chars() {
match c {
'\n' => out.push_str(r"\n"),
'\r' => out.push_str(r"\r"),
'\t' => out.push_str(r"\t"),
'\\' | '.' | '*' | '+' | '?' | '|' | '^' | '$' | '(' | ')' | '[' | ']' | '{' | '}'
| '#' => {
out.push('\\');
out.push(c);
}
c if c.is_ascii_whitespace() => {
out.push('\\');
out.push(c);
}
c => out.push(c),
}
}
out
}
#[derive(Debug)]
pub struct Regex {
inner: CompiledRegex,
pattern: String,
named_groups: Arc<HashMap<String, u32>>,
backtrack_limit: u64,
}
impl Regex {
pub fn new(pattern: &str) -> Result<Regex> {
let ast = parser::parse(pattern)?;
let hir = hir::translate(&ast)?;
let named_groups = Arc::new(hir.props.named_groups.clone());
let inner = engine::compile_from_hir(&hir)?;
Ok(Regex {
inner,
pattern: pattern.to_string(),
named_groups,
backtrack_limit: vm::backtracking::DEFAULT_BACKTRACK_LIMIT,
})
}
pub fn capture_names(&self) -> impl Iterator<Item = &str> {
self.named_groups.keys().map(|s| s.as_str())
}
pub fn as_str(&self) -> &str {
&self.pattern
}
pub fn is_match(&self, text: &str) -> bool {
self.inner.is_match(text.as_bytes())
}
pub fn find<'t>(&self, text: &'t str) -> Option<Match<'t>> {
self.inner
.find(text.as_bytes())
.map(|(start, end)| Match { text, start, end })
}
pub fn find_iter<'a>(&'a self, text: &'a str) -> Matches<'a> {
Matches::new(self, text)
}
pub fn captures<'t>(&self, text: &'t str) -> Option<Captures<'t>> {
self.inner.captures(text.as_bytes()).map(|slots| Captures {
text,
slots,
named_groups: Arc::clone(&self.named_groups),
})
}
pub fn try_is_match(&self, text: &str) -> Result<bool> {
Ok(self.try_find(text)?.is_some())
}
pub fn try_find<'t>(&self, text: &'t str) -> Result<Option<Match<'t>>> {
self.inner
.try_find_from(text.as_bytes(), 0, self.backtrack_limit)
.map(|found| found.map(|(start, end)| Match { text, start, end }))
.map_err(|_| self.match_limit_error())
}
pub fn try_captures<'t>(&self, text: &'t str) -> Result<Option<Captures<'t>>> {
self.inner
.try_captures_from(text.as_bytes(), 0, self.backtrack_limit)
.map(|found| {
found.map(|slots| Captures {
text,
slots,
named_groups: Arc::clone(&self.named_groups),
})
})
.map_err(|_| self.match_limit_error())
}
fn match_limit_error(&self) -> Error {
Error::new(error::ErrorKind::MatchLimitExceeded, &self.pattern)
}
pub fn captures_iter<'r, 't>(&'r self, text: &'t str) -> CapturesIter<'r, 't> {
CapturesIter {
regex: self,
text,
last_end: 0,
}
}
pub fn replace<'t>(&self, text: &'t str, rep: &str) -> std::borrow::Cow<'t, str> {
match self.find(text) {
None => std::borrow::Cow::Borrowed(text),
Some(m) => {
let bytes = text.as_bytes();
let mut result = Vec::with_capacity(text.len() + rep.len());
result.extend_from_slice(&bytes[..m.start()]);
result.extend_from_slice(rep.as_bytes());
result.extend_from_slice(&bytes[m.end()..]);
std::borrow::Cow::Owned(into_string_lossy(result))
}
}
}
pub fn engine_name(&self) -> &'static str {
self.inner.engine_name()
}
pub fn replace_all<'t>(&self, text: &'t str, rep: &str) -> std::borrow::Cow<'t, str> {
let bytes = text.as_bytes();
let mut last_end = 0;
let mut result = Vec::new();
let mut had_match = false;
for m in self.find_iter(text) {
had_match = true;
result.extend_from_slice(&bytes[last_end..m.start()]);
result.extend_from_slice(rep.as_bytes());
last_end = m.end();
}
if !had_match {
std::borrow::Cow::Borrowed(text)
} else {
result.extend_from_slice(&bytes[last_end..]);
std::borrow::Cow::Owned(into_string_lossy(result))
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Match<'t> {
text: &'t str,
start: usize,
end: usize,
}
impl<'t> Match<'t> {
pub fn start(&self) -> usize {
self.start
}
pub fn end(&self) -> usize {
self.end
}
pub fn as_str(&self) -> &'t str {
self.text.get(self.start..self.end).unwrap_or("")
}
pub fn as_bytes(&self) -> &'t [u8] {
self.text
.as_bytes()
.get(self.start..self.end)
.unwrap_or(&[])
}
pub fn range(&self) -> std::ops::Range<usize> {
self.start..self.end
}
pub fn len(&self) -> usize {
self.end - self.start
}
pub fn is_empty(&self) -> bool {
self.start == self.end
}
}
fn ceil_char_boundary(text: &str, i: usize) -> usize {
let mut j = i;
while j < text.len() && !text.is_char_boundary(j) {
j += 1;
}
j
}
fn into_string_lossy(bytes: Vec<u8>) -> String {
match String::from_utf8(bytes) {
Ok(s) => s,
Err(e) => String::from_utf8_lossy(e.as_bytes()).into_owned(),
}
}
pub struct Matches<'a> {
inner: MatchesInner<'a>,
text: &'a str,
}
impl<'a> std::fmt::Debug for Matches<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Matches")
.field("text_len", &self.text.len())
.finish_non_exhaustive()
}
}
enum MatchesInner<'a> {
TeddyFull(literal::FullMatchIter<'a, 'a>),
Generic { regex: &'a Regex, last_end: usize },
}
impl<'a> Matches<'a> {
fn new(regex: &'a Regex, text: &'a str) -> Self {
let inner = if regex.inner.is_full_match_prefilter() {
MatchesInner::TeddyFull(regex.inner.find_full_matches(text.as_bytes()))
} else {
MatchesInner::Generic { regex, last_end: 0 }
};
Matches { inner, text }
}
}
impl<'a> Iterator for Matches<'a> {
type Item = Match<'a>;
fn next(&mut self) -> Option<Match<'a>> {
match &mut self.inner {
MatchesInner::TeddyFull(iter) => {
iter.next().map(|(start, end)| Match {
text: self.text,
start,
end,
})
}
MatchesInner::Generic { regex, last_end } => {
if *last_end > self.text.len() {
return None;
}
match regex.inner.find_from(self.text.as_bytes(), *last_end) {
None => None,
Some((abs_start, abs_end)) => {
*last_end = if abs_start == abs_end {
ceil_char_boundary(self.text, abs_end + 1)
} else {
ceil_char_boundary(self.text, abs_end)
};
Some(Match {
text: self.text,
start: abs_start,
end: abs_end,
})
}
}
}
}
}
}
#[derive(Debug)]
pub struct CapturesIter<'r, 't> {
regex: &'r Regex,
text: &'t str,
last_end: usize,
}
impl<'r, 't> Iterator for CapturesIter<'r, 't> {
type Item = Captures<'t>;
fn next(&mut self) -> Option<Captures<'t>> {
if self.last_end > self.text.len() {
return None;
}
match self
.regex
.inner
.captures_from(self.text.as_bytes(), self.last_end)
{
None => None,
Some(slots) => {
let (start, end) = slots.first().and_then(|s| *s)?;
self.last_end = if start == end {
ceil_char_boundary(self.text, end + 1)
} else {
ceil_char_boundary(self.text, end)
};
Some(Captures {
text: self.text,
slots,
named_groups: Arc::clone(&self.regex.named_groups),
})
}
}
}
}
#[derive(Debug, Clone)]
pub struct Captures<'t> {
text: &'t str,
slots: Vec<Option<(usize, usize)>>,
named_groups: Arc<HashMap<String, u32>>,
}
impl<'t> Captures<'t> {
pub fn len(&self) -> usize {
self.slots.len()
}
pub fn is_empty(&self) -> bool {
self.slots.is_empty()
}
pub fn get(&self, i: usize) -> Option<Match<'t>> {
self.slots.get(i).and_then(|slot| {
slot.map(|(start, end)| Match {
text: self.text,
start,
end,
})
})
}
pub fn name(&self, name: &str) -> Option<Match<'t>> {
self.named_groups
.get(name)
.and_then(|&idx| self.get(idx as usize))
}
}
impl<'t> std::ops::Index<usize> for Captures<'t> {
type Output = str;
fn index(&self, i: usize) -> &str {
self.get(i)
.map(|m| m.as_str())
.unwrap_or_else(|| panic!("no capture group at index {}", i))
}
}
impl<'t> std::ops::Index<&str> for Captures<'t> {
type Output = str;
fn index(&self, name: &str) -> &str {
self.name(name)
.map(|m| m.as_str())
.unwrap_or_else(|| panic!("no capture group named '{}'", name))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_escape_shape() {
assert_eq!(escape(r"\.*+?|^$(){}[]"), r"\\\.\*\+\?\|\^\$\(\)\{\}\[\]");
assert_eq!(escape("plain"), "plain");
assert_eq!(escape(""), "");
assert_eq!(escape("plain text"), r"plain\ text");
assert_eq!(escape("a#b"), r"a\#b");
assert_eq!(escape("a\nb"), r"a\nb");
}
#[test]
fn test_ceil_char_boundary() {
let text = "aé世🎉";
assert_eq!(ceil_char_boundary(text, 0), 0);
assert_eq!(ceil_char_boundary(text, 1), 1);
assert_eq!(ceil_char_boundary(text, 2), 3);
assert_eq!(ceil_char_boundary(text, 3), 3);
assert_eq!(ceil_char_boundary(text, 4), 6);
assert_eq!(ceil_char_boundary(text, 5), 6);
assert_eq!(ceil_char_boundary(text, 7), 10);
assert_eq!(ceil_char_boundary(text, 10), 10);
assert_eq!(ceil_char_boundary(text, 11), 11);
let ascii = "abc";
for i in 0..=ascii.len() {
assert_eq!(ceil_char_boundary(ascii, i), i);
}
for i in 0..=text.len() {
let j = ceil_char_boundary(text, i);
assert!(j >= i);
assert!(text.is_char_boundary(j));
}
}
#[test]
fn test_into_string_lossy() {
assert_eq!(into_string_lossy("héllo".as_bytes().to_vec()), "héllo");
assert_eq!(into_string_lossy(Vec::new()), "");
assert_eq!(
into_string_lossy(vec![b'-', 0xB8, 0x96]),
"-\u{FFFD}\u{FFFD}"
);
}
}