Package groovy.util.regex

# Class RegexGuard

java.lang.Object
groovy.util.regex.RegexGuard

@Incubating public final class RegexGuard extends Object
Deadline-guarded regular expression evaluation, protecting against Regular Expression Denial of Service (ReDoS) when patterns or inputs come from untrusted sources. Java's regex engine has no native timeout, so catastrophic backtracking can hang a thread indefinitely.

The guard works by wrapping the input `CharSequence` so that `charAt` periodically checks a deadline and throws `RegexTimeoutException` once it has passed. Backtracking repeatedly re-reads input characters, so a runaway match is cut off shortly after the deadline without needing watchdog threads or thread interruption.

     import groovy.util.regex.RegexGuard
     import groovy.util.regex.RegexTimeoutException
     import java.time.Duration

     assert RegexGuard.matches(/gro+vy/, 'groovy', 200)
     def m = RegexGuard.matcher(/(\d+)/, 'abc 123', Duration.ofMillis(200))
     assert m.find() && m.group(1) == '123'

     try {
         RegexGuard.matches(/(.*,){16}X/, '1,' * 40, 200) // catastrophic backtracking
         assert false, 'should have timed out'
     } catch (RegexTimeoutException expected) {
     }

Notes and limitations:

- The deadline starts when the guarded matcher is created and covers all subsequent use of it, e.g. repeated `find()` calls.
- The clock is only consulted while the engine reads input characters, every 512 reads; evaluations finishing in fewer reads never pay for a clock call. Pathological patterns perform millions of reads per second, so overshoot past the deadline is negligible in practice.
- `Pattern.compile` itself is not guarded; pattern compilation is not subject to backtracking.

Since:
  6.0.0
See Also:
  - `SafeRegex`

- ## Method Summary

  Modifier and Type
  Method
  Description
  `static CharSequence`
  `ambientGuard(CharSequence input)`
  Runtime hook used by Groovy's regex operators and extension methods: wraps the input so that reads are guarded when an ambient guard (see `guard(long, Closure)`) is active on the current thread, and returns the input unchanged otherwise.
  `static Matcher`
  `findRegex(Object input, Object pattern, long millis)`
  Runtime hook used by the `SafeRegex` transform: equivalent to `matcher(Object, Object, long)` but with the operands in the `=~` operator's order, so that a rewritten expression still evaluates its operands left to right, exactly as the original did.
  `static <T> T`
  `guard(long millis, Closure<T> closure)`
  Runs the closure with an ambient regex timeout active on the current thread and returns the closure's result.
  `static CharSequence`
  `guard(CharSequence input, long millis)`
  Wraps a character sequence so that reads beyond the deadline throw `RegexTimeoutException`.
  `static CharSequence`
  `guard(CharSequence input, Duration timeout)`
  Variant of `guard(CharSequence, long)` taking the timeout as a `Duration`.
  `static <T> T`
  `guard(Duration timeout, Closure<T> closure)`
  Variant of `guard(long, Closure)` taking the timeout as a `Duration`.
  `static Matcher`
  `matcher(Object pattern, Object input, long millis)`
  Creates a matcher of the pattern over deadline-guarded input, like the `=~` operator.
  `static Matcher`
  `matcher(Object pattern, Object input, Duration timeout)`
  Variant of `matcher(Object, Object, long)` taking the timeout as a `Duration`.
  `static boolean`
  `matches(Object pattern, Object input, long millis)`
  Matches the whole input against the pattern like the `==~` operator, giving up once the timeout has elapsed.
  `static boolean`
  `matches(Object pattern, Object input, Duration timeout)`
  Variant of `matches(Object, Object, long)` taking the timeout as a `Duration`.
  `static boolean`
  `matchRegex(Object input, Object pattern, long millis)`
  Runtime hook used by the `SafeRegex` transform: equivalent to `matches(Object, Object, long)` but with the operands in the `==~` operator's order, so that a rewritten expression still evaluates its operands left to right, exactly as the original did.
  `static CharSequence`
  `prepare(CharSequence input)`
  Runtime hook used by Groovy's regex extension methods: normalizes an input for regex evaluation.

  ### Methods inherited from class java.lang.Object

  `clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait`

- ## Method Details

  - ### matches

    public static boolean matches(Object pattern, Object input, long millis)
    Matches the whole input against the pattern like the `==~` operator, giving up once the timeout has elapsed. Follows the operator's conventions: a `null` pattern or input yields `false`, non-`Pattern` patterns and non-`String` inputs are converted via their default string representation, and the matcher is stored for `Matcher.lastMatcher`.
    Parameters:
      `pattern` - the pattern, a `Pattern` or an object whose string representation is the regex
      `input` - the text to match
      `millis` - the timeout in milliseconds (must be positive)
    Returns:
      `true` if the whole input matches the pattern
    Throws:
      `RegexTimeoutException` - if evaluation exceeds the timeout

  - ### matches

    public static boolean matches(Object pattern, Object input, Duration timeout)
    Variant of `matches(Object, Object, long)` taking the timeout as a `Duration`.

  - ### matcher

    public static Matcher matcher(Object pattern, Object input, long millis)
    Creates a matcher of the pattern over deadline-guarded input, like the `=~` operator. Matcher operations that read the input, e.g. `find()` or `matches()`, throw `RegexTimeoutException` once the deadline, measured from this call, has passed.
    Parameters:
      `pattern` - the pattern, a `Pattern` or an object whose string representation is the regex
      `input` - the text to match
      `millis` - the timeout in milliseconds (must be positive)
    Returns:
      a matcher over the guarded input

  - ### matcher

    public static Matcher matcher(Object pattern, Object input, Duration timeout)
    Variant of `matcher(Object, Object, long)` taking the timeout as a `Duration`.

  - ### matchRegex

    public static boolean matchRegex(Object input, Object pattern, long millis)
    Runtime hook used by the `SafeRegex` transform: equivalent to `matches(Object, Object, long)` but with the operands in the `==~` operator's order, so that a rewritten expression still evaluates its operands left to right, exactly as the original did.
    Parameters:
      `input` - the text to match (the operator's left operand)
      `pattern` - the pattern (the operator's right operand)
      `millis` - the timeout in milliseconds (must be positive)
    Returns:
      `true` if the whole input matches the pattern
    Throws:
      `RegexTimeoutException` - if evaluation exceeds the timeout

  - ### findRegex

    public static Matcher findRegex(Object input, Object pattern, long millis)
    Runtime hook used by the `SafeRegex` transform: equivalent to `matcher(Object, Object, long)` but with the operands in the `=~` operator's order, so that a rewritten expression still evaluates its operands left to right, exactly as the original did.
    Parameters:
      `input` - the text to match (the operator's left operand)
      `pattern` - the pattern (the operator's right operand)
      `millis` - the timeout in milliseconds (must be positive)
    Returns:
      a matcher over the guarded input

  - ### guard

    public static CharSequence guard(CharSequence input, long millis)
    Wraps a character sequence so that reads beyond the deadline throw `RegexTimeoutException`. The deadline is attached to the data: it is a single total budget covering every subsequent regex evaluation against the guarded sequence, wherever and on whatever thread it happens. Groovy's regex-evaluating `CharSequence` extension methods (`find`, `findAll`, `findGroups`, `eachMatch`, `replaceAll`, `switch/case` matching, ...) honour the guard, as does anything that passes the sequence through to `java.util.regex` untouched, e.g. `Pattern.split` or manual matcher creation. This complements `guard(long, Closure)`, which instead guards a region of code with a per-evaluation timeout.

    Note the deadline is only consulted while the regex engine reads input characters, so a well-behaved evaluation over a short input may still succeed after the deadline; runaway evaluations are always cut off. A sequence other than a `String` (e.g. a `GString` or `StringBuilder`) is snapshotted via `toString()` first, so later mutations of the original are not seen by the guarded view.

    Parameters:
      `input` - the sequence to guard (must not be `null`)
      `millis` - the timeout in milliseconds (must be positive)
    Returns:
      a guarded view of the input

  - ### guard

    public static CharSequence guard(CharSequence input, Duration timeout)
    Variant of `guard(CharSequence, long)` taking the timeout as a `Duration`.

  - ### guard

    public static <T> T guard(long millis, Closure<T> closure)
    Runs the closure with an ambient regex timeout active on the current thread and returns the closure's result. While active, every regex evaluation reached through Groovy's regex operators (`==~`, `=~`, and `switch/case`, `grep` and `in` against a `Pattern`) or the `CharSequence` extension methods (`find`, `findAll`, `findGroups`, `findAllGroups`, `eachMatch`, `matches`, `replaceAll`, `replaceFirst`, `splitEachLine`, ...) is guarded: each individual evaluation that exceeds the timeout throws `RegexTimeoutException`.

         import groovy.util.regex.RegexGuard
         import groovy.util.regex.RegexTimeoutException

         def groups = RegexGuard.guard(200) {
             '6.0.0-beta-2'.findGroups(/(\d+)\.(\d+)\.(\d+)(?:-(.+))?/)
         }
         assert groups == ['6.0.0-beta-2', '6', '0', '0', 'beta-2']

         try {
             RegexGuard.guard(200) {
                 ('1,' * 40).findGroups(/(.*,){16}X/) // catastrophic backtracking
             }
             assert false, 'should have timed out'
         } catch (RegexTimeoutException expected) {
         }

    Semantics:
    - The timeout applies per evaluation, not to the block as a whole; any number of well-behaved evaluations may run within the scope.
    - The scope is dynamic: regex operations in methods called from the closure are guarded too, as long as they run on the same thread. Threads spawned within the closure are not covered; on JDK 25+, however, the guard does propagate into `StructuredTaskScope` forks, which by construction run within the enclosing scope.
    - Nested guards only tighten: the smaller per-evaluation timeout wins.
    - An explicit timeout, e.g. from `matches(Object, Object, long)` or a `SafeRegex` scope, is likewise capped by the ambient timeout.
    - A matcher created within the scope (e.g. via `=~`) keeps its deadline, measured from creation, when used after the block exits.
    - Direct `java.util.regex` calls, e.g. a hand-written `pattern.matcher(input)`, bypass the ambient guard; combine them with `guard(CharSequence, long)` instead. The same applies to the JDK's own `String` methods: on a `String` receiver, `matches(String)`, `replaceAll(String, String)`, `replaceFirst(String, String)` and `split(String)` dispatch to `java.lang.String` and never enter Groovy's runtime. Their `Pattern`-argument variants, e.g. `str.replaceAll(~/expensive/, 'x')`, are extension methods and are covered.
    Parameters:
      `millis` - the per-evaluation timeout in milliseconds (must be positive)
      `closure` - the code to run under the ambient guard
    Returns:
      the closure's result

  - ### guard

    public static <T> T guard(Duration timeout, Closure<T> closure)
    Variant of `guard(long, Closure)` taking the timeout as a `Duration`.

  - ### ambientGuard

    public static CharSequence ambientGuard(CharSequence input)
    Runtime hook used by Groovy's regex operators and extension methods: wraps the input so that reads are guarded when an ambient guard (see `guard(long, Closure)`) is active on the current thread, and returns the input unchanged otherwise.
    Parameters:
      `input` - the text a regex is about to be evaluated against
    Returns:
      the input, guarded if an ambient guard is active

  - ### prepare

    public static CharSequence prepare(CharSequence input)
    Runtime hook used by Groovy's regex extension methods: normalizes an input for regex evaluation. A `String`, or a sequence guarded via `guard(CharSequence, long)`, is passed through, so an attached deadline is honoured by the evaluation; any other sequence (e.g. a `GString` or `StringBuilder`) is snapshotted via `toString()`, as the extension methods have always done. An ambient guard (see `guard(long, Closure)`), if active, is then applied on top.
    Parameters:
      `input` - the text a regex is about to be evaluated against
    Returns:
      the normalized, possibly guarded, input