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
62 changes: 61 additions & 1 deletion src/main/java/build/buf/protovalidate/EvaluatorBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,13 @@
import dev.cel.common.types.StructTypeReference;
import dev.cel.common.values.CelByteString;
import dev.cel.runtime.CelEvaluationException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
Expand All @@ -57,6 +61,12 @@ final class EvaluatorBuilder {
FieldPathUtils.fieldPathElement(
FieldRules.getDescriptor().findFieldByNumber(FieldRules.CEL_EXPRESSION_FIELD_NUMBER));

private static final Set<String> STRUCT_TYPE_NAMES =
Collections.unmodifiableSet(
new HashSet<>(
Arrays.asList(
"google.protobuf.Struct", "google.protobuf.Value", "google.protobuf.ListValue")));

private volatile Map<Descriptor, MessageEvaluator> evaluatorCache = Collections.emptyMap();

private final Cel cel;
Expand Down Expand Up @@ -182,10 +192,60 @@ private MessageEvaluator createMessageEvaluator(Descriptor desc) throws Compilat
}
MessageEvaluator msgEval = new MessageEvaluator();
cache.put(desc, msgEval);
buildMessage(desc, msgEval);
if (!isRuleFreeStructType(desc)) {
buildMessage(desc, msgEval);
}
msgEval.finishBuilding();
return msgEval;
}

/**
* Struct, Value and ListValue form a rule-free cycle, which would otherwise be walked on every
* validation. Treat them as tautologies unless a non-standard descriptor adds rules.
*/
private boolean isRuleFreeStructType(Descriptor desc) throws CompilationException {
try {
Set<Descriptor> seen = new HashSet<>();
Deque<Descriptor> pending = new ArrayDeque<>();
pending.push(desc);
while (!pending.isEmpty()) {
Descriptor message = pending.pop();
if (!seen.add(message)) {
continue;
}
if (!STRUCT_TYPE_NAMES.contains(message.getFullName())
|| !resolver.resolveMessageRules(message).equals(MessageRules.getDefaultInstance())) {
return false;
}
for (Descriptors.OneofDescriptor oneof : message.getOneofs()) {
if (!resolver.resolveOneofRules(oneof).equals(OneofRules.getDefaultInstance())) {
return false;
}
}
for (FieldDescriptor field : message.getFields()) {
if (!resolver.resolveFieldRules(field).equals(FieldRules.getDefaultInstance())) {
return false;
}
if (field.getJavaType() != FieldDescriptor.JavaType.MESSAGE) {
continue;
}
if (field.isMapField()) {
FieldDescriptor mapValue = field.getMessageType().findFieldByNumber(2);
if (mapValue.getJavaType() == FieldDescriptor.JavaType.MESSAGE) {
pending.push(mapValue.getMessageType());
}
} else {
pending.push(field.getMessageType());
}
}
}
return true;
} catch (InvalidProtocolBufferException e) {
throw new CompilationException(
"failed to parse proto definition: " + desc.getFullName(), e);
}
}

private void buildMessage(Descriptor desc, MessageEvaluator msgEval)
throws CompilationException {
try {
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/build/buf/protovalidate/MessageEvaluator.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,22 @@ final class MessageEvaluator implements Evaluator {
/** List of {@link Evaluator}s that are applied to a message. */
private final List<Evaluator> evaluators = new ArrayList<>();

/**
* A recursive message type references its own evaluator before that evaluator is complete. Until
* then it must not look like a tautology, or the back-reference would be dropped for good.
*/
private boolean building = true;

/** Marks the evaluator as fully built, so {@link #tautology()} can report an accurate result. */
void finishBuilding() {
building = false;
}

@Override
public boolean tautology() {
if (building) {
return false;
}
for (Evaluator evaluator : evaluators) {
if (!evaluator.tautology()) {
return false;
Expand Down
Loading
Loading