Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions opa-builtins/opa-builtins-regex/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,17 @@ dependencies {
api(project(":opa-evaluator"))

implementation("dk.brics.automaton:automaton:1.11-8")

testImplementation("org.junit.jupiter:junit-jupiter:6.1.2")
testRuntimeOnly("org.junit.platform:junit-platform-launcher:6.1.2")
}

java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}

tasks.test {
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import io.github.open_policy_agent.opa.ast.builtin.BuiltinError;
import io.github.open_policy_agent.opa.ast.builtin.BuiltinProvider;
import io.github.open_policy_agent.opa.ast.builtin.OpaBuiltin;
Expand All @@ -22,6 +23,19 @@

public class RegexBuiltins implements BuiltinProvider {

/**
* Compiles `pattern`, reporting a syntax error as a {@link BuiltinError} rather than letting
* Java's unchecked {@link PatternSyntaxException} escape. OPA treats an invalid pattern as a
* builtin error, so the call is undefined by default and only aborts under strict-builtin-errors.
*/
private static Pattern compile(String pattern) {
try {
return Pattern.compile(pattern);
} catch (PatternSyntaxException e) {
throw new BuiltinError("error parsing regexp: " + e.getMessage());
}
}

@Override
public Map<String, BiFunction<EvaluationContext, RegoValue[], RegoValue>> builtins() {
RegexBuiltins instance = new RegexBuiltins();
Expand Down Expand Up @@ -51,7 +65,9 @@ public RegoBoolean match(EvaluationContext ctx, RegoValue[] args) {
String pattern = getArg(args, 0, RegoString.class).getValue();
String value = getArg(args, 1, RegoString.class).getValue();

return RegoBoolean.of(Pattern.compile(pattern).matcher(value).matches());
// Go's regexp.MatchString searches for a match anywhere in the input, so an unanchored
// pattern matches a substring. Matcher.matches() would instead require the whole input.
return RegoBoolean.of(compile(pattern).matcher(value).find());
}

@OpaBuiltin(
Expand Down Expand Up @@ -88,7 +104,7 @@ public RegoArray split(EvaluationContext ctx, RegoValue[] args) {
String value = getArg(args, 1, RegoString.class).getValue();

RegoArray result = new RegoArray();
for (String split : Pattern.compile(pattern).split(value, -1)) {
for (String split : compile(pattern).split(value, -1)) {
result.addValue(new RegoString(split));
}
return result;
Expand All @@ -115,7 +131,7 @@ public RegoArray find(EvaluationContext ctx, RegoValue[] args) {
}

RegoArray result = new RegoArray();
Matcher matcher = Pattern.compile(pattern).matcher(value);
Matcher matcher = compile(pattern).matcher(value);
int count = 0;
while (matcher.find() && count < number) {
result.addValue(new RegoString(matcher.group()));
Expand Down Expand Up @@ -144,7 +160,7 @@ public RegoArray findSubstringMatch(EvaluationContext ctx, RegoValue[] args) {
}

RegoArray result = new RegoArray();
Matcher matcher = Pattern.compile(pattern).matcher(value);
Matcher matcher = compile(pattern).matcher(value);
int count = 0;
while (matcher.find() && count < number) {
RegoArray matchGroups = new RegoArray();
Expand All @@ -171,7 +187,7 @@ public RegoString replace(EvaluationContext ctx, RegoValue[] args) {
String s = getArg(args, 0, RegoString.class).getValue();
String pattern = getArg(args, 1, RegoString.class).getValue();
String value = getArg(args, 2, RegoString.class).getValue();
return new RegoString(Pattern.compile(pattern).matcher(s).replaceAll(value));
return new RegoString(compile(pattern).matcher(s).replaceAll(value));
}

@OpaBuiltin(
Expand Down Expand Up @@ -230,7 +246,7 @@ public RegoBoolean templateMatch(EvaluationContext ctx, RegoValue[] args) {

// Match value against constructed pattern
String pattern = patternBuilder.toString();
return RegoBoolean.of(Pattern.compile(pattern).matcher(value).matches());
return RegoBoolean.of(compile(pattern).matcher(value).matches());
}

@OpaBuiltin(
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
#io.github.open_policy_agent.opa.ast.builtin.impls.RegexBuiltins
io.github.open_policy_agent.opa.ast.builtin.impls.RegexBuiltins
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package io.github.open_policy_agent.opa.ast.builtin.impls;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import io.github.open_policy_agent.opa.ast.builtin.BuiltinError;
import io.github.open_policy_agent.opa.ast.types.RegoBoolean;
import io.github.open_policy_agent.opa.ast.types.RegoInt32;
import io.github.open_policy_agent.opa.ast.types.RegoString;
import io.github.open_policy_agent.opa.ast.types.RegoValue;
import java.util.ServiceLoader;
import org.junit.jupiter.api.Test;

class RegexBuiltinsTest {

private final RegexBuiltins builtins = new RegexBuiltins();

/**
* The provider entry in META-INF/services was commented out, so none of these builtins were
* reachable for consumers even though the module was documented as supported.
*/
@Test
void providerIsDiscoverableViaServiceLoader() {
boolean found =
ServiceLoader.load(
io.github.open_policy_agent.opa.ast.builtin.BuiltinProvider.class,
RegexBuiltins.class.getClassLoader())
.stream()
.anyMatch(p -> p.type().equals(RegexBuiltins.class));

assertTrue(found, "RegexBuiltins should be registered as a BuiltinProvider");
}

@Test
void registersAllRegexBuiltins() {
assertTrue(
builtins
.builtins()
.keySet()
.containsAll(
java.util.List.of(
"regex.match",
"regex.is_valid",
"regex.split",
"regex.find_n",
"regex.find_all_string_submatch_n",
"regex.replace",
"regex.template_match",
"regex.globs_match")));
}

// regex.match mirrors Go's regexp.MatchString, which searches rather than matching the whole
// input. An unanchored pattern therefore matches a substring.
@Test
void matchSearchesRatherThanRequiringFullInput() {
assertEquals(RegoBoolean.TRUE, match("", "x"));
assertEquals(RegoBoolean.TRUE, match("b", "abc"));
assertEquals(RegoBoolean.TRUE, match("^[a-z]+\\[[0-9]+\\]$", "foo[1]"));
}

@Test
void matchHonoursAnchors() {
assertEquals(RegoBoolean.TRUE, match("^$", ""));
assertEquals(RegoBoolean.FALSE, match("^$", "something"));
assertEquals(RegoBoolean.FALSE, match("^b", "abc"));
}

// An invalid pattern is a builtin error in OPA, not an unchecked PatternSyntaxException.
@Test
void matchRaisesBuiltinErrorForInvalidPattern() {
assertThrows(BuiltinError.class, () -> match("$^[[[", "something"));
}

@Test
void replaceRaisesBuiltinErrorForInvalidPattern() {
RegoValue[] args = {new RegoString("foo"), new RegoString("["), new RegoString("$1")};

assertThrows(BuiltinError.class, () -> builtins.replace(null, args));
}

@Test
void splitRaisesBuiltinErrorForInvalidPattern() {
RegoValue[] args = {new RegoString("["), new RegoString("foo")};

assertThrows(BuiltinError.class, () -> builtins.split(null, args));
}

@Test
void findRaisesBuiltinErrorForInvalidPattern() {
RegoValue[] args = {new RegoString("["), new RegoString("foo"), RegoInt32.of(-1)};

assertThrows(BuiltinError.class, () -> builtins.find(null, args));
}

@Test
void findAllStringSubmatchRaisesBuiltinErrorForInvalidPattern() {
RegoValue[] args = {new RegoString("["), new RegoString("foo"), RegoInt32.of(-1)};

assertThrows(BuiltinError.class, () -> builtins.findSubstringMatch(null, args));
}

@Test
void templateMatchRaisesBuiltinErrorForInvalidPattern() {
RegoValue[] args = {
new RegoString("{[}"), new RegoString("foo"), new RegoString("{"), new RegoString("}")
};

assertThrows(BuiltinError.class, () -> builtins.templateMatch(null, args));
}

private RegoBoolean match(String pattern, String value) {
RegoValue[] args = {new RegoString(pattern), new RegoString(value)};
return builtins.match(null, args);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,6 @@ net.cidr_merge
net.lookup_ip_addr

# opa-builtins-regex
regex.find_all_string_submatch_n
regex.find_n
regex.globs_match
regex.is_valid
regex.match
regex.replace
regex.split
regex.template_match

# --- Not implemented anywhere in the SDK -------------------------------------

Expand Down
Loading