From 0a1511c51ad8c920181439eba9b0e1cd7cf7f9dd Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Wed, 16 Sep 2026 17:24:54 -0700 Subject: [PATCH] Implement OptimizedSelectable for RawProtoMessageLiteValue PiperOrigin-RevId: 982817297 --- .../java/dev/cel/common/values/BUILD.bazel | 92 ++ .../values/OptimizedSelectTraversal.java | 158 +++ .../common/values/OptimizedSelectable.java | 45 + .../values/ProtoLiteCelValueConverter.java | 101 +- .../common/values/ProtoMessageLiteValue.java | 16 +- .../values/RawProtoMessageLiteValue.java | 514 +++++++ .../dev/cel/common/values/SelectField.java | 124 ++ .../java/dev/cel/common/values/BUILD.bazel | 6 + .../values/OptimizedSelectTraversalTest.java | 424 ++++++ .../ProtoLiteCelValueConverterTest.java | 121 +- .../values/ProtoMessageLiteValueTest.java | 39 +- .../values/RawProtoMessageLiteValueTest.java | 1202 +++++++++++++++++ .../cel/common/values/SelectFieldTest.java | 133 ++ common/values/BUILD.bazel | 36 + 14 files changed, 2976 insertions(+), 35 deletions(-) create mode 100644 common/src/main/java/dev/cel/common/values/OptimizedSelectTraversal.java create mode 100644 common/src/main/java/dev/cel/common/values/OptimizedSelectable.java create mode 100644 common/src/main/java/dev/cel/common/values/RawProtoMessageLiteValue.java create mode 100644 common/src/main/java/dev/cel/common/values/SelectField.java create mode 100644 common/src/test/java/dev/cel/common/values/OptimizedSelectTraversalTest.java create mode 100644 common/src/test/java/dev/cel/common/values/RawProtoMessageLiteValueTest.java create mode 100644 common/src/test/java/dev/cel/common/values/SelectFieldTest.java 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 433dcd477..51fa12e98 100644 --- a/common/src/main/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/values/BUILD.bazel @@ -317,14 +317,18 @@ java_library( srcs = [ "ProtoLiteCelValueConverter.java", "ProtoMessageLiteValue.java", + "RawProtoMessageLiteValue.java", ], tags = [ ], deps = [ ":base_proto_cel_value_converter", + ":optimized_selectable", + ":select_field", ":values", "//:auto_value", "//common/annotations", + "//common/exceptions:attribute_not_found", "//common/internal:cel_lite_descriptor_pool", "//common/internal:well_known_proto", "//common/types", @@ -333,6 +337,7 @@ java_library( "//protobuf:cel_lite_descriptor", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", "@maven_android//:com_google_protobuf_protobuf_javalite", ], ) @@ -342,14 +347,18 @@ cel_android_library( srcs = [ "ProtoLiteCelValueConverter.java", "ProtoMessageLiteValue.java", + "RawProtoMessageLiteValue.java", ], tags = [ ], deps = [ ":base_proto_cel_value_converter_android", + ":optimized_selectable_android", + ":select_field_android", ":values_android", "//:auto_value", "//common/annotations", + "//common/exceptions:attribute_not_found", "//common/internal:cel_lite_descriptor_pool_android", "//common/internal:well_known_proto_android", "//common/types:type_providers_android", @@ -358,6 +367,7 @@ cel_android_library( "//protobuf:cel_lite_descriptor", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", "@maven_android//:com_google_guava_guava", "@maven_android//:com_google_protobuf_protobuf_javalite", ], @@ -428,3 +438,85 @@ cel_android_library( "@maven//:com_google_errorprone_error_prone_annotations", ], ) + +java_library( + name = "select_field", + srcs = ["SelectField.java"], + tags = [ + ], + deps = [ + "//:auto_value", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", + ], +) + +cel_android_library( + name = "select_field_android", + srcs = ["SelectField.java"], + tags = [ + ], + deps = [ + "//:auto_value", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + "@maven//:org_jspecify_jspecify", + "@maven_android//:com_google_guava_guava", + ], +) + +java_library( + name = "optimized_selectable", + srcs = ["OptimizedSelectable.java"], + tags = [ + ], + deps = [ + ":select_field", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +cel_android_library( + name = "optimized_selectable_android", + srcs = ["OptimizedSelectable.java"], + tags = [ + ], + deps = [ + ":select_field_android", + "//common/annotations", + "@maven//:com_google_errorprone_error_prone_annotations", + ], +) + +java_library( + name = "optimized_select_traversal", + srcs = ["OptimizedSelectTraversal.java"], + tags = [ + ], + deps = [ + ":optimized_selectable", + ":select_field", + ":values", + "//common/annotations", + "//common/exceptions:attribute_not_found", + "@maven//:com_google_guava_guava", + ], +) + +cel_android_library( + name = "optimized_select_traversal_android", + srcs = ["OptimizedSelectTraversal.java"], + tags = [ + ], + deps = [ + ":optimized_selectable_android", + ":select_field_android", + ":values_android", + "//common/annotations", + "//common/exceptions:attribute_not_found", + "@maven_android//:com_google_guava_guava", + ], +) diff --git a/common/src/main/java/dev/cel/common/values/OptimizedSelectTraversal.java b/common/src/main/java/dev/cel/common/values/OptimizedSelectTraversal.java new file mode 100644 index 000000000..2bb4e2935 --- /dev/null +++ b/common/src/main/java/dev/cel/common/values/OptimizedSelectTraversal.java @@ -0,0 +1,158 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.common.values; + +import com.google.common.collect.ImmutableList; +import dev.cel.common.annotations.Internal; +import dev.cel.common.exceptions.CelAttributeNotFoundException; +import java.util.Map; +import java.util.Optional; + +/** + * Walks a sequence of {@link SelectField} selections, dispatching each field over {@link + * OptimizedSelectable}, {@link SelectableValue}, or {@link Map}. + * + *

CEL Library Internals. Do Not Use. + */ +@Internal +public final class OptimizedSelectTraversal { + + /** + * Qualifies {@code target} through every field in {@code fields} and returns the terminal value. + * + * @param celValueConverter Converter for unadapted entries encountered in a root {@link Map}. + */ + public static Object qualify( + Object target, ImmutableList fields, CelValueConverter celValueConverter) { + Object current = target; + for (int i = 0; i < fields.size(); i++) { + current = qualifyField(current, fields.get(i), celValueConverter); + } + return current; + } + + /** + * 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, CelValueConverter celValueConverter) { + if (fields.isEmpty()) { + return false; + } + Object current = target; + int terminalIndex = fields.size() - 1; + for (int i = 0; i < terminalIndex; i++) { + Optional next = navigateField(current, fields.get(i), celValueConverter); + if (!next.isPresent()) { + return false; + } + current = next.get(); + } + return hasTerminalField(current, fields.get(terminalIndex)); + } + + // SelectableValue is only ever instantiated with String keys in the select path. + @SuppressWarnings("unchecked") + private static Object qualifyField( + Object target, SelectField field, CelValueConverter celValueConverter) { + if (target instanceof ErrorValue) { + return target; + } + if (target instanceof OptimizedSelectable) { + return ((OptimizedSelectable) target).selectByFieldNumber(field); + } + if (target instanceof SelectableValue) { + SelectableValue selectable = (SelectableValue) target; + if (field.defaultValue() != null) { + return selectable + .find(field.fieldName()) + .map(Object.class::cast) + .orElse(field.defaultValue()); + } + return selectable.select(field.fieldName()); + } + if (target instanceof Map) { + return getMapEntry((Map) target, field.fieldName(), celValueConverter); + } + throw CelAttributeNotFoundException.forFieldResolution(field.fieldName()); + } + + // SelectableValue is only ever instantiated with String keys in the select path. + @SuppressWarnings("unchecked") + private static Optional navigateField( + Object target, SelectField field, CelValueConverter celValueConverter) { + if (target instanceof ErrorValue) { + return Optional.of(target); + } + 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 Map) { + return findMapEntry((Map) target, field.fieldName(), celValueConverter); + } + throw CelAttributeNotFoundException.forFieldResolution(field.fieldName()); + } + + // SelectableValue 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) { + return false; + } + if (target instanceof OptimizedSelectable) { + return ((OptimizedSelectable) target).hasFieldByNumber(field); + } + if (target instanceof SelectableValue) { + return ((SelectableValue) target).find(field.fieldName()).isPresent(); + } + if (target instanceof Map) { + return ((Map) target).containsKey(field.fieldName()); + } + throw CelAttributeNotFoundException.forFieldResolution(field.fieldName()); + } + + private static Object getMapEntry( + Map map, String key, CelValueConverter celValueConverter) { + return findMapEntry(map, key, celValueConverter) + .orElseThrow(() -> CelAttributeNotFoundException.forMissingMapKey(key)); + } + + private static Optional findMapEntry( + Map map, String key, CelValueConverter celValueConverter) { + Object mapValue = map.get(key); + if (mapValue != null) { + return Optional.of(toStepTarget(mapValue, celValueConverter)); + } + if (!map.containsKey(key)) { + return Optional.empty(); + } + throw CelAttributeNotFoundException.of( + String.format("Map value cannot be null for key: %s", key)); + } + + static Object toStepTarget(Object value, CelValueConverter celValueConverter) { + if (value instanceof Map) { + return value; + } + return celValueConverter.toRuntimeValue(value); + } + + private OptimizedSelectTraversal() {} +} diff --git a/common/src/main/java/dev/cel/common/values/OptimizedSelectable.java b/common/src/main/java/dev/cel/common/values/OptimizedSelectable.java new file mode 100644 index 000000000..828d15227 --- /dev/null +++ b/common/src/main/java/dev/cel/common/values/OptimizedSelectable.java @@ -0,0 +1,45 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.common.values; + +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.annotations.Internal; +import java.util.Optional; + +/** + * Resolves an optimized field selection within a selection chain rewritten by the select optimizer. + * + *

Implementations resolve individual field selections against themselves by protobuf field + * number. Walking the chain across multiple fields and heterogeneous values belongs to {@link + * OptimizedSelectTraversal}. + * + *

CEL Library Internals. Do Not Use. + */ +@Internal +@Immutable +public interface OptimizedSelectable { + + /** Selects {@code field}, falling back to its default value or an empty submessage if absent. */ + Object selectByFieldNumber(SelectField field); + + /** Returns whether {@code field} is present. */ + boolean hasFieldByNumber(SelectField field); + + /** + * Returns the value of the field at {@code field} (a scalar or submessage) for an intermediate + * step of a presence test, or empty if absent. + */ + Optional findByFieldNumber(SelectField field); +} 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 64d6ec1d4..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,14 +17,15 @@ 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; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Multimap; 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; @@ -45,6 +46,7 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Optional; import java.util.TreeMap; /** @@ -60,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( @@ -67,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()) { @@ -80,7 +89,7 @@ private static Object readPrimitiveField( case INT64: return inputStream.readInt64(); case UINT32: - return UnsignedLong.fromLongBits(inputStream.readUInt32()); + return UnsignedLong.fromLongBits(Integer.toUnsignedLong(inputStream.readUInt32())); case UINT64: return UnsignedLong.fromLongBits(inputStream.readUInt64()); case BOOL: @@ -153,11 +162,45 @@ private MessageLite.Builder getDefaultMessageBuilder(String protoTypeName) { Object getDefaultCelValue(String protoTypeName, String fieldName) { MessageLiteDescriptor messageDescriptor = descriptorPool.getDescriptorOrThrow(protoTypeName); - FieldLiteDescriptor fieldDescriptor = messageDescriptor.getByFieldNameOrThrow(fieldName); + return getDefaultCelValue(messageDescriptor.getByFieldNameOrThrow(fieldName)); + } - Object defaultValue = getDefaultValue(fieldDescriptor); + Object getDefaultCelValue(FieldLiteDescriptor fieldDescriptor) { + return toRuntimeValue(getDefaultValue(fieldDescriptor)); + } - return toRuntimeValue(defaultValue); + Optional findFieldDescriptor(String protoTypeName, int fieldNumber) { + return descriptorPool + .findDescriptor(protoTypeName) + .flatMap(desc -> desc.findByFieldNumber(fieldNumber)); + } + + 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 @@ -193,7 +236,10 @@ protected Object fromWellKnownProto(MessageLiteOrBuilder msg, WellKnownProto wel descriptorPool .findDescriptor(message) .orElseThrow( - () -> new NoSuchElementException("Could not find a descriptor for: " + message)); + () -> + new NoSuchElementException( + "Could not find a descriptor for message of type: " + + message.getClass().getName())); return ProtoMessageLiteValue.create(message, descriptor.getProtoTypeName(), this); } @@ -260,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); @@ -344,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); } @@ -367,13 +415,11 @@ MessageFields readAllFields(byte[] bytes, String protoTypeName) throws IOExcepti return MessageFields.create(fieldValues.buildKeepingLast(), unknownFields); } - ImmutableMap readAllFields(MessageLite msg, String protoTypeName) - throws IOException { - return readAllFields(msg.toByteArray(), protoTypeName).values(); + MessageFields readMessageFields(MessageLite msg, String protoTypeName) throws IOException { + return readAllFields(msg.toByteArray(), protoTypeName); } - private static Object readUnknownField(int tagWireType, CodedInputStream inputStream) - throws IOException { + static Object readUnknownField(int tagWireType, CodedInputStream inputStream) throws IOException { switch (tagWireType) { case WireFormat.WIRETYPE_VARINT: return inputStream.readInt64(); @@ -393,16 +439,19 @@ private static Object readUnknownField(int tagWireType, CodedInputStream inputSt } @AutoValue - @SuppressWarnings("AutoValueImmutableFields") // Unknowns are inaccessible to users. + @AutoValue.CopyAnnotations + @Immutable + @SuppressWarnings("Immutable") // Safe immutable fields abstract static class MessageFields { abstract ImmutableMap values(); - abstract Multimap unknowns(); + abstract ImmutableListMultimap unknowns(); static MessageFields create( ImmutableMap fieldValues, Multimap unknownFields) { - return new AutoValue_ProtoLiteCelValueConverter_MessageFields(fieldValues, unknownFields); + return new AutoValue_ProtoLiteCelValueConverter_MessageFields( + fieldValues, ImmutableListMultimap.copyOf(unknownFields)); } } diff --git a/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java b/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java index 2e4d980c7..99e95ebd3 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java +++ b/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java @@ -17,11 +17,14 @@ import com.google.auto.value.AutoValue; import com.google.auto.value.extension.memoized.Memoized; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.errorprone.annotations.Immutable; import com.google.protobuf.MessageLite; +import dev.cel.common.annotations.Internal; import dev.cel.common.types.CelType; import dev.cel.common.types.StructTypeReference; +import dev.cel.common.values.ProtoLiteCelValueConverter.MessageFields; import java.io.IOException; import java.util.Optional; @@ -46,14 +49,23 @@ public abstract class ProtoMessageLiteValue extends StructValue fieldValues() { + MessageFields messageFields() { try { - return protoLiteCelValueConverter().readAllFields(value(), celType().name()); + return protoLiteCelValueConverter().readMessageFields(value(), celType().name()); } catch (IOException e) { throw new IllegalStateException("Unable to read message fields for " + celType().name(), e); } } + @Internal + public ImmutableMap fieldValues() { + return messageFields().values(); + } + + public ImmutableListMultimap unknownFields() { + return messageFields().unknowns(); + } + @Override public boolean isZeroValue() { return value().getDefaultInstanceForType().equals(value()); diff --git a/common/src/main/java/dev/cel/common/values/RawProtoMessageLiteValue.java b/common/src/main/java/dev/cel/common/values/RawProtoMessageLiteValue.java new file mode 100644 index 000000000..2a3bdf940 --- /dev/null +++ b/common/src/main/java/dev/cel/common/values/RawProtoMessageLiteValue.java @@ -0,0 +1,514 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +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; +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.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; + +/** + * RawProtoMessageLiteValue enables descriptorless evaluation of protobuf messages to address + * client-server version skew issues where newer fields or submessages lack generated classes and + * descriptors in the evaluation environment. + * + *

Rather than requiring compiled {@link MessageLite} classes or runtime schema descriptors, this + * value encapsulates the raw wire-format {@link ByteString} payload and performs classless, + * reflection-free field traversal directly over wire tags via {@link CodedInputStream}. + */ +@AutoValue +@AutoValue.CopyAnnotations +@Immutable +@SuppressWarnings("Immutable") // Immutable wire fields +@Internal +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; + } + + @Memoized + ImmutableListMultimap unknownFields() { + try { + CodedInputStream inputStream = rawWireBytes().newCodedInput(); + Multimap fields = Multimaps.newMultimap(new TreeMap<>(), ArrayList::new); + for (int tag = inputStream.readTag(); tag != 0; tag = inputStream.readTag()) { + int tagWireType = WireFormat.getTagWireType(tag); + int fieldNumber = WireFormat.getTagFieldNumber(tag); + fields.put( + fieldNumber, ProtoLiteCelValueConverter.readUnknownField(tagWireType, inputStream)); + } + return ImmutableListMultimap.copyOf(fields); + } catch (IOException e) { + throw new IllegalStateException("Failed to parse raw proto message wire bytes", e); + } + } + + @Override + public boolean isZeroValue() { + return rawWireBytes().isEmpty(); + } + + /** + * 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. + */ + @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) { + 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); + } + + 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) { + throw new UnsupportedOperationException("Groups are not supported"); + } + if (entries.isEmpty()) { + return isRepeated ? ImmutableList.of() : null; + } + if (isRepeated) { + ImmutableList.Builder listBuilder = ImmutableList.builder(); + for (Object raw : entries) { + if (fieldType.isPackable() && (raw instanceof ByteString)) { + listBuilder.addAll(decodePacked((ByteString) raw, fieldType)); + } else { + listBuilder.add(decodeWireValue(raw, fieldType, protoTypeName, converter)); + } + } + return listBuilder.build(); + } + if (fieldType == WireFormat.FieldType.MESSAGE) { + ByteString mergedBytes = ByteString.EMPTY; + for (Object item : entries) { + mergedBytes = mergedBytes.concat(requireType(item, ByteString.class, fieldType)); + } + return decodeWireValue(mergedBytes, fieldType, protoTypeName, converter); + } + // Protobuf "last one wins" semantics for non-repeated scalar fields + return decodeWireValue(Iterables.getLast(entries), fieldType, protoTypeName, converter); + } + + static Object decodeWireValue( + Object raw, int typeCode, String protoTypeName, ProtoLiteCelValueConverter converter) { + return decodeWireValue( + raw, + FieldLiteDescriptor.Type.forNumber(typeCode).toWireFormatFieldType(), + protoTypeName, + converter); + } + + static Object decodeWireValue( + Object raw, + WireFormat.FieldType fieldType, + String protoTypeName, + ProtoLiteCelValueConverter converter) { + switch (fieldType) { + case DOUBLE: + return Double.longBitsToDouble(requireType(raw, Long.class, fieldType)); + case FLOAT: + return (double) Float.intBitsToFloat(requireType(raw, Integer.class, fieldType)); + case INT64: + case SFIXED64: + return requireType(raw, Long.class, fieldType); + case INT32: + case ENUM: + return (long) requireType(raw, Long.class, fieldType).intValue(); + case UINT64: + case FIXED64: + return UnsignedLong.fromLongBits(requireType(raw, Long.class, fieldType)); + case FIXED32: + return UnsignedLong.fromLongBits( + Integer.toUnsignedLong(requireType(raw, Integer.class, fieldType))); + case BOOL: + return requireType(raw, Long.class, fieldType) != 0L; + case STRING: + ByteString stringBytes = requireType(raw, ByteString.class, fieldType); + if (!stringBytes.isValidUtf8()) { + throw new IllegalArgumentException("Invalid UTF-8 in string field"); + } + return stringBytes.toStringUtf8(); + case GROUP: + throw new UnsupportedOperationException("Groups are not supported"); + case MESSAGE: + 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: + return UnsignedLong.fromLongBits(requireType(raw, Long.class, fieldType) & 0xFFFFFFFFL); + case SFIXED32: + return (long) requireType(raw, Integer.class, fieldType); + case SINT32: + return (long) + CodedInputStream.decodeZigZag32(requireType(raw, Long.class, fieldType).intValue()); + case SINT64: + return CodedInputStream.decodeZigZag64(requireType(raw, Long.class, fieldType)); + } + throw new IllegalArgumentException("Unsupported proto field type: " + fieldType); + } + + private static T requireType( + Object raw, Class expectedType, WireFormat.FieldType fieldType) { + if (!expectedType.isInstance(raw)) { + throw new IllegalArgumentException( + String.format( + "Expected %s for wire type %s, but got: %s", + expectedType.getSimpleName(), + fieldType, + raw != null ? raw.getClass().getName() : "null")); + } + return expectedType.cast(raw); + } + + private static ImmutableList decodePacked( + ByteString bytes, WireFormat.FieldType fieldType) { + try { + CodedInputStream in = bytes.newCodedInput(); + ImmutableList.Builder builder = ImmutableList.builder(); + while (!in.isAtEnd()) { + switch (fieldType) { + case DOUBLE: + builder.add(Double.longBitsToDouble(in.readFixed64())); + break; + case FLOAT: + builder.add((double) Float.intBitsToFloat(in.readFixed32())); + break; + case INT64: + builder.add(in.readInt64()); + break; + case UINT64: + builder.add(UnsignedLong.fromLongBits(in.readUInt64())); + break; + case INT32: + builder.add((long) in.readInt32()); + break; + case FIXED64: + builder.add(UnsignedLong.fromLongBits(in.readFixed64())); + break; + case FIXED32: + builder.add(UnsignedLong.fromLongBits(Integer.toUnsignedLong(in.readFixed32()))); + break; + case BOOL: + builder.add(in.readBool()); + break; + case UINT32: + builder.add(UnsignedLong.fromLongBits(Integer.toUnsignedLong(in.readUInt32()))); + break; + case ENUM: + builder.add((long) in.readEnum()); + break; + case SFIXED32: + builder.add((long) in.readSFixed32()); + break; + case SFIXED64: + builder.add(in.readSFixed64()); + break; + case SINT32: + builder.add((long) in.readSInt32()); + break; + case SINT64: + builder.add(in.readSInt64()); + break; + default: + throw new IllegalArgumentException("Unsupported packed proto field type: " + fieldType); + } + } + return builder.build(); + } catch (IOException e) { + throw new IllegalStateException("Failed to parse packed repeated field", e); + } + } + + public static RawProtoMessageLiteValue create( + ByteString rawWireBytes, ProtoLiteCelValueConverter protoLiteCelValueConverter) { + return create(rawWireBytes, "", protoLiteCelValueConverter); + } + + public static RawProtoMessageLiteValue create( + ByteString rawWireBytes, + String protoTypeName, + ProtoLiteCelValueConverter protoLiteCelValueConverter) { + checkNotNull(rawWireBytes); + checkNotNull(protoTypeName); + checkNotNull(protoLiteCelValueConverter); + return new AutoValue_RawProtoMessageLiteValue( + rawWireBytes, StructTypeReference.create(protoTypeName), protoLiteCelValueConverter); + } + + RawProtoMessageLiteValue() {} +} diff --git a/common/src/main/java/dev/cel/common/values/SelectField.java b/common/src/main/java/dev/cel/common/values/SelectField.java new file mode 100644 index 000000000..f9f8b6906 --- /dev/null +++ b/common/src/main/java/dev/cel/common/values/SelectField.java @@ -0,0 +1,124 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.common.values; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.auto.value.AutoValue; +import com.google.errorprone.annotations.Immutable; +import dev.cel.common.annotations.Internal; +import org.jspecify.annotations.Nullable; + +/** + * Represents a single field selection in an optimized selection chain. + * + *

CEL Library Internals. Do Not Use. + */ +@Internal +@AutoValue +@AutoValue.CopyAnnotations +@Immutable +@SuppressWarnings("Immutable") // Default value is an immutable CEL literal or null +public abstract class SelectField { + + private static final int MAX_FIELD_NUMBER = 536870911; + + /** CEL-specific type code used to encode CEL maps on the wire. */ + public static final int CEL_MAP_TYPE_CODE = -1; + + /** Sentinel for a presence-test qualifier, whose 2-tuple carries no type code. */ + public static final int NO_TYPE_CODE = 0; + + /** Protobuf field type code for {@code TYPE_MESSAGE} ({@code FieldDescriptorProto.Type}). */ + public static final int MESSAGE_TYPE_CODE = 11; + + // Mirrors FieldDescriptorProto.Type. Not validated against a protobuf enum because the :values + // target is deliberately protobuf-free; keep in sync with CelLiteDescriptor.FieldLiteDescriptor. + private static final int MIN_PROTO_TYPE_CODE = 1; // TYPE_DOUBLE + private static final int MAX_PROTO_TYPE_CODE = 18; // TYPE_SINT64 + private static final int GROUP_PROTO_TYPE_CODE = 10; // Unsupported by CEL. + + /** Protobuf field number of this hop. */ + public abstract int fieldNumber(); + + /** Protobuf field name or map key of this hop. */ + public abstract String fieldName(); + + /** + * Protobuf wire type code (1..18, except 10), {@link #CEL_MAP_TYPE_CODE}, or {@link + * #NO_TYPE_CODE}. + */ + public abstract int typeCode(); + + /** + * Default value for this hop, or null if unspecified. When non-null, this must be an immutable + * CEL literal value. + */ + public abstract @Nullable Object defaultValue(); + + /** + * Creates a presence-test qualifier hop. + * + * @param fieldNumber Protobuf field number. Takes {@code long} for compatibility with CEL's + * int64 constant representations. + * @param fieldName Protobuf field name. + */ + public static SelectField create(long fieldNumber, String fieldName) { + checkArgument( + fieldNumber >= 1 && fieldNumber <= MAX_FIELD_NUMBER, + "Field number out of protobuf range: %s", + fieldNumber); + checkNotNull(fieldName); + return new AutoValue_SelectField( + (int) fieldNumber, fieldName, NO_TYPE_CODE, /* defaultValue= */ null); + } + + /** + * Creates a fully-specified field selection hop with type code and optional default value. + * + * @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. + */ + public static SelectField create( + long fieldNumber, String fieldName, long typeCode, @Nullable Object defaultValue) { + 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); + } + + /** + * Returns whether {@code typeCode} is a protobuf field type code CEL supports, or the {@link + * #CEL_MAP_TYPE_CODE} sentinel. + */ + public static boolean isSupportedTypeCode(long typeCode) { + if (typeCode == CEL_MAP_TYPE_CODE) { + return true; + } + return typeCode >= MIN_PROTO_TYPE_CODE + && typeCode <= MAX_PROTO_TYPE_CODE + && typeCode != GROUP_PROTO_TYPE_CODE; + } + + SelectField() {} +} 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 76c761567..1732c6667 100644 --- a/common/src/test/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/test/java/dev/cel/common/values/BUILD.bazel @@ -15,12 +15,14 @@ java_library( "//common:cel_ast", "//common:cel_descriptor_util", "//common:options", + "//common/exceptions:attribute_not_found", "//common/internal:cel_descriptor_pools", "//common/internal:cel_lite_descriptor_pool", "//common/internal:default_lite_descriptor_pool", "//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", @@ -28,10 +30,14 @@ java_library( "//common/values:cel_value_provider", "//common/values:combined_cel_value_converter", "//common/values:combined_cel_value_provider", + "//common/values:optimized_select_traversal", + "//common/values:optimized_selectable", "//common/values:proto_message_lite_value", "//common/values:proto_message_lite_value_provider", "//common/values:proto_message_value", "//common/values:proto_message_value_provider", + "//common/values:select_field", + "//protobuf:cel_lite_descriptor", "//testing/protos:test_all_types_cel_java_proto3", "@cel_spec//proto/cel/expr/conformance/proto2:test_all_types_java_proto", "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto", diff --git a/common/src/test/java/dev/cel/common/values/OptimizedSelectTraversalTest.java b/common/src/test/java/dev/cel/common/values/OptimizedSelectTraversalTest.java new file mode 100644 index 000000000..239eae57f --- /dev/null +++ b/common/src/test/java/dev/cel/common/values/OptimizedSelectTraversalTest.java @@ -0,0 +1,424 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +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.ImmutableMap; +import com.google.testing.junit.testparameterinjector.TestParameter; +import com.google.testing.junit.testparameterinjector.TestParameterInjector; +import dev.cel.common.exceptions.CelAttributeNotFoundException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(TestParameterInjector.class) +public final class OptimizedSelectTraversalTest { + + private static final CelValueConverter DEFAULT_CONVERTER = CelValueConverter.getDefaultInstance(); + + private enum TargetType { + MAP { + @Override + Object createTarget(Map data) { + return ImmutableMap.copyOf(data); + } + + @Override + Object createNestedTarget(Map innerData) { + return ImmutableMap.of("outer_key", ImmutableMap.copyOf(innerData)); + } + }, + OPTIMIZED_SELECTABLE { + @Override + Object createTarget(Map data) { + return new FakeOptimizedSelectable(data); + } + + @Override + Object createNestedTarget(Map innerData) { + return new FakeOptimizedSelectable( + ImmutableMap.of("outer_key", new FakeOptimizedSelectable(innerData))); + } + }, + SELECTABLE_VALUE { + @Override + Object createTarget(Map data) { + return new FakeSelectableValue(data); + } + + @Override + Object createNestedTarget(Map innerData) { + return new FakeSelectableValue( + ImmutableMap.of("outer_key", new FakeSelectableValue(innerData))); + } + }; + + abstract Object createTarget(Map data); + + abstract Object createNestedTarget(Map innerData); + } + + @SuppressWarnings("Immutable") + private enum NestedPresenceTestCase { + ALL_PRESENT( + ImmutableMap.of("inner_key", "nested_val"), "outer_key", "inner_key", /* expected= */ true), + INTERMEDIATE_MISSING( + ImmutableMap.of("inner_key", "nested_val"), + "missing_outer", + "inner_key", + /* expected= */ false), + TERMINAL_MISSING( + ImmutableMap.of("other_key", "nested_val"), + "outer_key", + "missing_terminal", + /* expected= */ false); + + final ImmutableMap innerData; + final String outerField; + final String innerField; + final boolean expected; + + NestedPresenceTestCase( + ImmutableMap innerData, + String outerField, + String innerField, + boolean expected) { + this.innerData = innerData; + this.outerField = outerField; + this.innerField = innerField; + this.expected = expected; + } + } + + @Test + public void qualify_emptyFields_returnsTargetInstance() { + Object target = new Object(); + + Object result = OptimizedSelectTraversal.qualify(target, ImmutableList.of(), DEFAULT_CONVERTER); + + assertThat(result).isSameInstanceAs(target); + } + + @Test + public void qualify_singleField_success(@TestParameter TargetType targetType) { + Object target = targetType.createTarget(ImmutableMap.of("key", "value")); + ImmutableList fields = ImmutableList.of(SelectField.create(1L, "key", 9, "")); + + Object result = OptimizedSelectTraversal.qualify(target, fields, DEFAULT_CONVERTER); + + assertThat(result).isEqualTo("value"); + } + + @Test + public void qualify_nested_success(@TestParameter TargetType targetType) { + Object target = targetType.createNestedTarget(ImmutableMap.of("inner_key", "nested_value")); + ImmutableList fields = + ImmutableList.of( + SelectField.create(1L, "outer_key", SelectField.CEL_MAP_TYPE_CODE, ImmutableMap.of()), + SelectField.create(2L, "inner_key", 9, "")); + + Object result = OptimizedSelectTraversal.qualify(target, fields, DEFAULT_CONVERTER); + + assertThat(result).isEqualTo("nested_value"); + } + + @Test + public void qualify_singleField_missingThrowsException(@TestParameter TargetType targetType) { + Object target = targetType.createTarget(ImmutableMap.of("present", "value")); + ImmutableList fields = + ImmutableList.of(SelectField.create(1L, "missing", 9, null)); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> OptimizedSelectTraversal.qualify(target, fields, DEFAULT_CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("missing"); + } + + @Test + public void qualify_map_nullValue_throwsException() { + Map map = new HashMap<>(); + map.put("null_key", null); + ImmutableList fields = ImmutableList.of(SelectField.create(1L, "null_key")); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> OptimizedSelectTraversal.qualify(map, fields, DEFAULT_CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("Map value cannot be null for key: null_key"); + } + + @Test + public void qualify_optimizedSelectable_absentWithDefaultValue_returnsDefault() { + FakeOptimizedSelectable selectable = new FakeOptimizedSelectable(ImmutableMap.of()); + ImmutableList fields = + ImmutableList.of(SelectField.create(1L, "absent", 9, "default_fallback")); + + Object result = OptimizedSelectTraversal.qualify(selectable, fields, DEFAULT_CONVERTER); + + assertThat(result).isEqualTo("default_fallback"); + } + + @Test + public void qualify_selectableValue_absentWithDefaultValue_returnsDefault() { + FakeSelectableValue selectable = new FakeSelectableValue(ImmutableMap.of()); + ImmutableList fields = + ImmutableList.of(SelectField.create(1L, "absent", 9, "default_fallback")); + + Object result = OptimizedSelectTraversal.qualify(selectable, fields, DEFAULT_CONVERTER); + + assertThat(result).isEqualTo("default_fallback"); + } + + @Test + public void qualify_rootMap_convertsUnadaptedEntryWithConverter() { + TrackingConverter customConverter = new TrackingConverter(); + ImmutableMap rootMap = ImmutableMap.of("step1", "adapt_to_selectable"); + ImmutableList fields = + ImmutableList.of(SelectField.create(1L, "step1"), SelectField.create(2L, "leaf", 9, "")); + + Object result = OptimizedSelectTraversal.qualify(rootMap, fields, customConverter); + + assertThat(result).isEqualTo("custom_adapted"); + assertThat(customConverter.callCount.get()).isEqualTo(1); + } + + @Test + public void qualify_unsupportedTarget_throwsException() { + ImmutableList fields = ImmutableList.of(SelectField.create(1L, "invalid_field")); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> OptimizedSelectTraversal.qualify(12345L, fields, DEFAULT_CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("invalid_field"); + } + + @Test + public void qualify_intermediateUnsupportedTarget_throwsException() { + ImmutableMap map = ImmutableMap.of("scalar", 999L); + ImmutableList fields = + ImmutableList.of( + SelectField.create(1L, "scalar", 3, 0L), SelectField.create(2L, "unreachable", 9, "")); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> OptimizedSelectTraversal.qualify(map, fields, DEFAULT_CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("unreachable"); + } + + @Test + public void hasField_emptyFields_returnsFalse() { + Object target = ImmutableMap.of("key", "value"); + + boolean hasField = + OptimizedSelectTraversal.hasField(target, ImmutableList.of(), DEFAULT_CONVERTER); + + assertThat(hasField).isFalse(); + } + + @Test + public void hasField_singleField( + @TestParameter TargetType targetType, + @TestParameter({"present_key", "missing_key"}) String queryKey) { + Object target = targetType.createTarget(ImmutableMap.of("present_key", "val")); + ImmutableList fields = ImmutableList.of(SelectField.create(1L, queryKey)); + + boolean hasField = OptimizedSelectTraversal.hasField(target, fields, DEFAULT_CONVERTER); + + assertThat(hasField).isEqualTo(queryKey.equals("present_key")); + } + + @Test + public void hasField_nestedFields( + @TestParameter TargetType targetType, @TestParameter NestedPresenceTestCase testCase) { + Object target = targetType.createNestedTarget(testCase.innerData); + ImmutableList fields = + ImmutableList.of( + SelectField.create(1L, testCase.outerField), + SelectField.create(2L, testCase.innerField)); + + boolean hasField = OptimizedSelectTraversal.hasField(target, fields, DEFAULT_CONVERTER); + + assertThat(hasField).isEqualTo(testCase.expected); + } + + @Test + public void hasField_map_terminalNullValue_returnsTrue() { + Map map = new HashMap<>(); + map.put("null_key", null); + ImmutableList fields = ImmutableList.of(SelectField.create(1L, "null_key")); + + boolean hasField = OptimizedSelectTraversal.hasField(map, fields, DEFAULT_CONVERTER); + + assertThat(hasField).isTrue(); + } + + @Test + public void hasField_map_intermediateNullValue_throwsException() { + Map map = new HashMap<>(); + map.put("child", null); + ImmutableList fields = + ImmutableList.of(SelectField.create(1L, "child"), SelectField.create(2L, "leaf")); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> OptimizedSelectTraversal.hasField(map, fields, DEFAULT_CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("Map value cannot be null for key: child"); + } + + @Test + public void hasField_intermediateUnsupportedTarget_throwsException() { + ImmutableMap map = ImmutableMap.of("scalar_key", 100L); + ImmutableList fields = + ImmutableList.of(SelectField.create(1L, "scalar_key"), SelectField.create(2L, "child_key")); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> OptimizedSelectTraversal.hasField(map, fields, DEFAULT_CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("child_key"); + } + + @Test + public void hasField_rootMap_convertsUnadaptedEntryWithConverter() { + TrackingConverter customConverter = new TrackingConverter(); + ImmutableMap rootMap = ImmutableMap.of("step1", "adapt_to_selectable"); + ImmutableList fields = + ImmutableList.of(SelectField.create(1L, "step1"), SelectField.create(2L, "leaf")); + + boolean hasField = OptimizedSelectTraversal.hasField(rootMap, fields, customConverter); + + assertThat(hasField).isTrue(); + assertThat(customConverter.callCount.get()).isEqualTo(1); + } + + @Test + public void hasField_unsupportedTarget_throwsException() { + ImmutableList fields = ImmutableList.of(SelectField.create(1L, "invalid_field")); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> OptimizedSelectTraversal.hasField(12345L, fields, DEFAULT_CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("invalid_field"); + } + + @Test + public void qualify_errorValue_propagatesError() { + ErrorValue error = ErrorValue.create(1L, new RuntimeException("test error")); + ImmutableList fields = + ImmutableList.of(SelectField.create(1L, "field1"), SelectField.create(2L, "field2")); + + Object result = OptimizedSelectTraversal.qualify(error, fields, DEFAULT_CONVERTER); + + assertThat(result).isSameInstanceAs(error); + } + + @Test + public void hasField_errorValue_returnsFalse() { + ErrorValue error = ErrorValue.create(1L, new RuntimeException("test error")); + ImmutableList fields = + ImmutableList.of(SelectField.create(1L, "field1"), SelectField.create(2L, "field2")); + + boolean result = OptimizedSelectTraversal.hasField(error, fields, DEFAULT_CONVERTER); + + assertThat(result).isFalse(); + } + + @SuppressWarnings("Immutable") + private static final class FakeOptimizedSelectable implements OptimizedSelectable { + private final ImmutableMap values; + + @Override + public Object selectByFieldNumber(SelectField field) { + Object value = values.get(field.fieldName()); + if (value != null) { + return value; + } + if (field.defaultValue() != null) { + return field.defaultValue(); + } + throw CelAttributeNotFoundException.forFieldResolution(field.fieldName()); + } + + @Override + public boolean hasFieldByNumber(SelectField field) { + return values.containsKey(field.fieldName()); + } + + @Override + public Optional findByFieldNumber(SelectField field) { + return Optional.ofNullable(values.get(field.fieldName())); + } + + FakeOptimizedSelectable(Map values) { + this.values = ImmutableMap.copyOf(values); + } + } + + @SuppressWarnings("Immutable") + private static final class FakeSelectableValue implements SelectableValue { + private final ImmutableMap values; + + @Override + public Object select(String field) { + Object value = values.get(field); + if (value != null) { + return value; + } + throw CelAttributeNotFoundException.forFieldResolution(field); + } + + @Override + public Optional find(String field) { + return Optional.ofNullable(values.get(field)); + } + + FakeSelectableValue(Map values) { + this.values = ImmutableMap.copyOf(values); + } + } + + @SuppressWarnings("Immutable") + private static final class TrackingConverter extends CelValueConverter { + private final AtomicInteger callCount = new AtomicInteger(); + + @Override + public Object toRuntimeValue(Object value) { + callCount.incrementAndGet(); + if (Objects.equals(value, "adapt_to_selectable")) { + return new FakeOptimizedSelectable(ImmutableMap.of("leaf", "custom_adapted")); + } + return super.toRuntimeValue(value); + } + } +} 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/ProtoMessageLiteValueTest.java b/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java index dbfb55cf9..88799878e 100644 --- a/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java +++ b/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java @@ -21,11 +21,17 @@ import com.google.common.collect.ImmutableSet; import com.google.common.primitives.UnsignedLong; import com.google.protobuf.Any; +import com.google.protobuf.BoolValue; import com.google.protobuf.ByteString; +import com.google.protobuf.BytesValue; +import com.google.protobuf.CodedOutputStream; +import com.google.protobuf.DoubleValue; import com.google.protobuf.DynamicMessage; +import com.google.protobuf.ExtensionRegistryLite; import com.google.protobuf.FloatValue; import com.google.protobuf.Int32Value; import com.google.protobuf.Int64Value; +import com.google.protobuf.StringValue; import com.google.protobuf.Timestamp; import com.google.protobuf.UInt32Value; import com.google.protobuf.UInt64Value; @@ -37,6 +43,7 @@ import dev.cel.expr.conformance.proto3.TestAllTypes.NestedEnum; import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; import dev.cel.expr.conformance.proto3.TestAllTypesCelDescriptor; +import java.io.ByteArrayOutputStream; import java.time.Duration; import java.time.Instant; import org.junit.Test; @@ -153,19 +160,17 @@ public void selectField_success(@TestParameter SelectFieldTestCase testCase) { .setSingleDouble(2.5d) .setSingleString("test") .setSingleBytes(ByteString.copyFrom(new byte[] {0x01})) - .setSingleAny( - Any.pack(DynamicMessage.newBuilder(com.google.protobuf.BoolValue.of(true)).build())) + .setSingleAny(Any.pack(DynamicMessage.newBuilder(BoolValue.of(true)).build())) .setSingleDuration(com.google.protobuf.Duration.newBuilder().setSeconds(100)) .setSingleTimestamp(Timestamp.newBuilder().setSeconds(100)) .setSingleInt32Wrapper(Int32Value.of(5)) .setSingleInt64Wrapper(Int64Value.of(10L)) .setSingleUint32Wrapper(UInt32Value.of(1)) .setSingleUint64Wrapper(UInt64Value.of(UnsignedLong.MAX_VALUE.longValue())) - .setSingleStringWrapper(com.google.protobuf.StringValue.of("hello")) + .setSingleStringWrapper(StringValue.of("hello")) .setSingleFloatWrapper(FloatValue.of(7.5f)) - .setSingleDoubleWrapper(com.google.protobuf.DoubleValue.of(8.5d)) - .setSingleBytesWrapper( - com.google.protobuf.BytesValue.of(ByteString.copyFrom(new byte[] {0x02}))) + .setSingleDoubleWrapper(DoubleValue.of(8.5d)) + .setSingleBytesWrapper(BytesValue.of(ByteString.copyFrom(new byte[] {0x02}))) .addRepeatedInt64(5L) .addRepeatedInt64(6L) .addRepeatedUint64(7L) @@ -253,4 +258,26 @@ public void selectField_defaultValue(@TestParameter DefaultValueTestCase testCas assertThat(selectedValue).isEqualTo(testCase.value); } + + @Test + public void unknownFields_retainsUnknownWireFields() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeInt64(999, 12345L); + cos.writeString(1000, "hello unknown"); + cos.flush(); + + TestAllTypes msgWithUnknown = + TestAllTypes.parseFrom(baos.toByteArray(), ExtensionRegistryLite.getEmptyRegistry()); + ProtoMessageLiteValue messageLiteValue = + ProtoMessageLiteValue.create( + msgWithUnknown, + "cel.expr.conformance.proto3.TestAllTypes", + PROTO_LITE_CEL_VALUE_CONVERTER); + + assertThat(messageLiteValue.unknownFields()).valuesForKey(999).containsExactly(12345L); + assertThat(messageLiteValue.unknownFields()) + .valuesForKey(1000) + .containsExactly(ByteString.copyFromUtf8("hello unknown")); + } } diff --git a/common/src/test/java/dev/cel/common/values/RawProtoMessageLiteValueTest.java b/common/src/test/java/dev/cel/common/values/RawProtoMessageLiteValueTest.java new file mode 100644 index 000000000..180883f60 --- /dev/null +++ b/common/src/test/java/dev/cel/common/values/RawProtoMessageLiteValueTest.java @@ -0,0 +1,1202 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.common.values; + +import static com.google.common.truth.Truth.assertThat; +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; + +@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", EMPTY_CONVERTER); + + assertThat(value.rawWireBytes()).isEqualTo(bytes); + assertThat(value.value()).isSameInstanceAs(value); + assertThat(value.celType().name()).isEqualTo("custom.Message"); + } + + @Test + public void create_defaultsEmptyTypeName() { + ByteString bytes = ByteString.copyFromUtf8("test"); + RawProtoMessageLiteValue value = RawProtoMessageLiteValue.create(bytes, EMPTY_CONVERTER); + + assertThat(value.rawWireBytes()).isEqualTo(bytes); + assertThat(value.celType().name()).isEmpty(); + } + + @Test + public void select_throwsCelAttributeNotFoundException() { + RawProtoMessageLiteValue value = + RawProtoMessageLiteValue.create(ByteString.EMPTY, "custom.Message", EMPTY_CONVERTER); + + assertThrows(CelAttributeNotFoundException.class, () -> value.select("field")); + } + + @Test + public void find_throwsCelAttributeNotFoundException() { + RawProtoMessageLiteValue value = + RawProtoMessageLiteValue.create(ByteString.EMPTY, "custom.Message", EMPTY_CONVERTER); + + assertThrows(CelAttributeNotFoundException.class, () -> value.find("field")); + } + + @Test + public void isZeroValue_emptyBytes_returnsTrue() { + RawProtoMessageLiteValue value = + RawProtoMessageLiteValue.create(ByteString.EMPTY, EMPTY_CONVERTER); + + assertThat(value.isZeroValue()).isTrue(); + } + + @Test + public void isZeroValue_nonEmptyBytes_returnsFalse() { + RawProtoMessageLiteValue value = + RawProtoMessageLiteValue.create(ByteString.copyFromUtf8("data"), EMPTY_CONVERTER); + + assertThat(value.isZeroValue()).isFalse(); + } + + @Test + 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()), 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.hasFieldByNumber(field1)).isTrue(); + assertThat(value.hasFieldByNumber(field2)).isFalse(); + } + + @Test + public void unknownFields_parsesWireTags() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeInt64(1, 42L); + cos.writeFixed32(2, 100); + cos.writeFixed64(3, 200L); + cos.writeString(4, "hello"); + cos.flush(); + + RawProtoMessageLiteValue value = + RawProtoMessageLiteValue.create(ByteString.copyFrom(baos.toByteArray()), EMPTY_CONVERTER); + + assertThat(value.unknownFields()).valuesForKey(1).containsExactly(42L); + assertThat(value.unknownFields()).valuesForKey(2).containsExactly(100); + assertThat(value.unknownFields()).valuesForKey(3).containsExactly(200L); + assertThat(value.unknownFields()) + .valuesForKey(4) + .containsExactly(ByteString.copyFromUtf8("hello")); + } + + @Test + public void decodeWireEntries_emptySingularEntries_returnsNull() { + Object intResult = + decodeWireEntries( + ImmutableList.of(), + FieldLiteDescriptor.Type.INT64.getNumber(), + "custom.Message", + /* isRepeated= */ false); + Object messageResult = + decodeWireEntries( + ImmutableList.of(), + FieldLiteDescriptor.Type.MESSAGE.getNumber(), + "custom.Message", + /* isRepeated= */ false); + + assertThat(intResult).isNull(); + assertThat(messageResult).isNull(); + } + + @Test + public void decodeWireEntries_emptyRepeatedEntries_returnsEmptyList() { + Object result = + decodeWireEntries( + ImmutableList.of(), + FieldLiteDescriptor.Type.INT64.getNumber(), + "custom.Message", + /* isRepeated= */ true); + + assertThat((Iterable) result).isEmpty(); + } + + @Test + public void decodeWireEntries_nonRepeated_lastOneWins() { + Object decoded = + decodeWireEntries( + ImmutableList.of(10L, 20L, 30L), + FieldLiteDescriptor.Type.INT64.getNumber(), + "custom.Message", + /* isRepeated= */ false); + + assertThat(decoded).isEqualTo(30L); + } + + @Test + public void decodeWireEntries_repeatedUnpacked() { + Object decoded = + decodeWireEntries( + ImmutableList.of(10L, 20L, 30L), + FieldLiteDescriptor.Type.INT64.getNumber(), + "custom.Message", + /* isRepeated= */ true); + + assertThat(decoded).isEqualTo(ImmutableList.of(10L, 20L, 30L)); + } + + @Test + public void decodeWireEntries_packedInt32() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeInt32NoTag(1); + cos.writeInt32NoTag(2); + cos.writeInt32NoTag(3); + cos.flush(); + + Object decoded = + decodeWireEntries( + ImmutableList.of(ByteString.copyFrom(baos.toByteArray())), + FieldLiteDescriptor.Type.INT32.getNumber(), + "custom.Message", + /* isRepeated= */ true); + + assertThat(decoded).isEqualTo(ImmutableList.of(1L, 2L, 3L)); + } + + @Test + public void decodeWireEntries_packedInt64() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeInt64NoTag(100L); + cos.writeInt64NoTag(200L); + cos.flush(); + + Object decoded = + decodeWireEntries( + ImmutableList.of(ByteString.copyFrom(baos.toByteArray())), + FieldLiteDescriptor.Type.INT64.getNumber(), + "custom.Message", + /* isRepeated= */ true); + + assertThat(decoded).isEqualTo(ImmutableList.of(100L, 200L)); + } + + @Test + public void decodeWireEntries_packedUint32() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeUInt32NoTag(50); + cos.flush(); + + Object decoded = + decodeWireEntries( + ImmutableList.of(ByteString.copyFrom(baos.toByteArray())), + FieldLiteDescriptor.Type.UINT32.getNumber(), + "custom.Message", + /* isRepeated= */ true); + + assertThat(decoded).isEqualTo(ImmutableList.of(UnsignedLong.fromLongBits(50L))); + } + + @Test + public void decodeWireEntries_packedUint64() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeUInt64NoTag(999L); + cos.flush(); + + Object decoded = + decodeWireEntries( + ImmutableList.of(ByteString.copyFrom(baos.toByteArray())), + FieldLiteDescriptor.Type.UINT64.getNumber(), + "custom.Message", + /* isRepeated= */ true); + + assertThat(decoded).isEqualTo(ImmutableList.of(UnsignedLong.fromLongBits(999L))); + } + + @Test + public void decodeWireEntries_packedSint32AndSint64() throws Exception { + ByteArrayOutputStream baos32 = new ByteArrayOutputStream(); + CodedOutputStream cos32 = CodedOutputStream.newInstance(baos32); + cos32.writeSInt32NoTag(-10); + cos32.writeSInt32NoTag(20); + cos32.flush(); + + Object decoded32 = + decodeWireEntries( + ImmutableList.of(ByteString.copyFrom(baos32.toByteArray())), + FieldLiteDescriptor.Type.SINT32.getNumber(), + "custom.Message", + /* isRepeated= */ true); + + assertThat(decoded32).isEqualTo(ImmutableList.of(-10L, 20L)); + + ByteArrayOutputStream baos64 = new ByteArrayOutputStream(); + CodedOutputStream cos64 = CodedOutputStream.newInstance(baos64); + cos64.writeSInt64NoTag(-100L); + cos64.writeSInt64NoTag(200L); + cos64.flush(); + + Object decoded64 = + decodeWireEntries( + ImmutableList.of(ByteString.copyFrom(baos64.toByteArray())), + FieldLiteDescriptor.Type.SINT64.getNumber(), + "custom.Message", + /* isRepeated= */ true); + + assertThat(decoded64).isEqualTo(ImmutableList.of(-100L, 200L)); + } + + @Test + public void decodeWireEntries_packedFixedAndSFixed() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeFixed32NoTag(10); + cos.writeFixed64NoTag(20L); + cos.writeSFixed32NoTag(-30); + cos.writeSFixed64NoTag(-40L); + cos.flush(); + + assertThat( + decodeWireEntries( + ImmutableList.of(ByteString.copyFrom(baos.toByteArray()).substring(0, 4)), + FieldLiteDescriptor.Type.FIXED32.getNumber(), + "custom.Message", + /* isRepeated= */ true)) + .isEqualTo(ImmutableList.of(UnsignedLong.fromLongBits(10L))); + + assertThat( + decodeWireEntries( + ImmutableList.of(ByteString.copyFrom(baos.toByteArray()).substring(4, 12)), + FieldLiteDescriptor.Type.FIXED64.getNumber(), + "custom.Message", + /* isRepeated= */ true)) + .isEqualTo(ImmutableList.of(UnsignedLong.fromLongBits(20L))); + + assertThat( + decodeWireEntries( + ImmutableList.of(ByteString.copyFrom(baos.toByteArray()).substring(12, 16)), + FieldLiteDescriptor.Type.SFIXED32.getNumber(), + "custom.Message", + /* isRepeated= */ true)) + .isEqualTo(ImmutableList.of(-30L)); + + assertThat( + decodeWireEntries( + ImmutableList.of(ByteString.copyFrom(baos.toByteArray()).substring(16, 24)), + FieldLiteDescriptor.Type.SFIXED64.getNumber(), + "custom.Message", + /* isRepeated= */ true)) + .isEqualTo(ImmutableList.of(-40L)); + } + + @Test + public void decodeWireEntries_packedBoolFloatDoubleEnum() throws Exception { + ByteArrayOutputStream baosBool = new ByteArrayOutputStream(); + CodedOutputStream cosBool = CodedOutputStream.newInstance(baosBool); + cosBool.writeBoolNoTag(true); + cosBool.writeBoolNoTag(false); + cosBool.flush(); + + assertThat( + decodeWireEntries( + ImmutableList.of(ByteString.copyFrom(baosBool.toByteArray())), + FieldLiteDescriptor.Type.BOOL.getNumber(), + "custom.Message", + /* isRepeated= */ true)) + .isEqualTo(ImmutableList.of(true, false)); + + ByteArrayOutputStream baosFloat = new ByteArrayOutputStream(); + CodedOutputStream cosFloat = CodedOutputStream.newInstance(baosFloat); + cosFloat.writeFloatNoTag(1.5f); + cosFloat.flush(); + + assertThat( + decodeWireEntries( + ImmutableList.of(ByteString.copyFrom(baosFloat.toByteArray())), + FieldLiteDescriptor.Type.FLOAT.getNumber(), + "custom.Message", + /* isRepeated= */ true)) + .isEqualTo(ImmutableList.of(1.5d)); + + ByteArrayOutputStream baosDouble = new ByteArrayOutputStream(); + CodedOutputStream cosDouble = CodedOutputStream.newInstance(baosDouble); + cosDouble.writeDoubleNoTag(3.14d); + cosDouble.flush(); + + assertThat( + decodeWireEntries( + ImmutableList.of(ByteString.copyFrom(baosDouble.toByteArray())), + FieldLiteDescriptor.Type.DOUBLE.getNumber(), + "custom.Message", + /* isRepeated= */ true)) + .isEqualTo(ImmutableList.of(3.14d)); + + ByteArrayOutputStream baosEnum = new ByteArrayOutputStream(); + CodedOutputStream cosEnum = CodedOutputStream.newInstance(baosEnum); + cosEnum.writeEnumNoTag(2); + cosEnum.flush(); + + assertThat( + decodeWireEntries( + ImmutableList.of(ByteString.copyFrom(baosEnum.toByteArray())), + FieldLiteDescriptor.Type.ENUM.getNumber(), + "custom.Message", + /* isRepeated= */ true)) + .isEqualTo(ImmutableList.of(2L)); + } + + @Test + public void decodeWireValue_allScalarWireTypes() { + assertThat( + decodeWireValue( + Double.doubleToRawLongBits(2.5d), WireFormat.FieldType.DOUBLE, "custom.Message")) + .isEqualTo(2.5d); + + assertThat( + decodeWireValue( + Float.floatToRawIntBits(1.5f), WireFormat.FieldType.FLOAT, "custom.Message")) + .isEqualTo(1.5d); + + assertThat(decodeWireValue(42L, WireFormat.FieldType.INT64, "custom.Message")).isEqualTo(42L); + + assertThat(decodeWireValue(42L, WireFormat.FieldType.INT32, "custom.Message")).isEqualTo(42L); + + assertThat(decodeWireValue(42L, WireFormat.FieldType.UINT64, "custom.Message")) + .isEqualTo(UnsignedLong.fromLongBits(42L)); + + assertThat(decodeWireValue(42L, WireFormat.FieldType.UINT32, "custom.Message")) + .isEqualTo(UnsignedLong.fromLongBits(42L)); + + assertThat(decodeWireValue(100, WireFormat.FieldType.FIXED32, "custom.Message")) + .isEqualTo(UnsignedLong.fromLongBits(100L)); + + assertThat(decodeWireValue(100L, WireFormat.FieldType.FIXED64, "custom.Message")) + .isEqualTo(UnsignedLong.fromLongBits(100L)); + + assertThat(decodeWireValue(-50, WireFormat.FieldType.SFIXED32, "custom.Message")) + .isEqualTo(-50L); + + assertThat(decodeWireValue(-50L, WireFormat.FieldType.SFIXED64, "custom.Message")) + .isEqualTo(-50L); + + assertThat(decodeWireValue(1L, WireFormat.FieldType.BOOL, "custom.Message")).isEqualTo(true); + + assertThat(decodeWireValue(0L, WireFormat.FieldType.BOOL, "custom.Message")).isEqualTo(false); + + assertThat( + decodeWireValue( + ByteString.copyFromUtf8("hello"), WireFormat.FieldType.STRING, "custom.Message")) + .isEqualTo("hello"); + + assertThat( + decodeWireValue( + ByteString.copyFromUtf8("bytes"), WireFormat.FieldType.BYTES, "custom.Message")) + .isEqualTo(CelByteString.of("bytes".getBytes(UTF_8))); + + assertThat( + decodeWireValue( + 1L, // zigzag 1 -> -1 + WireFormat.FieldType.SINT32, + "custom.Message")) + .isEqualTo(-1L); + + assertThat( + decodeWireValue( + 1L, // zigzag 1 -> -1 + WireFormat.FieldType.SINT64, + "custom.Message")) + .isEqualTo(-1L); + + assertThat(decodeWireValue(3L, WireFormat.FieldType.ENUM, "custom.Message")).isEqualTo(3L); + } + + @Test + public void decodeWireValue_messageType_returnsRawProtoMessageLiteValue() { + Object submessage = + decodeWireValue( + ByteString.copyFromUtf8("raw"), WireFormat.FieldType.MESSAGE, "sub.Message"); + + assertThat(submessage).isInstanceOf(RawProtoMessageLiteValue.class); + assertThat(((RawProtoMessageLiteValue) submessage).celType().name()).isEqualTo("sub.Message"); + } + + @Test + public void decodeWireValue_groupType_throwsUnsupportedOperationException() { + ByteString rawBytes = ByteString.copyFromUtf8("raw"); + + UnsupportedOperationException thrown = + assertThrows( + UnsupportedOperationException.class, + () -> decodeWireValue(rawBytes, WireFormat.FieldType.GROUP, "group.Message")); + + assertThat(thrown).hasMessageThat().contains("Groups are not supported"); + } + + @Test + public void decodeWireEntries_groupType_throwsUnsupportedOperationException() { + ImmutableList rawEntries = ImmutableList.of(); + int groupTypeCode = FieldLiteDescriptor.Type.GROUP.getNumber(); + + UnsupportedOperationException thrown = + assertThrows( + UnsupportedOperationException.class, + () -> + decodeWireEntries( + rawEntries, groupTypeCode, "group.Message", /* isRepeated= */ false)); + + assertThat(thrown).hasMessageThat().contains("Groups are not supported"); + } + + @Test + public void decodeWireEntries_invalidTypeCode_throwsIllegalArgumentException() { + ImmutableList rawEntries = ImmutableList.of(); + + assertThrows( + IllegalArgumentException.class, + () -> decodeWireEntries(rawEntries, 999, "custom.Message", /* isRepeated= */ false)); + } + + @Test + public void decodeWireValue_invalidTypeCode_throws() { + assertThrows(IllegalArgumentException.class, () -> decodeWireValue(42L, 0, "custom.Message")); + + assertThrows(IllegalArgumentException.class, () -> decodeWireValue(42L, 999, "custom.Message")); + } + + @Test + public void decodeWireValue_int32HighBits_truncatedToSigned32Bit() { + Object decodedHigh = + decodeWireValue(0x100000005L, WireFormat.FieldType.INT32, "custom.Message"); + Object decodedNegative = + decodeWireValue(0xFFFFFFFF80000000L, WireFormat.FieldType.INT32, "custom.Message"); + + assertThat(decodedHigh).isEqualTo(5L); + assertThat(decodedNegative).isEqualTo(-2147483648L); + } + + @Test + public void decodeWireValue_enumHighBits_truncatedToSigned32Bit() { + Object decodedHigh = decodeWireValue(0x100000005L, WireFormat.FieldType.ENUM, "custom.Message"); + + assertThat(decodedHigh).isEqualTo(5L); + } + + @Test + public void decodeWireValue_typeMismatch_throwsIllegalArgumentException() { + IllegalArgumentException thrownInt64 = + assertThrows( + IllegalArgumentException.class, + () -> decodeWireValue("not a long", WireFormat.FieldType.INT64, "custom.Message")); + assertThat(thrownInt64).hasMessageThat().contains("Expected Long for wire type INT64"); + + IllegalArgumentException thrownString = + assertThrows( + IllegalArgumentException.class, + () -> decodeWireValue(100L, WireFormat.FieldType.STRING, "custom.Message")); + assertThat(thrownString).hasMessageThat().contains("Expected ByteString for wire type STRING"); + + IllegalArgumentException thrownBytes = + assertThrows( + IllegalArgumentException.class, + () -> decodeWireValue(100L, WireFormat.FieldType.BYTES, "custom.Message")); + assertThat(thrownBytes).hasMessageThat().contains("Expected ByteString for wire type BYTES"); + + IllegalArgumentException thrownMessage = + assertThrows( + IllegalArgumentException.class, + () -> decodeWireValue(100L, WireFormat.FieldType.MESSAGE, "custom.Message")); + assertThat(thrownMessage) + .hasMessageThat() + .contains("Expected ByteString for wire type MESSAGE"); + + IllegalArgumentException thrownFloat = + assertThrows( + IllegalArgumentException.class, + () -> decodeWireValue(100L, WireFormat.FieldType.FLOAT, "custom.Message")); + assertThat(thrownFloat).hasMessageThat().contains("Expected Integer for wire type FLOAT"); + + IllegalArgumentException thrownDouble = + assertThrows( + IllegalArgumentException.class, + () -> decodeWireValue(100, WireFormat.FieldType.DOUBLE, "custom.Message")); + assertThat(thrownDouble).hasMessageThat().contains("Expected Long for wire type DOUBLE"); + } + + @Test + public void decodeWireValue_invalidUtf8String_throwsIllegalArgumentException() { + ByteString invalidUtf8 = ByteString.copyFrom(new byte[] {(byte) 0xC0, (byte) 0xAF}); + + IllegalArgumentException thrown = + assertThrows( + IllegalArgumentException.class, + () -> decodeWireValue(invalidUtf8, WireFormat.FieldType.STRING, "custom.Message")); + assertThat(thrown).hasMessageThat().contains("Invalid UTF-8 in string field"); + } + + @Test + public void decodeWireEntries_multiChunkPackedRepeated() throws Exception { + ByteArrayOutputStream baos1 = new ByteArrayOutputStream(); + CodedOutputStream cos1 = CodedOutputStream.newInstance(baos1); + cos1.writeInt32NoTag(1); + cos1.writeInt32NoTag(2); + cos1.flush(); + + ByteArrayOutputStream baos2 = new ByteArrayOutputStream(); + CodedOutputStream cos2 = CodedOutputStream.newInstance(baos2); + cos2.writeInt32NoTag(3); + cos2.writeInt32NoTag(4); + cos2.flush(); + + Object decoded = + decodeWireEntries( + ImmutableList.of( + ByteString.copyFrom(baos1.toByteArray()), ByteString.copyFrom(baos2.toByteArray())), + FieldLiteDescriptor.Type.INT32.getNumber(), + "custom.Message", + /* isRepeated= */ true); + + assertThat((Iterable) decoded).containsExactly(1L, 2L, 3L, 4L).inOrder(); + } + + @Test + public void decodeWireEntries_mixedPackedAndUnpackedRepeated() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeInt32NoTag(2); + cos.writeInt32NoTag(3); + cos.flush(); + + Object decoded = + decodeWireEntries( + ImmutableList.of(1L, ByteString.copyFrom(baos.toByteArray()), 4L), + FieldLiteDescriptor.Type.INT32.getNumber(), + "custom.Message", + /* isRepeated= */ true); + + assertThat((Iterable) decoded).containsExactly(1L, 2L, 3L, 4L).inOrder(); + } + + @Test + public void decodeWireEntries_singularMessage_mergesChunks() throws Exception { + ByteArrayOutputStream baos1 = new ByteArrayOutputStream(); + CodedOutputStream cos1 = CodedOutputStream.newInstance(baos1); + cos1.writeInt64(1, 100L); + cos1.flush(); + + ByteArrayOutputStream baos2 = new ByteArrayOutputStream(); + CodedOutputStream cos2 = CodedOutputStream.newInstance(baos2); + cos2.writeInt64(2, 200L); + cos2.flush(); + + Object decoded = + decodeWireEntries( + ImmutableList.of( + ByteString.copyFrom(baos1.toByteArray()), ByteString.copyFrom(baos2.toByteArray())), + FieldLiteDescriptor.Type.MESSAGE.getNumber(), + "sub.Message", + /* isRepeated= */ false); + + assertThat(decoded).isInstanceOf(RawProtoMessageLiteValue.class); + RawProtoMessageLiteValue rawMessage = (RawProtoMessageLiteValue) decoded; + assertThat(rawMessage.unknownFields()).valuesForKey(1).containsExactly(100L); + assertThat(rawMessage.unknownFields()).valuesForKey(2).containsExactly(200L); + } + + @Test + public void decodeWireValue_uint32HighBit_correctUnsignedLong() { + Object decoded = decodeWireValue(0xFFFFFFFFL, WireFormat.FieldType.UINT32, "custom.Message"); + + assertThat(decoded).isEqualTo(UnsignedLong.valueOf(4294967295L)); + } + + @Test + public void decodeWireValue_fixed32HighBit_correctUnsignedLong() { + Object decoded = decodeWireValue(-1, WireFormat.FieldType.FIXED32, "custom.Message"); + + assertThat(decoded).isEqualTo(UnsignedLong.valueOf(4294967295L)); + } + + @Test + public void decodeWireEntries_repeatedString() { + Object decoded = + decodeWireEntries( + ImmutableList.of(ByteString.copyFromUtf8("foo"), ByteString.copyFromUtf8("bar")), + FieldLiteDescriptor.Type.STRING.getNumber(), + "custom.Message", + /* isRepeated= */ true); + + assertThat((Iterable) decoded).containsExactly("foo", "bar").inOrder(); + } + + @Test + public void decodeWireEntries_repeatedBytes() { + Object decoded = + decodeWireEntries( + ImmutableList.of(ByteString.copyFromUtf8("foo"), ByteString.copyFromUtf8("bar")), + FieldLiteDescriptor.Type.BYTES.getNumber(), + "custom.Message", + /* isRepeated= */ true); + + assertThat((Iterable) decoded) + .containsExactly( + CelByteString.of("foo".getBytes(UTF_8)), CelByteString.of("bar".getBytes(UTF_8))) + .inOrder(); + } + + @Test + public void decodeWireEntries_repeatedMessage() { + Object decoded = + decodeWireEntries( + ImmutableList.of(ByteString.copyFromUtf8("msg1"), ByteString.copyFromUtf8("msg2")), + FieldLiteDescriptor.Type.MESSAGE.getNumber(), + "sub.Message", + /* isRepeated= */ true); + + assertThat((Iterable) decoded) + .containsExactly( + RawProtoMessageLiteValue.create( + ByteString.copyFromUtf8("msg1"), "sub.Message", EMPTY_CONVERTER), + RawProtoMessageLiteValue.create( + ByteString.copyFromUtf8("msg2"), "sub.Message", EMPTY_CONVERTER)) + .inOrder(); + } + + @Test + public void decodeWireEntries_packedTruncated_throwsIllegalStateException() { + // Varint with MSB set (0x80) indicates continuation, but stream ends prematurely. + ByteString truncated = ByteString.copyFrom(new byte[] {(byte) 0x80}); + + IllegalStateException thrown = + assertThrows( + IllegalStateException.class, + () -> + decodeWireEntries( + ImmutableList.of(truncated), + FieldLiteDescriptor.Type.INT32.getNumber(), + "custom.Message", + /* isRepeated= */ true)); + + 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"); + } +} diff --git a/common/src/test/java/dev/cel/common/values/SelectFieldTest.java b/common/src/test/java/dev/cel/common/values/SelectFieldTest.java new file mode 100644 index 000000000..ba9dc7008 --- /dev/null +++ b/common/src/test/java/dev/cel/common/values/SelectFieldTest.java @@ -0,0 +1,133 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package dev.cel.common.values; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.common.testing.EqualsTester; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class SelectFieldTest { + + @Test + public void create_twoArguments_success() { + SelectField field = SelectField.create(1L, "foo"); + + assertThat(field.fieldNumber()).isEqualTo(1); + assertThat(field.fieldName()).isEqualTo("foo"); + assertThat(field.typeCode()).isEqualTo(SelectField.NO_TYPE_CODE); + assertThat(field.defaultValue()).isNull(); + } + + @Test + public void create_fourArguments_success() { + SelectField field = SelectField.create(2L, "bar", 9, "default_str"); + + assertThat(field.fieldNumber()).isEqualTo(2); + assertThat(field.fieldName()).isEqualTo("bar"); + assertThat(field.typeCode()).isEqualTo(9); + assertThat(field.defaultValue()).isEqualTo("default_str"); + } + + @Test + public void create_mapTypeCode_success() { + SelectField field = SelectField.create(3L, "map_field", -1, null); + + assertThat(field.typeCode()).isEqualTo(-1); + } + + @Test + public void create_twoArgNullFieldName_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> SelectField.create(1L, null)); + } + + @Test + public void create_fourArgNullFieldName_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> SelectField.create(1L, null, 9, null)); + } + + @Test + public void create_fieldNumberBelowMinimum_throwsIllegalArgumentException() { + IllegalArgumentException thrown = + assertThrows(IllegalArgumentException.class, () -> SelectField.create(0L, "foo")); + + assertThat(thrown).hasMessageThat().contains("Field number out of protobuf range: 0"); + } + + @Test + public void create_fieldNumberNegative_throwsIllegalArgumentException() { + IllegalArgumentException thrown = + assertThrows(IllegalArgumentException.class, () -> SelectField.create(-1L, "foo")); + + assertThat(thrown).hasMessageThat().contains("Field number out of protobuf range: -1"); + } + + @Test + public void create_fieldNumberAboveMaximum_throwsIllegalArgumentException() { + IllegalArgumentException thrown = + assertThrows(IllegalArgumentException.class, () -> SelectField.create(536870912L, "foo")); + + assertThat(thrown).hasMessageThat().contains("Field number out of protobuf range: 536870912"); + } + + @Test + public void create_typeCodeZero_throwsIllegalArgumentException() { + IllegalArgumentException thrown = + assertThrows(IllegalArgumentException.class, () -> SelectField.create(1L, "foo", 0, null)); + + assertThat(thrown).hasMessageThat().contains("Invalid protobuf type code: 0"); + } + + @Test + public void create_typeCodeAboveMaximum_throwsIllegalArgumentException() { + IllegalArgumentException thrown = + assertThrows(IllegalArgumentException.class, () -> SelectField.create(1L, "foo", 19, null)); + + assertThat(thrown).hasMessageThat().contains("Invalid protobuf type code: 19"); + } + + @Test + public void create_typeCodeBelowSentinel_throwsIllegalArgumentException() { + IllegalArgumentException thrown = + assertThrows(IllegalArgumentException.class, () -> SelectField.create(1L, "foo", -2, null)); + + assertThat(thrown).hasMessageThat().contains("Invalid protobuf type code: -2"); + } + + @Test + public void create_typeCodeGroupProto_throwsIllegalArgumentException() { + IllegalArgumentException thrown = + assertThrows(IllegalArgumentException.class, () -> SelectField.create(1L, "foo", 10, null)); + + assertThat(thrown).hasMessageThat().contains("Invalid protobuf type code: 10"); + } + + @Test + public void equalsAndHashCode_testedProperly() { + new EqualsTester() + .addEqualityGroup(SelectField.create(1L, "foo"), SelectField.create(1L, "foo")) + .addEqualityGroup(SelectField.create(2L, "foo"), SelectField.create(2L, "foo")) + .addEqualityGroup(SelectField.create(1L, "bar"), SelectField.create(1L, "bar")) + .addEqualityGroup( + SelectField.create(1L, "foo", 9, "default"), + SelectField.create(1L, "foo", 9, "default")) + .addEqualityGroup(SelectField.create(1L, "foo", 9, "other_default")) + .testEquals(); + } +} diff --git a/common/values/BUILD.bazel b/common/values/BUILD.bazel index 9853289a9..192f01de8 100644 --- a/common/values/BUILD.bazel +++ b/common/values/BUILD.bazel @@ -126,3 +126,39 @@ cel_android_library( name = "base_proto_message_value_provider_android", exports = ["//common/src/main/java/dev/cel/common/values:base_proto_message_value_provider_android"], ) + +java_library( + name = "select_field", + visibility = ["//:internal"], + exports = ["//common/src/main/java/dev/cel/common/values:select_field"], +) + +cel_android_library( + name = "select_field_android", + visibility = ["//:internal"], + exports = ["//common/src/main/java/dev/cel/common/values:select_field_android"], +) + +java_library( + name = "optimized_selectable", + visibility = ["//:internal"], + exports = ["//common/src/main/java/dev/cel/common/values:optimized_selectable"], +) + +cel_android_library( + name = "optimized_selectable_android", + visibility = ["//:internal"], + exports = ["//common/src/main/java/dev/cel/common/values:optimized_selectable_android"], +) + +java_library( + name = "optimized_select_traversal", + visibility = ["//:internal"], + exports = ["//common/src/main/java/dev/cel/common/values:optimized_select_traversal"], +) + +cel_android_library( + name = "optimized_select_traversal_android", + visibility = ["//:internal"], + exports = ["//common/src/main/java/dev/cel/common/values:optimized_select_traversal_android"], +)