use std::error::Error;
use std::fmt::{Display, Formatter};
use regex::Regex;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PatternUtilsError {
NullPointer,
PatternSyntax {
pattern: String,
message: String,
},
}
impl PatternUtilsError {
#[must_use]
pub const fn class_name(&self) -> &'static str {
match self {
Self::NullPointer => "java.lang.NullPointerException",
Self::PatternSyntax { .. } => "java.util.regex.PatternSyntaxException",
}
}
#[must_use]
pub fn get_message(&self) -> Option<&str> {
match self {
Self::NullPointer => None,
Self::PatternSyntax { message, .. } => Some(message),
}
}
#[must_use]
pub fn get_pattern(&self) -> Option<&str> {
match self {
Self::NullPointer => None,
Self::PatternSyntax { pattern, .. } => Some(pattern),
}
}
}
impl Display for PatternUtilsError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
if let Some(message) = self.get_message() {
formatter.write_str(message)?;
}
Ok(())
}
}
impl Error for PatternUtilsError {}
#[derive(Clone, Debug)]
pub struct StringPattern {
process_pattern: String,
regex: Regex,
}
impl StringPattern {
#[must_use]
pub fn as_str(&self) -> &str {
&self.process_pattern
}
pub fn matches(&self, input: Option<&str>) -> Result<bool, PatternUtilsError> {
let input = input.ok_or(PatternUtilsError::NullPointer)?;
Ok(self.regex.is_match(input))
}
}
pub struct PatternUtils;
impl PatternUtils {
pub fn str_pattern_to_pattern(
pattern: Option<&str>,
) -> Result<StringPattern, PatternUtilsError> {
let pattern = pattern.ok_or(PatternUtilsError::NullPointer)?;
let process_pattern = pattern_source(pattern);
let translated_pattern = translate_java_regex(&process_pattern);
let full_match_pattern = format!(r"\A(?:{translated_pattern})\z");
let regex =
Regex::new(&full_match_pattern).map_err(|error| PatternUtilsError::PatternSyntax {
pattern: process_pattern.clone(),
message: error.to_string(),
})?;
Ok(StringPattern {
process_pattern,
regex,
})
}
}
fn pattern_source(pattern: &str) -> String {
let pattern = pattern
.replace('.', r"\.")
.replace('(', r"\(")
.replace(')', r"\)")
.replace('[', r"\[")
.replace(']', r"\]")
.replace('?', r"\?")
.replace('$', r"\$")
.replace('+', r"\+")
.replace('*', "(?:.*?)");
format!("^{pattern}$")
}
fn translate_java_regex(pattern: &str) -> String {
let mut translated = String::with_capacity(pattern.len() + 16);
let mut remaining = pattern;
while !remaining.is_empty() {
if let Some(quoted) = remaining.strip_prefix(r"\Q") {
if let Some(end) = quoted.find(r"\E") {
translated.push_str(®ex::escape("ed[..end]));
remaining = "ed[end + 2..];
} else {
translated.push_str(®ex::escape(quoted));
remaining = "";
}
continue;
}
if let Some(suffix) = remaining.strip_prefix("(?:.*?)") {
translated.push_str(r"(?:[^\n\r\u{0085}\u{2028}\u{2029}]*?)");
remaining = suffix;
continue;
}
let character = remaining.chars().next().expect("non-empty pattern");
remaining = &remaining[character.len_utf8()..];
if character != '\\' {
translated.push(character);
continue;
}
let Some(escaped) = remaining.chars().next() else {
translated.push('\\');
continue;
};
remaining = &remaining[escaped.len_utf8()..];
match escaped {
'd' => translated.push_str("[0-9]"),
'D' => translated.push_str("[^0-9]"),
'w' => translated.push_str("[A-Za-z_0-9]"),
'W' => translated.push_str("[^A-Za-z_0-9]"),
's' => translated.push_str(r"[\x20\t\n\x0B\f\r]"),
'S' => translated.push_str(r"[^\x20\t\n\x0B\f\r]"),
'h' => translated.push_str(
r"[\x20\t\u{00A0}\u{1680}\u{180E}\u{2000}-\u{200A}\u{202F}\u{205F}\u{3000}]",
),
'H' => translated.push_str(
r"[^\x20\t\u{00A0}\u{1680}\u{180E}\u{2000}-\u{200A}\u{202F}\u{205F}\u{3000}]",
),
'v' => translated.push_str(r"[\n\x0B\f\r\u{0085}\u{2028}\u{2029}]"),
'V' => translated.push_str(r"[^\n\x0B\f\r\u{0085}\u{2028}\u{2029}]"),
'R' => translated.push_str(r"(?:\r\n|[\n\x0B\f\r\u{0085}\u{2028}\u{2029}])"),
other => {
translated.push('\\');
translated.push(other);
}
}
}
translated
}
#[cfg(test)]
mod tests {
use std::fmt::Write;
use super::{PatternUtils, PatternUtilsError, translate_java_regex};
struct FailingWriter;
impl Write for FailingWriter {
fn write_str(&mut self, _value: &str) -> std::fmt::Result {
Err(std::fmt::Error)
}
}
#[test]
fn converts_in_exact_java_replacement_order_and_matches_entire_input() {
let pattern =
PatternUtils::str_pattern_to_pattern(Some("a.(b)[c]?$+*")).expect("valid pattern");
assert_eq!(pattern.as_str(), r"^a\.\(b\)\[c\]\?\$\+(?:.*?)$");
assert_eq!(pattern.matches(Some("a.(b)[c]?$+tail")), Ok(true));
assert_eq!(pattern.matches(Some("prefixa.(b)[c]?$+tail")), Ok(false));
}
#[test]
fn preserves_java_ascii_classes_and_line_terminator_dot_rules() {
let digit = PatternUtils::str_pattern_to_pattern(Some(r"\d*")).expect("digit");
assert_eq!(digit.matches(Some("1tail")), Ok(true));
assert_eq!(digit.matches(Some("١tail")), Ok(false));
assert_eq!(digit.matches(Some("1line\nbreak")), Ok(false));
let classes = [
(r"\D", "x", true),
(r"\w", "_", true),
(r"\W", "-", true),
(r"\s", "\t", true),
(r"\S", "x", true),
(r"\h", "\u{3000}", true),
(r"\H", "x", true),
(r"\v", "\u{2028}", true),
(r"\V", "x", true),
(r"\R", "\r\n", true),
];
for (source, input, expected) in classes {
let pattern = PatternUtils::str_pattern_to_pattern(Some(source)).expect("class");
assert_eq!(pattern.matches(Some(input)), Ok(expected));
}
}
#[test]
fn preserves_regex_operators_that_java_source_does_not_escape() {
for (source, matching, rejected) in [
("foo|bar", "foo", "foobar"),
("a{2}", "aa", "a"),
("^name", "name", "xname"),
] {
let pattern = PatternUtils::str_pattern_to_pattern(Some(source)).expect("pattern");
assert_eq!(pattern.matches(Some(matching)), Ok(true));
assert_eq!(pattern.matches(Some(rejected)), Ok(false));
}
}
#[test]
fn preserves_java_quoted_literals_before_interpreting_generated_wildcards() {
let quoted =
PatternUtils::str_pattern_to_pattern(Some(r"\Qfoo|*\E")).expect("quoted pattern");
assert_eq!(quoted.as_str(), r"^\Qfoo|(?:.*?)\E$");
assert_eq!(quoted.matches(Some("foo|(?:.*?)")), Ok(true));
assert_eq!(quoted.matches(Some("foo|anything")), Ok(false));
let unterminated =
PatternUtils::str_pattern_to_pattern(Some(r"\Qfoo")).expect("unterminated quote");
assert_eq!(unterminated.matches(Some("foo$")), Ok(true));
assert_eq!(unterminated.matches(Some("foo")), Ok(false));
}
#[test]
fn maps_null_and_syntax_failures_with_stable_metadata() {
let null = PatternUtils::str_pattern_to_pattern(None).expect_err("null");
assert_eq!(null, PatternUtilsError::NullPointer);
assert_eq!(null.class_name(), "java.lang.NullPointerException");
assert_eq!(null.get_message(), None);
assert_eq!(null.get_pattern(), None);
assert_eq!(null.to_string(), "");
let syntax = PatternUtils::str_pattern_to_pattern(Some("{")).expect_err("syntax");
assert_eq!(
syntax.class_name(),
"java.util.regex.PatternSyntaxException"
);
assert_eq!(syntax.get_pattern(), Some("^{$"));
assert!(syntax.get_message().is_some());
assert!(!syntax.to_string().is_empty());
assert!(write!(&mut FailingWriter, "{syntax}").is_err());
}
#[test]
fn preserves_the_trailing_escape_effect_on_the_generated_end_anchor() {
assert_eq!(translate_java_regex(r"^abc\"), r"^abc\");
let pattern = PatternUtils::str_pattern_to_pattern(Some(r"abc\")).expect("escaped anchor");
assert_eq!(pattern.as_str(), r"^abc\$");
assert_eq!(pattern.matches(Some("abc$")), Ok(true));
assert_eq!(pattern.matches(Some("abc")), Ok(false));
}
}