#[derive(Debug, Clone, PartialEq)]
pub struct LiteralSet {
pub literals: Vec<Literal>,
pub kind: LiteralKind,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Literal {
pub text: String,
pub is_exact: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LiteralKind {
Prefix,
Suffix,
Inner,
None,
}
impl LiteralSet {
pub fn empty() -> Self {
LiteralSet {
literals: Vec::new(),
kind: LiteralKind::None,
}
}
pub fn is_empty(&self) -> bool {
self.literals.is_empty()
}
pub fn longest_common_prefix(&self) -> Option<&str> {
if self.literals.is_empty() {
return None;
}
let first = &self.literals[0].text;
let mut prefix_len = first.len();
for lit in &self.literals[1..] {
let common = first
.chars()
.zip(lit.text.chars())
.take_while(|(a, b)| a == b)
.count();
prefix_len = prefix_len.min(common);
if prefix_len == 0 {
return None;
}
}
Some(
&first[..first
.char_indices()
.nth(prefix_len)
.map(|(i, _)| i)
.unwrap_or(first.len())],
)
}
}
pub fn extract_from_pattern(pattern: &str) -> LiteralSet {
if let Some(literals) = extract_alternation_literals(pattern) {
if !literals.is_empty() {
return LiteralSet {
literals,
kind: LiteralKind::Prefix,
};
}
}
if let Some(prefix) = extract_simple_prefix(pattern) {
if prefix.len() >= 3 {
return LiteralSet {
literals: vec![Literal {
text: prefix,
is_exact: true,
}],
kind: LiteralKind::Prefix,
};
}
}
if let Some(anchor) = find_inner_anchor(pattern) {
return LiteralSet {
literals: vec![Literal {
text: anchor,
is_exact: false,
}],
kind: LiteralKind::Inner,
};
}
LiteralSet::empty()
}
fn extract_simple_prefix(pattern: &str) -> Option<String> {
let mut prefix = String::new();
let mut chars = pattern.chars().peekable();
while let Some(&ch) = chars.peek() {
match ch {
'.' | '*' | '+' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' => break,
'\\' => {
chars.next();
if let Some(next) = chars.peek() {
if !matches!(next, 'd' | 'w' | 's' | 'D' | 'W' | 'S' | 'b' | 'B') {
prefix.push(*next);
chars.next();
} else {
break;
}
}
}
_ => {
prefix.push(ch);
chars.next();
}
}
}
if prefix.is_empty() {
None
} else {
Some(prefix)
}
}
fn extract_alternation_prefix(pattern: &str) -> Option<String> {
if !pattern.starts_with('(') {
return None;
}
let end = pattern.find(')')?;
let inner = &pattern[1..end];
if !inner.contains('|') {
return None;
}
let branches: Vec<&str> = inner.split('|').collect();
if branches.len() < 2 {
return None;
}
let first = branches[0];
let mut prefix_len = first.len();
for branch in &branches[1..] {
let common = first
.chars()
.zip(branch.chars())
.take_while(|(a, b)| a == b)
.count();
prefix_len = prefix_len.min(common);
if prefix_len == 0 {
return None;
}
}
Some(first[..prefix_len].to_string())
}
fn extract_alternation_literals(pattern: &str) -> Option<Vec<Literal>> {
let inner = if pattern.starts_with('(') {
let end = pattern.find(')')?;
&pattern[1..end]
} else if pattern.contains('|') && !pattern.contains('(') {
pattern
} else {
return None;
};
if !inner.contains('|') {
return None;
}
let branches: Vec<&str> = inner.split('|').collect();
if branches.len() < 2 {
return None;
}
let mut literals = Vec::new();
for branch in branches {
if let Some(prefix) = extract_simple_prefix(branch) {
if prefix.len() >= 2 {
literals.push(Literal {
text: prefix,
is_exact: false,
});
}
} else if !branch.is_empty() {
let first_char: String = branch.chars().take(1).collect();
if !first_char.is_empty() {
literals.push(Literal {
text: first_char,
is_exact: false,
});
}
}
}
if literals.is_empty() {
None
} else {
Some(literals)
}
}
fn find_inner_anchor(pattern: &str) -> Option<String> {
let bytes = pattern.as_bytes();
let mut i = 1;
while i < bytes.len() {
let prev = bytes[i - 1];
let curr = bytes[i];
if curr == b'\\' && i + 1 < bytes.len() {
let next = bytes[i + 1];
if matches!(
next,
b'.' | b'*' | b'+' | b'?' | b'[' | b']' | b'(' | b')' | b'{' | b'}' | b'\\' | b'|'
) {
if matches!(prev, b'+' | b'*' | b'?') {
return Some((next as char).to_string());
}
}
i += 2; continue;
}
if matches!(prev, b'+' | b'*' | b'?')
&& curr.is_ascii()
&& !curr.is_ascii_alphanumeric()
&& curr != b'\\'
{
if i >= 2 && bytes[i - 2] == b'\\' {
i += 1;
continue;
}
return Some((curr as char).to_string());
}
i += 1;
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_from_pattern() {
let lits = extract_from_pattern("hello.*");
assert_eq!(lits.literals.len(), 1);
assert_eq!(lits.literals[0].text, "hello");
let lits = extract_from_pattern("(http|https)://.*");
assert_eq!(lits.literals.len(), 2);
assert_eq!(lits.literals[0].text, "http");
assert_eq!(lits.literals[1].text, "https");
assert_eq!(lits.kind, LiteralKind::Prefix);
let lits = extract_from_pattern(r"\w+@\w+\.\w+");
assert_eq!(lits.literals.len(), 1);
assert_eq!(lits.literals[0].text, "@");
assert_eq!(lits.kind, LiteralKind::Inner);
}
}