Skip to content

Commit

Permalink
Add CelOptions for designating Regex program size
Browse files Browse the repository at this point in the history
Addresses #545

PiperOrigin-RevId: 714298569
  • Loading branch information
l46kok authored and copybara-github committed Jan 11, 2025
1 parent a9678c4 commit baf01fc
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 9 deletions.
2 changes: 1 addition & 1 deletion WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ maven_install(
"com.google.guava:guava-testlib:33.3.1-jre",
"com.google.protobuf:protobuf-java:4.28.3",
"com.google.protobuf:protobuf-java-util:4.28.3",
"com.google.re2j:re2j:1.7",
"com.google.re2j:re2j:1.8",
"com.google.testparameterinjector:test-parameter-injector:1.18",
"com.google.truth.extensions:truth-java8-extension:1.4.4",
"com.google.truth.extensions:truth-proto-extension:1.4.4",
Expand Down
33 changes: 33 additions & 0 deletions bundle/src/test/java/dev/cel/bundle/CelImplTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2032,6 +2032,39 @@ public void program_comprehensionDisabled_throws() throws Exception {
assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.ITERATION_BUDGET_EXCEEDED);
}

@Test
public void program_regexProgramSizeUnderLimit_success() throws Exception {
Cel cel =
standardCelBuilderWithMacros()
.setOptions(CelOptions.current().regexMaxProgramSize(7).build())
.build();
// See
// https://github.com/google/re2j/blob/84237cbbd0fbd637c6eb6856717c1e248daae729/javatests/com/google/re2j/PatternTest.java#L175 for program size
CelAbstractSyntaxTree ast = cel.compile("'foo'.matches('(a+b)')").getAst();

assertThat(cel.createProgram(ast).eval()).isEqualTo(false);
}

@Test
public void program_regexProgramSizeExceedsLimit_throws() throws Exception {
Cel cel =
standardCelBuilderWithMacros()
.setOptions(CelOptions.current().regexMaxProgramSize(6).build())
.build();
// See
// https://github.com/google/re2j/blob/84237cbbd0fbd637c6eb6856717c1e248daae729/javatests/com/google/re2j/PatternTest.java#L175 for program size
CelAbstractSyntaxTree ast = cel.compile("'foo'.matches('(a+b)')").getAst();

CelEvaluationException e =
assertThrows(CelEvaluationException.class, () -> cel.createProgram(ast).eval());
assertThat(e)
.hasMessageThat()
.contains(
"evaluation error: Regex pattern exceeds allowed program size. Allowed: 6, Provided:"
+ " 7");
assertThat(e.getErrorCode()).isEqualTo(CelErrorCode.INVALID_ARGUMENT);
}

@Test
public void toBuilder_isImmutable() {
CelBuilder celBuilder = CelFactory.standardCelBuilder();
Expand Down
20 changes: 18 additions & 2 deletions common/src/main/java/dev/cel/common/CelOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public enum ProtoUnsetFieldOptions {
// Do not bind a field if it is unset. Repeated fields are bound as empty list.
SKIP,
// Bind the (proto api) default value for a field.
BIND_DEFAULT;
BIND_DEFAULT
}

public static final CelOptions DEFAULT = current().build();
Expand Down Expand Up @@ -121,6 +121,8 @@ public enum ProtoUnsetFieldOptions {

public abstract boolean enableComprehension();

public abstract int regexMaxProgramSize();

public abstract Builder toBuilder();

public ImmutableSet<ExprFeatures> toExprFeatures() {
Expand Down Expand Up @@ -218,7 +220,8 @@ public static Builder newBuilder() {
.enableStringConversion(true)
.enableStringConcatenation(true)
.enableListConcatenation(true)
.enableComprehension(true);
.enableComprehension(true)
.regexMaxProgramSize(-1);
}

/**
Expand Down Expand Up @@ -571,6 +574,19 @@ public abstract static class Builder {
*/
public abstract Builder enableComprehension(boolean value);

/**
* Set maximum program size for RE2J regex.
*
* <p>The program size is a very approximate measure of a regexp's "cost". Larger numbers are
* more expensive than smaller numbers.
*
* <p>A negative {@code value} will disable the check.
*
* <p>There's no guarantee that RE2 program size has the exact same value across other CEL
* implementations (C++ and Go).
*/
public abstract Builder regexMaxProgramSize(int value);

public abstract CelOptions build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
import com.google.protobuf.Timestamp;
import com.google.protobuf.util.Durations;
import com.google.protobuf.util.Timestamps;
import com.google.re2j.PatternSyntaxException;
import dev.cel.common.CelErrorCode;
import dev.cel.common.CelOptions;
import dev.cel.common.internal.ComparisonFunctions;
Expand Down Expand Up @@ -1000,7 +999,7 @@ public enum StringMatchers implements StandardOverload {
(String string, String regexp) -> {
try {
return RuntimeHelpers.matches(string, regexp, bindingHelper.celOptions);
} catch (PatternSyntaxException e) {
} catch (RuntimeException e) {
throw new CelEvaluationException(
e.getMessage(), e, CelErrorCode.INVALID_ARGUMENT);
}
Expand All @@ -1015,7 +1014,7 @@ public enum StringMatchers implements StandardOverload {
(String string, String regexp) -> {
try {
return RuntimeHelpers.matches(string, regexp, bindingHelper.celOptions);
} catch (PatternSyntaxException e) {
} catch (RuntimeException e) {
throw new CelEvaluationException(
e.getMessage(), e, CelErrorCode.INVALID_ARGUMENT);
}
Expand Down
16 changes: 13 additions & 3 deletions runtime/src/main/java/dev/cel/runtime/RuntimeHelpers.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,22 @@ public static boolean matches(String string, String regexp) {
}

public static boolean matches(String string, String regexp, CelOptions celOptions) {
Pattern pattern = Pattern.compile(regexp);
int maxProgramSize = celOptions.regexMaxProgramSize();
if (maxProgramSize >= 0 && pattern.programSize() > maxProgramSize) {
throw new IllegalArgumentException(
String.format(
"Regex pattern exceeds allowed program size. Allowed: %d, Provided: %d",
maxProgramSize, pattern.programSize()));
}

if (!celOptions.enableRegexPartialMatch()) {
// Uses re2 for consistency across languages.
return Pattern.matches(regexp, string);
return pattern.matcher(string).matches();
}
// Return an unanchored match for the presence of the regexp anywher in the string.
return Pattern.compile(regexp).matcher(string).find();

// Return an unanchored match for the presence of the regexp anywhere in the string.
return pattern.matcher(string).find();
}

/** Create a compiled pattern for the given regular expression. */
Expand Down

0 comments on commit baf01fc

Please sign in to comment.