Expand description
Java’s java.util.regex in Rust: a JavaPattern compiles a
java.util.regex.Pattern and answers Matcher.matches(),
String.replaceAll and String.replaceFirst as JDK 25 does.
Krabka reads three Apache Kafka configs whose values are Java regular
expressions: ssl.principal.mapping.rules, the Kerberos auth_to_local
rules, and the match patterns of a KIP-714 CLIENT_METRICS
subscription. A rule that matches differently from Java maps a principal
to another name, so the crate’s job is Java’s answer, quirks included.
use {assert2::assert, krabka_java_regex::JavaPattern};
let rule = JavaPattern::compile("CN=([^,]+),OU=(?i)ops.*")?;
assert!(rule.matches("CN=alice,OU=OPS-EU")?);
assert!(rule.replace("CN=alice,OU=ops", "$1", true)? == "alice");§How it reads a pattern as Java does
The regex crate differs from Java in the two places these lean on.
Matcher.matches() succeeds when some way of matching spans the whole
input, including through backtracking and lazy quantifiers, where a
leftmost-first search stops at the first match it finds. And
Matcher.replaceAll reads $1_x as group 1 followed by _x, where the
regex crate reads a group named 1_x. JavaPattern gives both the
Java answer.
The pattern itself is rewritten by translate_java first, which also gives
Java’s reading of \w, ., and (?i): Java folds ASCII case only, unless
(?u) asks for Unicode case as well, so (?i)service- does not match
ſervice- (long s). A backreference under a case flag, \R, and a group
inside a look-around or an atomic group match in ways fancy_regex cannot
give, so a pattern that has one runs on the matcher in backtrack
instead. So does a pattern fancy_regex cannot compile, such as a
look-behind that is not of one length with \R in it, or a very large
counted repeat. A pattern Java’s Pattern.compile refuses is refused.
§Limits
A search takes at most 1,000,000 backtracking steps, fancy_regex’s
default; on the backtracking matcher each iteration of a repeat is a step
too, so (){1000000000} is an error and not a billion iterations. The
backtracking matcher keeps its alternatives on a stack of its own, so a
long input cannot overflow the thread’s. A search past the limit is a
MatchError, not a no match. Java answers such a search, however
slowly.
The matcher answers a pattern fancy_regex cannot compile, where it
knows every node of it: a look-behind that is not of one length with
\R, a look-around, an atomic group or a group in it, such as
(?<=\R?x)y, and a counted repeat too large for the regex crate to
build at its default size limit, such as x{1,1000000000}, in a
look-behind or outside one. It counts the iterations of a repeat, so such
a pattern compiles in milliseconds, but it tries every start in Java’s
window of a look-behind, which for such a repeat is the whole input, and a
long input can take it past the step limit.
JavaPattern::compile refuses the patterns below, which Java compiles,
rather than match them another way. Each is a PatternError.
- A Unicode block, such as
\p{InGreek}, and\X. - A named char,
\N{name}. - A quantifier of a look-around, such as
(?=(a))*, whichfancy_regexcannot parse. - A backreference to a group the pattern does not have, such as
(a)\2, whichfancy_regexrefuses, and which never matches in Java. - A look-behind whose maximum length overflows Java’s
int, such as(?<=a*b*), where Java then tries fewer starts than the look-behind allows. Where the overflowed maximum still covers every start, as in(?<=a*bc), the pattern compiles. - A look-behind whose maximum overflows, in a pattern with a char past
U+FFFF from the look-behind on, as in
(?<=a*b*c*x)y😀. Java then counts the look-behind’s window in code points (BehindS), with arithmetic the crate does not follow.
One more input limit applies at match time. Where an overflowed maximum is
at least 2^30 and covers every start only up to that length, as in
(?<=a*b*c*x), an input of more UTF-16 units than the maximum is a
MatchError.
Java reads a char past U+FFFF as two UTF-16 units, and its look-behind
counts its window in units where the pattern has no such char from the
look-behind on, so it may start between the two units and read a lone low
surrogate there. An input with such a char goes to the backtracking matcher
where the pattern has such a look-behind, and the matcher reads it as Java
does. A group the look-behind starts or ends between the two units holds a
lone surrogate in Java, which a Rust string cannot, so a replacement that
takes its text is a MatchError. A search still starts at a char only,
where Java’s Matcher.find also tries the low surrogate of one: Java
finds (?<!^). in 😀x at the low surrogate and at x, and the crate at
x only.
Structs§
- Java
Pattern - A
java.util.regex.Patterncompiled formatches()andreplaceAll/replaceFirst. - Match
Error - A match this crate gives no answer for: a replacement string Java’s
Matcher.appendReplacementrefuses, which Java reports asIllegalArgumentExceptionorIndexOutOfBoundsException, or a search that ran past the backtrack limit. - Pattern
Error - A pattern Java’s
Pattern.compilerefuses with aPatternSyntaxException, or one this crate cannot read as Java does.