pub fn is_plain_literal(s: &str) -> bool {
!s.bytes().any(|b| {
matches!(
b,
b'.' | b'\\'
| b'|'
| b'('
| b')'
| b'['
| b']'
| b'{'
| b'}'
| b'^'
| b'$'
| b'*'
| b'+'
| b'?'
)
})
}
pub fn is_fixed_string(s: &str) -> bool {
!s.is_empty() && is_plain_literal(s)
}
pub enum Anchor {
Start,
End,
}
pub fn single_anchor(inner: &str) -> Option<(Anchor, &str)> {
let (anchor, rest) = if let Some(rest) = inner.strip_prefix('^') {
if rest.ends_with('$') {
return None;
}
(Anchor::Start, rest)
} else {
(Anchor::End, inner.strip_suffix('$')?)
};
is_fixed_string(rest).then_some((anchor, rest))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plain_literal_rejects_metacharacters() {
assert!(is_plain_literal("abc"));
assert!(is_plain_literal("hello world"));
assert!(!is_plain_literal("a.b"));
assert!(!is_plain_literal("a\\.b"));
assert!(!is_plain_literal("^abc"));
assert!(!is_plain_literal("a+b"));
}
#[test]
fn fixed_string_requires_nonempty_plain() {
assert!(is_fixed_string("abc"));
assert!(!is_fixed_string(""));
assert!(!is_fixed_string("a.b"));
}
#[test]
fn single_anchor_classifies_one_end() {
assert!(matches!(
single_anchor("^abc"),
Some((Anchor::Start, "abc"))
));
assert!(matches!(single_anchor("abc$"), Some((Anchor::End, "abc"))));
assert!(single_anchor("^abc$").is_none());
assert!(single_anchor("abc").is_none());
assert!(single_anchor("^").is_none());
assert!(single_anchor("$").is_none());
assert!(single_anchor("^a.b").is_none());
}
}