use aho_corasick::AhoCorasick;
use memchr::{memchr, memchr_iter, memmem};
use std::sync::Arc;
#[inline]
pub fn find_literal(text: &str, literal: &str) -> Option<(usize, usize)> {
let needle = literal.as_bytes();
let haystack = text.as_bytes();
let needle_len = needle.len();
if needle_len == 0 {
return Some((0, 0));
}
if needle_len > haystack.len() {
return None;
}
if needle_len == 1 {
return memchr(needle[0], haystack).map(|pos| (pos, pos + 1));
}
let first_byte = needle[0];
let mut pos = 0;
let end = haystack.len() - needle_len + 1;
while pos < end {
if let Some(found) = memchr(first_byte, &haystack[pos..end]) {
let start = pos + found;
if haystack[start..start + needle_len] == *needle {
return Some((start, start + needle_len));
}
pos = start + 1;
} else {
break;
}
}
None
}
#[inline]
pub fn find_literal_case_insensitive(
text: &str,
literal_lowercase: &str,
) -> Option<(usize, usize)> {
let needle = literal_lowercase.as_bytes();
let haystack = text.as_bytes();
let needle_len = needle.len();
if needle_len == 0 {
return Some((0, 0));
}
if needle_len > haystack.len() {
return None;
}
if needle_len == 1 {
let lower = needle[0];
let upper = lower.to_ascii_uppercase();
if lower == upper {
return memchr(lower, haystack).map(|pos| (pos, pos + 1));
}
return memchr::memchr2(lower, upper, haystack).map(|pos| (pos, pos + 1));
}
let first_lower = needle[0];
let first_upper = first_lower.to_ascii_uppercase();
let mut pos = 0;
let end = haystack.len() - needle_len + 1;
while pos < end {
let next_pos = if first_lower == first_upper {
memchr(first_lower, &haystack[pos..end]).map(|p| pos + p)
} else {
memchr::memchr2(first_lower, first_upper, &haystack[pos..end]).map(|p| pos + p)
};
if let Some(start) = next_pos {
if matches_case_insensitive(&haystack[start..start + needle_len], needle) {
return Some((start, start + needle_len));
}
pos = start + 1;
} else {
break;
}
}
None
}
#[inline(always)]
fn ascii_lowercase_byte(b: u8) -> u8 {
let is_upper = (b >= b'A') & (b <= b'Z');
b + (is_upper as u8 * 32)
}
#[inline(always)]
fn matches_case_insensitive(haystack: &[u8], needle_lowercase: &[u8]) -> bool {
debug_assert_eq!(haystack.len(), needle_lowercase.len());
let len = haystack.len();
for i in 0..len {
if ascii_lowercase_byte(haystack[i]) != needle_lowercase[i] {
return false;
}
}
true
}
#[inline]
pub fn find_literal_plus_whitespace(text: &str, literal: &str) -> Option<(usize, usize)> {
let finder = memmem::Finder::new(literal.as_bytes());
for pos in finder.find_iter(text.as_bytes()) {
let after = pos + literal.len();
if after >= text.len() {
continue;
}
let rest = &text[after..];
let mut matched = 0;
for ch in rest.chars() {
if ch.is_whitespace() {
matched += ch.len_utf8();
} else {
break;
}
}
if matched > 0 {
return Some((pos, after + matched));
}
}
None
}
#[inline]
pub fn find_literal_dot_star_literal(
text: &str,
prefix: &str,
suffix: &str,
lazy: bool,
) -> Option<(usize, usize)> {
find_literal_dot_star_literal_at(text, prefix, suffix, lazy, 0)
}
#[inline]
pub fn find_literal_dot_star_literal_at(
text: &str,
prefix: &str,
suffix: &str,
lazy: bool,
start_pos: usize,
) -> Option<(usize, usize)> {
if start_pos > text.len() || prefix.is_empty() || suffix.is_empty() {
return None;
}
let bytes = text.as_bytes();
let prefix_bytes = prefix.as_bytes();
let suffix_bytes = suffix.as_bytes();
let mut search_pos = start_pos;
while search_pos <= bytes.len() {
let prefix_start = find_bytes_at(bytes, prefix_bytes, search_pos)?;
let after_prefix = prefix_start + prefix_bytes.len();
let line_end = memchr(b'\n', &bytes[after_prefix..])
.map(|pos| after_prefix + pos)
.unwrap_or(bytes.len());
if let Some(suffix_start) = if lazy {
find_bytes_at(bytes, suffix_bytes, after_prefix)
.filter(|&pos| pos + suffix_bytes.len() <= line_end)
} else {
rfind_bytes_in_range(bytes, suffix_bytes, after_prefix, line_end)
} {
return Some((prefix_start, suffix_start + suffix_bytes.len()));
}
search_pos = prefix_start + 1;
}
None
}
#[inline]
pub fn find_literal_dot_star_literal_all(
text: &str,
prefix: &str,
suffix: &str,
lazy: bool,
) -> Vec<(usize, usize)> {
let mut results = Vec::new();
let mut pos = 0;
while pos <= text.len() {
if let Some((start, end)) =
find_literal_dot_star_literal_at(text, prefix, suffix, lazy, pos)
{
results.push((start, end));
pos = end.max(pos + 1);
} else {
break;
}
}
results
}
#[inline]
fn find_bytes_at(haystack: &[u8], needle: &[u8], start_pos: usize) -> Option<usize> {
if needle.len() == 1 {
memchr(needle[0], haystack.get(start_pos..)?).map(|pos| start_pos + pos)
} else {
memmem::find(haystack.get(start_pos..)?, needle).map(|pos| start_pos + pos)
}
}
#[inline]
fn rfind_bytes_in_range(
haystack: &[u8],
needle: &[u8],
start_pos: usize,
end_pos: usize,
) -> Option<usize> {
if start_pos > end_pos || needle.len() > end_pos.saturating_sub(start_pos) {
return None;
}
let range = &haystack[start_pos..end_pos];
if needle.len() == 1 {
memchr_iter(needle[0], range)
.next_back()
.map(|pos| start_pos + pos)
} else {
memmem::find_iter(range, needle)
.last()
.map(|pos| start_pos + pos)
}
}
#[inline]
pub fn find_digit_run(text: &str) -> Option<(usize, usize)> {
let bytes = text.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i].is_ascii_digit() {
let start = i;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
return Some((start, i));
}
i += 1;
}
None
}
#[inline]
pub fn find_word_run(text: &str) -> Option<(usize, usize)> {
let bytes = text.as_bytes();
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if b.is_ascii_alphanumeric() || b == b'_' {
let start = i;
while i < bytes.len() {
let b = bytes[i];
if b.is_ascii_alphanumeric() || b == b'_' {
i += 1;
} else {
break;
}
}
return Some((start, i));
}
i += 1;
}
None
}
#[inline]
pub fn find_quoted_string(text: &str) -> Option<(usize, usize)> {
let bytes = text.as_bytes();
let start = memchr(b'"', bytes)?;
if start + 1 >= bytes.len() {
return None;
}
let end = memchr(b'"', &bytes[start + 1..])?;
if end == 0 {
return None;
}
Some((start, start + 1 + end + 1))
}
#[inline]
pub fn find_literal_all(text: &str, literal: &str) -> Vec<(usize, usize)> {
let mut results = Vec::new();
let len = literal.len();
if len >= 3 {
let finder = memmem::Finder::new(literal.as_bytes());
for pos in finder.find_iter(text.as_bytes()) {
results.push((pos, pos + len));
}
} else if len == 1 {
let byte = literal.as_bytes()[0];
for pos in memchr_iter(byte, text.as_bytes()) {
results.push((pos, pos + 1));
}
} else {
let mut pos = 0;
while let Some(idx) = text[pos..].find(literal) {
let abs_pos = pos + idx;
results.push((abs_pos, abs_pos + len));
pos = abs_pos + len;
}
}
results
}
#[inline]
pub fn find_literal_case_insensitive_all(
text: &str,
literal_lowercase: &str,
) -> Vec<(usize, usize)> {
let mut results = Vec::new();
let needle = literal_lowercase.as_bytes();
let haystack = text.as_bytes();
let needle_len = needle.len();
if needle_len == 0 {
return results;
}
if needle_len > haystack.len() {
return results;
}
if needle_len == 1 {
let lower = needle[0];
let upper = lower.to_ascii_uppercase();
if lower == upper {
for pos in memchr_iter(lower, haystack) {
results.push((pos, pos + 1));
}
} else {
for pos in memchr::memchr2_iter(lower, upper, haystack) {
results.push((pos, pos + 1));
}
}
return results;
}
let first_lower = needle[0];
let first_upper = first_lower.to_ascii_uppercase();
let mut pos = 0;
let end = haystack.len() - needle_len + 1;
while pos < end {
let next_pos = if first_lower == first_upper {
memchr(first_lower, &haystack[pos..end]).map(|p| pos + p)
} else {
memchr::memchr2(first_lower, first_upper, &haystack[pos..end]).map(|p| pos + p)
};
if let Some(start) = next_pos {
if matches_case_insensitive(&haystack[start..start + needle_len], needle) {
results.push((start, start + needle_len));
pos = start + needle_len; } else {
pos = start + 1;
}
} else {
break;
}
}
results
}
#[inline]
pub fn find_literal_plus_whitespace_all(text: &str, literal: &str) -> Vec<(usize, usize)> {
let mut results = Vec::new();
let finder = memmem::Finder::new(literal.as_bytes());
for pos in finder.find_iter(text.as_bytes()) {
let after = pos + literal.len();
if after >= text.len() {
continue;
}
let rest = &text[after..];
let mut matched = 0;
for ch in rest.chars() {
if ch.is_whitespace() {
matched += ch.len_utf8();
} else {
break;
}
}
if matched > 0 {
results.push((pos, after + matched));
}
}
results
}
#[inline]
pub fn find_digit_run_all(text: &str) -> Vec<(usize, usize)> {
let mut results = Vec::new();
let bytes = text.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i].is_ascii_digit() {
let start = i;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
results.push((start, i));
} else {
i += 1;
}
}
results
}
#[inline]
pub fn find_word_run_all(text: &str) -> Vec<(usize, usize)> {
let mut results = Vec::new();
let bytes = text.as_bytes();
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if b.is_ascii_alphanumeric() || b == b'_' {
let start = i;
while i < bytes.len() {
let b = bytes[i];
if b.is_ascii_alphanumeric() || b == b'_' {
i += 1;
} else {
break;
}
}
results.push((start, i));
} else {
i += 1;
}
}
results
}
#[inline]
pub fn find_identifier_run(text: &str) -> Option<(usize, usize)> {
let bytes = text.as_bytes();
for i in 0..bytes.len() {
let b = bytes[i];
if b.is_ascii_alphabetic() || b == b'_' {
let start = i;
let mut j = i + 1;
while j < bytes.len() {
let b = bytes[j];
if b.is_ascii_alphanumeric() || b == b'_' {
j += 1;
} else {
break;
}
}
return Some((start, j));
}
}
None
}
#[inline]
pub fn find_identifier_run_all(text: &str) -> Vec<(usize, usize)> {
let mut results = Vec::new();
let bytes = text.as_bytes();
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if b.is_ascii_alphabetic() || b == b'_' {
let start = i;
i += 1;
while i < bytes.len() {
let b = bytes[i];
if b.is_ascii_alphanumeric() || b == b'_' {
i += 1;
} else {
break;
}
}
results.push((start, i));
} else {
i += 1;
}
}
results
}
#[inline]
pub fn find_quoted_string_all(text: &str) -> Vec<(usize, usize)> {
let mut results = Vec::new();
let bytes = text.as_bytes();
let mut pos = 0;
while pos < bytes.len() {
if let Some(start) = memchr(b'"', &bytes[pos..]) {
let abs_start = pos + start;
if abs_start + 1 < bytes.len() {
if let Some(end) = memchr(b'"', &bytes[abs_start + 1..]) {
if end > 0 {
results.push((abs_start, abs_start + 1 + end + 1));
pos = abs_start + 1 + end + 1;
continue;
}
}
}
pos = abs_start + 1;
} else {
break;
}
}
results
}
#[inline]
pub fn find_literal_ws_quoted(text: &str, literal: &str) -> Option<(usize, usize)> {
let finder = memmem::Finder::new(literal.as_bytes());
for pos in finder.find_iter(text.as_bytes()) {
let after = pos + literal.len();
if after >= text.len() {
continue;
}
let rest = &text[after..];
let mut ws_end = 0;
for ch in rest.chars() {
if ch.is_whitespace() {
ws_end += ch.len_utf8();
} else {
break;
}
}
if ws_end > 0 && after + ws_end < text.len() {
let after_ws = after + ws_end;
if text.as_bytes()[after_ws] == b'"' {
if let Some(end) = memchr(b'"', &text.as_bytes()[after_ws + 1..]) {
if end > 0 {
return Some((pos, after_ws + 1 + end + 1));
}
}
}
}
}
None
}
#[inline]
pub fn find_literal_ws_quoted_all(text: &str, literal: &str) -> Vec<(usize, usize)> {
let mut results = Vec::new();
let finder = memmem::Finder::new(literal.as_bytes());
for pos in finder.find_iter(text.as_bytes()) {
let after = pos + literal.len();
if after >= text.len() {
continue;
}
let rest = &text[after..];
let mut ws_end = 0;
for ch in rest.chars() {
if ch.is_whitespace() {
ws_end += ch.len_utf8();
} else {
break;
}
}
if ws_end > 0 && after + ws_end < text.len() {
let after_ws = after + ws_end;
if text.as_bytes()[after_ws] == b'"' {
if let Some(end) = memchr(b'"', &text.as_bytes()[after_ws + 1..]) {
if end > 0 {
results.push((pos, after_ws + 1 + end + 1));
}
}
}
}
}
results
}
#[inline]
pub fn find_literal_ws_digits(text: &str, literal: &str) -> Option<(usize, usize)> {
let finder = memmem::Finder::new(literal.as_bytes());
for pos in finder.find_iter(text.as_bytes()) {
let after = pos + literal.len();
if after >= text.len() {
continue;
}
let rest = &text[after..];
let bytes = rest.as_bytes();
let mut ws_end = 0;
for ch in rest.chars() {
if ch.is_whitespace() {
ws_end += ch.len_utf8();
} else {
break;
}
}
if ws_end > 0 && ws_end < bytes.len() && bytes[ws_end].is_ascii_digit() {
let mut digit_end = ws_end;
while digit_end < bytes.len() && bytes[digit_end].is_ascii_digit() {
digit_end += 1;
}
return Some((pos, after + digit_end));
}
}
None
}
#[inline]
pub fn find_literal_ws_digits_all(text: &str, literal: &str) -> Vec<(usize, usize)> {
let mut results = Vec::new();
let finder = memmem::Finder::new(literal.as_bytes());
for pos in finder.find_iter(text.as_bytes()) {
let after = pos + literal.len();
if after >= text.len() {
continue;
}
let rest = &text[after..];
let bytes = rest.as_bytes();
let mut ws_end = 0;
for ch in rest.chars() {
if ch.is_whitespace() {
ws_end += ch.len_utf8();
} else {
break;
}
}
if ws_end > 0 && ws_end < bytes.len() && bytes[ws_end].is_ascii_digit() {
let mut digit_end = ws_end;
while digit_end < bytes.len() && bytes[digit_end].is_ascii_digit() {
digit_end += 1;
}
results.push((pos, after + digit_end));
}
}
results
}
#[inline]
pub fn find_word_compare_digit(text: &str) -> Option<(usize, usize)> {
let bytes = text.as_bytes();
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if b.is_ascii_alphanumeric() || b == b'_' {
let start = i;
while i < bytes.len() {
let b = bytes[i];
if b.is_ascii_alphanumeric() || b == b'_' {
i += 1;
} else {
break;
}
}
while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t' || bytes[i] == b'\n') {
i += 1;
}
if i + 1 < bytes.len() && bytes[i] == b'>' && bytes[i + 1] == b'=' {
i += 2;
while i < bytes.len()
&& (bytes[i] == b' ' || bytes[i] == b'\t' || bytes[i] == b'\n')
{
i += 1;
}
if i < bytes.len() && bytes[i].is_ascii_digit() {
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
return Some((start, i));
}
}
i = start + 1;
} else {
i += 1;
}
}
None
}
#[inline]
pub fn find_word_compare_digit_all(text: &str) -> Vec<(usize, usize)> {
let mut results = Vec::new();
let bytes = text.as_bytes();
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if b.is_ascii_alphanumeric() || b == b'_' {
let start = i;
while i < bytes.len() {
let b = bytes[i];
if b.is_ascii_alphanumeric() || b == b'_' {
i += 1;
} else {
break;
}
}
let _word_end = i;
let _saved_i = i;
while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t' || bytes[i] == b'\n') {
i += 1;
}
if i + 1 < bytes.len() && bytes[i] == b'>' && bytes[i + 1] == b'=' {
i += 2;
while i < bytes.len()
&& (bytes[i] == b' ' || bytes[i] == b'\t' || bytes[i] == b'\n')
{
i += 1;
}
if i < bytes.len() && bytes[i].is_ascii_digit() {
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
results.push((start, i));
continue; }
}
i = start + 1;
} else {
i += 1;
}
}
results
}
#[inline]
pub fn find_alternation(ac: &AhoCorasick, text: &str) -> Option<(usize, usize)> {
ac.find(text).map(|m| (m.start(), m.end()))
}
#[inline]
pub fn find_alternation_all(ac: &AhoCorasick, text: &str) -> Vec<(usize, usize)> {
ac.find_iter(text).map(|m| (m.start(), m.end())).collect()
}
fn strip_simple_captures(pattern: &str) -> String {
let mut result = String::with_capacity(pattern.len());
let mut depth = 0;
let chars: Vec<char> = pattern.chars().collect();
let mut i = 0;
while i < chars.len() {
match chars[i] {
'(' => {
depth += 1;
i += 1;
}
')' => {
if depth > 0 {
depth -= 1;
}
i += 1;
}
_ => {
result.push(chars[i]);
i += 1;
}
}
}
result
}
fn detect_literal_dot_star_literal(pattern: &str) -> Option<(String, String, bool)> {
let (separator, lazy) = if pattern.contains(".*?") {
(".*?", true)
} else if pattern.contains(".*") {
(".*", false)
} else {
return None;
};
let mut parts = pattern.split(separator);
let prefix = parts.next()?;
let suffix = parts.next()?;
if parts.next().is_some()
|| prefix.is_empty()
|| suffix.is_empty()
|| !is_plain_literal_fragment(prefix)
|| !is_plain_literal_fragment(suffix)
{
return None;
}
Some((prefix.to_string(), suffix.to_string(), lazy))
}
fn is_plain_literal_fragment(fragment: &str) -> bool {
!fragment.contains([
'\\', '[', ']', '(', ')', '*', '+', '?', '{', '}', '|', '.', '^', '$',
])
}
pub fn detect_fast_path(pattern: &str) -> Option<FastPath> {
if pattern.starts_with('^') || pattern.ends_with('$') {
return None;
}
if let Some(rest) = pattern.strip_prefix("(?i)") {
let normalized = strip_simple_captures(rest);
if !normalized.contains([
'\\', '[', ']', '(', ')', '*', '+', '?', '{', '}', '.', '^', '$',
]) {
if normalized.contains('|') {
let alternatives: Vec<String> =
normalized.split('|').map(|s| s.to_string()).collect();
if alternatives.iter().all(|alt| !alt.is_empty()) {
if let Ok(ac) = AhoCorasick::builder()
.match_kind(aho_corasick::MatchKind::LeftmostFirst)
.ascii_case_insensitive(true)
.build(&alternatives)
{
return Some(FastPath::Alternation(Arc::new(ac)));
}
}
} else {
return Some(FastPath::LiteralCaseInsensitive(normalized.to_lowercase()));
}
}
}
let normalized = strip_simple_captures(pattern);
if let Some((prefix, suffix, lazy)) = detect_literal_dot_star_literal(&normalized) {
return Some(FastPath::LiteralDotStarLiteral {
prefix,
suffix,
lazy,
});
}
if !normalized.contains(['\\', '[', ']', '(', ')', '*', '+', '?', '{', '}', '|', '.']) {
return Some(FastPath::Literal(normalized.to_string()));
}
if normalized == r"\d+" {
return Some(FastPath::DigitRun);
}
if normalized == r"\w+" {
return Some(FastPath::WordRun);
}
if normalized == r"[a-zA-Z_]\w*" {
return Some(FastPath::IdentifierRun);
}
if let Some(rest) = normalized.strip_suffix(r"\s+") {
if !rest.is_empty()
&& !rest.contains(['\\', '[', ']', '(', ')', '*', '+', '?', '{', '}', '|', '.'])
{
return Some(FastPath::LiteralPlusWhitespace(rest.to_string()));
}
}
if normalized == r#""[^"]+""# {
return Some(FastPath::QuotedString);
}
if let Some(mid) = normalized.strip_suffix(r#""[^"]+""#) {
if let Some(literal) = mid.strip_suffix(r"\s+") {
if !literal.is_empty()
&& !literal.contains(['\\', '[', ']', '(', ')', '*', '+', '?', '{', '}', '|', '.'])
{
return Some(FastPath::LiteralWhitespaceQuoted(literal.to_string()));
}
}
}
if let Some(mid) = normalized.strip_suffix(r"\d+") {
if let Some(literal) = mid.strip_suffix(r"\s+") {
if !literal.is_empty()
&& !literal.contains(['\\', '[', ']', '(', ')', '*', '+', '?', '{', '}', '|', '.'])
{
return Some(FastPath::LiteralWhitespaceDigits(literal.to_string()));
}
}
}
if let Some(mid) = normalized.strip_suffix(r"\w+") {
if let Some(literal) = mid.strip_suffix(r"\s+") {
if !literal.is_empty()
&& !literal.contains(['\\', '[', ']', '(', ')', '*', '+', '?', '{', '}', '|', '.'])
{
return Some(FastPath::LiteralWhitespaceWord(literal.to_string()));
}
}
}
if normalized.contains('|')
&& !pattern.contains(['\\', '[', ']', '(', ')', '*', '+', '?', '{', '}', '.'])
{
let alternatives: Vec<String> = normalized.split('|').map(|s| s.to_string()).collect();
if alternatives.iter().all(|alt| !alt.is_empty()) {
if let Ok(ac) = AhoCorasick::builder()
.match_kind(aho_corasick::MatchKind::LeftmostFirst)
.build(&alternatives)
{
return Some(FastPath::Alternation(Arc::new(ac)));
}
}
}
None
}
#[inline]
pub fn find_digit_run_at(text: &str, start_pos: usize) -> Option<(usize, usize)> {
if start_pos >= text.len() {
return None;
}
let bytes = &text.as_bytes()[start_pos..];
let mut i = 0;
while i < bytes.len() {
if bytes[i].is_ascii_digit() {
let match_start = start_pos + i;
let mut end = i;
while end < bytes.len() && bytes[end].is_ascii_digit() {
end += 1;
}
return Some((match_start, start_pos + end));
}
i += 1;
}
None
}
#[inline]
pub fn find_word_run_at(text: &str, start_pos: usize) -> Option<(usize, usize)> {
if start_pos >= text.len() {
return None;
}
let bytes = &text.as_bytes()[start_pos..];
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if b.is_ascii_alphanumeric() || b == b'_' {
let match_start = start_pos + i;
let mut end = i;
while end < bytes.len() {
let b = bytes[end];
if b.is_ascii_alphanumeric() || b == b'_' {
end += 1;
} else {
break;
}
}
return Some((match_start, start_pos + end));
}
i += 1;
}
None
}
#[inline]
pub fn find_identifier_run_at(text: &str, start_pos: usize) -> Option<(usize, usize)> {
if start_pos >= text.len() {
return None;
}
let bytes = &text.as_bytes()[start_pos..];
let mut i = 0;
while i < bytes.len() {
let b = bytes[i];
if b.is_ascii_alphabetic() || b == b'_' {
let match_start = start_pos + i;
let mut end = i + 1;
while end < bytes.len() {
let b = bytes[end];
if b.is_ascii_alphanumeric() || b == b'_' {
end += 1;
} else {
break;
}
}
return Some((match_start, start_pos + end));
}
i += 1;
}
None
}
#[inline]
pub fn find_quoted_string_at(text: &str, start_pos: usize) -> Option<(usize, usize)> {
if start_pos >= text.len() {
return None;
}
let bytes = text.as_bytes();
let search_bytes = &bytes[start_pos..];
let first_quote = memchr(b'"', search_bytes)?;
let quote_pos = start_pos + first_quote;
if quote_pos + 1 >= bytes.len() {
return None;
}
let closing_quote = memchr(b'"', &bytes[quote_pos + 1..])?;
if closing_quote == 0 {
return None;
}
Some((quote_pos, quote_pos + 1 + closing_quote + 1))
}
#[inline]
pub fn find_literal_at(text: &str, literal: &str, start_pos: usize) -> Option<(usize, usize)> {
if start_pos >= text.len() {
return None;
}
let search_text = &text[start_pos..];
let len = literal.len();
if len >= 3 {
let finder = memmem::Finder::new(literal.as_bytes());
finder
.find(search_text.as_bytes())
.map(|pos| (start_pos + pos, start_pos + pos + len))
} else if len == 1 {
let byte = literal.as_bytes()[0];
memchr(byte, search_text.as_bytes()).map(|pos| (start_pos + pos, start_pos + pos + 1))
} else {
search_text
.find(literal)
.map(|pos| (start_pos + pos, start_pos + pos + len))
}
}
#[inline]
pub fn find_literal_plus_whitespace_at(
text: &str,
literal: &str,
start_pos: usize,
) -> Option<(usize, usize)> {
if start_pos >= text.len() {
return None;
}
let search_text = &text[start_pos..];
let finder = memmem::Finder::new(literal.as_bytes());
let lit_pos = finder.find(search_text.as_bytes())?;
let abs_pos = start_pos + lit_pos;
let after = abs_pos + literal.len();
if after >= text.len() {
return None;
}
let rest = &text[after..];
let mut matched = 0;
for ch in rest.chars() {
if ch.is_whitespace() {
matched += ch.len_utf8();
} else {
break;
}
}
if matched > 0 {
Some((abs_pos, after + matched))
} else {
None
}
}
#[inline]
pub fn find_literal_ws_word(text: &str, literal: &str) -> Option<(usize, usize)> {
let finder = memmem::Finder::new(literal.as_bytes());
for pos in finder.find_iter(text.as_bytes()) {
let after = pos + literal.len();
if after >= text.len() {
continue;
}
let rest = &text[after..];
let mut ws_count = 0;
for ch in rest.chars() {
if ch.is_whitespace() {
ws_count += ch.len_utf8();
} else {
break;
}
}
if ws_count == 0 {
continue;
}
let word_start = after + ws_count;
if word_start >= text.len() {
continue;
}
let bytes = &text.as_bytes()[word_start..];
let mut word_len = 0;
for &b in bytes {
if b.is_ascii_alphanumeric() || b == b'_' {
word_len += 1;
} else {
break;
}
}
if word_len > 0 {
return Some((pos, word_start + word_len));
}
}
None
}
#[inline]
pub fn find_literal_ws_word_all(text: &str, literal: &str) -> Vec<(usize, usize)> {
let mut results = Vec::new();
let finder = memmem::Finder::new(literal.as_bytes());
for pos in finder.find_iter(text.as_bytes()) {
let after = pos + literal.len();
if after >= text.len() {
continue;
}
let rest = &text[after..];
let mut ws_count = 0;
for ch in rest.chars() {
if ch.is_whitespace() {
ws_count += ch.len_utf8();
} else {
break;
}
}
if ws_count == 0 {
continue;
}
let word_start = after + ws_count;
if word_start >= text.len() {
continue;
}
let bytes = &text.as_bytes()[word_start..];
let mut word_len = 0;
for &b in bytes {
if b.is_ascii_alphanumeric() || b == b'_' {
word_len += 1;
} else {
break;
}
}
if word_len > 0 {
results.push((pos, word_start + word_len));
}
}
results
}
#[inline]
pub fn find_literal_ws_word_at(
text: &str,
literal: &str,
start_pos: usize,
) -> Option<(usize, usize)> {
if start_pos >= text.len() {
return None;
}
let search_text = &text[start_pos..];
find_literal_ws_word(search_text, literal)
.map(|(rel_start, rel_end)| (start_pos + rel_start, start_pos + rel_end))
}
#[derive(Clone)]
pub enum FastPath {
Literal(String),
LiteralCaseInsensitive(String), LiteralPlusWhitespace(String),
LiteralWhitespaceQuoted(String), LiteralWhitespaceDigits(String), LiteralWhitespaceWord(String), LiteralDotStarLiteral {
prefix: String,
suffix: String,
lazy: bool,
},
WordCompareDigit, Alternation(Arc<AhoCorasick>), DigitRun,
WordRun,
IdentifierRun, QuotedString,
CaptureDFA(Arc<crate::engine::capture_dfa::CaptureDFA>), }
impl std::fmt::Debug for FastPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FastPath::Literal(s) => write!(f, "Literal({:?})", s),
FastPath::LiteralCaseInsensitive(s) => write!(f, "LiteralCaseInsensitive({:?})", s),
FastPath::LiteralPlusWhitespace(s) => write!(f, "LiteralPlusWhitespace({:?})", s),
FastPath::LiteralWhitespaceQuoted(s) => write!(f, "LiteralWhitespaceQuoted({:?})", s),
FastPath::LiteralWhitespaceDigits(s) => write!(f, "LiteralWhitespaceDigits({:?})", s),
FastPath::LiteralWhitespaceWord(s) => write!(f, "LiteralWhitespaceWord({:?})", s),
FastPath::LiteralDotStarLiteral {
prefix,
suffix,
lazy,
} => write!(
f,
"LiteralDotStarLiteral({:?}, {:?}, lazy={})",
prefix, suffix, lazy
),
FastPath::WordCompareDigit => write!(f, "WordCompareDigit"),
FastPath::Alternation(_) => write!(f, "Alternation(<AhoCorasick>)"),
FastPath::DigitRun => write!(f, "DigitRun"),
FastPath::WordRun => write!(f, "WordRun"),
FastPath::IdentifierRun => write!(f, "IdentifierRun"),
FastPath::QuotedString => write!(f, "QuotedString"),
FastPath::CaptureDFA(_) => write!(f, "CaptureDFA"),
}
}
}
impl FastPath {
#[inline]
pub fn find(&self, text: &str) -> Option<(usize, usize)> {
match self {
FastPath::Literal(s) => find_literal(text, s),
FastPath::LiteralCaseInsensitive(s) => find_literal_case_insensitive(text, s),
FastPath::LiteralPlusWhitespace(s) => find_literal_plus_whitespace(text, s),
FastPath::LiteralWhitespaceQuoted(s) => find_literal_ws_quoted(text, s),
FastPath::LiteralWhitespaceDigits(s) => find_literal_ws_digits(text, s),
FastPath::LiteralWhitespaceWord(s) => find_literal_ws_word(text, s),
FastPath::LiteralDotStarLiteral {
prefix,
suffix,
lazy,
} => find_literal_dot_star_literal(text, prefix, suffix, *lazy),
FastPath::WordCompareDigit => find_word_compare_digit(text),
FastPath::Alternation(ac) => find_alternation(ac, text),
FastPath::DigitRun => find_digit_run(text),
FastPath::WordRun => find_word_run(text),
FastPath::IdentifierRun => find_identifier_run(text),
FastPath::QuotedString => find_quoted_string(text),
FastPath::CaptureDFA(dfa) => dfa.find(text),
}
}
#[inline]
pub fn find_all(&self, text: &str) -> Vec<(usize, usize)> {
match self {
FastPath::Literal(s) => find_literal_all(text, s),
FastPath::LiteralCaseInsensitive(s) => find_literal_case_insensitive_all(text, s),
FastPath::LiteralPlusWhitespace(s) => find_literal_plus_whitespace_all(text, s),
FastPath::LiteralWhitespaceQuoted(s) => find_literal_ws_quoted_all(text, s),
FastPath::LiteralWhitespaceDigits(s) => find_literal_ws_digits_all(text, s),
FastPath::LiteralWhitespaceWord(s) => find_literal_ws_word_all(text, s),
FastPath::LiteralDotStarLiteral {
prefix,
suffix,
lazy,
} => find_literal_dot_star_literal_all(text, prefix, suffix, *lazy),
FastPath::WordCompareDigit => find_word_compare_digit_all(text),
FastPath::Alternation(ac) => find_alternation_all(ac, text),
FastPath::DigitRun => find_digit_run_all(text),
FastPath::WordRun => find_word_run_all(text),
FastPath::IdentifierRun => find_identifier_run_all(text),
FastPath::QuotedString => find_quoted_string_all(text),
FastPath::CaptureDFA(dfa) => {
let mut results = Vec::new();
let mut pos = 0;
while pos < text.len() {
if let Some((start, end)) = dfa.find(&text[pos..]) {
let abs_start = pos + start;
let abs_end = pos + end;
results.push((abs_start, abs_end));
pos = abs_end.max(pos + 1); } else {
break;
}
}
results
}
}
}
#[inline]
pub fn find_at(&self, text: &str, start_pos: usize) -> Option<(usize, usize)> {
match self {
FastPath::Literal(s) => find_literal_at(text, s, start_pos),
FastPath::LiteralPlusWhitespace(s) => {
find_literal_plus_whitespace_at(text, s, start_pos)
}
FastPath::LiteralWhitespaceWord(s) => find_literal_ws_word_at(text, s, start_pos),
FastPath::LiteralDotStarLiteral {
prefix,
suffix,
lazy,
} => find_literal_dot_star_literal_at(text, prefix, suffix, *lazy, start_pos),
FastPath::DigitRun => find_digit_run_at(text, start_pos),
FastPath::WordRun => find_word_run_at(text, start_pos),
FastPath::IdentifierRun => find_identifier_run_at(text, start_pos),
FastPath::QuotedString => find_quoted_string_at(text, start_pos),
_ => {
if start_pos >= text.len() {
return None;
}
let remaining = &text[start_pos..];
self.find(remaining)
.map(|(rel_start, rel_end)| (start_pos + rel_start, start_pos + rel_end))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_fast_paths() {
assert!(matches!(detect_fast_path(r"\d+"), Some(FastPath::DigitRun)));
assert!(matches!(detect_fast_path(r"\w+"), Some(FastPath::WordRun)));
assert!(matches!(
detect_fast_path("hello"),
Some(FastPath::Literal(_))
));
assert!(matches!(
detect_fast_path(r"rule\s+"),
Some(FastPath::LiteralPlusWhitespace(_))
));
}
#[test]
fn test_fast_paths() {
let text = "rule Test 123 hello";
let fp = FastPath::DigitRun;
assert_eq!(fp.find(text), Some((10, 13)));
let fp = FastPath::WordRun;
assert_eq!(fp.find(text), Some((0, 4)));
let fp = FastPath::Literal("hello".to_string());
assert_eq!(fp.find(text), Some((14, 19)));
}
}