diff --git a/common/src/main/java/dev/cel/common/values/RawProtoMessageLiteValue.java b/common/src/main/java/dev/cel/common/values/RawProtoMessageLiteValue.java
index 2a3bdf940..349356ca8 100644
--- a/common/src/main/java/dev/cel/common/values/RawProtoMessageLiteValue.java
+++ b/common/src/main/java/dev/cel/common/values/RawProtoMessageLiteValue.java
@@ -193,14 +193,29 @@ private static Object decodeWireField(
fieldDescriptor != null
? fieldDescriptor.getEncodingType() == EncodingType.LIST
: field.defaultValue() instanceof List;
- String protoTypeName =
- fieldDescriptor != null
- ? fieldDescriptor.getFieldProtoTypeName()
- : UNKNOWN_MESSAGE_TYPE_NAME;
+ String protoTypeName = resolveProtoTypeName(field, fieldDescriptor);
return decodeWireEntries(unknowns, typeCode, protoTypeName, isRepeated, converter);
}
+ /**
+ * Resolves the protobuf message type name for a field.
+ *
+ *
Prefers the descriptor's message type name when present. Under runtime version skew (where
+ * the descriptor is omitted), falls back to the type name preserved in {@link SelectField} from
+ * the optimizer, or {@link #UNKNOWN_MESSAGE_TYPE_NAME} if unspecified.
+ */
+ private static String resolveProtoTypeName(
+ SelectField field, @Nullable FieldLiteDescriptor fieldDescriptor) {
+ if (fieldDescriptor != null) {
+ return fieldDescriptor.getFieldProtoTypeName();
+ }
+ if (!field.protoTypeName().isEmpty()) {
+ return field.protoTypeName();
+ }
+ return UNKNOWN_MESSAGE_TYPE_NAME;
+ }
+
private static Object resolveDefault(
SelectField field,
@Nullable FieldLiteDescriptor fieldDescriptor,
@@ -211,7 +226,8 @@ private static Object resolveDefault(
if (fieldDescriptor == null) {
if (field.typeCode() == FieldLiteDescriptor.Type.MESSAGE.getNumber()) {
- return create(ByteString.EMPTY, UNKNOWN_MESSAGE_TYPE_NAME, converter);
+ return create(
+ ByteString.EMPTY, resolveProtoTypeName(field, /* fieldDescriptor= */ null), converter);
}
throw CelAttributeNotFoundException.forFieldResolution(field.fieldName());
}
@@ -249,6 +265,9 @@ static boolean isPresentInWire(
? fieldDescriptor.getProtoFieldType().getNumber()
: field.typeCode();
+ // In protobuf wire format, a zero-length entry for a singular field (e.g. empty string,
+ // bytes, or empty submessage) represents explicit presence on the wire. Only packed repeated
+ // fields with empty payload represent an empty/absent collection.
if (!isRepeated) {
return true;
}
diff --git a/common/src/main/java/dev/cel/common/values/SelectField.java b/common/src/main/java/dev/cel/common/values/SelectField.java
index 8bee5a0d7..c89d3bf1c 100644
--- a/common/src/main/java/dev/cel/common/values/SelectField.java
+++ b/common/src/main/java/dev/cel/common/values/SelectField.java
@@ -69,6 +69,12 @@ public abstract class SelectField {
*/
public abstract @Nullable Object defaultValue();
+ /**
+ * Protobuf message type name of this hop (e.g. {@code "google.protobuf.Duration"}, {@code
+ * "com.example.User"}), or empty string if unknown or not a message field.
+ */
+ public abstract String protoTypeName();
+
/**
* Creates a presence-test qualifier hop.
*
@@ -83,7 +89,11 @@ public static SelectField create(long fieldNumber, String fieldName) {
fieldNumber);
checkNotNull(fieldName);
return new AutoValue_SelectField(
- (int) fieldNumber, fieldName, NO_TYPE_CODE, /* defaultValue= */ null);
+ (int) fieldNumber,
+ fieldName,
+ NO_TYPE_CODE,
+ /* defaultValue= */ null,
+ /* protoTypeName= */ "");
}
/**
@@ -98,13 +108,36 @@ public static SelectField create(long fieldNumber, String fieldName) {
*/
public static SelectField create(
long fieldNumber, String fieldName, long typeCode, @Nullable Object defaultValue) {
+ return create(fieldNumber, fieldName, typeCode, defaultValue, /* protoTypeName= */ "");
+ }
+
+ /**
+ * Creates a fully-specified field selection hop with type code, optional default value, and
+ * protobuf type name.
+ *
+ * @param fieldNumber Protobuf field number. Takes {@code long} for compatibility with CEL's int64
+ * constant representations.
+ * @param fieldName Protobuf field name.
+ * @param typeCode Protobuf wire type code or {@link #CEL_MAP_TYPE_CODE}. Takes {@code long} for
+ * compatibility with CEL's int64 constant representations.
+ * @param defaultValue Default value for the field, or null if unspecified.
+ * @param protoTypeName Protobuf message type name, or empty string if unspecified.
+ */
+ public static SelectField create(
+ long fieldNumber,
+ String fieldName,
+ long typeCode,
+ @Nullable Object defaultValue,
+ String protoTypeName) {
checkArgument(
fieldNumber >= 1 && fieldNumber <= MAX_FIELD_NUMBER,
"Field number out of protobuf range: %s",
fieldNumber);
checkNotNull(fieldName);
checkArgument(isSupportedTypeCode(typeCode), "Invalid protobuf type code: %s", typeCode);
- return new AutoValue_SelectField((int) fieldNumber, fieldName, (int) typeCode, defaultValue);
+ checkNotNull(protoTypeName);
+ return new AutoValue_SelectField(
+ (int) fieldNumber, fieldName, (int) typeCode, defaultValue, protoTypeName);
}
/**
diff --git a/common/src/test/java/dev/cel/common/values/RawProtoMessageLiteValueTest.java b/common/src/test/java/dev/cel/common/values/RawProtoMessageLiteValueTest.java
index 9a4079a4c..c1abcc97c 100644
--- a/common/src/test/java/dev/cel/common/values/RawProtoMessageLiteValueTest.java
+++ b/common/src/test/java/dev/cel/common/values/RawProtoMessageLiteValueTest.java
@@ -40,6 +40,7 @@
import dev.cel.protobuf.CelLiteDescriptor.MessageLiteDescriptor;
import java.io.ByteArrayOutputStream;
import java.time.Duration;
+import java.time.Instant;
import java.util.NoSuchElementException;
import java.util.Optional;
import org.junit.Test;
@@ -1260,4 +1261,332 @@ public void selectByFieldNumber_absentMessageFieldWithoutDescriptor_returnsUnkno
.hasMessageThat()
.contains("Decoding unknown map field from wire bytes is unsupported");
}
+
+ @SuppressWarnings("ImmutableEnumChecker") // Test only
+ private enum WellKnownFieldWithoutDescriptorTestCase {
+ DURATION(
+ TestAllTypes.newBuilder()
+ .setSingleDuration(ProtoTimeUtils.toProtoDuration(Duration.ofSeconds(120L, 500L)))
+ .build(),
+ SelectField.create(
+ TestAllTypes.SINGLE_DURATION_FIELD_NUMBER,
+ "single_duration",
+ FieldLiteDescriptor.Type.MESSAGE.getNumber(),
+ Duration.ZERO,
+ "google.protobuf.Duration"),
+ Duration.ofSeconds(120L, 500L),
+ Duration.ZERO),
+ TIMESTAMP(
+ TestAllTypes.newBuilder()
+ .setSingleTimestamp(
+ ProtoTimeUtils.toProtoTimestamp(Instant.ofEpochSecond(1700000000L, 123456789L)))
+ .build(),
+ SelectField.create(
+ TestAllTypes.SINGLE_TIMESTAMP_FIELD_NUMBER,
+ "single_timestamp",
+ FieldLiteDescriptor.Type.MESSAGE.getNumber(),
+ Instant.EPOCH,
+ "google.protobuf.Timestamp"),
+ Instant.ofEpochSecond(1700000000L, 123456789L),
+ Instant.EPOCH);
+
+ private final TestAllTypes populatedProto;
+ private final SelectField selectField;
+ private final Object expectedPopulatedValue;
+ private final Object expectedDefaultValue;
+
+ WellKnownFieldWithoutDescriptorTestCase(
+ TestAllTypes populatedProto,
+ SelectField selectField,
+ Object expectedPopulatedValue,
+ Object expectedDefaultValue) {
+ this.populatedProto = populatedProto;
+ this.selectField = selectField;
+ this.expectedPopulatedValue = expectedPopulatedValue;
+ this.expectedDefaultValue = expectedDefaultValue;
+ }
+ }
+
+ @Test
+ public void selectByFieldNumber_wellKnownFieldWithoutDescriptor_decodesOrReturnsDefault(
+ @TestParameter WellKnownFieldWithoutDescriptorTestCase testCase) {
+ RawProtoMessageLiteValue populatedRaw =
+ RawProtoMessageLiteValue.create(
+ testCase.populatedProto.toByteString(),
+ "cel.expr.conformance.proto3.TestAllTypes",
+ EMPTY_CONVERTER);
+ RawProtoMessageLiteValue emptyRaw =
+ RawProtoMessageLiteValue.create(
+ TestAllTypes.getDefaultInstance().toByteString(),
+ "cel.expr.conformance.proto3.TestAllTypes",
+ EMPTY_CONVERTER);
+
+ Object populatedSelected = populatedRaw.selectByFieldNumber(testCase.selectField);
+ Object emptySelected = emptyRaw.selectByFieldNumber(testCase.selectField);
+
+ assertThat(populatedSelected).isEqualTo(testCase.expectedPopulatedValue);
+ assertThat(emptySelected).isEqualTo(testCase.expectedDefaultValue);
+ }
+
+ @Test
+ public void findByFieldNumber_wellKnownFieldWithoutDescriptor_returnsOptionalValue(
+ @TestParameter WellKnownFieldWithoutDescriptorTestCase testCase) {
+ RawProtoMessageLiteValue populatedRaw =
+ RawProtoMessageLiteValue.create(
+ testCase.populatedProto.toByteString(),
+ "cel.expr.conformance.proto3.TestAllTypes",
+ EMPTY_CONVERTER);
+ RawProtoMessageLiteValue emptyRaw =
+ RawProtoMessageLiteValue.create(
+ TestAllTypes.getDefaultInstance().toByteString(),
+ "cel.expr.conformance.proto3.TestAllTypes",
+ EMPTY_CONVERTER);
+
+ Optional