From 6b9dc054502137ab7e727833d15b2635146187b6 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Tue, 22 Sep 2026 16:01:38 -0700 Subject: [PATCH] Tighten OptimizedSelectTraversal to only work on struct-like interfaces PiperOrigin-RevId: 986295188 --- .../java/dev/cel/common/values/BUILD.bazel | 4 + .../values/OptimizedSelectTraversal.java | 27 +- .../values/ProtoLiteCelValueConverter.java | 74 +- .../values/RawProtoMessageLiteValue.java | 294 +++++++- .../java/dev/cel/common/values/BUILD.bazel | 1 + .../values/OptimizedSelectTraversalTest.java | 33 +- .../ProtoLiteCelValueConverterTest.java | 121 ++- .../values/RawProtoMessageLiteValueTest.java | 708 ++++++++++++++---- 8 files changed, 1051 insertions(+), 211 deletions(-) diff --git a/common/src/main/java/dev/cel/common/values/BUILD.bazel b/common/src/main/java/dev/cel/common/values/BUILD.bazel index 7073686b0..51fa12e98 100644 --- a/common/src/main/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/values/BUILD.bazel @@ -323,6 +323,8 @@ java_library( ], deps = [ ":base_proto_cel_value_converter", + ":optimized_selectable", + ":select_field", ":values", "//:auto_value", "//common/annotations", @@ -351,6 +353,8 @@ cel_android_library( ], deps = [ ":base_proto_cel_value_converter_android", + ":optimized_selectable_android", + ":select_field_android", ":values_android", "//:auto_value", "//common/annotations", diff --git a/common/src/main/java/dev/cel/common/values/OptimizedSelectTraversal.java b/common/src/main/java/dev/cel/common/values/OptimizedSelectTraversal.java index 9fb08a0a2..4de724fef 100644 --- a/common/src/main/java/dev/cel/common/values/OptimizedSelectTraversal.java +++ b/common/src/main/java/dev/cel/common/values/OptimizedSelectTraversal.java @@ -20,8 +20,11 @@ import java.util.Optional; /** - * Walks a sequence of {@link SelectField} selections, dispatching each field over {@link - * OptimizedSelectable} or {@link SelectableValue}. + * Walks a sequence of {@link SelectField} selections over a struct target. + * + *

Each hop resolves by field number through {@link OptimizedSelectable} when the target + * implements it, and otherwise by field name through {@link StructValue}. Any other target, + * including maps and optional values, raises {@link CelAttributeNotFoundException}. * *

CEL Library Internals. Do Not Use. */ @@ -41,8 +44,6 @@ public static Object qualify(Object target, ImmutableList fields) { /** * Presence tests the terminal field of {@code fields}, navigating through all preceding fields. - * - *

Absence of any intermediate field short-circuits to {@code false}. */ public static boolean hasField(Object target, ImmutableList fields) { if (fields.isEmpty()) { @@ -64,7 +65,7 @@ public static boolean hasField(Object target, ImmutableList fields) return hasTerminalField(current, fields.get(terminalIndex)); } - // SelectableValue is only ever instantiated with String keys in the select path. + // StructValue is only ever instantiated with String keys in the select path. @SuppressWarnings("unchecked") private static Object qualifyField(Object target, SelectField field) { if (target instanceof ErrorValue) { @@ -73,8 +74,8 @@ private static Object qualifyField(Object target, SelectField field) { if (target instanceof OptimizedSelectable) { return ((OptimizedSelectable) target).selectByFieldNumber(field); } - if (target instanceof SelectableValue) { - SelectableValue selectable = (SelectableValue) target; + if (target instanceof StructValue) { + StructValue selectable = (StructValue) target; if (field.defaultValue() != null) { return selectable .find(field.fieldName()) @@ -86,7 +87,7 @@ private static Object qualifyField(Object target, SelectField field) { throw CelAttributeNotFoundException.forFieldResolution(field.fieldName()); } - // SelectableValue is only ever instantiated with String keys in the select path. + // StructValue is only ever instantiated with String keys in the select path. @SuppressWarnings("unchecked") private static Optional navigateField(Object target, SelectField field) { if (target instanceof ErrorValue) { @@ -95,13 +96,13 @@ private static Optional navigateField(Object target, SelectField field) if (target instanceof OptimizedSelectable) { return ((OptimizedSelectable) target).findByFieldNumber(field); } - if (target instanceof SelectableValue) { - return ((SelectableValue) target).find(field.fieldName()).map(Object.class::cast); + if (target instanceof StructValue) { + return ((StructValue) target).find(field.fieldName()).map(Object.class::cast); } throw CelAttributeNotFoundException.forFieldResolution(field.fieldName()); } - // SelectableValue is only ever instantiated with String keys in the select path. + // StructValue is only ever instantiated with String keys in the select path. @SuppressWarnings("unchecked") private static boolean hasTerminalField(Object target, SelectField field) { if (target instanceof ErrorValue) { @@ -110,8 +111,8 @@ private static boolean hasTerminalField(Object target, SelectField field) { if (target instanceof OptimizedSelectable) { return ((OptimizedSelectable) target).hasFieldByNumber(field); } - if (target instanceof SelectableValue) { - return ((SelectableValue) target).find(field.fieldName()).isPresent(); + if (target instanceof StructValue) { + return ((StructValue) target).find(field.fieldName()).isPresent(); } throw CelAttributeNotFoundException.forFieldResolution(field.fieldName()); } diff --git a/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java b/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java index 093819198..0d53903a5 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java @@ -17,7 +17,6 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.auto.value.AutoValue; -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Defaults; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; @@ -26,6 +25,7 @@ import com.google.common.collect.Multimaps; import com.google.common.primitives.UnsignedLong; import com.google.errorprone.annotations.Immutable; +import com.google.protobuf.ByteString; import com.google.protobuf.CodedInputStream; import com.google.protobuf.ExtensionRegistryLite; import com.google.protobuf.MessageLite; @@ -62,6 +62,9 @@ @Immutable @Internal public final class ProtoLiteCelValueConverter extends BaseProtoCelValueConverter { + static final String MAP_KEY_FIELD_NAME = "key"; + static final String MAP_VALUE_FIELD_NAME = "value"; + private final CelLiteDescriptorPool descriptorPool; public static ProtoLiteCelValueConverter newInstance( @@ -69,6 +72,10 @@ public static ProtoLiteCelValueConverter newInstance( return new ProtoLiteCelValueConverter(celLiteDescriptorPool); } + boolean hasDescriptor(String protoTypeName) { + return descriptorPool.findDescriptor(protoTypeName).isPresent(); + } + private static Object readPrimitiveField( CodedInputStream inputStream, FieldLiteDescriptor fieldDescriptor) throws IOException { switch (fieldDescriptor.getProtoFieldType()) { @@ -155,22 +162,45 @@ private MessageLite.Builder getDefaultMessageBuilder(String protoTypeName) { Object getDefaultCelValue(String protoTypeName, String fieldName) { MessageLiteDescriptor messageDescriptor = descriptorPool.getDescriptorOrThrow(protoTypeName); - FieldLiteDescriptor fieldDescriptor = messageDescriptor.getByFieldNameOrThrow(fieldName); - - Object defaultValue = getDefaultValue(fieldDescriptor); + return getDefaultCelValue(messageDescriptor.getByFieldNameOrThrow(fieldName)); + } - return toRuntimeValue(defaultValue); + Object getDefaultCelValue(FieldLiteDescriptor fieldDescriptor) { + return toRuntimeValue(getDefaultValue(fieldDescriptor)); } - public Optional findFieldDescriptor(String protoTypeName, int fieldNumber) { + Optional findFieldDescriptor(String protoTypeName, int fieldNumber) { return descriptorPool .findDescriptor(protoTypeName) .flatMap(desc -> desc.findByFieldNumber(fieldNumber)); } - public Optional findDefaultCelValue(String protoTypeName, int fieldNumber) { - return findFieldDescriptor(protoTypeName, fieldNumber) - .map(fieldDescriptor -> toRuntimeValue(getDefaultValue(fieldDescriptor))); + Optional tryDecodeWellKnownProto(ByteString bytes, String protoTypeName) { + Optional wellKnownProto = WellKnownProto.getByTypeName(protoTypeName); + if (!wellKnownProto.isPresent()) { + return Optional.empty(); + } + + return descriptorPool + .findDescriptor(protoTypeName) + .map( + descriptor -> + decodeWellKnownProto(bytes, protoTypeName, descriptor, wellKnownProto.get())); + } + + private Object decodeWellKnownProto( + ByteString bytes, + String protoTypeName, + MessageLiteDescriptor descriptor, + WellKnownProto wellKnownProto) { + try { + MessageLite.Builder builder = descriptor.newMessageBuilder(); + builder.mergeFrom(bytes, ExtensionRegistryLite.getEmptyRegistry()); + return fromWellKnownProto(builder.build(), wellKnownProto); + } catch (IOException e) { + throw new IllegalArgumentException( + "Failed to decode well-known proto of type: " + protoTypeName, e); + } } @Override @@ -276,16 +306,21 @@ private ImmutableList readPackedRepeatedFields( private Map.Entry readSingleMapEntry( CodedInputStream inputStream, FieldLiteDescriptor fieldDescriptor) throws IOException { + String entryTypeName = fieldDescriptor.getFieldProtoTypeName(); ImmutableMap singleMapEntry = - readAllFields(inputStream.readByteArray(), fieldDescriptor.getFieldProtoTypeName()) - .values(); - Object key = checkNotNull(singleMapEntry.get("key")); - Object value = checkNotNull(singleMapEntry.get("value")); + readAllFields(inputStream.readByteArray(), entryTypeName).values(); + Object key = singleMapEntry.get(MAP_KEY_FIELD_NAME); + if (key == null) { + key = getDefaultCelValue(entryTypeName, MAP_KEY_FIELD_NAME); + } + Object value = singleMapEntry.get(MAP_VALUE_FIELD_NAME); + if (value == null) { + value = getDefaultCelValue(entryTypeName, MAP_VALUE_FIELD_NAME); + } return new AbstractMap.SimpleEntry<>(key, value); } - @VisibleForTesting MessageFields readAllFields(byte[] bytes, String protoTypeName) throws IOException { MessageLiteDescriptor messageDescriptor = descriptorPool.getDescriptorOrThrow(protoTypeName); CodedInputStream inputStream = CodedInputStream.newInstance(bytes); @@ -360,19 +395,16 @@ MessageFields readAllFields(byte[] bytes, String protoTypeName) throws IOExcepti if (fieldDescriptor.getEncodingType().equals(EncodingType.LIST)) { String fieldName = fieldDescriptor.getFieldName(); List repeatedValues = - repeatedFieldValues.computeIfAbsent( - fieldNumber, - (unused) -> { - List newList = new ArrayList<>(); - fieldValues.put(fieldName, newList); - return newList; - }); + repeatedFieldValues.computeIfAbsent(fieldNumber, (unused) -> new ArrayList<>()); if (payload instanceof Collection) { repeatedValues.addAll((Collection) payload); } else { repeatedValues.add(payload); } + if (!repeatedValues.isEmpty()) { + fieldValues.put(fieldName, repeatedValues); + } } else { fieldValues.put(fieldDescriptor.getFieldName(), payload); } 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 cd8990be9..2a3bdf940 100644 --- a/common/src/main/java/dev/cel/common/values/RawProtoMessageLiteValue.java +++ b/common/src/main/java/dev/cel/common/values/RawProtoMessageLiteValue.java @@ -15,12 +15,15 @@ package dev.cel.common.values; import static com.google.common.base.Preconditions.checkNotNull; +import static dev.cel.common.values.ProtoLiteCelValueConverter.MAP_KEY_FIELD_NAME; +import static dev.cel.common.values.ProtoLiteCelValueConverter.MAP_VALUE_FIELD_NAME; import com.google.auto.value.AutoValue; import com.google.auto.value.extension.memoized.Memoized; import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; @@ -28,15 +31,20 @@ import com.google.errorprone.annotations.Immutable; import com.google.protobuf.ByteString; import com.google.protobuf.CodedInputStream; -import com.google.protobuf.MessageLite; import com.google.protobuf.WireFormat; import dev.cel.common.annotations.Internal; import dev.cel.common.exceptions.CelAttributeNotFoundException; +import dev.cel.common.internal.WellKnownProto; import dev.cel.common.types.CelType; import dev.cel.common.types.StructTypeReference; import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor; +import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor.EncodingType; +import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor.JavaType; import java.io.IOException; import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.TreeMap; import org.jspecify.annotations.Nullable; @@ -55,21 +63,25 @@ @Immutable @SuppressWarnings("Immutable") // Immutable wire fields @Internal -public abstract class RawProtoMessageLiteValue - extends StructValue { +public abstract class RawProtoMessageLiteValue extends StructValue + implements OptimizedSelectable { + + private static final String UNKNOWN_MESSAGE_TYPE_NAME = "cel.@unknownMessage"; abstract ByteString rawWireBytes(); + @Override + public abstract CelType celType(); + + abstract ProtoLiteCelValueConverter protoLiteCelValueConverter(); + @Override public RawProtoMessageLiteValue value() { return this; } - @Override - public abstract CelType celType(); - @Memoized - public ImmutableListMultimap unknownFields() { + ImmutableListMultimap unknownFields() { try { CodedInputStream inputStream = rawWireBytes().newCodedInput(); Multimap fields = Multimaps.newMultimap(new TreeMap<>(), ArrayList::new); @@ -85,10 +97,6 @@ public ImmutableListMultimap unknownFields() { } } - public boolean hasField(int fieldNumber) { - return unknownFields().containsKey(fieldNumber); - } - @Override public boolean isZeroValue() { return rawWireBytes().isEmpty(); @@ -97,25 +105,232 @@ public boolean isZeroValue() { /** * Direct field selection by name is unsupported on {@link RawProtoMessageLiteValue} because raw * wire bytes lack message descriptors, and field names are not preserved on the protobuf wire. - * - *

Field traversal on classless messages must be performed via optimized attribute steps - * ({@code cel.@attribute} and {@code cel.@hasField}), where the AST optimizer supplies the - * pre-resolved protobuf field numbers. - * - * @throws CelAttributeNotFoundException always, indicating the field cannot be resolved by name. */ @Override public Object select(String field) { throw CelAttributeNotFoundException.forFieldResolution(field); } + /** + * Direct field presence testing by name is unsupported on {@link RawProtoMessageLiteValue} + * because raw wire bytes lack message descriptors and field names. + */ @Override public Optional find(String field) { - return Optional.empty(); + throw CelAttributeNotFoundException.forFieldResolution(field); + } + + @Override + public Object selectByFieldNumber(SelectField field) { + int fieldNumber = field.fieldNumber(); + FieldLiteDescriptor fieldDescriptor = + protoLiteCelValueConverter() + .findFieldDescriptor(celType().name(), fieldNumber) + .orElse(null); + return selectWireOrDefault( + field, fieldDescriptor, unknownFields().get(fieldNumber), protoLiteCelValueConverter()); + } + + @Override + public boolean hasFieldByNumber(SelectField field) { + int fieldNumber = field.fieldNumber(); + FieldLiteDescriptor fieldDescriptor = + protoLiteCelValueConverter() + .findFieldDescriptor(celType().name(), fieldNumber) + .orElse(null); + return isPresentInWire(field, fieldDescriptor, unknownFields().get(fieldNumber)); + } + + @Override + public Optional findByFieldNumber(SelectField field) { + int fieldNumber = field.fieldNumber(); + FieldLiteDescriptor fieldDescriptor = + protoLiteCelValueConverter() + .findFieldDescriptor(celType().name(), fieldNumber) + .orElse(null); + return navigateWire( + field, fieldDescriptor, unknownFields().get(fieldNumber), protoLiteCelValueConverter()); + } + + /** + * Decodes a field value from preserved wire bytes, falling back to schema or default values. + * + *

Package-private: shared with {@code ProtoMessageLiteValue} for unknown field resolution. + */ + static Object selectWireOrDefault( + SelectField field, + @Nullable FieldLiteDescriptor fieldDescriptor, + ImmutableList unknowns, + ProtoLiteCelValueConverter converter) { + if (unknowns.isEmpty()) { + return resolveDefault(field, fieldDescriptor, converter); + } + return decodeWireField(field, fieldDescriptor, unknowns, converter); + } + + private static Object decodeWireField( + SelectField field, + @Nullable FieldLiteDescriptor fieldDescriptor, + ImmutableList unknowns, + ProtoLiteCelValueConverter converter) { + if (fieldDescriptor != null && fieldDescriptor.getEncodingType() == EncodingType.MAP) { + return decodeMapEntries(unknowns, fieldDescriptor, converter); + } + + int typeCode = + fieldDescriptor != null + ? fieldDescriptor.getProtoFieldType().getNumber() + : field.typeCode(); + if (typeCode == SelectField.CEL_MAP_TYPE_CODE) { + throw new UnsupportedOperationException( + "Decoding unknown map field from wire bytes is unsupported: " + field.fieldName()); + } + if (typeCode == SelectField.NO_TYPE_CODE) { + throw CelAttributeNotFoundException.forFieldResolution(field.fieldName()); + } + + boolean isRepeated = + fieldDescriptor != null + ? fieldDescriptor.getEncodingType() == EncodingType.LIST + : field.defaultValue() instanceof List; + String protoTypeName = + fieldDescriptor != null + ? fieldDescriptor.getFieldProtoTypeName() + : UNKNOWN_MESSAGE_TYPE_NAME; + + return decodeWireEntries(unknowns, typeCode, protoTypeName, isRepeated, converter); + } + + private static Object resolveDefault( + SelectField field, + @Nullable FieldLiteDescriptor fieldDescriptor, + ProtoLiteCelValueConverter converter) { + if (field.defaultValue() != null) { + return field.defaultValue(); + } + + if (fieldDescriptor == null) { + if (field.typeCode() == FieldLiteDescriptor.Type.MESSAGE.getNumber()) { + return create(ByteString.EMPTY, UNKNOWN_MESSAGE_TYPE_NAME, converter); + } + throw CelAttributeNotFoundException.forFieldResolution(field.fieldName()); + } + + String protoTypeName = fieldDescriptor.getFieldProtoTypeName(); + if (fieldDescriptor.getEncodingType() == EncodingType.SINGULAR + && fieldDescriptor.getJavaType() == JavaType.MESSAGE + && !WellKnownProto.isWrapperType(protoTypeName) + && !converter.hasDescriptor(protoTypeName)) { + return create(ByteString.EMPTY, protoTypeName, converter); + } + + return converter.getDefaultCelValue(fieldDescriptor); + } + + /** + * Returns whether a field has presence in preserved wire bytes. + * + *

Package-private: shared with {@code ProtoMessageLiteValue} for unknown field resolution. + */ + static boolean isPresentInWire( + SelectField field, + @Nullable FieldLiteDescriptor fieldDescriptor, + ImmutableList unknowns) { + if (unknowns.isEmpty()) { + return false; + } + + boolean isRepeated = + fieldDescriptor != null + ? fieldDescriptor.getEncodingType() == EncodingType.LIST + : field.defaultValue() instanceof List; + int typeCode = + fieldDescriptor != null + ? fieldDescriptor.getProtoFieldType().getNumber() + : field.typeCode(); + + if (!isRepeated) { + return true; + } + + boolean isPackable = + typeCode != FieldLiteDescriptor.Type.STRING.getNumber() + && typeCode != FieldLiteDescriptor.Type.BYTES.getNumber() + && typeCode != FieldLiteDescriptor.Type.MESSAGE.getNumber() + && typeCode != FieldLiteDescriptor.Type.GROUP.getNumber(); + if (!isPackable) { + return true; + } + + for (Object raw : unknowns) { + if (!(raw instanceof ByteString) || !((ByteString) raw).isEmpty()) { + return true; + } + } + return false; + } + + /** + * Navigates a field on preserved wire bytes, returning empty if absent. + * + *

Package-private: shared with {@code ProtoMessageLiteValue} for unknown field resolution. + */ + static Optional navigateWire( + SelectField field, + @Nullable FieldLiteDescriptor fieldDescriptor, + ImmutableList unknowns, + ProtoLiteCelValueConverter converter) { + if (!isPresentInWire(field, fieldDescriptor, unknowns)) { + return Optional.empty(); + } + if (fieldDescriptor != null || field.typeCode() != SelectField.NO_TYPE_CODE) { + return Optional.of(selectWireOrDefault(field, fieldDescriptor, unknowns, converter)); + } + Object lastEntry = unknowns.get(unknowns.size() - 1); + if (lastEntry instanceof ByteString) { + return Optional.of( + decodeWireEntries( + unknowns, + FieldLiteDescriptor.Type.MESSAGE.getNumber(), + UNKNOWN_MESSAGE_TYPE_NAME, + /* isRepeated= */ false, + converter)); + } + return Optional.of(lastEntry); + } + + private static ImmutableMap decodeMapEntries( + ImmutableList unknowns, + FieldLiteDescriptor mapFieldDescriptor, + ProtoLiteCelValueConverter converter) { + String entryTypeName = mapFieldDescriptor.getFieldProtoTypeName(); + Object defaultKey = converter.getDefaultCelValue(entryTypeName, MAP_KEY_FIELD_NAME); + Object defaultValue = converter.getDefaultCelValue(entryTypeName, MAP_VALUE_FIELD_NAME); + Map resultMap = new LinkedHashMap<>(); + for (Object raw : unknowns) { + ByteString bytes = requireType(raw, ByteString.class, WireFormat.FieldType.MESSAGE); + try { + ImmutableMap entryFields = + converter.readAllFields(bytes.toByteArray(), entryTypeName).values(); + Object key = entryFields.get(MAP_KEY_FIELD_NAME); + key = (key == null) ? defaultKey : converter.toRuntimeValue(key); + Object value = entryFields.get(MAP_VALUE_FIELD_NAME); + value = (value == null) ? defaultValue : converter.toRuntimeValue(value); + resultMap.put(key, value); + } catch (IOException e) { + throw new IllegalArgumentException( + "Failed to decode map entry for field: " + mapFieldDescriptor.getFieldName(), e); + } + } + return ImmutableMap.copyOf(resultMap); } - public static @Nullable Object decodeWireEntries( - ImmutableCollection entries, int typeCode, String protoTypeName, boolean isRepeated) { + static @Nullable Object decodeWireEntries( + ImmutableCollection entries, + int typeCode, + String protoTypeName, + boolean isRepeated, + ProtoLiteCelValueConverter converter) { WireFormat.FieldType fieldType = FieldLiteDescriptor.Type.forNumber(typeCode).toWireFormatFieldType(); if (fieldType == WireFormat.FieldType.GROUP) { @@ -130,7 +345,7 @@ public Optional find(String field) { if (fieldType.isPackable() && (raw instanceof ByteString)) { listBuilder.addAll(decodePacked((ByteString) raw, fieldType)); } else { - listBuilder.add(decodeWireValue(raw, fieldType, protoTypeName)); + listBuilder.add(decodeWireValue(raw, fieldType, protoTypeName, converter)); } } return listBuilder.build(); @@ -140,18 +355,26 @@ public Optional find(String field) { for (Object item : entries) { mergedBytes = mergedBytes.concat(requireType(item, ByteString.class, fieldType)); } - return decodeWireValue(mergedBytes, fieldType, protoTypeName); + return decodeWireValue(mergedBytes, fieldType, protoTypeName, converter); } // Protobuf "last one wins" semantics for non-repeated scalar fields - return decodeWireValue(Iterables.getLast(entries), fieldType, protoTypeName); + return decodeWireValue(Iterables.getLast(entries), fieldType, protoTypeName, converter); } - static Object decodeWireValue(Object raw, int typeCode, String protoTypeName) { + static Object decodeWireValue( + Object raw, int typeCode, String protoTypeName, ProtoLiteCelValueConverter converter) { return decodeWireValue( - raw, FieldLiteDescriptor.Type.forNumber(typeCode).toWireFormatFieldType(), protoTypeName); + raw, + FieldLiteDescriptor.Type.forNumber(typeCode).toWireFormatFieldType(), + protoTypeName, + converter); } - static Object decodeWireValue(Object raw, WireFormat.FieldType fieldType, String protoTypeName) { + static Object decodeWireValue( + Object raw, + WireFormat.FieldType fieldType, + String protoTypeName, + ProtoLiteCelValueConverter converter) { switch (fieldType) { case DOUBLE: return Double.longBitsToDouble(requireType(raw, Long.class, fieldType)); @@ -180,8 +403,10 @@ static Object decodeWireValue(Object raw, WireFormat.FieldType fieldType, String case GROUP: throw new UnsupportedOperationException("Groups are not supported"); case MESSAGE: - return RawProtoMessageLiteValue.create( - requireType(raw, ByteString.class, fieldType), protoTypeName); + ByteString msgBytes = requireType(raw, ByteString.class, fieldType); + return converter + .tryDecodeWellKnownProto(msgBytes, protoTypeName) + .orElseGet(() -> create(msgBytes, protoTypeName, converter)); case BYTES: return CelByteString.of(requireType(raw, ByteString.class, fieldType).toByteArray()); case UINT32: @@ -269,15 +494,20 @@ private static ImmutableList decodePacked( } } - public static RawProtoMessageLiteValue create(ByteString rawWireBytes) { - return create(rawWireBytes, ""); + public static RawProtoMessageLiteValue create( + ByteString rawWireBytes, ProtoLiteCelValueConverter protoLiteCelValueConverter) { + return create(rawWireBytes, "", protoLiteCelValueConverter); } - public static RawProtoMessageLiteValue create(ByteString rawWireBytes, String protoTypeName) { + public static RawProtoMessageLiteValue create( + ByteString rawWireBytes, + String protoTypeName, + ProtoLiteCelValueConverter protoLiteCelValueConverter) { checkNotNull(rawWireBytes); checkNotNull(protoTypeName); + checkNotNull(protoLiteCelValueConverter); return new AutoValue_RawProtoMessageLiteValue( - rawWireBytes, StructTypeReference.create(protoTypeName)); + rawWireBytes, StructTypeReference.create(protoTypeName), protoLiteCelValueConverter); } RawProtoMessageLiteValue() {} diff --git a/common/src/test/java/dev/cel/common/values/BUILD.bazel b/common/src/test/java/dev/cel/common/values/BUILD.bazel index 6b83f0a60..1732c6667 100644 --- a/common/src/test/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/test/java/dev/cel/common/values/BUILD.bazel @@ -22,6 +22,7 @@ java_library( "//common/internal:default_message_factory", "//common/internal:dynamic_proto", "//common/internal:proto_message_factory", + "//common/internal:proto_time_utils", "//common/types", "//common/types:type_providers", "//common/values", diff --git a/common/src/test/java/dev/cel/common/values/OptimizedSelectTraversalTest.java b/common/src/test/java/dev/cel/common/values/OptimizedSelectTraversalTest.java index 0fd1295c5..198e5bef0 100644 --- a/common/src/test/java/dev/cel/common/values/OptimizedSelectTraversalTest.java +++ b/common/src/test/java/dev/cel/common/values/OptimizedSelectTraversalTest.java @@ -22,6 +22,8 @@ import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import dev.cel.common.exceptions.CelAttributeNotFoundException; +import dev.cel.common.types.CelType; +import dev.cel.common.types.StructTypeReference; import java.util.Map; import java.util.Optional; import org.junit.Test; @@ -43,16 +45,16 @@ Object createNestedTarget(Map innerData) { ImmutableMap.of("outer_key", new FakeOptimizedSelectable(innerData))); } }, - SELECTABLE_VALUE { + STRUCT_VALUE { @Override Object createTarget(Map data) { - return new FakeSelectableValue(data); + return new FakeStructValue(data); } @Override Object createNestedTarget(Map innerData) { - return new FakeSelectableValue( - ImmutableMap.of("outer_key", new FakeSelectableValue(innerData))); + return new FakeStructValue( + ImmutableMap.of("outer_key", new FakeStructValue(innerData))); } }; @@ -151,8 +153,8 @@ public void qualify_optimizedSelectable_absentWithDefaultValue_returnsDefault() } @Test - public void qualify_selectableValue_absentWithDefaultValue_returnsDefault() { - FakeSelectableValue selectable = new FakeSelectableValue(ImmutableMap.of()); + public void qualify_structValue_absentWithDefaultValue_returnsDefault() { + FakeStructValue selectable = new FakeStructValue(ImmutableMap.of()); ImmutableList fields = ImmutableList.of(SelectField.create(1L, "absent", 9, "default_fallback")); @@ -304,9 +306,24 @@ public Optional findByFieldNumber(SelectField field) { } @SuppressWarnings("Immutable") - private static final class FakeSelectableValue implements SelectableValue { + private static final class FakeStructValue extends StructValue { private final ImmutableMap values; + @Override + public Object value() { + return values; + } + + @Override + public boolean isZeroValue() { + return values.isEmpty(); + } + + @Override + public CelType celType() { + return StructTypeReference.create("test.FakeStruct"); + } + @Override public Object select(String field) { Object value = values.get(field); @@ -321,7 +338,7 @@ public Optional find(String field) { return Optional.ofNullable(values.get(field)); } - FakeSelectableValue(Map values) { + FakeStructValue(Map values) { this.values = ImmutableMap.copyOf(values); } } diff --git a/common/src/test/java/dev/cel/common/values/ProtoLiteCelValueConverterTest.java b/common/src/test/java/dev/cel/common/values/ProtoLiteCelValueConverterTest.java index 3b66171e4..0a11c23e5 100644 --- a/common/src/test/java/dev/cel/common/values/ProtoLiteCelValueConverterTest.java +++ b/common/src/test/java/dev/cel/common/values/ProtoLiteCelValueConverterTest.java @@ -15,6 +15,7 @@ package dev.cel.common.values; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableListMultimap; @@ -44,13 +45,36 @@ import dev.cel.common.values.ProtoLiteCelValueConverter.MessageFields; import dev.cel.expr.conformance.proto3.TestAllTypes; import dev.cel.expr.conformance.proto3.TestAllTypesCelDescriptor; +import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor; +import dev.cel.protobuf.CelLiteDescriptor.MessageLiteDescriptor; +import java.io.IOException; import java.time.Instant; import java.util.LinkedHashMap; +import java.util.NoSuchElementException; +import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(TestParameterInjector.class) public class ProtoLiteCelValueConverterTest { + private static final CelLiteDescriptorPool EMPTY_DESCRIPTOR_POOL = + new CelLiteDescriptorPool() { + @Override + public Optional findDescriptor(String protoTypeName) { + return Optional.empty(); + } + + @Override + public Optional findDescriptor(MessageLite messageLite) { + return Optional.empty(); + } + + @Override + public MessageLiteDescriptor getDescriptorOrThrow(String protoTypeName) { + throw new NoSuchElementException(protoTypeName); + } + }; + private static final CelLiteDescriptorPool DESCRIPTOR_POOL = DefaultLiteDescriptorPool.newInstance( ImmutableSet.of(TestAllTypesCelDescriptor.getDescriptor())); @@ -307,7 +331,7 @@ public void readAllFields_unknownFieldsWithValues() throws Exception { LinkedHashMap mapBoolDoubleValues = (LinkedHashMap) fields.values().get("map_bool_double"); assertThat(mapBoolDoubleValues).containsExactly(true, 1.5d, false, 2.5d).inOrder(); - Multimap unknownValues = fields.unknowns(); + ImmutableListMultimap unknownValues = fields.unknowns(); assertThat(unknownValues) .containsExactly( 2500, @@ -326,4 +350,99 @@ public void readAllFields_unknownFieldsWithValues() throws Exception { ByteString.copyFromUtf8("\n\003bar\020\005")) .inOrder(); } + + @Test + public void getDefaultCelValue_fieldDescriptor_returnsDefault() { + FieldLiteDescriptor fieldDescriptor = + DESCRIPTOR_POOL + .getDescriptorOrThrow("cel.expr.conformance.proto3.TestAllTypes") + .getByFieldNameOrThrow("single_string"); + + Object defaultValue = PROTO_LITE_CEL_VALUE_CONVERTER.getDefaultCelValue(fieldDescriptor); + + assertThat(defaultValue).isEqualTo(""); + } + + @Test + public void getDefaultCelValue_nestedMessageWithoutDescriptor_throwsNoSuchElementException() { + FieldLiteDescriptor nestedMsgField = + DESCRIPTOR_POOL + .getDescriptorOrThrow("cel.expr.conformance.proto3.TestAllTypes") + .getByFieldNameOrThrow("single_nested_message"); + ProtoLiteCelValueConverter converterWithoutNested = + ProtoLiteCelValueConverter.newInstance(EMPTY_DESCRIPTOR_POOL); + + assertThrows( + NoSuchElementException.class, + () -> converterWithoutNested.getDefaultCelValue(nestedMsgField)); + } + + @Test + public void tryDecodeWellKnownProto_validBytes_returnsDecodedValue() { + Int32Value int32Value = Int32Value.of(42); + + Optional decoded = + PROTO_LITE_CEL_VALUE_CONVERTER.tryDecodeWellKnownProto( + int32Value.toByteString(), "google.protobuf.Int32Value"); + + assertThat(decoded).hasValue(42L); + } + + @Test + public void tryDecodeWellKnownProto_notWellKnownType_returnsEmpty() { + Optional decoded = + PROTO_LITE_CEL_VALUE_CONVERTER.tryDecodeWellKnownProto( + ByteString.EMPTY, "cel.expr.conformance.proto3.TestAllTypes"); + + assertThat(decoded).isEmpty(); + } + + @Test + public void tryDecodeWellKnownProto_missingDescriptor_returnsEmpty() { + ProtoLiteCelValueConverter converter = + ProtoLiteCelValueConverter.newInstance(EMPTY_DESCRIPTOR_POOL); + + Optional decoded = + converter.tryDecodeWellKnownProto(ByteString.EMPTY, "google.protobuf.Int32Value"); + + assertThat(decoded).isEmpty(); + } + + @Test + public void tryDecodeWellKnownProto_invalidBytes_throwsIllegalArgumentException() { + ByteString corruptBytes = ByteString.copyFrom(new byte[] {(byte) 0xFF, (byte) 0xFF}); + + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + PROTO_LITE_CEL_VALUE_CONVERTER.tryDecodeWellKnownProto( + corruptBytes, "google.protobuf.Int32Value")); + + assertThat(exception) + .hasMessageThat() + .contains("Failed to decode well-known proto of type: google.protobuf.Int32Value"); + assertThat(exception).hasCauseThat().isInstanceOf(IOException.class); + } + + @Test + public void tryDecodeWellKnownProto_anyType_throwsUnsupportedOperationException() { + UnsupportedOperationException exception = + assertThrows( + UnsupportedOperationException.class, + () -> + PROTO_LITE_CEL_VALUE_CONVERTER.tryDecodeWellKnownProto( + ByteString.EMPTY, "google.protobuf.Any")); + + assertThat(exception).hasMessageThat().contains("ANY_VALUE"); + } + + @Test + public void hasDescriptor_returnsExpectedResult() { + assertThat( + PROTO_LITE_CEL_VALUE_CONVERTER.hasDescriptor( + "cel.expr.conformance.proto3.TestAllTypes")) + .isTrue(); + assertThat(PROTO_LITE_CEL_VALUE_CONVERTER.hasDescriptor("unknown.Type")).isFalse(); + } } 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 8f5ac623a..180883f60 100644 --- a/common/src/test/java/dev/cel/common/values/RawProtoMessageLiteValueTest.java +++ b/common/src/test/java/dev/cel/common/values/RawProtoMessageLiteValueTest.java @@ -18,25 +18,64 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertThrows; +import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; import com.google.common.primitives.UnsignedLong; import com.google.protobuf.ByteString; import com.google.protobuf.CodedOutputStream; +import com.google.protobuf.Int64Value; +import com.google.protobuf.MessageLite; import com.google.protobuf.WireFormat; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; import dev.cel.common.exceptions.CelAttributeNotFoundException; +import dev.cel.common.internal.CelLiteDescriptorPool; +import dev.cel.common.internal.DefaultLiteDescriptorPool; +import dev.cel.common.internal.ProtoTimeUtils; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypesCelDescriptor; import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor; +import dev.cel.protobuf.CelLiteDescriptor.MessageLiteDescriptor; import java.io.ByteArrayOutputStream; +import java.time.Duration; +import java.util.NoSuchElementException; +import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; -@RunWith(JUnit4.class) +@RunWith(TestParameterInjector.class) public final class RawProtoMessageLiteValueTest { + private static final ProtoLiteCelValueConverter CONVERTER = + ProtoLiteCelValueConverter.newInstance( + DefaultLiteDescriptorPool.newInstance( + ImmutableSet.of(TestAllTypesCelDescriptor.getDescriptor()))); + + private static final ProtoLiteCelValueConverter EMPTY_CONVERTER = + ProtoLiteCelValueConverter.newInstance(DefaultLiteDescriptorPool.newInstance()); + + private static Object decodeWireEntries( + ImmutableCollection entries, int typeCode, String protoTypeName, boolean isRepeated) { + return RawProtoMessageLiteValue.decodeWireEntries( + entries, typeCode, protoTypeName, isRepeated, EMPTY_CONVERTER); + } + + private static Object decodeWireValue( + Object raw, WireFormat.FieldType fieldType, String protoTypeName) { + return RawProtoMessageLiteValue.decodeWireValue(raw, fieldType, protoTypeName, EMPTY_CONVERTER); + } + + private static Object decodeWireValue(Object raw, int typeCode, String protoTypeName) { + return RawProtoMessageLiteValue.decodeWireValue(raw, typeCode, protoTypeName, EMPTY_CONVERTER); + } + @Test public void create_accessorsAndType() { ByteString bytes = ByteString.copyFromUtf8("test"); - RawProtoMessageLiteValue value = RawProtoMessageLiteValue.create(bytes, "custom.Message"); + RawProtoMessageLiteValue value = + RawProtoMessageLiteValue.create(bytes, "custom.Message", EMPTY_CONVERTER); assertThat(value.rawWireBytes()).isEqualTo(bytes); assertThat(value.value()).isSameInstanceAs(value); @@ -44,9 +83,9 @@ public void create_accessorsAndType() { } @Test - public void create_singleArgDefaultsEmptyTypeName() { + public void create_defaultsEmptyTypeName() { ByteString bytes = ByteString.copyFromUtf8("test"); - RawProtoMessageLiteValue value = RawProtoMessageLiteValue.create(bytes); + RawProtoMessageLiteValue value = RawProtoMessageLiteValue.create(bytes, EMPTY_CONVERTER); assertThat(value.rawWireBytes()).isEqualTo(bytes); assertThat(value.celType().name()).isEmpty(); @@ -55,22 +94,23 @@ public void create_singleArgDefaultsEmptyTypeName() { @Test public void select_throwsCelAttributeNotFoundException() { RawProtoMessageLiteValue value = - RawProtoMessageLiteValue.create(ByteString.EMPTY, "custom.Message"); + RawProtoMessageLiteValue.create(ByteString.EMPTY, "custom.Message", EMPTY_CONVERTER); assertThrows(CelAttributeNotFoundException.class, () -> value.select("field")); } @Test - public void find_returnsEmptyOptional() { + public void find_throwsCelAttributeNotFoundException() { RawProtoMessageLiteValue value = - RawProtoMessageLiteValue.create(ByteString.EMPTY, "custom.Message"); + RawProtoMessageLiteValue.create(ByteString.EMPTY, "custom.Message", EMPTY_CONVERTER); - assertThat(value.find("field")).isEmpty(); + assertThrows(CelAttributeNotFoundException.class, () -> value.find("field")); } @Test public void isZeroValue_emptyBytes_returnsTrue() { - RawProtoMessageLiteValue value = RawProtoMessageLiteValue.create(ByteString.EMPTY); + RawProtoMessageLiteValue value = + RawProtoMessageLiteValue.create(ByteString.EMPTY, EMPTY_CONVERTER); assertThat(value.isZeroValue()).isTrue(); } @@ -78,22 +118,27 @@ public void isZeroValue_emptyBytes_returnsTrue() { @Test public void isZeroValue_nonEmptyBytes_returnsFalse() { RawProtoMessageLiteValue value = - RawProtoMessageLiteValue.create(ByteString.copyFromUtf8("data")); + RawProtoMessageLiteValue.create(ByteString.copyFromUtf8("data"), EMPTY_CONVERTER); assertThat(value.isZeroValue()).isFalse(); } @Test - public void hasField_returnsExpectedPresence() throws Exception { + public void hasFieldByNumber_scalarField_returnsExpectedPresence() throws Exception { ByteArrayOutputStream baos = new ByteArrayOutputStream(); CodedOutputStream cos = CodedOutputStream.newInstance(baos); cos.writeInt64(1, 42L); cos.flush(); RawProtoMessageLiteValue value = - RawProtoMessageLiteValue.create(ByteString.copyFrom(baos.toByteArray())); + RawProtoMessageLiteValue.create(ByteString.copyFrom(baos.toByteArray()), EMPTY_CONVERTER); + + SelectField field1 = + SelectField.create(1L, "single_int64", FieldLiteDescriptor.Type.INT64.getNumber(), 0L); + SelectField field2 = + SelectField.create(2L, "single_int64", FieldLiteDescriptor.Type.INT64.getNumber(), 0L); - assertThat(value.hasField(1)).isTrue(); - assertThat(value.hasField(2)).isFalse(); + assertThat(value.hasFieldByNumber(field1)).isTrue(); + assertThat(value.hasFieldByNumber(field2)).isFalse(); } @Test @@ -107,7 +152,7 @@ public void unknownFields_parsesWireTags() throws Exception { cos.flush(); RawProtoMessageLiteValue value = - RawProtoMessageLiteValue.create(ByteString.copyFrom(baos.toByteArray())); + RawProtoMessageLiteValue.create(ByteString.copyFrom(baos.toByteArray()), EMPTY_CONVERTER); assertThat(value.unknownFields()).valuesForKey(1).containsExactly(42L); assertThat(value.unknownFields()).valuesForKey(2).containsExactly(100); @@ -120,13 +165,13 @@ public void unknownFields_parsesWireTags() throws Exception { @Test public void decodeWireEntries_emptySingularEntries_returnsNull() { Object intResult = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(), FieldLiteDescriptor.Type.INT64.getNumber(), "custom.Message", /* isRepeated= */ false); Object messageResult = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(), FieldLiteDescriptor.Type.MESSAGE.getNumber(), "custom.Message", @@ -139,7 +184,7 @@ public void decodeWireEntries_emptySingularEntries_returnsNull() { @Test public void decodeWireEntries_emptyRepeatedEntries_returnsEmptyList() { Object result = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(), FieldLiteDescriptor.Type.INT64.getNumber(), "custom.Message", @@ -151,7 +196,7 @@ public void decodeWireEntries_emptyRepeatedEntries_returnsEmptyList() { @Test public void decodeWireEntries_nonRepeated_lastOneWins() { Object decoded = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(10L, 20L, 30L), FieldLiteDescriptor.Type.INT64.getNumber(), "custom.Message", @@ -163,7 +208,7 @@ public void decodeWireEntries_nonRepeated_lastOneWins() { @Test public void decodeWireEntries_repeatedUnpacked() { Object decoded = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(10L, 20L, 30L), FieldLiteDescriptor.Type.INT64.getNumber(), "custom.Message", @@ -182,7 +227,7 @@ public void decodeWireEntries_packedInt32() throws Exception { cos.flush(); Object decoded = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFrom(baos.toByteArray())), FieldLiteDescriptor.Type.INT32.getNumber(), "custom.Message", @@ -200,7 +245,7 @@ public void decodeWireEntries_packedInt64() throws Exception { cos.flush(); Object decoded = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFrom(baos.toByteArray())), FieldLiteDescriptor.Type.INT64.getNumber(), "custom.Message", @@ -217,7 +262,7 @@ public void decodeWireEntries_packedUint32() throws Exception { cos.flush(); Object decoded = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFrom(baos.toByteArray())), FieldLiteDescriptor.Type.UINT32.getNumber(), "custom.Message", @@ -234,7 +279,7 @@ public void decodeWireEntries_packedUint64() throws Exception { cos.flush(); Object decoded = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFrom(baos.toByteArray())), FieldLiteDescriptor.Type.UINT64.getNumber(), "custom.Message", @@ -252,7 +297,7 @@ public void decodeWireEntries_packedSint32AndSint64() throws Exception { cos32.flush(); Object decoded32 = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFrom(baos32.toByteArray())), FieldLiteDescriptor.Type.SINT32.getNumber(), "custom.Message", @@ -267,7 +312,7 @@ public void decodeWireEntries_packedSint32AndSint64() throws Exception { cos64.flush(); Object decoded64 = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFrom(baos64.toByteArray())), FieldLiteDescriptor.Type.SINT64.getNumber(), "custom.Message", @@ -287,7 +332,7 @@ public void decodeWireEntries_packedFixedAndSFixed() throws Exception { cos.flush(); assertThat( - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFrom(baos.toByteArray()).substring(0, 4)), FieldLiteDescriptor.Type.FIXED32.getNumber(), "custom.Message", @@ -295,7 +340,7 @@ public void decodeWireEntries_packedFixedAndSFixed() throws Exception { .isEqualTo(ImmutableList.of(UnsignedLong.fromLongBits(10L))); assertThat( - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFrom(baos.toByteArray()).substring(4, 12)), FieldLiteDescriptor.Type.FIXED64.getNumber(), "custom.Message", @@ -303,7 +348,7 @@ public void decodeWireEntries_packedFixedAndSFixed() throws Exception { .isEqualTo(ImmutableList.of(UnsignedLong.fromLongBits(20L))); assertThat( - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFrom(baos.toByteArray()).substring(12, 16)), FieldLiteDescriptor.Type.SFIXED32.getNumber(), "custom.Message", @@ -311,7 +356,7 @@ public void decodeWireEntries_packedFixedAndSFixed() throws Exception { .isEqualTo(ImmutableList.of(-30L)); assertThat( - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFrom(baos.toByteArray()).substring(16, 24)), FieldLiteDescriptor.Type.SFIXED64.getNumber(), "custom.Message", @@ -328,7 +373,7 @@ public void decodeWireEntries_packedBoolFloatDoubleEnum() throws Exception { cosBool.flush(); assertThat( - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFrom(baosBool.toByteArray())), FieldLiteDescriptor.Type.BOOL.getNumber(), "custom.Message", @@ -341,7 +386,7 @@ public void decodeWireEntries_packedBoolFloatDoubleEnum() throws Exception { cosFloat.flush(); assertThat( - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFrom(baosFloat.toByteArray())), FieldLiteDescriptor.Type.FLOAT.getNumber(), "custom.Message", @@ -354,7 +399,7 @@ public void decodeWireEntries_packedBoolFloatDoubleEnum() throws Exception { cosDouble.flush(); assertThat( - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFrom(baosDouble.toByteArray())), FieldLiteDescriptor.Type.DOUBLE.getNumber(), "custom.Message", @@ -367,7 +412,7 @@ public void decodeWireEntries_packedBoolFloatDoubleEnum() throws Exception { cosEnum.flush(); assertThat( - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFrom(baosEnum.toByteArray())), FieldLiteDescriptor.Type.ENUM.getNumber(), "custom.Message", @@ -378,99 +423,72 @@ public void decodeWireEntries_packedBoolFloatDoubleEnum() throws Exception { @Test public void decodeWireValue_allScalarWireTypes() { assertThat( - RawProtoMessageLiteValue.decodeWireValue( + decodeWireValue( Double.doubleToRawLongBits(2.5d), WireFormat.FieldType.DOUBLE, "custom.Message")) .isEqualTo(2.5d); assertThat( - RawProtoMessageLiteValue.decodeWireValue( + decodeWireValue( Float.floatToRawIntBits(1.5f), WireFormat.FieldType.FLOAT, "custom.Message")) .isEqualTo(1.5d); - assertThat( - RawProtoMessageLiteValue.decodeWireValue( - 42L, WireFormat.FieldType.INT64, "custom.Message")) - .isEqualTo(42L); + assertThat(decodeWireValue(42L, WireFormat.FieldType.INT64, "custom.Message")).isEqualTo(42L); - assertThat( - RawProtoMessageLiteValue.decodeWireValue( - 42L, WireFormat.FieldType.INT32, "custom.Message")) - .isEqualTo(42L); + assertThat(decodeWireValue(42L, WireFormat.FieldType.INT32, "custom.Message")).isEqualTo(42L); - assertThat( - RawProtoMessageLiteValue.decodeWireValue( - 42L, WireFormat.FieldType.UINT64, "custom.Message")) + assertThat(decodeWireValue(42L, WireFormat.FieldType.UINT64, "custom.Message")) .isEqualTo(UnsignedLong.fromLongBits(42L)); - assertThat( - RawProtoMessageLiteValue.decodeWireValue( - 42L, WireFormat.FieldType.UINT32, "custom.Message")) + assertThat(decodeWireValue(42L, WireFormat.FieldType.UINT32, "custom.Message")) .isEqualTo(UnsignedLong.fromLongBits(42L)); - assertThat( - RawProtoMessageLiteValue.decodeWireValue( - 100, WireFormat.FieldType.FIXED32, "custom.Message")) + assertThat(decodeWireValue(100, WireFormat.FieldType.FIXED32, "custom.Message")) .isEqualTo(UnsignedLong.fromLongBits(100L)); - assertThat( - RawProtoMessageLiteValue.decodeWireValue( - 100L, WireFormat.FieldType.FIXED64, "custom.Message")) + assertThat(decodeWireValue(100L, WireFormat.FieldType.FIXED64, "custom.Message")) .isEqualTo(UnsignedLong.fromLongBits(100L)); - assertThat( - RawProtoMessageLiteValue.decodeWireValue( - -50, WireFormat.FieldType.SFIXED32, "custom.Message")) + assertThat(decodeWireValue(-50, WireFormat.FieldType.SFIXED32, "custom.Message")) .isEqualTo(-50L); - assertThat( - RawProtoMessageLiteValue.decodeWireValue( - -50L, WireFormat.FieldType.SFIXED64, "custom.Message")) + assertThat(decodeWireValue(-50L, WireFormat.FieldType.SFIXED64, "custom.Message")) .isEqualTo(-50L); - assertThat( - RawProtoMessageLiteValue.decodeWireValue( - 1L, WireFormat.FieldType.BOOL, "custom.Message")) - .isEqualTo(true); + assertThat(decodeWireValue(1L, WireFormat.FieldType.BOOL, "custom.Message")).isEqualTo(true); - assertThat( - RawProtoMessageLiteValue.decodeWireValue( - 0L, WireFormat.FieldType.BOOL, "custom.Message")) - .isEqualTo(false); + assertThat(decodeWireValue(0L, WireFormat.FieldType.BOOL, "custom.Message")).isEqualTo(false); assertThat( - RawProtoMessageLiteValue.decodeWireValue( + decodeWireValue( ByteString.copyFromUtf8("hello"), WireFormat.FieldType.STRING, "custom.Message")) .isEqualTo("hello"); assertThat( - RawProtoMessageLiteValue.decodeWireValue( + decodeWireValue( ByteString.copyFromUtf8("bytes"), WireFormat.FieldType.BYTES, "custom.Message")) .isEqualTo(CelByteString.of("bytes".getBytes(UTF_8))); assertThat( - RawProtoMessageLiteValue.decodeWireValue( + decodeWireValue( 1L, // zigzag 1 -> -1 WireFormat.FieldType.SINT32, "custom.Message")) .isEqualTo(-1L); assertThat( - RawProtoMessageLiteValue.decodeWireValue( + decodeWireValue( 1L, // zigzag 1 -> -1 WireFormat.FieldType.SINT64, "custom.Message")) .isEqualTo(-1L); - assertThat( - RawProtoMessageLiteValue.decodeWireValue( - 3L, WireFormat.FieldType.ENUM, "custom.Message")) - .isEqualTo(3L); + assertThat(decodeWireValue(3L, WireFormat.FieldType.ENUM, "custom.Message")).isEqualTo(3L); } @Test public void decodeWireValue_messageType_returnsRawProtoMessageLiteValue() { Object submessage = - RawProtoMessageLiteValue.decodeWireValue( + decodeWireValue( ByteString.copyFromUtf8("raw"), WireFormat.FieldType.MESSAGE, "sub.Message"); assertThat(submessage).isInstanceOf(RawProtoMessageLiteValue.class); @@ -484,9 +502,7 @@ public void decodeWireValue_groupType_throwsUnsupportedOperationException() { UnsupportedOperationException thrown = assertThrows( UnsupportedOperationException.class, - () -> - RawProtoMessageLiteValue.decodeWireValue( - rawBytes, WireFormat.FieldType.GROUP, "group.Message")); + () -> decodeWireValue(rawBytes, WireFormat.FieldType.GROUP, "group.Message")); assertThat(thrown).hasMessageThat().contains("Groups are not supported"); } @@ -500,7 +516,7 @@ public void decodeWireEntries_groupType_throwsUnsupportedOperationException() { assertThrows( UnsupportedOperationException.class, () -> - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( rawEntries, groupTypeCode, "group.Message", /* isRepeated= */ false)); assertThat(thrown).hasMessageThat().contains("Groups are not supported"); @@ -512,30 +528,22 @@ public void decodeWireEntries_invalidTypeCode_throwsIllegalArgumentException() { assertThrows( IllegalArgumentException.class, - () -> - RawProtoMessageLiteValue.decodeWireEntries( - rawEntries, 999, "custom.Message", /* isRepeated= */ false)); + () -> decodeWireEntries(rawEntries, 999, "custom.Message", /* isRepeated= */ false)); } @Test public void decodeWireValue_invalidTypeCode_throws() { - assertThrows( - IllegalArgumentException.class, - () -> RawProtoMessageLiteValue.decodeWireValue(42L, 0, "custom.Message")); + assertThrows(IllegalArgumentException.class, () -> decodeWireValue(42L, 0, "custom.Message")); - assertThrows( - IllegalArgumentException.class, - () -> RawProtoMessageLiteValue.decodeWireValue(42L, 999, "custom.Message")); + assertThrows(IllegalArgumentException.class, () -> decodeWireValue(42L, 999, "custom.Message")); } @Test public void decodeWireValue_int32HighBits_truncatedToSigned32Bit() { Object decodedHigh = - RawProtoMessageLiteValue.decodeWireValue( - 0x100000005L, WireFormat.FieldType.INT32, "custom.Message"); + decodeWireValue(0x100000005L, WireFormat.FieldType.INT32, "custom.Message"); Object decodedNegative = - RawProtoMessageLiteValue.decodeWireValue( - 0xFFFFFFFF80000000L, WireFormat.FieldType.INT32, "custom.Message"); + decodeWireValue(0xFFFFFFFF80000000L, WireFormat.FieldType.INT32, "custom.Message"); assertThat(decodedHigh).isEqualTo(5L); assertThat(decodedNegative).isEqualTo(-2147483648L); @@ -543,9 +551,7 @@ public void decodeWireValue_int32HighBits_truncatedToSigned32Bit() { @Test public void decodeWireValue_enumHighBits_truncatedToSigned32Bit() { - Object decodedHigh = - RawProtoMessageLiteValue.decodeWireValue( - 0x100000005L, WireFormat.FieldType.ENUM, "custom.Message"); + Object decodedHigh = decodeWireValue(0x100000005L, WireFormat.FieldType.ENUM, "custom.Message"); assertThat(decodedHigh).isEqualTo(5L); } @@ -555,33 +561,25 @@ public void decodeWireValue_typeMismatch_throwsIllegalArgumentException() { IllegalArgumentException thrownInt64 = assertThrows( IllegalArgumentException.class, - () -> - RawProtoMessageLiteValue.decodeWireValue( - "not a long", WireFormat.FieldType.INT64, "custom.Message")); + () -> decodeWireValue("not a long", WireFormat.FieldType.INT64, "custom.Message")); assertThat(thrownInt64).hasMessageThat().contains("Expected Long for wire type INT64"); IllegalArgumentException thrownString = assertThrows( IllegalArgumentException.class, - () -> - RawProtoMessageLiteValue.decodeWireValue( - 100L, WireFormat.FieldType.STRING, "custom.Message")); + () -> decodeWireValue(100L, WireFormat.FieldType.STRING, "custom.Message")); assertThat(thrownString).hasMessageThat().contains("Expected ByteString for wire type STRING"); IllegalArgumentException thrownBytes = assertThrows( IllegalArgumentException.class, - () -> - RawProtoMessageLiteValue.decodeWireValue( - 100L, WireFormat.FieldType.BYTES, "custom.Message")); + () -> decodeWireValue(100L, WireFormat.FieldType.BYTES, "custom.Message")); assertThat(thrownBytes).hasMessageThat().contains("Expected ByteString for wire type BYTES"); IllegalArgumentException thrownMessage = assertThrows( IllegalArgumentException.class, - () -> - RawProtoMessageLiteValue.decodeWireValue( - 100L, WireFormat.FieldType.MESSAGE, "custom.Message")); + () -> decodeWireValue(100L, WireFormat.FieldType.MESSAGE, "custom.Message")); assertThat(thrownMessage) .hasMessageThat() .contains("Expected ByteString for wire type MESSAGE"); @@ -589,17 +587,13 @@ public void decodeWireValue_typeMismatch_throwsIllegalArgumentException() { IllegalArgumentException thrownFloat = assertThrows( IllegalArgumentException.class, - () -> - RawProtoMessageLiteValue.decodeWireValue( - 100L, WireFormat.FieldType.FLOAT, "custom.Message")); + () -> decodeWireValue(100L, WireFormat.FieldType.FLOAT, "custom.Message")); assertThat(thrownFloat).hasMessageThat().contains("Expected Integer for wire type FLOAT"); IllegalArgumentException thrownDouble = assertThrows( IllegalArgumentException.class, - () -> - RawProtoMessageLiteValue.decodeWireValue( - 100, WireFormat.FieldType.DOUBLE, "custom.Message")); + () -> decodeWireValue(100, WireFormat.FieldType.DOUBLE, "custom.Message")); assertThat(thrownDouble).hasMessageThat().contains("Expected Long for wire type DOUBLE"); } @@ -610,9 +604,7 @@ public void decodeWireValue_invalidUtf8String_throwsIllegalArgumentException() { IllegalArgumentException thrown = assertThrows( IllegalArgumentException.class, - () -> - RawProtoMessageLiteValue.decodeWireValue( - invalidUtf8, WireFormat.FieldType.STRING, "custom.Message")); + () -> decodeWireValue(invalidUtf8, WireFormat.FieldType.STRING, "custom.Message")); assertThat(thrown).hasMessageThat().contains("Invalid UTF-8 in string field"); } @@ -631,7 +623,7 @@ public void decodeWireEntries_multiChunkPackedRepeated() throws Exception { cos2.flush(); Object decoded = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of( ByteString.copyFrom(baos1.toByteArray()), ByteString.copyFrom(baos2.toByteArray())), FieldLiteDescriptor.Type.INT32.getNumber(), @@ -650,7 +642,7 @@ public void decodeWireEntries_mixedPackedAndUnpackedRepeated() throws Exception cos.flush(); Object decoded = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(1L, ByteString.copyFrom(baos.toByteArray()), 4L), FieldLiteDescriptor.Type.INT32.getNumber(), "custom.Message", @@ -672,7 +664,7 @@ public void decodeWireEntries_singularMessage_mergesChunks() throws Exception { cos2.flush(); Object decoded = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of( ByteString.copyFrom(baos1.toByteArray()), ByteString.copyFrom(baos2.toByteArray())), FieldLiteDescriptor.Type.MESSAGE.getNumber(), @@ -687,18 +679,14 @@ public void decodeWireEntries_singularMessage_mergesChunks() throws Exception { @Test public void decodeWireValue_uint32HighBit_correctUnsignedLong() { - Object decoded = - RawProtoMessageLiteValue.decodeWireValue( - 0xFFFFFFFFL, WireFormat.FieldType.UINT32, "custom.Message"); + Object decoded = decodeWireValue(0xFFFFFFFFL, WireFormat.FieldType.UINT32, "custom.Message"); assertThat(decoded).isEqualTo(UnsignedLong.valueOf(4294967295L)); } @Test public void decodeWireValue_fixed32HighBit_correctUnsignedLong() { - Object decoded = - RawProtoMessageLiteValue.decodeWireValue( - -1, WireFormat.FieldType.FIXED32, "custom.Message"); + Object decoded = decodeWireValue(-1, WireFormat.FieldType.FIXED32, "custom.Message"); assertThat(decoded).isEqualTo(UnsignedLong.valueOf(4294967295L)); } @@ -706,7 +694,7 @@ public void decodeWireValue_fixed32HighBit_correctUnsignedLong() { @Test public void decodeWireEntries_repeatedString() { Object decoded = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFromUtf8("foo"), ByteString.copyFromUtf8("bar")), FieldLiteDescriptor.Type.STRING.getNumber(), "custom.Message", @@ -718,7 +706,7 @@ public void decodeWireEntries_repeatedString() { @Test public void decodeWireEntries_repeatedBytes() { Object decoded = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFromUtf8("foo"), ByteString.copyFromUtf8("bar")), FieldLiteDescriptor.Type.BYTES.getNumber(), "custom.Message", @@ -733,7 +721,7 @@ public void decodeWireEntries_repeatedBytes() { @Test public void decodeWireEntries_repeatedMessage() { Object decoded = - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(ByteString.copyFromUtf8("msg1"), ByteString.copyFromUtf8("msg2")), FieldLiteDescriptor.Type.MESSAGE.getNumber(), "sub.Message", @@ -741,8 +729,10 @@ public void decodeWireEntries_repeatedMessage() { assertThat((Iterable) decoded) .containsExactly( - RawProtoMessageLiteValue.create(ByteString.copyFromUtf8("msg1"), "sub.Message"), - RawProtoMessageLiteValue.create(ByteString.copyFromUtf8("msg2"), "sub.Message")) + RawProtoMessageLiteValue.create( + ByteString.copyFromUtf8("msg1"), "sub.Message", EMPTY_CONVERTER), + RawProtoMessageLiteValue.create( + ByteString.copyFromUtf8("msg2"), "sub.Message", EMPTY_CONVERTER)) .inOrder(); } @@ -755,7 +745,7 @@ public void decodeWireEntries_packedTruncated_throwsIllegalStateException() { assertThrows( IllegalStateException.class, () -> - RawProtoMessageLiteValue.decodeWireEntries( + decodeWireEntries( ImmutableList.of(truncated), FieldLiteDescriptor.Type.INT32.getNumber(), "custom.Message", @@ -763,4 +753,450 @@ public void decodeWireEntries_packedTruncated_throwsIllegalStateException() { assertThat(thrown).hasMessageThat().contains("Failed to parse packed repeated field"); } + + @Test + public void selectByFieldNumber_presentOnWire_decoded() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeString(14, "hello"); + cos.flush(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.copyFrom(baos.toByteArray()), + "cel.expr.conformance.proto3.TestAllTypes", + EMPTY_CONVERTER); + + Object val = raw.selectByFieldNumber(SelectField.create(14L, "single_string", 9, "")); + + assertThat(val).isEqualTo("hello"); + } + + @Test + public void selectByFieldNumber_absentWithDefaultValue_returnsDefault() { + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.EMPTY, "cel.expr.conformance.proto3.TestAllTypes", EMPTY_CONVERTER); + + Object val = raw.selectByFieldNumber(SelectField.create(14L, "single_string", 9, "default")); + + assertThat(val).isEqualTo("default"); + } + + @Test + public void selectByFieldNumber_withConverter_resolvesDescriptor() { + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.EMPTY, "cel.expr.conformance.proto3.TestAllTypes", CONVERTER); + + Object val = raw.selectByFieldNumber(SelectField.create(14L, "single_string")); + + assertThat(val).isEqualTo(""); + } + + @Test + public void + selectByFieldNumber_absentSubmessageWithMissingChildDescriptor_returnsEmptyRawProtoMessageLiteValue() { + CelLiteDescriptorPool poolWithoutNested = + new CelLiteDescriptorPool() { + @Override + public Optional findDescriptor(String protoTypeName) { + if (protoTypeName.equals("cel.expr.conformance.proto3.TestAllTypes")) { + return DefaultLiteDescriptorPool.newInstance( + ImmutableSet.of(TestAllTypesCelDescriptor.getDescriptor())) + .findDescriptor(protoTypeName); + } + return Optional.empty(); + } + + @Override + public Optional findDescriptor(MessageLite messageLite) { + return Optional.empty(); + } + + @Override + public MessageLiteDescriptor getDescriptorOrThrow(String protoTypeName) { + return findDescriptor(protoTypeName) + .orElseThrow(() -> new NoSuchElementException(protoTypeName)); + } + }; + ProtoLiteCelValueConverter converter = + ProtoLiteCelValueConverter.newInstance(poolWithoutNested); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.EMPTY, "cel.expr.conformance.proto3.TestAllTypes", converter); + + Object val = raw.selectByFieldNumber(SelectField.create(21L, "single_nested_message")); + + assertThat(val).isInstanceOf(RawProtoMessageLiteValue.class); + RawProtoMessageLiteValue rawChild = (RawProtoMessageLiteValue) val; + assertThat(rawChild.rawWireBytes()).isEqualTo(ByteString.EMPTY); + assertThat(rawChild.celType().name()) + .isEqualTo("cel.expr.conformance.proto3.TestAllTypes.NestedMessage"); + } + + @Test + public void + selectByFieldNumber_absentRepeatedMessageWithMissingChildDescriptor_returnsEmptyList() { + CelLiteDescriptorPool poolWithoutNested = + new CelLiteDescriptorPool() { + @Override + public Optional findDescriptor(String protoTypeName) { + if (protoTypeName.equals("cel.expr.conformance.proto3.TestAllTypes")) { + return DefaultLiteDescriptorPool.newInstance( + ImmutableSet.of(TestAllTypesCelDescriptor.getDescriptor())) + .findDescriptor(protoTypeName); + } + return Optional.empty(); + } + + @Override + public Optional findDescriptor(MessageLite messageLite) { + return Optional.empty(); + } + + @Override + public MessageLiteDescriptor getDescriptorOrThrow(String protoTypeName) { + return findDescriptor(protoTypeName) + .orElseThrow(() -> new NoSuchElementException(protoTypeName)); + } + }; + ProtoLiteCelValueConverter converter = + ProtoLiteCelValueConverter.newInstance(poolWithoutNested); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.EMPTY, "cel.expr.conformance.proto3.TestAllTypes", converter); + + Object val = + raw.selectByFieldNumber( + SelectField.create( + TestAllTypes.REPEATED_NESTED_MESSAGE_FIELD_NUMBER, "repeated_nested_message")); + + assertThat(val).isEqualTo(ImmutableList.of()); + } + + @Test + public void selectByFieldNumber_unknownFieldWithoutTypeCode_throwsCelAttributeNotFoundException() + throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeString(999, "unknown"); + cos.flush(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.copyFrom(baos.toByteArray()), + "cel.expr.conformance.proto3.TestAllTypes", + CONVERTER); + SelectField selectField = SelectField.create(999L, "unknown_field"); + + assertThrows(CelAttributeNotFoundException.class, () -> raw.selectByFieldNumber(selectField)); + } + + @Test + public void hasFieldByNumber_wirePresent_returnsTrue() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeString(TestAllTypes.SINGLE_STRING_FIELD_NUMBER, "present"); + cos.flush(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.copyFrom(baos.toByteArray()), + "cel.expr.conformance.proto3.TestAllTypes", + EMPTY_CONVERTER); + + assertThat( + raw.hasFieldByNumber( + SelectField.create(TestAllTypes.SINGLE_STRING_FIELD_NUMBER, "single_string"))) + .isTrue(); + } + + @Test + public void hasFieldByNumber_wireAbsent_returnsFalse() { + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.EMPTY, "cel.expr.conformance.proto3.TestAllTypes", EMPTY_CONVERTER); + + assertThat( + raw.hasFieldByNumber( + SelectField.create(TestAllTypes.SINGLE_STRING_FIELD_NUMBER, "single_string"))) + .isFalse(); + } + + @Test + public void hasFieldByNumber_emptyPackedRepeated_returnsFalse( + @TestParameter boolean withDescriptor) throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeBytes(TestAllTypes.REPEATED_INT32_FIELD_NUMBER, ByteString.EMPTY); + cos.flush(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.copyFrom(baos.toByteArray()), + "cel.expr.conformance.proto3.TestAllTypes", + withDescriptor ? CONVERTER : EMPTY_CONVERTER); + SelectField selectField = + withDescriptor + ? SelectField.create(TestAllTypes.REPEATED_INT32_FIELD_NUMBER, "repeated_int32") + : SelectField.create( + TestAllTypes.REPEATED_INT32_FIELD_NUMBER, + "repeated_int32", + FieldLiteDescriptor.Type.INT32.getNumber(), + ImmutableList.of()); + + assertThat(raw.hasFieldByNumber(selectField)).isFalse(); + } + + @Test + public void hasFieldByNumber_nonEmptyPackedRepeated_returnsTrue() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + ByteArrayOutputStream packed = new ByteArrayOutputStream(); + CodedOutputStream packedCos = CodedOutputStream.newInstance(packed); + packedCos.writeInt32NoTag(42); + packedCos.flush(); + cos.writeBytes( + TestAllTypes.REPEATED_INT32_FIELD_NUMBER, ByteString.copyFrom(packed.toByteArray())); + cos.flush(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.copyFrom(baos.toByteArray()), + "cel.expr.conformance.proto3.TestAllTypes", + CONVERTER); + + assertThat( + raw.hasFieldByNumber( + SelectField.create(TestAllTypes.REPEATED_INT32_FIELD_NUMBER, "repeated_int32"))) + .isTrue(); + } + + @Test + public void hasFieldByNumber_emptyByteStringOnScalarPackableField_returnsTrue( + @TestParameter boolean withDescriptor) throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeByteArray(TestAllTypes.SINGLE_INT32_FIELD_NUMBER, new byte[0]); + cos.flush(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.copyFrom(baos.toByteArray()), + "cel.expr.conformance.proto3.TestAllTypes", + withDescriptor ? CONVERTER : EMPTY_CONVERTER); + SelectField selectField = + withDescriptor + ? SelectField.create(TestAllTypes.SINGLE_INT32_FIELD_NUMBER, "single_int32") + : SelectField.create( + TestAllTypes.SINGLE_INT32_FIELD_NUMBER, + "single_int32", + FieldLiteDescriptor.Type.INT32.getNumber(), + 0); + + assertThat(raw.hasFieldByNumber(selectField)).isTrue(); + } + + @Test + public void findByFieldNumber_intermediatePresent_returnsSubmessage() throws Exception { + ByteArrayOutputStream subBaos1 = new ByteArrayOutputStream(); + CodedOutputStream subCos1 = CodedOutputStream.newInstance(subBaos1); + subCos1.writeInt32(1, 42); + subCos1.flush(); + + ByteArrayOutputStream subBaos2 = new ByteArrayOutputStream(); + CodedOutputStream subCos2 = CodedOutputStream.newInstance(subBaos2); + subCos2.writeInt32(2, 84); + subCos2.flush(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeBytes(21, ByteString.copyFrom(subBaos1.toByteArray())); + cos.writeBytes(21, ByteString.copyFrom(subBaos2.toByteArray())); + cos.flush(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.copyFrom(baos.toByteArray()), + "cel.expr.conformance.proto3.TestAllTypes", + EMPTY_CONVERTER); + + Optional nav = raw.findByFieldNumber(SelectField.create(21L, "single_nested_message")); + + RawProtoMessageLiteValue expected = + RawProtoMessageLiteValue.create( + ByteString.copyFrom(subBaos1.toByteArray()) + .concat(ByteString.copyFrom(subBaos2.toByteArray())), + "cel.@unknownMessage", + EMPTY_CONVERTER); + assertThat(nav).hasValue(expected); + } + + @Test + public void findByFieldNumber_intermediateAbsent_returnsEmpty() { + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.EMPTY, "cel.expr.conformance.proto3.TestAllTypes", EMPTY_CONVERTER); + + Optional nav = raw.findByFieldNumber(SelectField.create(999L, "absent")); + + assertThat(nav).isEmpty(); + } + + @SuppressWarnings("ImmutableEnumChecker") // Test only + private enum SelectByFieldNumberTestCase { + INT64(SelectField.create(TestAllTypes.SINGLE_INT64_FIELD_NUMBER, "single_int64"), 99L), + STRING(SelectField.create(TestAllTypes.SINGLE_STRING_FIELD_NUMBER, "single_string"), "hello"), + MAP_STRING_STRING( + SelectField.create(TestAllTypes.MAP_STRING_STRING_FIELD_NUMBER, "map_string_string"), + ImmutableMap.of("k1", "v1", "k2", "v2")), + MAP_INT32_BYTES( + SelectField.create(TestAllTypes.MAP_INT32_BYTES_FIELD_NUMBER, "map_int32_bytes"), + ImmutableMap.of( + 0L, CelByteString.copyFromUtf8("val_for_default_key"), 42L, CelByteString.EMPTY)), + DURATION( + SelectField.create(TestAllTypes.SINGLE_DURATION_FIELD_NUMBER, "single_duration"), + Duration.ofSeconds(10L, 500L)), + INT64_WRAPPER( + SelectField.create(TestAllTypes.SINGLE_INT64_WRAPPER_FIELD_NUMBER, "single_int64_wrapper"), + 12345L); + + private final SelectField selectField; + private final Object expectedValue; + + private SelectByFieldNumberTestCase(SelectField selectField, Object expectedValue) { + this.selectField = selectField; + this.expectedValue = expectedValue; + } + } + + @Test + public void selectByFieldNumber_withDescriptor_decodesExpectedValue( + @TestParameter SelectByFieldNumberTestCase testCase) { + TestAllTypes proto = + TestAllTypes.newBuilder() + .setSingleInt64(99L) + .setSingleString("hello") + .putMapStringString("k1", "v1") + .putMapStringString("k2", "v2") + .putMapInt32Bytes(0, ByteString.copyFromUtf8("val_for_default_key")) + .putMapInt32Bytes(42, ByteString.EMPTY) + .setSingleDuration(ProtoTimeUtils.toProtoDuration(Duration.ofSeconds(10L, 500L))) + .setSingleInt64Wrapper(Int64Value.of(12345L)) + .build(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + proto.toByteString(), "cel.expr.conformance.proto3.TestAllTypes", CONVERTER); + + Object selected = raw.selectByFieldNumber(testCase.selectField); + + assertThat(selected).isEqualTo(testCase.expectedValue); + } + + @Test + public void findByFieldNumber_scalarField_returnsScalar(@TestParameter boolean withDescriptor) { + TestAllTypes proto = TestAllTypes.newBuilder().setSingleInt64(99L).build(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + proto.toByteString(), + "cel.expr.conformance.proto3.TestAllTypes", + withDescriptor ? CONVERTER : EMPTY_CONVERTER); + + Optional nav = + raw.findByFieldNumber( + SelectField.create(TestAllTypes.SINGLE_INT64_FIELD_NUMBER, "single_int64")); + + assertThat(nav).hasValue(99L); + } + + @Test + public void selectByFieldNumber_unsetWrapperFieldWithoutWrapperDescriptor_returnsNullValue() { + MessageLiteDescriptor testAllTypesDesc = + TestAllTypesCelDescriptor.getDescriptor() + .getProtoTypeNamesToDescriptors() + .get("cel.expr.conformance.proto3.TestAllTypes"); + CelLiteDescriptorPool poolWithoutWrappers = + new CelLiteDescriptorPool() { + @Override + public Optional findDescriptor(String protoTypeName) { + if (protoTypeName.equals(testAllTypesDesc.getProtoTypeName())) { + return Optional.of(testAllTypesDesc); + } + return Optional.empty(); + } + + @Override + public Optional findDescriptor(MessageLite messageLite) { + return findDescriptor(messageLite.getClass().getName()); + } + + @Override + public MessageLiteDescriptor getDescriptorOrThrow(String protoTypeName) { + return findDescriptor(protoTypeName) + .orElseThrow(() -> new NoSuchElementException(protoTypeName)); + } + }; + ProtoLiteCelValueConverter converter = + ProtoLiteCelValueConverter.newInstance(poolWithoutWrappers); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.EMPTY, "cel.expr.conformance.proto3.TestAllTypes", converter); + + Object result = + raw.selectByFieldNumber( + SelectField.create( + TestAllTypes.SINGLE_INT64_WRAPPER_FIELD_NUMBER, "single_int64_wrapper")); + + assertThat(result).isEqualTo(NullValue.NULL_VALUE); + } + + @Test + public void findByFieldNumber_typedFieldWithoutDescriptor_returnsSelectedValue() { + TestAllTypes proto = TestAllTypes.newBuilder().setSingleUint32(123).build(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + proto.toByteString(), "cel.expr.conformance.proto3.TestAllTypes", EMPTY_CONVERTER); + + Optional nav = + raw.findByFieldNumber( + SelectField.create( + TestAllTypes.SINGLE_UINT32_FIELD_NUMBER, + "single_uint32", + FieldLiteDescriptor.Type.UINT32.getNumber(), + 0L)); + + assertThat(nav).hasValue(UnsignedLong.fromLongBits(123L)); + } + + @Test + public void selectByFieldNumber_absentMessageFieldWithoutDescriptor_returnsUnknownMessage() { + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create(ByteString.EMPTY, EMPTY_CONVERTER); + + Object selected = + raw.selectByFieldNumber( + SelectField.create( + 21L, "single_nested_message", FieldLiteDescriptor.Type.MESSAGE.getNumber(), null)); + + assertThat(selected).isInstanceOf(RawProtoMessageLiteValue.class); + RawProtoMessageLiteValue message = (RawProtoMessageLiteValue) selected; + assertThat(message.rawWireBytes()).isEqualTo(ByteString.EMPTY); + assertThat(message.celType().name()).isEqualTo("cel.@unknownMessage"); + } + + @Test + public void + selectByFieldNumber_unknownMapFieldWithWireEntries_throwsUnsupportedOperationException() { + TestAllTypes proto = TestAllTypes.newBuilder().putMapStringString("key", "val").build(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + proto.toByteString(), "cel.expr.conformance.proto3.TestAllTypes", EMPTY_CONVERTER); + SelectField field = + SelectField.create( + TestAllTypes.MAP_STRING_STRING_FIELD_NUMBER, + "map_string_string", + SelectField.CEL_MAP_TYPE_CODE, + null); + + UnsupportedOperationException e = + assertThrows(UnsupportedOperationException.class, () -> raw.selectByFieldNumber(field)); + + assertThat(e) + .hasMessageThat() + .contains("Decoding unknown map field from wire bytes is unsupported"); + } }