Skip to main content

Crate krabka_java_regex

Crate krabka_java_regex 

Source
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))*, which fancy_regex cannot parse.
  • A backreference to a group the pattern does not have, such as (a)\2, which fancy_regex refuses, 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§

JavaPattern
A java.util.regex.Pattern compiled for matches() and replaceAll/replaceFirst.
MatchError
A match this crate gives no answer for: a replacement string Java’s Matcher.appendReplacement refuses, which Java reports as IllegalArgumentException or IndexOutOfBoundsException, or a search that ran past the backtrack limit.
PatternError
A pattern Java’s Pattern.compile refuses with a PatternSyntaxException, or one this crate cannot read as Java does.