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..792f88486 100644 --- a/common/src/main/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/values/BUILD.bazel @@ -17,7 +17,9 @@ CEL_VALUES_SOURCES = [ "ErrorValue.java", "NullValue.java", "OpaqueValue.java", + "OptimizedSelectable.java", "OptionalValue.java", + "SelectField.java", "SelectableValue.java", "StructValue.java", ] @@ -167,6 +169,7 @@ java_library( ":preadapted_list", "//:auto_value", "//common/annotations", + "//common/exceptions:attribute_not_found", "//common/types", "//common/types:type_providers", "@maven//:com_google_errorprone_error_prone_annotations", @@ -218,6 +221,7 @@ cel_android_library( ":preadapted_list_android", "//:auto_value", "//common/annotations", + "//common/exceptions:attribute_not_found", "//common/types:type_providers_android", "//common/types:types_android", "@maven//:com_google_errorprone_error_prone_annotations", @@ -317,6 +321,7 @@ java_library( srcs = [ "ProtoLiteCelValueConverter.java", "ProtoMessageLiteValue.java", + "RawProtoMessageLiteValue.java", ], tags = [ ], @@ -325,6 +330,7 @@ java_library( ":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 +339,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,6 +349,7 @@ cel_android_library( srcs = [ "ProtoLiteCelValueConverter.java", "ProtoMessageLiteValue.java", + "RawProtoMessageLiteValue.java", ], tags = [ ], @@ -350,6 +358,7 @@ cel_android_library( ":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", ], 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..3885e38b5 --- /dev/null +++ b/common/src/main/java/dev/cel/common/values/OptimizedSelectable.java @@ -0,0 +1,152 @@ +// 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 com.google.errorprone.annotations.Immutable; +import dev.cel.common.annotations.Internal; +import dev.cel.common.exceptions.CelAttributeNotFoundException; +import java.util.Map; +import java.util.Optional; +import org.jspecify.annotations.Nullable; + +/** + * Represents a value capable of evaluating optimized field selection and presence testing for field + * selection optimization. + * + *

CEL Library Internals. Do Not Use. + */ +@Internal +@Immutable +public interface OptimizedSelectable { + + /** + * Evaluates a multi-hop qualification chain across the provided fields. + * + *

Implementations may override this method to provide an allocation-free traversal over + * underlying schema or message descriptors. The default implementation iterates sequentially over + * {@link #selectField} for intermediate submessages and the leaf. + */ + default Object qualify(ImmutableList fields) { + Object current = this; + for (SelectField field : fields) { + if (current instanceof OptimizedSelectable) { + current = ((OptimizedSelectable) current).selectField(field); + } else if (current instanceof SelectableValue) { + Optional found = + SelectField.findField((SelectableValue) current, field.fieldName()); + if (found.isPresent()) { + current = found.get(); + } else if (field.defaultValue() != null) { + current = field.defaultValue(); + } else { + throw CelAttributeNotFoundException.forFieldResolution(field.fieldName()); + } + } else if (current instanceof Map) { + Map map = (Map) current; + Object mapValue = map.get(field.fieldName()); + if (mapValue != null) { + current = mapValue; + } else if (map.containsKey(field.fieldName())) { + current = NullValue.NULL_VALUE; + } else { + throw CelAttributeNotFoundException.forMissingMapKey(field.fieldName()); + } + } else { + throw CelAttributeNotFoundException.forFieldResolution(field.fieldName()); + } + } + return current; + } + + /** + * Performs field selection for a single hop in an optimized selection chain. + * + * @param field The field hop descriptor containing field number, name, type code, and default + * value. + * @return The selected field value, or default value / empty submessage if absent. + */ + Object selectField(SelectField field); + + /** + * Evaluates presence testing across the provided fields. + * + *

Implementations may override this method to provide an allocation-free traversal over + * underlying schema or message descriptors. The default implementation iterates sequentially over + * {@link #navigateField} for intermediate submessages and {@link #hasField} for the leaf. + */ + default boolean hasField(ImmutableList fields) { + if (fields.isEmpty()) { + return false; + } + int lastIndex = fields.size() - 1; + Object current = this; + for (int i = 0; i < lastIndex; i++) { + SelectField field = fields.get(i); + if (current instanceof OptimizedSelectable) { + current = ((OptimizedSelectable) current).navigateField(field); + if (current == null) { + return false; + } + } else if (current instanceof SelectableValue) { + Optional found = + SelectField.findField((SelectableValue) current, field.fieldName()); + if (!found.isPresent()) { + return false; + } + current = found.get(); + } else if (current instanceof Map) { + Map map = (Map) current; + if (!map.containsKey(field.fieldName())) { + return false; + } + Object mapValue = map.get(field.fieldName()); + current = (mapValue != null) ? mapValue : NullValue.NULL_VALUE; + } else { + throw CelAttributeNotFoundException.forFieldResolution(field.fieldName()); + } + } + SelectField terminalField = fields.get(lastIndex); + if (current instanceof OptimizedSelectable) { + return ((OptimizedSelectable) current).hasField(terminalField); + } else if (current instanceof SelectableValue) { + return SelectField.findField((SelectableValue) current, terminalField.fieldName()) + .isPresent(); + } else if (current instanceof Map) { + return ((Map) current).containsKey(terminalField.fieldName()); + } else { + throw CelAttributeNotFoundException.forFieldResolution(terminalField.fieldName()); + } + } + + /** + * Evaluates presence testing for a single hop in an optimized selection chain. + * + * @param field The field hop descriptor. + * @return True if the field is present, false otherwise. + */ + boolean hasField(SelectField field); + + /** + * Navigates into an intermediate submessage for presence testing. + * + *

Returns {@code null} instead of an {@link Optional} to avoid object allocation overhead on + * hot-path intermediate navigation hops. + * + * @param field The field hop descriptor. + * @return The submessage if present, or {@code null} if absent. + */ + @Nullable Object navigateField(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..0b582785d 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; /** @@ -62,8 +64,36 @@ public final class ProtoLiteCelValueConverter extends BaseProtoCelValueConverter { private final CelLiteDescriptorPool descriptorPool; + 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("Descriptor not found: " + protoTypeName); + } + }; + + private static final ProtoLiteCelValueConverter DEFAULT_INSTANCE = + new ProtoLiteCelValueConverter(EMPTY_DESCRIPTOR_POOL); + + public static ProtoLiteCelValueConverter newInstance() { + return DEFAULT_INSTANCE; + } + public static ProtoLiteCelValueConverter newInstance( CelLiteDescriptorPool celLiteDescriptorPool) { + if (celLiteDescriptorPool == EMPTY_DESCRIPTOR_POOL) { + return DEFAULT_INSTANCE; + } return new ProtoLiteCelValueConverter(celLiteDescriptorPool); } @@ -80,7 +110,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: @@ -160,6 +190,39 @@ Object getDefaultCelValue(String protoTypeName, String fieldName) { return toRuntimeValue(defaultValue); } + Optional findFieldDescriptor(String protoTypeName, int fieldNumber) { + return descriptorPool + .findDescriptor(protoTypeName) + .flatMap(desc -> desc.findByFieldNumber(fieldNumber)); + } + + Optional findDefaultCelValue(FieldLiteDescriptor fieldDescriptor) { + try { + return Optional.of(toRuntimeValue(getDefaultValue(fieldDescriptor))); + } catch (NoSuchElementException e) { + return Optional.empty(); + } + } + + Optional tryDecodeWellKnownProto(ByteString bytes, String protoTypeName) { + Optional wellKnownProto = WellKnownProto.getByTypeName(protoTypeName); + if (!wellKnownProto.isPresent()) { + return Optional.empty(); + } + Optional descriptor = descriptorPool.findDescriptor(protoTypeName); + if (!descriptor.isPresent()) { + return Optional.empty(); + } + try { + MessageLite.Builder builder = descriptor.get().newMessageBuilder(); + builder.mergeFrom(bytes, ExtensionRegistryLite.getEmptyRegistry()); + return Optional.of(fromWellKnownProto(builder.build(), wellKnownProto.get())); + } catch (IOException e) { + throw new IllegalArgumentException( + "Failed to decode well-known proto of type: " + protoTypeName, e); + } + } + @Override @SuppressWarnings("LiteProtoToString") // No alternative identifier to use. Debug only info is OK. public Object toRuntimeValue(Object value) { @@ -193,7 +256,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); } @@ -269,7 +335,6 @@ private Map.Entry readSingleMapEntry( 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 +409,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 +429,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 +453,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..7e5dcd425 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java +++ b/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java @@ -17,13 +17,17 @@ 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.types.CelType; import dev.cel.common.types.StructTypeReference; +import dev.cel.common.values.ProtoLiteCelValueConverter.MessageFields; +import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor; import java.io.IOException; import java.util.Optional; +import org.jspecify.annotations.Nullable; /** * ProtoMessageLiteValue is a struct value with protobuf support for {@link MessageLite}. @@ -35,7 +39,8 @@ */ @AutoValue @Immutable -public abstract class ProtoMessageLiteValue extends StructValue { +public abstract class ProtoMessageLiteValue extends StructValue + implements OptimizedSelectable { @Override public abstract MessageLite value(); @@ -46,14 +51,22 @@ 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); } } + private ImmutableMap fieldValues() { + return messageFields().values(); + } + + ImmutableListMultimap unknownFields() { + return messageFields().unknowns(); + } + @Override public boolean isZeroValue() { return value().getDefaultInstanceForType().equals(value()); @@ -72,6 +85,51 @@ public Optional find(String field) { .map(value -> protoLiteCelValueConverter().toRuntimeValue(fieldValue)); } + @Override + public Object selectField(SelectField field) { + int fieldNumber = field.fieldNumber(); + Optional fieldDescriptor = + protoLiteCelValueConverter().findFieldDescriptor(celType().name(), fieldNumber); + String currentFieldName = + fieldDescriptor.map(FieldLiteDescriptor::getFieldName).orElse(field.fieldName()); + Object fieldValue = fieldValues().get(currentFieldName); + if (fieldValue != null) { + return protoLiteCelValueConverter().toRuntimeValue(fieldValue); + } + return RawProtoMessageLiteValue.selectUnknownOrDefault( + field, fieldDescriptor, unknownFields().get(fieldNumber), protoLiteCelValueConverter()); + } + + @Override + public boolean hasField(SelectField field) { + int fieldNumber = field.fieldNumber(); + Optional fieldDescriptor = + protoLiteCelValueConverter().findFieldDescriptor(celType().name(), fieldNumber); + String currentFieldName = + fieldDescriptor.map(FieldLiteDescriptor::getFieldName).orElse(field.fieldName()); + Object fieldValue = fieldValues().get(currentFieldName); + if (fieldValue != null) { + return true; + } + return RawProtoMessageLiteValue.isPresentInUnknowns( + field, fieldDescriptor, unknownFields().get(fieldNumber), protoLiteCelValueConverter()); + } + + @Override + public @Nullable Object navigateField(SelectField field) { + int fieldNumber = field.fieldNumber(); + Optional fieldDescriptor = + protoLiteCelValueConverter().findFieldDescriptor(celType().name(), fieldNumber); + String currentFieldName = + fieldDescriptor.map(FieldLiteDescriptor::getFieldName).orElse(field.fieldName()); + Object fieldValue = fieldValues().get(currentFieldName); + if (fieldValue != null) { + return protoLiteCelValueConverter().toRuntimeValue(fieldValue); + } + return RawProtoMessageLiteValue.navigateUnknown( + field, fieldDescriptor, unknownFields().get(fieldNumber), protoLiteCelValueConverter()); + } + public static ProtoMessageLiteValue create( MessageLite value, String typeName, ProtoLiteCelValueConverter protoLiteCelValueConverter) { Preconditions.checkNotNull(value); @@ -79,4 +137,7 @@ public static ProtoMessageLiteValue create( return new AutoValue_ProtoMessageLiteValue( value, StructTypeReference.create(typeName), protoLiteCelValueConverter); } + + // Package-private constructor to prevent subclassing outside the package. + ProtoMessageLiteValue() {} } 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..c9e9716a5 --- /dev/null +++ b/common/src/main/java/dev/cel/common/values/RawProtoMessageLiteValue.java @@ -0,0 +1,505 @@ +// 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 com.google.auto.value.AutoValue; +import com.google.auto.value.extension.memoized.Memoized; +import com.google.common.annotations.VisibleForTesting; +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.MessageLite; +import com.google.protobuf.WireFormat; +import dev.cel.common.annotations.Internal; +import dev.cel.common.exceptions.CelAttributeNotFoundException; +import dev.cel.common.types.CelType; +import dev.cel.common.types.StructTypeReference; +import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor; +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 { + + static final String UNKNOWN_MESSAGE_TYPE_NAME = "cel.@unknownMessage"; + static final int CEL_MAP_TYPE_CODE = -1; + + 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); + } + } + + boolean hasField(int fieldNumber) { + return unknownFields().containsKey(fieldNumber); + } + + @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. + * + *

Field traversal on classless messages must be performed via optimized attribute steps + * ({@code cel.@attribute} and {@code cel.@hasField}), where the AST optimizer supplies the + * pre-resolved protobuf field numbers. + * + * @throws CelAttributeNotFoundException always, indicating the field cannot be resolved by name. + */ + @Override + public Object select(String field) { + throw CelAttributeNotFoundException.forFieldResolution(field); + } + + @Override + public Optional find(String field) { + return Optional.empty(); + } + + @Override + public Object selectField(SelectField field) { + int fieldNumber = field.fieldNumber(); + Optional fieldDescriptor = + protoLiteCelValueConverter().findFieldDescriptor(celType().name(), fieldNumber); + return selectUnknownOrDefault( + field, fieldDescriptor, unknownFields().get(fieldNumber), protoLiteCelValueConverter()); + } + + @Override + public boolean hasField(SelectField field) { + int fieldNumber = field.fieldNumber(); + Optional fieldDescriptor = + protoLiteCelValueConverter().findFieldDescriptor(celType().name(), fieldNumber); + return isPresentInUnknowns( + field, fieldDescriptor, unknownFields().get(fieldNumber), protoLiteCelValueConverter()); + } + + @Override + public @Nullable Object navigateField(SelectField field) { + int fieldNumber = field.fieldNumber(); + Optional fieldDescriptor = + protoLiteCelValueConverter().findFieldDescriptor(celType().name(), fieldNumber); + return navigateUnknown( + field, fieldDescriptor, unknownFields().get(fieldNumber), protoLiteCelValueConverter()); + } + + static Object selectUnknownOrDefault( + SelectField field, + Optional fieldDescriptor, + ImmutableList unknowns, + ProtoLiteCelValueConverter converter) { + if (!unknowns.isEmpty()) { + if (fieldDescriptor.isPresent() + && fieldDescriptor.get().getEncodingType() == FieldLiteDescriptor.EncodingType.MAP) { + return decodeMapEntries(unknowns, fieldDescriptor.get(), converter); + } + int typeCode; + boolean isRepeated; + String protoTypeName; + if (fieldDescriptor.isPresent()) { + FieldLiteDescriptor fd = fieldDescriptor.get(); + typeCode = fd.getProtoFieldType().getNumber(); + isRepeated = fd.getEncodingType() == FieldLiteDescriptor.EncodingType.LIST; + protoTypeName = fd.getFieldProtoTypeName(); + } else { + typeCode = field.typeCode(); + isRepeated = field.defaultValue() instanceof List; + protoTypeName = UNKNOWN_MESSAGE_TYPE_NAME; + } + if (typeCode == SelectField.CEL_MAP_TYPE_CODE) { + throw new UnsupportedOperationException( + "Decoding unknown map field from wire bytes is unsupported: " + field.fieldName()); + } + return decodeWireEntries(unknowns, typeCode, protoTypeName, isRepeated, converter); + } + + if (field.defaultValue() != null) { + return field.defaultValue(); + } + + Optional typeDefault = fieldDescriptor.flatMap(converter::findDefaultCelValue); + if (typeDefault.isPresent()) { + return typeDefault.get(); + } + + int typeCode = + fieldDescriptor.map(d -> d.getProtoFieldType().getNumber()).orElse(field.typeCode()); + String protoTypeName = + fieldDescriptor + .map(FieldLiteDescriptor::getFieldProtoTypeName) + .orElse(UNKNOWN_MESSAGE_TYPE_NAME); + + if (typeCode == FieldLiteDescriptor.Type.MESSAGE.getNumber()) { + Optional wellKnown = + converter.tryDecodeWellKnownProto(ByteString.EMPTY, protoTypeName); + if (wellKnown.isPresent()) { + return wellKnown.get(); + } + return create(ByteString.EMPTY, protoTypeName, converter); + } + + throw CelAttributeNotFoundException.forFieldResolution(field.fieldName()); + } + + static boolean isPresentInUnknowns( + SelectField field, + Optional fieldDescriptor, + ImmutableList unknowns, + ProtoLiteCelValueConverter converter) { + if (unknowns.isEmpty()) { + return false; + } + boolean isRepeated; + int typeCode; + if (fieldDescriptor.isPresent()) { + FieldLiteDescriptor fd = fieldDescriptor.get(); + isRepeated = fd.getEncodingType() == FieldLiteDescriptor.EncodingType.LIST; + typeCode = fd.getProtoFieldType().getNumber(); + } else { + isRepeated = field.defaultValue() instanceof List; + typeCode = field.typeCode(); + } + + if (isRepeated) { + 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; + } + return true; + } + + static @Nullable Object navigateUnknown( + SelectField field, + Optional fieldDescriptor, + ImmutableList unknowns, + ProtoLiteCelValueConverter converter) { + if (!isPresentInUnknowns(field, fieldDescriptor, unknowns, converter)) { + return null; + } + if (fieldDescriptor.isPresent() || field.typeCode() != SelectField.NO_TYPE_CODE) { + return selectUnknownOrDefault(field, fieldDescriptor, unknowns, converter); + } + Object lastEntry = unknowns.get(unknowns.size() - 1); + if (lastEntry instanceof ByteString) { + return decodeWireEntries( + unknowns, + FieldLiteDescriptor.Type.MESSAGE.getNumber(), + UNKNOWN_MESSAGE_TYPE_NAME, + /* isRepeated= */ false, + converter); + } + return lastEntry; + } + + private static ImmutableMap decodeMapEntries( + ImmutableList unknowns, + FieldLiteDescriptor mapFieldDescriptor, + ProtoLiteCelValueConverter converter) { + String entryTypeName = mapFieldDescriptor.getFieldProtoTypeName(); + 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("key"); + if (key == null) { + key = converter.getDefaultCelValue(entryTypeName, "key"); + } else { + key = converter.toRuntimeValue(key); + } + Object value = entryFields.get("value"); + if (value == null) { + value = converter.getDefaultCelValue(entryTypeName, "value"); + } else { + value = 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); + } + + @VisibleForTesting + static @Nullable Object decodeWireEntries( + ImmutableCollection entries, int typeCode, String protoTypeName, boolean isRepeated) { + return decodeWireEntries( + entries, typeCode, protoTypeName, isRepeated, ProtoLiteCelValueConverter.newInstance()); + } + + 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); + } + + @VisibleForTesting + static Object decodeWireValue(Object raw, int typeCode, String protoTypeName) { + return decodeWireValue( + raw, FieldLiteDescriptor.Type.forNumber(typeCode).toWireFormatFieldType(), protoTypeName); + } + + @VisibleForTesting + static Object decodeWireValue(Object raw, WireFormat.FieldType fieldType, String protoTypeName) { + return decodeWireValue(raw, fieldType, protoTypeName, ProtoLiteCelValueConverter.newInstance()); + } + + private 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); + Optional wellKnown = converter.tryDecodeWellKnownProto(msgBytes, protoTypeName); + if (wellKnown.isPresent()) { + return wellKnown.get(); + } + return RawProtoMessageLiteValue.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) { + return create(rawWireBytes, "", ProtoLiteCelValueConverter.newInstance()); + } + + public static RawProtoMessageLiteValue create(ByteString rawWireBytes, String protoTypeName) { + return create(rawWireBytes, protoTypeName, ProtoLiteCelValueConverter.newInstance()); + } + + 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..ef9e085b1 --- /dev/null +++ b/common/src/main/java/dev/cel/common/values/SelectField.java @@ -0,0 +1,82 @@ +// 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 java.util.Optional; +import org.jspecify.annotations.Nullable; + +/** + * Represents a single field selection hop 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 { + + public static final long MAX_FIELD_NUMBER = 536870911L; + public static final int CEL_MAP_TYPE_CODE = -1; + + static final int NO_TYPE_CODE = 0; + + public abstract int fieldNumber(); + + public abstract String fieldName(); + + public abstract int typeCode(); + + public abstract @Nullable Object defaultValue(); + + 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); + } + + public static SelectField create( + long fieldNumber, String fieldName, int typeCode, @Nullable Object defaultValue) { + checkArgument( + fieldNumber >= 1 && fieldNumber <= MAX_FIELD_NUMBER, + "Field number out of protobuf range: %s", + fieldNumber); + checkNotNull(fieldName); + checkArgument( + typeCode == CEL_MAP_TYPE_CODE || (typeCode >= 1 && typeCode <= 18), + "Invalid protobuf type code: %s", + typeCode); + return new AutoValue_SelectField((int) fieldNumber, fieldName, typeCode, defaultValue); + } + + @SuppressWarnings("unchecked") // Structs and maps qualified by a select chain are String keyed. + static Optional findField(SelectableValue selectable, String fieldName) { + return (Optional) ((SelectableValue) selectable).find(fieldName); + } + + // Package-private constructor to prevent subclassing outside the package. + 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..f1ee2ad3e 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", @@ -32,6 +34,7 @@ java_library( "//common/values:proto_message_lite_value_provider", "//common/values:proto_message_value", "//common/values:proto_message_value_provider", + "//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/ProtoMessageLiteValueTest.java b/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java index dbfb55cf9..db1ca87c0 100644 --- a/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java +++ b/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java @@ -15,28 +15,37 @@ 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.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; 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.expr.conformance.proto3.TestAllTypes; 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 +162,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 +260,296 @@ 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")); + } + + @Test + public void selectField_knownField_returnsValue() { + TestAllTypes proto = TestAllTypes.newBuilder().setSingleString("foo").build(); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + Object result = val.selectField(SelectField.create(14L, "single_string", 9, "default")); + + assertThat(result).isEqualTo("foo"); + } + + @Test + public void selectField_unknownWireField_decoded() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeInt64(999, 42L); + cos.flush(); + TestAllTypes proto = + TestAllTypes.parseFrom(baos.toByteArray(), ExtensionRegistryLite.getEmptyRegistry()); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + Object result = val.selectField(SelectField.create(999L, "unknown_field", 3, 0L)); + + assertThat(result).isEqualTo(42L); + } + + @Test + public void selectField_unknownRepeatedWireField_decoded() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeInt64(999, 10L); + cos.writeInt64(999, 20L); + cos.flush(); + TestAllTypes proto = + TestAllTypes.parseFrom(baos.toByteArray(), ExtensionRegistryLite.getEmptyRegistry()); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + Object result = + val.selectField(SelectField.create(999L, "unknown_repeated", 3, ImmutableList.of())); + + assertThat((Iterable) result).containsExactly(10L, 20L).inOrder(); + } + + @Test + public void selectField_renamedField_resolvesByFieldNumber() { + TestAllTypes proto = TestAllTypes.newBuilder().setSingleString("foo").build(); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + Object result = val.selectField(SelectField.create(14L, "renamed_string", 9, "default")); + + assertThat(result).isEqualTo("foo"); + } + + @Test + public void selectField_renamedMapField_resolvesByFieldNumber() { + TestAllTypes proto = TestAllTypes.newBuilder().putMapStringString("k", "v").build(); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + Object result = val.selectField(SelectField.create(61L, "renamed_map", -1, ImmutableMap.of())); + + assertThat(result).isEqualTo(ImmutableMap.of("k", "v")); + } + + @Test + public void selectField_renamedRepeatedField_resolvesByFieldNumber() { + TestAllTypes proto = + TestAllTypes.newBuilder().addRepeatedInt64(10L).addRepeatedInt64(20L).build(); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + Object result = + val.selectField(SelectField.create(32L, "renamed_repeated", 3, ImmutableList.of())); + + assertThat(result).isEqualTo(ImmutableList.of(10L, 20L)); + } + + @Test + public void navigateField_intermediateUnknownSubmessage_returnsRawProtoMessage() + throws Exception { + ByteArrayOutputStream subBaos = new ByteArrayOutputStream(); + CodedOutputStream subCos = CodedOutputStream.newInstance(subBaos); + subCos.writeString(1, "inner"); + subCos.flush(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeBytes(998, ByteString.copyFrom(subBaos.toByteArray())); + cos.flush(); + TestAllTypes proto = + TestAllTypes.parseFrom(baos.toByteArray(), ExtensionRegistryLite.getEmptyRegistry()); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + Object nav = val.navigateField(SelectField.create(998L, "unknown_submessage")); + + assertThat(nav).isInstanceOf(RawProtoMessageLiteValue.class); + } + + @Test + public void hasField_knownField_returnsTrue() { + TestAllTypes proto = TestAllTypes.newBuilder().setSingleString("present").build(); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + assertThat(val.hasField(SelectField.create(14L, "single_string"))).isTrue(); + } + + @Test + public void hasField_unknownFieldPresent_returnsTrue() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeInt64(999, 42L); + cos.flush(); + TestAllTypes proto = + TestAllTypes.parseFrom(baos.toByteArray(), ExtensionRegistryLite.getEmptyRegistry()); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + assertThat(val.hasField(SelectField.create(999L, "unknown_present"))).isTrue(); + } + + @Test + public void hasField_unknownFieldAbsent_returnsFalse() { + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + TestAllTypes.getDefaultInstance(), + "cel.expr.conformance.proto3.TestAllTypes", + PROTO_LITE_CEL_VALUE_CONVERTER); + + assertThat(val.hasField(SelectField.create(888L, "unknown_absent"))).isFalse(); + } + + @Test + public void hasField_renamedField_resolvesByFieldNumber() { + TestAllTypes proto = TestAllTypes.newBuilder().setSingleString("present").build(); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + assertThat(val.hasField(SelectField.create(14L, "renamed_string"))).isTrue(); + } + + @Test + public void hasField_renamedMapField_resolvesByFieldNumber() { + TestAllTypes proto = TestAllTypes.newBuilder().putMapStringString("k", "v").build(); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + assertThat(val.hasField(SelectField.create(61L, "renamed_map"))).isTrue(); + } + + @Test + public void hasField_renamedRepeatedField_resolvesByFieldNumber() { + TestAllTypes proto = TestAllTypes.newBuilder().addRepeatedInt64(10L).build(); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + assertThat(val.hasField(SelectField.create(32L, "renamed_repeated"))).isTrue(); + } + + @Test + public void qualify_emptyList_returnsThis() { + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + TestAllTypes.getDefaultInstance(), + "cel.expr.conformance.proto3.TestAllTypes", + PROTO_LITE_CEL_VALUE_CONVERTER); + + assertThat(val.qualify(ImmutableList.of())).isSameInstanceAs(val); + } + + @Test + public void hasField_emptyList_returnsFalse() { + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + TestAllTypes.getDefaultInstance(), + "cel.expr.conformance.proto3.TestAllTypes", + PROTO_LITE_CEL_VALUE_CONVERTER); + + assertThat(val.hasField(ImmutableList.of())).isFalse(); + } + + @Test + public void qualify_crossTypeToMap_resolvesMapKey() { + TestAllTypes proto = TestAllTypes.newBuilder().putMapStringString("k", "v").build(); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + Object result = + val.qualify( + ImmutableList.of( + SelectField.create(61L, "map_string_string", -1, ImmutableMap.of()), + SelectField.create(1L, "k", 9, ""))); + + assertThat(result).isEqualTo("v"); + } + + @Test + public void hasField_crossTypeToMap_resolvesPresence() { + TestAllTypes proto = TestAllTypes.newBuilder().putMapStringString("k", "v").build(); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + assertThat( + val.hasField( + ImmutableList.of( + SelectField.create(61L, "map_string_string"), SelectField.create(1L, "k")))) + .isTrue(); + assertThat( + val.hasField( + ImmutableList.of( + SelectField.create(61L, "map_string_string"), + SelectField.create(1L, "missing")))) + .isFalse(); + } + + @Test + public void qualify_intermediateScalar_throwsCelAttributeNotFoundWithChildFieldName() { + TestAllTypes proto = TestAllTypes.newBuilder().setSingleInt64(42L).build(); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> + val.qualify( + ImmutableList.of( + SelectField.create(2L, "single_int64", 3, 0L), + SelectField.create(3L, "leaf_field", 9, "")))); + + assertThat(thrown).hasMessageThat().contains("leaf_field"); + } + + @Test + public void hasField_intermediateScalar_throwsCelAttributeNotFoundWithChildFieldName() { + TestAllTypes proto = TestAllTypes.newBuilder().setSingleInt64(42L).build(); + ProtoMessageLiteValue val = + ProtoMessageLiteValue.create( + proto, "cel.expr.conformance.proto3.TestAllTypes", PROTO_LITE_CEL_VALUE_CONVERTER); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> + val.hasField( + ImmutableList.of( + SelectField.create(2L, "single_int64"), + SelectField.create(3L, "leaf_field")))); + + assertThat(thrown).hasMessageThat().contains("leaf_field"); + } } 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..8358d8050 --- /dev/null +++ b/common/src/test/java/dev/cel/common/values/RawProtoMessageLiteValueTest.java @@ -0,0 +1,985 @@ +// 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.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.WireFormat; +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 java.io.ByteArrayOutputStream; +import java.time.Duration; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class RawProtoMessageLiteValueTest { + + private static final CelLiteDescriptorPool DESCRIPTOR_POOL = + DefaultLiteDescriptorPool.newInstance( + ImmutableSet.of(TestAllTypesCelDescriptor.getDescriptor())); + + private static final ProtoLiteCelValueConverter CONVERTER = + ProtoLiteCelValueConverter.newInstance(DESCRIPTOR_POOL); + + @Test + public void create_accessorsAndType() { + ByteString bytes = ByteString.copyFromUtf8("test"); + RawProtoMessageLiteValue value = RawProtoMessageLiteValue.create(bytes, "custom.Message"); + + assertThat(value.rawWireBytes()).isEqualTo(bytes); + assertThat(value.value()).isSameInstanceAs(value); + assertThat(value.celType().name()).isEqualTo("custom.Message"); + } + + @Test + public void create_singleArgDefaultsEmptyTypeName() { + ByteString bytes = ByteString.copyFromUtf8("test"); + RawProtoMessageLiteValue value = RawProtoMessageLiteValue.create(bytes); + + assertThat(value.rawWireBytes()).isEqualTo(bytes); + assertThat(value.celType().name()).isEmpty(); + } + + @Test + public void select_throwsCelAttributeNotFoundException() { + RawProtoMessageLiteValue value = + RawProtoMessageLiteValue.create(ByteString.EMPTY, "custom.Message"); + + assertThrows(CelAttributeNotFoundException.class, () -> value.select("field")); + } + + @Test + public void find_returnsEmptyOptional() { + RawProtoMessageLiteValue value = + RawProtoMessageLiteValue.create(ByteString.EMPTY, "custom.Message"); + + assertThat(value.find("field")).isEmpty(); + } + + @Test + public void isZeroValue_emptyBytes_returnsTrue() { + RawProtoMessageLiteValue value = RawProtoMessageLiteValue.create(ByteString.EMPTY); + + assertThat(value.isZeroValue()).isTrue(); + } + + @Test + public void isZeroValue_nonEmptyBytes_returnsFalse() { + RawProtoMessageLiteValue value = + RawProtoMessageLiteValue.create(ByteString.copyFromUtf8("data")); + + assertThat(value.isZeroValue()).isFalse(); + } + + @Test + public void hasField_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())); + + assertThat(value.hasField(1)).isTrue(); + assertThat(value.hasField(2)).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())); + + 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 = + RawProtoMessageLiteValue.decodeWireEntries( + ImmutableList.of(), + FieldLiteDescriptor.Type.INT64.getNumber(), + "custom.Message", + /* isRepeated= */ false); + Object messageResult = + RawProtoMessageLiteValue.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 = + RawProtoMessageLiteValue.decodeWireEntries( + ImmutableList.of(), + FieldLiteDescriptor.Type.INT64.getNumber(), + "custom.Message", + /* isRepeated= */ true); + + assertThat((Iterable) result).isEmpty(); + } + + @Test + public void decodeWireEntries_nonRepeated_lastOneWins() { + Object decoded = + RawProtoMessageLiteValue.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 = + RawProtoMessageLiteValue.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 = + RawProtoMessageLiteValue.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 = + RawProtoMessageLiteValue.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 = + RawProtoMessageLiteValue.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 = + RawProtoMessageLiteValue.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 = + RawProtoMessageLiteValue.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 = + RawProtoMessageLiteValue.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( + RawProtoMessageLiteValue.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( + RawProtoMessageLiteValue.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( + RawProtoMessageLiteValue.decodeWireEntries( + ImmutableList.of(ByteString.copyFrom(baos.toByteArray()).substring(12, 16)), + FieldLiteDescriptor.Type.SFIXED32.getNumber(), + "custom.Message", + /* isRepeated= */ true)) + .isEqualTo(ImmutableList.of(-30L)); + + assertThat( + RawProtoMessageLiteValue.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( + RawProtoMessageLiteValue.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( + RawProtoMessageLiteValue.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( + RawProtoMessageLiteValue.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( + RawProtoMessageLiteValue.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( + RawProtoMessageLiteValue.decodeWireValue( + Double.doubleToRawLongBits(2.5d), WireFormat.FieldType.DOUBLE, "custom.Message")) + .isEqualTo(2.5d); + + assertThat( + RawProtoMessageLiteValue.decodeWireValue( + Float.floatToRawIntBits(1.5f), WireFormat.FieldType.FLOAT, "custom.Message")) + .isEqualTo(1.5d); + + assertThat( + RawProtoMessageLiteValue.decodeWireValue( + 42L, WireFormat.FieldType.INT64, "custom.Message")) + .isEqualTo(42L); + + assertThat( + RawProtoMessageLiteValue.decodeWireValue( + 42L, WireFormat.FieldType.INT32, "custom.Message")) + .isEqualTo(42L); + + assertThat( + RawProtoMessageLiteValue.decodeWireValue( + 42L, WireFormat.FieldType.UINT64, "custom.Message")) + .isEqualTo(UnsignedLong.fromLongBits(42L)); + + assertThat( + RawProtoMessageLiteValue.decodeWireValue( + 42L, WireFormat.FieldType.UINT32, "custom.Message")) + .isEqualTo(UnsignedLong.fromLongBits(42L)); + + assertThat( + RawProtoMessageLiteValue.decodeWireValue( + 100, WireFormat.FieldType.FIXED32, "custom.Message")) + .isEqualTo(UnsignedLong.fromLongBits(100L)); + + assertThat( + RawProtoMessageLiteValue.decodeWireValue( + 100L, WireFormat.FieldType.FIXED64, "custom.Message")) + .isEqualTo(UnsignedLong.fromLongBits(100L)); + + assertThat( + RawProtoMessageLiteValue.decodeWireValue( + -50, WireFormat.FieldType.SFIXED32, "custom.Message")) + .isEqualTo(-50L); + + assertThat( + RawProtoMessageLiteValue.decodeWireValue( + -50L, WireFormat.FieldType.SFIXED64, "custom.Message")) + .isEqualTo(-50L); + + assertThat( + RawProtoMessageLiteValue.decodeWireValue( + 1L, WireFormat.FieldType.BOOL, "custom.Message")) + .isEqualTo(true); + + assertThat( + RawProtoMessageLiteValue.decodeWireValue( + 0L, WireFormat.FieldType.BOOL, "custom.Message")) + .isEqualTo(false); + + assertThat( + RawProtoMessageLiteValue.decodeWireValue( + ByteString.copyFromUtf8("hello"), WireFormat.FieldType.STRING, "custom.Message")) + .isEqualTo("hello"); + + assertThat( + RawProtoMessageLiteValue.decodeWireValue( + ByteString.copyFromUtf8("bytes"), WireFormat.FieldType.BYTES, "custom.Message")) + .isEqualTo(CelByteString.of("bytes".getBytes(UTF_8))); + + assertThat( + RawProtoMessageLiteValue.decodeWireValue( + 1L, // zigzag 1 -> -1 + WireFormat.FieldType.SINT32, + "custom.Message")) + .isEqualTo(-1L); + + assertThat( + RawProtoMessageLiteValue.decodeWireValue( + 1L, // zigzag 1 -> -1 + WireFormat.FieldType.SINT64, + "custom.Message")) + .isEqualTo(-1L); + + assertThat( + RawProtoMessageLiteValue.decodeWireValue( + 3L, WireFormat.FieldType.ENUM, "custom.Message")) + .isEqualTo(3L); + } + + @Test + public void decodeWireValue_messageType_returnsRawProtoMessageLiteValue() { + Object submessage = + RawProtoMessageLiteValue.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, + () -> + RawProtoMessageLiteValue.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, + () -> + RawProtoMessageLiteValue.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, + () -> + RawProtoMessageLiteValue.decodeWireEntries( + rawEntries, 999, "custom.Message", /* isRepeated= */ false)); + } + + @Test + public void decodeWireValue_invalidTypeCode_throws() { + assertThrows( + IllegalArgumentException.class, + () -> RawProtoMessageLiteValue.decodeWireValue(42L, 0, "custom.Message")); + + assertThrows( + IllegalArgumentException.class, + () -> RawProtoMessageLiteValue.decodeWireValue(42L, 999, "custom.Message")); + } + + @Test + public void decodeWireValue_int32HighBits_truncatedToSigned32Bit() { + Object decodedHigh = + RawProtoMessageLiteValue.decodeWireValue( + 0x100000005L, WireFormat.FieldType.INT32, "custom.Message"); + Object decodedNegative = + RawProtoMessageLiteValue.decodeWireValue( + 0xFFFFFFFF80000000L, WireFormat.FieldType.INT32, "custom.Message"); + + assertThat(decodedHigh).isEqualTo(5L); + assertThat(decodedNegative).isEqualTo(-2147483648L); + } + + @Test + public void decodeWireValue_enumHighBits_truncatedToSigned32Bit() { + Object decodedHigh = + RawProtoMessageLiteValue.decodeWireValue( + 0x100000005L, WireFormat.FieldType.ENUM, "custom.Message"); + + assertThat(decodedHigh).isEqualTo(5L); + } + + @Test + public void decodeWireValue_typeMismatch_throwsIllegalArgumentException() { + IllegalArgumentException thrownInt64 = + assertThrows( + IllegalArgumentException.class, + () -> + RawProtoMessageLiteValue.decodeWireValue( + "not a long", WireFormat.FieldType.INT64, "custom.Message")); + assertThat(thrownInt64).hasMessageThat().contains("Expected Long for wire type INT64"); + + IllegalArgumentException thrownString = + assertThrows( + IllegalArgumentException.class, + () -> + RawProtoMessageLiteValue.decodeWireValue( + 100L, WireFormat.FieldType.STRING, "custom.Message")); + assertThat(thrownString).hasMessageThat().contains("Expected ByteString for wire type STRING"); + + IllegalArgumentException thrownBytes = + assertThrows( + IllegalArgumentException.class, + () -> + RawProtoMessageLiteValue.decodeWireValue( + 100L, WireFormat.FieldType.BYTES, "custom.Message")); + assertThat(thrownBytes).hasMessageThat().contains("Expected ByteString for wire type BYTES"); + + IllegalArgumentException thrownMessage = + assertThrows( + IllegalArgumentException.class, + () -> + RawProtoMessageLiteValue.decodeWireValue( + 100L, WireFormat.FieldType.MESSAGE, "custom.Message")); + assertThat(thrownMessage) + .hasMessageThat() + .contains("Expected ByteString for wire type MESSAGE"); + + IllegalArgumentException thrownFloat = + assertThrows( + IllegalArgumentException.class, + () -> + RawProtoMessageLiteValue.decodeWireValue( + 100L, WireFormat.FieldType.FLOAT, "custom.Message")); + assertThat(thrownFloat).hasMessageThat().contains("Expected Integer for wire type FLOAT"); + + IllegalArgumentException thrownDouble = + assertThrows( + IllegalArgumentException.class, + () -> + RawProtoMessageLiteValue.decodeWireValue( + 100, WireFormat.FieldType.DOUBLE, "custom.Message")); + 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, + () -> + RawProtoMessageLiteValue.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 = + RawProtoMessageLiteValue.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 = + RawProtoMessageLiteValue.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 = + RawProtoMessageLiteValue.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 = + RawProtoMessageLiteValue.decodeWireValue( + 0xFFFFFFFFL, WireFormat.FieldType.UINT32, "custom.Message"); + + assertThat(decoded).isEqualTo(UnsignedLong.valueOf(4294967295L)); + } + + @Test + public void decodeWireValue_fixed32HighBit_correctUnsignedLong() { + Object decoded = + RawProtoMessageLiteValue.decodeWireValue( + -1, WireFormat.FieldType.FIXED32, "custom.Message"); + + assertThat(decoded).isEqualTo(UnsignedLong.valueOf(4294967295L)); + } + + @Test + public void decodeWireEntries_repeatedString() { + Object decoded = + RawProtoMessageLiteValue.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 = + RawProtoMessageLiteValue.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 = + RawProtoMessageLiteValue.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"), + RawProtoMessageLiteValue.create(ByteString.copyFromUtf8("msg2"), "sub.Message")) + .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, + () -> + RawProtoMessageLiteValue.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 selectField_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"); + + Object val = raw.selectField(SelectField.create(14L, "single_string", 9, "")); + + assertThat(val).isEqualTo("hello"); + } + + @Test + public void selectField_absentWithDefaultValue_returnsDefault() { + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.EMPTY, "cel.expr.conformance.proto3.TestAllTypes"); + + Object val = raw.selectField(SelectField.create(14L, "single_string", 9, "default")); + + assertThat(val).isEqualTo("default"); + } + + @Test + public void selectField_withConverter_resolvesDescriptor() { + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.EMPTY, "cel.expr.conformance.proto3.TestAllTypes", CONVERTER); + + Object val = raw.selectField(SelectField.create(14L, "single_string")); + + assertThat(val).isEqualTo(""); + } + + @Test + public void hasField_wirePresent_returnsTrue() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeString(14, "present"); + cos.flush(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.copyFrom(baos.toByteArray()), "cel.expr.conformance.proto3.TestAllTypes"); + + assertThat(raw.hasField(SelectField.create(14L, "single_string"))).isTrue(); + } + + @Test + public void hasField_wireAbsent_returnsFalse() { + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.EMPTY, "cel.expr.conformance.proto3.TestAllTypes"); + + assertThat(raw.hasField(SelectField.create(15L, "other_field"))).isFalse(); + } + + @Test + public void hasField_emptyPackedRepeated_withDescriptor_returnsFalse() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeBytes(31, ByteString.EMPTY); + cos.flush(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.copyFrom(baos.toByteArray()), + "cel.expr.conformance.proto3.TestAllTypes", + CONVERTER); + + assertThat(raw.hasField(SelectField.create(31L, "repeated_int32"))).isFalse(); + } + + @Test + public void hasField_emptyPackedRepeated_withoutDescriptor_returnsFalse() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeBytes(31, ByteString.EMPTY); + cos.flush(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.copyFrom(baos.toByteArray()), "cel.expr.conformance.proto3.TestAllTypes"); + + assertThat(raw.hasField(SelectField.create(31L, "repeated_int32", 5, ImmutableList.of()))) + .isFalse(); + } + + @Test + public void hasField_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(31, ByteString.copyFrom(packed.toByteArray())); + cos.flush(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.copyFrom(baos.toByteArray()), + "cel.expr.conformance.proto3.TestAllTypes", + CONVERTER); + + assertThat(raw.hasField(SelectField.create(31L, "repeated_int32"))).isTrue(); + } + + @Test + public void navigateField_intermediatePresentAndAbsent() throws Exception { + ByteArrayOutputStream subBaos = new ByteArrayOutputStream(); + CodedOutputStream subCos = CodedOutputStream.newInstance(subBaos); + subCos.writeInt32(1, 42); + subCos.flush(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeBytes(21, ByteString.copyFrom(subBaos.toByteArray())); + cos.flush(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.copyFrom(baos.toByteArray()), "cel.expr.conformance.proto3.TestAllTypes"); + + Object nav = raw.navigateField(SelectField.create(21L, "single_nested_message")); + + assertThat(nav).isInstanceOf(RawProtoMessageLiteValue.class); + assertThat(raw.navigateField(SelectField.create(999L, "absent"))).isNull(); + } + + @Test + public void selectField_mapFieldWithDescriptor_decodesEntries() { + TestAllTypes proto = + TestAllTypes.newBuilder() + .putMapStringString("k1", "v1") + .putMapStringString("k2", "v2") + .build(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + proto.toByteString(), "cel.expr.conformance.proto3.TestAllTypes", CONVERTER); + + Object selected = + raw.selectField(SelectField.create(61L, "map_string_string", -1, ImmutableMap.of())); + + assertThat(selected).isEqualTo(ImmutableMap.of("k1", "v1", "k2", "v2")); + } + + @Test + public void selectField_wellKnownDuration_convertsToJavaDuration() { + TestAllTypes proto = + TestAllTypes.newBuilder() + .setSingleDuration(ProtoTimeUtils.toProtoDuration(Duration.ofSeconds(10L, 500L))) + .build(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + proto.toByteString(), "cel.expr.conformance.proto3.TestAllTypes", CONVERTER); + + Object selected = + raw.selectField(SelectField.create(101L, "single_duration", 11, Duration.ZERO)); + + assertThat(selected).isEqualTo(Duration.ofSeconds(10L, 500L)); + } + + @Test + public void selectField_wellKnownInt64Wrapper_convertsToLong() { + TestAllTypes proto = + TestAllTypes.newBuilder().setSingleInt64Wrapper(Int64Value.of(12345L)).build(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + proto.toByteString(), "cel.expr.conformance.proto3.TestAllTypes", CONVERTER); + + Object selected = + raw.selectField( + SelectField.create( + TestAllTypes.SINGLE_INT64_WRAPPER_FIELD_NUMBER, + "single_int64_wrapper", + 11, + NullValue.NULL_VALUE)); + + assertThat(selected).isEqualTo(12345L); + } + + @Test + public void navigateField_scalarFieldWithDescriptor_returnsScalar() { + TestAllTypes proto = TestAllTypes.newBuilder().setSingleInt64(99L).build(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + proto.toByteString(), "cel.expr.conformance.proto3.TestAllTypes", CONVERTER); + + Object nav = raw.navigateField(SelectField.create(2L, "single_int64")); + + assertThat(nav).isEqualTo(99L); + } + + @Test + public void navigateField_scalarFieldWithoutDescriptor_returnsScalar() { + TestAllTypes proto = TestAllTypes.newBuilder().setSingleInt64(99L).build(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + proto.toByteString(), "cel.expr.conformance.proto3.TestAllTypes"); + + Object nav = raw.navigateField(SelectField.create(2L, "single_int64")); + + assertThat(nav).isEqualTo(99L); + } +} 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..1cecdbfb6 --- /dev/null +++ b/common/src/test/java/dev/cel/common/values/SelectFieldTest.java @@ -0,0 +1,105 @@ +// 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_nullFieldName_throwsNullPointerException() { + assertThrows(NullPointerException.class, () -> SelectField.create(1L, null)); + 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_invalidTypeCode_throwsIllegalArgumentException() { + assertThrows(IllegalArgumentException.class, () -> SelectField.create(1L, "foo", 0, null)); + assertThrows(IllegalArgumentException.class, () -> SelectField.create(1L, "foo", 19, null)); + assertThrows(IllegalArgumentException.class, () -> SelectField.create(1L, "foo", -2, null)); + assertThrows(IllegalArgumentException.class, () -> SelectField.create(1L, "foo", 999, null)); + } + + @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/protobuf/src/main/java/dev/cel/protobuf/CelLiteDescriptor.java b/protobuf/src/main/java/dev/cel/protobuf/CelLiteDescriptor.java index c066bb18e..fcee6215a 100644 --- a/protobuf/src/main/java/dev/cel/protobuf/CelLiteDescriptor.java +++ b/protobuf/src/main/java/dev/cel/protobuf/CelLiteDescriptor.java @@ -18,6 +18,7 @@ import com.google.errorprone.annotations.Immutable; import com.google.protobuf.MessageLite; +import com.google.protobuf.WireFormat; import dev.cel.common.annotations.Internal; import java.util.Collections; import java.util.HashMap; @@ -184,24 +185,95 @@ public enum JavaType { *

This is exactly the same as com.google.protobuf.Descriptors#Type */ public enum Type { - DOUBLE, - FLOAT, - INT64, - UINT64, - INT32, - FIXED64, - FIXED32, - BOOL, - STRING, - GROUP, - MESSAGE, - BYTES, - UINT32, - ENUM, - SFIXED32, - SFIXED64, - SINT32, - SINT64 + DOUBLE(1, WireFormat.FieldType.DOUBLE), + FLOAT(2, WireFormat.FieldType.FLOAT), + INT64(3, WireFormat.FieldType.INT64), + UINT64(4, WireFormat.FieldType.UINT64), + INT32(5, WireFormat.FieldType.INT32), + FIXED64(6, WireFormat.FieldType.FIXED64), + FIXED32(7, WireFormat.FieldType.FIXED32), + BOOL(8, WireFormat.FieldType.BOOL), + STRING(9, WireFormat.FieldType.STRING), + GROUP(10, WireFormat.FieldType.GROUP), + MESSAGE(11, WireFormat.FieldType.MESSAGE), + BYTES(12, WireFormat.FieldType.BYTES), + UINT32(13, WireFormat.FieldType.UINT32), + ENUM(14, WireFormat.FieldType.ENUM), + SFIXED32(15, WireFormat.FieldType.SFIXED32), + SFIXED64(16, WireFormat.FieldType.SFIXED64), + SINT32(17, WireFormat.FieldType.SINT32), + SINT64(18, WireFormat.FieldType.SINT64); + + private final int number; + private final WireFormat.FieldType wireFormatFieldType; + + /** Gets the type number corresponding to {@code FieldDescriptorProto.Type#getNumber()}. */ + public int getNumber() { + return number; + } + + /** Converts this type to the corresponding {@link WireFormat.FieldType}. */ + public WireFormat.FieldType toWireFormatFieldType() { + return wireFormatFieldType; + } + + /** + * Returns the {@link Type} for the specified protobuf type number. + * + * @throws IllegalArgumentException if the number does not correspond to a valid protobuf + * type. + */ + public static Type forNumber(int number) { + switch (number) { + case 1: + return DOUBLE; + case 2: + return FLOAT; + case 3: + return INT64; + case 4: + return UINT64; + case 5: + return INT32; + case 6: + return FIXED64; + case 7: + return FIXED32; + case 8: + return BOOL; + case 9: + return STRING; + case 10: + return GROUP; + case 11: + return MESSAGE; + case 12: + return BYTES; + case 13: + return UINT32; + case 14: + return ENUM; + case 15: + return SFIXED32; + case 16: + return SFIXED64; + case 17: + return SINT32; + case 18: + return SINT64; + default: + throw new IllegalArgumentException("Unsupported proto type code: " + number); + } + } + + private Type(int number, WireFormat.FieldType wireFormatFieldType) { + this.number = number; + this.wireFormatFieldType = Objects.requireNonNull(wireFormatFieldType); + } + } + + public int getFieldNumber() { + return fieldNumber; } public String getFieldName() { @@ -269,9 +341,9 @@ public FieldLiteDescriptor( String fieldProtoTypeName) { this.fieldNumber = fieldNumber; this.fieldName = Objects.requireNonNull(fieldName); - this.javaType = javaType; - this.encodingType = encodingType; - this.protoFieldType = protoFieldType; + this.javaType = Objects.requireNonNull(javaType); + this.encodingType = Objects.requireNonNull(encodingType); + this.protoFieldType = Objects.requireNonNull(protoFieldType); this.isPacked = isPacked; this.fieldProtoTypeName = Objects.requireNonNull(fieldProtoTypeName); } diff --git a/protobuf/src/test/java/dev/cel/protobuf/BUILD.bazel b/protobuf/src/test/java/dev/cel/protobuf/BUILD.bazel index 58e298b29..635379aab 100644 --- a/protobuf/src/test/java/dev/cel/protobuf/BUILD.bazel +++ b/protobuf/src/test/java/dev/cel/protobuf/BUILD.bazel @@ -16,6 +16,7 @@ java_test( "@cel_spec//proto/cel/expr/conformance/proto3:test_all_types_java_proto_lite", "@maven//:com_google_testparameterinjector_test_parameter_injector", "@maven//:junit_junit", + "@maven_android//:com_google_protobuf_protobuf_javalite", ], ) diff --git a/protobuf/src/test/java/dev/cel/protobuf/CelLiteDescriptorTest.java b/protobuf/src/test/java/dev/cel/protobuf/CelLiteDescriptorTest.java index 1ceed29bb..95dacd6ef 100644 --- a/protobuf/src/test/java/dev/cel/protobuf/CelLiteDescriptorTest.java +++ b/protobuf/src/test/java/dev/cel/protobuf/CelLiteDescriptorTest.java @@ -15,7 +15,10 @@ package dev.cel.protobuf; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import com.google.protobuf.WireFormat; +import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import dev.cel.expr.conformance.proto3.TestAllTypesCelLiteDescriptor; import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor; @@ -146,4 +149,96 @@ public void fieldDescriptor_nestedMessage_fullyQualifiedNames() { assertThat(fieldLiteDescriptor.getFieldProtoTypeName()) .isEqualTo("cel.expr.conformance.proto3.TestAllTypes.NestedMessage"); } + + private enum ProtoFieldTypeTestCase { + DOUBLE(FieldLiteDescriptor.Type.DOUBLE, 1, WireFormat.FieldType.DOUBLE), + FLOAT(FieldLiteDescriptor.Type.FLOAT, 2, WireFormat.FieldType.FLOAT), + INT64(FieldLiteDescriptor.Type.INT64, 3, WireFormat.FieldType.INT64), + UINT64(FieldLiteDescriptor.Type.UINT64, 4, WireFormat.FieldType.UINT64), + INT32(FieldLiteDescriptor.Type.INT32, 5, WireFormat.FieldType.INT32), + FIXED64(FieldLiteDescriptor.Type.FIXED64, 6, WireFormat.FieldType.FIXED64), + FIXED32(FieldLiteDescriptor.Type.FIXED32, 7, WireFormat.FieldType.FIXED32), + BOOL(FieldLiteDescriptor.Type.BOOL, 8, WireFormat.FieldType.BOOL), + STRING(FieldLiteDescriptor.Type.STRING, 9, WireFormat.FieldType.STRING), + GROUP(FieldLiteDescriptor.Type.GROUP, 10, WireFormat.FieldType.GROUP), + MESSAGE(FieldLiteDescriptor.Type.MESSAGE, 11, WireFormat.FieldType.MESSAGE), + BYTES(FieldLiteDescriptor.Type.BYTES, 12, WireFormat.FieldType.BYTES), + UINT32(FieldLiteDescriptor.Type.UINT32, 13, WireFormat.FieldType.UINT32), + ENUM(FieldLiteDescriptor.Type.ENUM, 14, WireFormat.FieldType.ENUM), + SFIXED32(FieldLiteDescriptor.Type.SFIXED32, 15, WireFormat.FieldType.SFIXED32), + SFIXED64(FieldLiteDescriptor.Type.SFIXED64, 16, WireFormat.FieldType.SFIXED64), + SINT32(FieldLiteDescriptor.Type.SINT32, 17, WireFormat.FieldType.SINT32), + SINT64(FieldLiteDescriptor.Type.SINT64, 18, WireFormat.FieldType.SINT64); + + private final FieldLiteDescriptor.Type type; + private final int expectedNumber; + private final WireFormat.FieldType expectedWireType; + + ProtoFieldTypeTestCase( + FieldLiteDescriptor.Type type, int expectedNumber, WireFormat.FieldType expectedWireType) { + this.type = type; + this.expectedNumber = expectedNumber; + this.expectedWireType = expectedWireType; + } + } + + @Test + public void protoFieldType_numbersAndWireTypes(@TestParameter ProtoFieldTypeTestCase testCase) { + assertThat(testCase.type.getNumber()).isEqualTo(testCase.expectedNumber); + assertThat(testCase.type.toWireFormatFieldType()).isEqualTo(testCase.expectedWireType); + } + + @Test + public void protoFieldType_forNumber_roundTripAllTypes( + @TestParameter FieldLiteDescriptor.Type type) { + assertThat(FieldLiteDescriptor.Type.forNumber(type.getNumber())).isEqualTo(type); + } + + @Test + public void protoFieldType_forNumber_outOfRange_throws( + @TestParameter({"-2147483648", "-1", "0", "19", "100", "2147483647"}) int invalidNumber) { + IllegalArgumentException e = + assertThrows( + IllegalArgumentException.class, + () -> FieldLiteDescriptor.Type.forNumber(invalidNumber)); + + assertThat(e).hasMessageThat().isEqualTo("Unsupported proto type code: " + invalidNumber); + } + + @Test + public void fieldLiteDescriptor_nullParameters_throws() { + assertThrows( + NullPointerException.class, + () -> + new FieldLiteDescriptor( + 1, + "field", + null, + EncodingType.SINGULAR, + FieldLiteDescriptor.Type.INT32, + false, + "")); + assertThrows( + NullPointerException.class, + () -> + new FieldLiteDescriptor( + 1, + "field", + FieldLiteDescriptor.JavaType.INT, + null, + FieldLiteDescriptor.Type.INT32, + false, + "")); + assertThrows( + NullPointerException.class, + () -> + new FieldLiteDescriptor( + 1, + "field", + FieldLiteDescriptor.JavaType.INT, + EncodingType.SINGULAR, + null, + false, + "")); + } } diff --git a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel index 9518e1601..f21e99d88 100644 --- a/runtime/src/main/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/main/java/dev/cel/runtime/BUILD.bazel @@ -41,6 +41,7 @@ LITE_RUNTIME_SOURCES = [ # keep sorted LITE_RUNTIME_IMPL_SOURCES = [ + "LiteAttributeStep.java", "LiteRuntimeImpl.java", ] @@ -990,9 +991,11 @@ java_library( ":program", ":runtime_equality", ":runtime_helpers", + ":unknown_attributes", "//common:cel_ast", "//common:container", "//common:options", + "//common/exceptions:attribute_not_found", "//common/types:default_type_provider", "//common/types:type_providers", "//common/values", @@ -1003,6 +1006,7 @@ java_library( "@maven//:com_google_code_findbugs_annotations", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", ], ) @@ -1018,9 +1022,11 @@ cel_android_library( ":program_android", ":runtime_equality_android", ":runtime_helpers_android", + ":unknown_attributes_android", "//common:cel_ast_android", "//common:container_android", "//common:options", + "//common/exceptions:attribute_not_found", "//common/types:default_type_provider_android", "//common/types:type_providers_android", "//common/values:cel_value_provider_android", @@ -1031,6 +1037,7 @@ cel_android_library( "@maven//:com_google_code_findbugs_annotations", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", + "@maven//:org_jspecify_jspecify", "@maven_android//:com_google_guava_guava", ], ) diff --git a/runtime/src/main/java/dev/cel/runtime/LiteAttributeStep.java b/runtime/src/main/java/dev/cel/runtime/LiteAttributeStep.java new file mode 100644 index 000000000..cc3cd048e --- /dev/null +++ b/runtime/src/main/java/dev/cel/runtime/LiteAttributeStep.java @@ -0,0 +1,317 @@ +// 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.runtime; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.ImmutableList; +import dev.cel.common.exceptions.CelAttributeNotFoundException; +import dev.cel.common.values.CelValueConverter; +import dev.cel.common.values.ErrorValue; +import dev.cel.common.values.NullValue; +import dev.cel.common.values.OptimizedSelectable; +import dev.cel.common.values.OptionalValue; +import dev.cel.common.values.SelectField; +import dev.cel.common.values.SelectableValue; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.jspecify.annotations.Nullable; + +/** + * LiteAttributeStep evaluates the optimized {@code cel.@attribute} and {@code cel.@hasField} + * expressions emitted by {@code SelectOptimizer}. + * + *

Qualification and presence testing dispatch polymorphically over {@link OptimizedSelectable} + * (for proto messages with descriptor-based or classless wire traversal), {@link SelectableValue}, + * and {@link Map}. + * + *

CEL Library Internals. Do Not Use. + */ +final class LiteAttributeStep { + + /** + * Qualifies an attribute by applying each qualifier in {@code qualifierLists} in order. + * + *

Each qualifier is a {@code [field_number, field_name, type_code]} 3-tuple (for submessages), + * or a {@code [field_number, field_name, type_code, default_value]} 4-tuple (for leaf fields with + * pre-resolved default values). + */ + static @Nullable Object qualifyAttribute( + @Nullable Object target, List qualifierLists, CelValueConverter celValueConverter) { + checkNotNull(qualifierLists); + checkNotNull(celValueConverter); + + ImmutableList fields = toSelectFields(qualifierLists); + + if (target instanceof CelUnknownSet + || target instanceof ErrorValue + || target instanceof Exception) { + return target; + } + + if (target == null || target instanceof NullValue) { + if (fields.isEmpty()) { + return target; + } + throw CelAttributeNotFoundException.forFieldResolution(fields.get(0).fieldName()); + } + + if (fields.isEmpty()) { + return celValueConverter.maybeUnwrap(target); + } + + Object runtimeTarget = toStepTarget(target, celValueConverter); + if (runtimeTarget instanceof OptionalValue) { + throw new UnsupportedOperationException( + "Optional operands are not yet supported by the lite select-optimized runtime"); + } + + if (runtimeTarget instanceof OptimizedSelectable) { + Object result = ((OptimizedSelectable) runtimeTarget).qualify(fields); + return celValueConverter.maybeUnwrap(result); + } + + Object obj = runtimeTarget; + for (SelectField field : fields) { + if (obj instanceof OptimizedSelectable) { + obj = ((OptimizedSelectable) obj).selectField(field); + } else if (obj instanceof SelectableValue) { + Optional found = find((SelectableValue) obj, field.fieldName()); + if (found.isPresent()) { + obj = toStepTarget(found.get(), celValueConverter); + } else if (field.defaultValue() != null) { + obj = field.defaultValue(); + } else { + throw CelAttributeNotFoundException.forFieldResolution(field.fieldName()); + } + } else if (obj instanceof Map) { + Map map = (Map) obj; + Object mapValue = map.get(field.fieldName()); + if (mapValue != null) { + obj = toStepTarget(mapValue, celValueConverter); + } else if (map.containsKey(field.fieldName())) { + obj = NullValue.NULL_VALUE; + } else { + throw CelAttributeNotFoundException.forMissingMapKey(field.fieldName()); + } + } else { + throw CelAttributeNotFoundException.forFieldResolution(field.fieldName()); + } + } + + if (obj instanceof Map) { + obj = celValueConverter.toRuntimeValue(obj); + } + return celValueConverter.maybeUnwrap(obj); + } + + /** + * Tests presence of an attribute by navigating the leading qualifiers in {@code qualifierLists} + * and presence testing the last one. + * + *

Each qualifier is a {@code [field_number, field_name]} pair. + */ + static Object hasField( + @Nullable Object target, List qualifierLists, CelValueConverter celValueConverter) { + checkNotNull(qualifierLists); + checkNotNull(celValueConverter); + + ImmutableList fields = toPresenceFields(qualifierLists); + + if (target instanceof CelUnknownSet + || target instanceof ErrorValue + || target instanceof Exception) { + return target; + } + + if (fields.isEmpty()) { + return false; + } + + if (target == null || target instanceof NullValue) { + throw CelAttributeNotFoundException.forFieldResolution(fields.get(0).fieldName()); + } + + Object runtimeTarget = toStepTarget(target, celValueConverter); + if (runtimeTarget instanceof OptionalValue) { + throw new UnsupportedOperationException( + "Optional operands are not yet supported by the lite select-optimized runtime"); + } + + if (runtimeTarget instanceof OptimizedSelectable) { + return ((OptimizedSelectable) runtimeTarget).hasField(fields); + } + + int lastIndex = fields.size() - 1; + Object obj = runtimeTarget; + for (int i = 0; i < lastIndex; i++) { + SelectField field = fields.get(i); + if (obj instanceof OptimizedSelectable) { + obj = ((OptimizedSelectable) obj).navigateField(field); + if (obj == null) { + return false; + } + } else if (obj instanceof SelectableValue) { + Optional found = find((SelectableValue) obj, field.fieldName()); + if (!found.isPresent()) { + return false; + } + obj = toStepTarget(found.get(), celValueConverter); + } else if (obj instanceof Map) { + Map map = (Map) obj; + if (!map.containsKey(field.fieldName())) { + return false; + } + Object mapValue = map.get(field.fieldName()); + obj = (mapValue != null) ? toStepTarget(mapValue, celValueConverter) : NullValue.NULL_VALUE; + } else { + throw CelAttributeNotFoundException.forFieldResolution(field.fieldName()); + } + } + + SelectField terminalField = fields.get(lastIndex); + if (obj instanceof OptimizedSelectable) { + return ((OptimizedSelectable) obj).hasField(terminalField); + } else if (obj instanceof SelectableValue) { + return find((SelectableValue) obj, terminalField.fieldName()).isPresent(); + } else if (obj instanceof Map) { + return ((Map) obj).containsKey(terminalField.fieldName()); + } else { + throw CelAttributeNotFoundException.forFieldResolution(terminalField.fieldName()); + } + } + + private static Object toStepTarget(Object value, CelValueConverter celValueConverter) { + if (value instanceof Map) { + return value; + } + return celValueConverter.toRuntimeValue(value); + } + + @SuppressWarnings("unchecked") // Structs and maps qualified by a select chain are String keyed. + private static Optional find(SelectableValue selectable, String fieldName) { + return (Optional) ((SelectableValue) selectable).find(fieldName); + } + + private static SelectField toSelectField(Object item) { + List qualifier = asQualifier(item, /* minSize= */ 3); + long fieldNumber = fieldNumberOf(qualifier); + String fieldName = fieldNameOf(qualifier); + int typeCode = typeCodeOf(qualifier); + Object defaultValue = defaultValueOf(qualifier); + return SelectField.create(fieldNumber, fieldName, typeCode, defaultValue); + } + + private static SelectField toPresenceField(Object item) { + List qualifier = asQualifier(item, /* minSize= */ 2); + long fieldNumber = fieldNumberOf(qualifier); + String fieldName = fieldNameOf(qualifier); + if (qualifier.size() >= 3) { + int typeCode = typeCodeOf(qualifier); + Object defaultValue = defaultValueOf(qualifier); + return SelectField.create(fieldNumber, fieldName, typeCode, defaultValue); + } + return SelectField.create(fieldNumber, fieldName); + } + + private static ImmutableList toSelectFields(List qualifierLists) { + ImmutableList.Builder builder = + ImmutableList.builderWithExpectedSize(qualifierLists.size()); + for (Object item : qualifierLists) { + builder.add(toSelectField(item)); + } + return builder.build(); + } + + private static ImmutableList toPresenceFields(List qualifierLists) { + ImmutableList.Builder builder = + ImmutableList.builderWithExpectedSize(qualifierLists.size()); + for (Object item : qualifierLists) { + builder.add(toPresenceField(item)); + } + return builder.build(); + } + + /** Validates and returns a single {@code [field_number, field_name, ...]} qualifier tuple. */ + private static List asQualifier(Object item, int minSize) { + if (!(item instanceof List)) { + throw new IllegalArgumentException("Expected qualifier list, got: " + item); + } + List qualifier = (List) item; + if (qualifier.size() < minSize + || !isInteger(qualifier.get(0)) + || !(qualifier.get(1) instanceof String) + || (minSize > 2 && !isInteger(qualifier.get(2)))) { + throw new IllegalArgumentException("Invalid qualifier format: " + qualifier); + } + return qualifier; + } + + private static boolean isInteger(Object raw) { + return raw instanceof Long + || raw instanceof Integer + || raw instanceof Short + || raw instanceof Byte; + } + + private static long fieldNumberOf(List qualifier) { + Object raw = qualifier.get(0); + if (isInteger(raw)) { + long fieldNumber = ((Number) raw).longValue(); + if (fieldNumber <= 0 || fieldNumber > SelectField.MAX_FIELD_NUMBER) { + throw new IllegalArgumentException( + "Invalid protobuf field number: " + + fieldNumber + + " (must be between 1 and " + + SelectField.MAX_FIELD_NUMBER + + ")"); + } + return fieldNumber; + } + throw new IllegalArgumentException( + "Expected integer field number in qualifier[0], got: " + raw); + } + + private static String fieldNameOf(List qualifier) { + return (String) qualifier.get(1); + } + + private static int typeCodeOf(List qualifier) { + Object raw = qualifier.get(2); + if (isInteger(raw)) { + long typeCode = ((Number) raw).longValue(); + if (typeCode != SelectField.CEL_MAP_TYPE_CODE && (typeCode < 1 || typeCode > 18)) { + throw new IllegalArgumentException("Invalid protobuf type code: " + typeCode); + } + return (int) typeCode; + } + throw new IllegalArgumentException("Expected integer type code in qualifier[2], got: " + raw); + } + + /** + * Returns the pre-resolved default value from a 4-tuple qualifier {@code [field_number, + * field_name, type_code, default_value]}, or {@code null} for 3-tuple qualifiers (submessages + * without an inlined default). + */ + private static @Nullable Object defaultValueOf(List qualifier) { + if (qualifier.size() > 3) { + return qualifier.get(3); + } + return null; + } + + private LiteAttributeStep() {} +} diff --git a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel index f898b66fe..336db4f64 100644 --- a/runtime/src/test/java/dev/cel/runtime/BUILD.bazel +++ b/runtime/src/test/java/dev/cel/runtime/BUILD.bazel @@ -40,6 +40,7 @@ java_library( "//common:options", "//common:proto_v1alpha1_ast", "//common/ast", + "//common/exceptions:attribute_not_found", "//common/exceptions:bad_format", "//common/exceptions:divide_by_zero", "//common/exceptions:numeric_overflow", @@ -57,6 +58,7 @@ java_library( "//common/types:message_type_provider", "//common/values", "//common/values:cel_byte_string", + "//common/values:proto_message_lite_value", "//common/values:proto_message_lite_value_provider", "//compiler", "//compiler:compiler_builder", @@ -76,6 +78,7 @@ java_library( "//runtime:late_function_binding", "//runtime:lite_runtime", "//runtime:lite_runtime_factory", + "//runtime:lite_runtime_impl", "//runtime:partial_vars", "//runtime:proto_message_activation_factory", "//runtime:proto_message_runtime_equality", diff --git a/runtime/src/test/java/dev/cel/runtime/LiteAttributeStepTest.java b/runtime/src/test/java/dev/cel/runtime/LiteAttributeStepTest.java new file mode 100644 index 000000000..29478b54b --- /dev/null +++ b/runtime/src/test/java/dev/cel/runtime/LiteAttributeStepTest.java @@ -0,0 +1,1346 @@ +// 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.runtime; + +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.protobuf.ByteString; +import com.google.protobuf.CodedOutputStream; +import com.google.protobuf.ExtensionRegistryLite; +import dev.cel.common.exceptions.CelAttributeNotFoundException; +import dev.cel.common.values.ErrorValue; +import dev.cel.common.values.NullValue; +import dev.cel.common.values.OptionalValue; +import dev.cel.common.values.ProtoLiteCelValueConverter; +import dev.cel.common.values.ProtoMessageLiteValue; +import dev.cel.common.values.ProtoMessageLiteValueProvider; +import dev.cel.common.values.RawProtoMessageLiteValue; +import dev.cel.common.values.SelectableValue; +import dev.cel.expr.conformance.proto3.TestAllTypes; +import dev.cel.expr.conformance.proto3.TestAllTypesCelDescriptor; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class LiteAttributeStepTest { + + private static final ProtoLiteCelValueConverter CONVERTER = + (ProtoLiteCelValueConverter) + ProtoMessageLiteValueProvider.newInstance(TestAllTypesCelDescriptor.getDescriptor()) + .protoCelValueConverter(); + + private static final class TestSelectableValue implements SelectableValue { + private final ImmutableMap values; + + @Override + public Object select(String field) { + if (values.containsKey(field)) { + return values.get(field); + } + throw new NoSuchElementException("Field not found: " + field); + } + + @Override + public Optional find(String field) { + return Optional.ofNullable(values.get(field)); + } + + private TestSelectableValue(ImmutableMap values) { + this.values = values; + } + } + + private static ProtoMessageLiteValue createProtoMessageWithUnknowns( + TestAllTypes knownMessage, byte[] unknownBytes) throws IOException { + ByteArrayOutputStream combined = new ByteArrayOutputStream(); + knownMessage.writeTo(combined); + combined.write(unknownBytes); + TestAllTypes parsed = + TestAllTypes.parseFrom(combined.toByteArray(), ExtensionRegistryLite.getEmptyRegistry()); + return ProtoMessageLiteValue.create( + parsed, "cel.expr.conformance.proto3.TestAllTypes", CONVERTER); + } + + @Test + public void qualifyAttribute_nullTarget_emptyQualifiers_returnsNull() { + Object result = LiteAttributeStep.qualifyAttribute(null, ImmutableList.of(), CONVERTER); + + assertThat(result).isNull(); + } + + @Test + public void qualifyAttribute_nonNullTarget_emptyQualifiers_returnsTarget() { + Object result = LiteAttributeStep.qualifyAttribute("target_val", ImmutableList.of(), CONVERTER); + + assertThat(result).isEqualTo("target_val"); + } + + @Test + public void qualifyAttribute_nullTarget_throwsCelAttributeNotFoundException() { + ImmutableList qualifiers = ImmutableList.of(ImmutableList.of(1, "field", 9, "default_val")); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> LiteAttributeStep.qualifyAttribute(null, qualifiers, CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("field"); + } + + @Test + public void qualifyAttribute_nullValueTarget_throwsCelAttributeNotFoundException() { + ImmutableList qualifiers = ImmutableList.of(ImmutableList.of(1, "field", 9, "default_val")); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> LiteAttributeStep.qualifyAttribute(NullValue.NULL_VALUE, qualifiers, CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("field"); + } + + @Test + public void qualifyAttribute_selectable_missingFieldWithNullDefault_throwsException() { + TestSelectableValue selectable = new TestSelectableValue(ImmutableMap.of()); + ImmutableList qualifiers = ImmutableList.of(Arrays.asList(1, "missing", 9, null)); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> LiteAttributeStep.qualifyAttribute(selectable, qualifiers, CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("missing"); + } + + @Test + public void qualifyAttribute_selectableValue_missingFieldNoDefault_throwsException() { + TestSelectableValue selectable = new TestSelectableValue(ImmutableMap.of()); + ImmutableList qualifiers = ImmutableList.of(ImmutableList.of(1, "missing", 9)); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> LiteAttributeStep.qualifyAttribute(selectable, qualifiers, CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("missing"); + } + + @Test + public void qualifyAttribute_emptyOptional_throwsUnsupportedOperationException() { + ImmutableList qualifiers = ImmutableList.of(ImmutableList.of(1, "field", 9, "default_val")); + + UnsupportedOperationException thrown = + assertThrows( + UnsupportedOperationException.class, + () -> LiteAttributeStep.qualifyAttribute(OptionalValue.EMPTY, qualifiers, CONVERTER)); + + assertThat(thrown) + .hasMessageThat() + .contains("Optional operands are not yet supported by the lite select-optimized runtime"); + } + + @Test + public void qualifyAttribute_optionalContainingNullValue_throwsUnsupportedOperationException() { + OptionalValue target = OptionalValue.create(NullValue.NULL_VALUE); + ImmutableList qualifiers = ImmutableList.of(ImmutableList.of(1, "field", 9, "default_val")); + + UnsupportedOperationException thrown = + assertThrows( + UnsupportedOperationException.class, + () -> LiteAttributeStep.qualifyAttribute(target, qualifiers, CONVERTER)); + + assertThat(thrown) + .hasMessageThat() + .contains("Optional operands are not yet supported by the lite select-optimized runtime"); + } + + @Test + public void qualifyAttribute_optionalPresent_throwsUnsupportedOperationException() { + OptionalValue target = OptionalValue.create(ImmutableMap.of("field", "present_val")); + ImmutableList qualifiers = ImmutableList.of(ImmutableList.of(1, "field", 9, "default_val")); + + UnsupportedOperationException thrown = + assertThrows( + UnsupportedOperationException.class, + () -> LiteAttributeStep.qualifyAttribute(target, qualifiers, CONVERTER)); + + assertThat(thrown) + .hasMessageThat() + .contains("Optional operands are not yet supported by the lite select-optimized runtime"); + } + + @Test + public void qualifyAttribute_protoMessageLite_renamedField_resolvesPopulatedValue() { + TestAllTypes proto = TestAllTypes.newBuilder().setSingleString("known_val").build(); + ProtoMessageLiteValue message = + ProtoMessageLiteValue.create(proto, "cel.expr.conformance.proto3.TestAllTypes", CONVERTER); + // Field 14 is "single_string" in descriptor, but qualifier carries renamed AST name. + ImmutableList qualifiers = + ImmutableList.of(ImmutableList.of(14, "renamed_single_string", 9, "default_val")); + + Object result = LiteAttributeStep.qualifyAttribute(message, qualifiers, CONVERTER); + + assertThat(result).isEqualTo("known_val"); + } + + @Test + public void qualifyAttribute_protoMessageLite_renamedIntermediateSubmessage_resolvesLeafValue() { + TestAllTypes.NestedMessage nested = TestAllTypes.NestedMessage.newBuilder().setBb(42).build(); + TestAllTypes proto = TestAllTypes.newBuilder().setSingleNestedMessage(nested).build(); + ProtoMessageLiteValue message = + ProtoMessageLiteValue.create(proto, "cel.expr.conformance.proto3.TestAllTypes", CONVERTER); + // Field 21 is "single_nested_message" in descriptor, but qualifier carries renamed AST name. + ImmutableList qualifiers = + ImmutableList.of( + ImmutableList.of(21, "renamed_single_nested_message", 11), + ImmutableList.of(1, "bb", 5, 0L)); + + Object result = LiteAttributeStep.qualifyAttribute(message, qualifiers, CONVERTER); + + assertThat(result).isEqualTo(42L); + } + + @Test + public void qualifyAttribute_protoMessageLite_knownFieldValue() { + TestAllTypes proto = TestAllTypes.newBuilder().setSingleString("known_val").build(); + ProtoMessageLiteValue message = + ProtoMessageLiteValue.create(proto, "cel.expr.conformance.proto3.TestAllTypes", CONVERTER); + + Object result = + LiteAttributeStep.qualifyAttribute( + message, + ImmutableList.of(ImmutableList.of(14, "single_string", 9, "default_val")), + CONVERTER); + + assertThat(result).isEqualTo("known_val"); + } + + @Test + public void qualifyAttribute_protoMessageLite_unknownFieldValue() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeString(999, "unknown_val"); + cos.flush(); + ProtoMessageLiteValue message = + createProtoMessageWithUnknowns(TestAllTypes.getDefaultInstance(), baos.toByteArray()); + + Object result = + LiteAttributeStep.qualifyAttribute( + message, + ImmutableList.of(ImmutableList.of(999, "unknown_field", 9, "default_val")), + CONVERTER); + + assertThat(result).isEqualTo("unknown_val"); + } + + @Test + public void qualifyAttribute_protoMessageLite_missingFieldReturnsDefault() { + ProtoMessageLiteValue message = + ProtoMessageLiteValue.create( + TestAllTypes.getDefaultInstance(), + "cel.expr.conformance.proto3.TestAllTypes", + CONVERTER); + + Object result = + LiteAttributeStep.qualifyAttribute( + message, + ImmutableList.of(ImmutableList.of(9999, "missing_field", 9, "default_val")), + CONVERTER); + + assertThat(result).isEqualTo("default_val"); + } + + @Test + public void qualifyAttribute_protoMessageLite_knownMapField() { + TestAllTypes proto = TestAllTypes.newBuilder().putMapStringString("key_1", "val_1").build(); + ProtoMessageLiteValue message = + ProtoMessageLiteValue.create(proto, "cel.expr.conformance.proto3.TestAllTypes", CONVERTER); + + Object result = + LiteAttributeStep.qualifyAttribute( + message, + ImmutableList.of(ImmutableList.of(61, "map_string_string", -1, ImmutableMap.of())), + CONVERTER); + + assertThat(result).isEqualTo(ImmutableMap.of("key_1", "val_1")); + } + + @Test + public void qualifyAttribute_protoMessageLite_unsetMapFieldReturnsDefault() { + ProtoMessageLiteValue message = + ProtoMessageLiteValue.create( + TestAllTypes.getDefaultInstance(), + "cel.expr.conformance.proto3.TestAllTypes", + CONVERTER); + + Object result = + LiteAttributeStep.qualifyAttribute( + message, + ImmutableList.of(ImmutableList.of(61, "map_string_string", -1, ImmutableMap.of())), + CONVERTER); + + assertThat((Map) result).isEmpty(); + } + + @Test + public void qualifyAttribute_proto2CustomDefault_takesPrecedenceOverTypeDefault() { + ProtoMessageLiteValue message = + ProtoMessageLiteValue.create( + TestAllTypes.getDefaultInstance(), + "cel.expr.conformance.proto3.TestAllTypes", + CONVERTER); + + Object result = + LiteAttributeStep.qualifyAttribute( + message, ImmutableList.of(ImmutableList.of(1, "single_int32", 5, -32L)), CONVERTER); + + assertThat(result).isEqualTo(-32L); + } + + @Test + public void qualifyAttribute_protoMessageLite_knownUnsetSubmessage_returnsDefaultInstance() { + ProtoMessageLiteValue message = + ProtoMessageLiteValue.create( + TestAllTypes.getDefaultInstance(), + "cel.expr.conformance.proto3.TestAllTypes", + CONVERTER); + + Object result = + LiteAttributeStep.qualifyAttribute( + message, + ImmutableList.of(ImmutableList.of(21, "single_nested_message", 11)), + CONVERTER); + + assertThat(result).isEqualTo(TestAllTypes.NestedMessage.getDefaultInstance()); + } + + @Test + public void qualifyAttribute_protoMessageLite_unknownSubmessage_hasSentinelProtoTypeName() + throws Exception { + ByteArrayOutputStream subBaos = new ByteArrayOutputStream(); + CodedOutputStream subCos = CodedOutputStream.newInstance(subBaos); + subCos.writeInt32(1, 55); + subCos.flush(); + ByteArrayOutputStream rootBaos = new ByteArrayOutputStream(); + CodedOutputStream rootCos = CodedOutputStream.newInstance(rootBaos); + rootCos.writeBytes(999, ByteString.copyFrom(subBaos.toByteArray())); + rootCos.flush(); + ProtoMessageLiteValue message = + createProtoMessageWithUnknowns(TestAllTypes.getDefaultInstance(), rootBaos.toByteArray()); + + Object result = + LiteAttributeStep.qualifyAttribute( + message, ImmutableList.of(ImmutableList.of(999, "unknown_submessage", 11)), CONVERTER); + + assertThat(result).isInstanceOf(RawProtoMessageLiteValue.class); + RawProtoMessageLiteValue nested = (RawProtoMessageLiteValue) result; + assertThat(nested.celType().name()).isEqualTo("cel.@unknownMessage"); + } + + @Test + public void qualifyAttribute_rawProtoMessageLite_unknownFieldValue() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeString(10, "raw_val"); + cos.flush(); + RawProtoMessageLiteValue rawMessage = + RawProtoMessageLiteValue.create(ByteString.copyFrom(baos.toByteArray())); + + Object result = + LiteAttributeStep.qualifyAttribute( + rawMessage, + ImmutableList.of(ImmutableList.of(10, "raw_field", 9, "default_val")), + CONVERTER); + + assertThat(result).isEqualTo("raw_val"); + } + + @Test + public void qualifyAttribute_rawProtoMessageLite_repeatedField() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeInt32(31, 10); + cos.writeInt32(31, 20); + cos.flush(); + RawProtoMessageLiteValue rawMessage = + RawProtoMessageLiteValue.create(ByteString.copyFrom(baos.toByteArray())); + + Object result = + LiteAttributeStep.qualifyAttribute( + rawMessage, + ImmutableList.of(ImmutableList.of(31, "repeated_int32", 5, ImmutableList.of())), + CONVERTER); + + assertThat((Iterable) result).containsExactly(10L, 20L).inOrder(); + } + + @Test + public void qualifyAttribute_rawProtoMessageLite_missingFieldReturnsDefault() { + RawProtoMessageLiteValue rawMessage = RawProtoMessageLiteValue.create(ByteString.EMPTY); + + Object result = + LiteAttributeStep.qualifyAttribute( + rawMessage, + ImmutableList.of(ImmutableList.of(99, "missing_field", 9, "default_val")), + CONVERTER); + + assertThat(result).isEqualTo("default_val"); + } + + @Test + public void qualifyAttribute_rawProto_missingFieldNoDefault_throwsException() { + RawProtoMessageLiteValue rawMessage = RawProtoMessageLiteValue.create(ByteString.EMPTY); + ImmutableList qualifiers = ImmutableList.of(ImmutableList.of(99, "missing_field", 9)); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> LiteAttributeStep.qualifyAttribute(rawMessage, qualifiers, CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("missing_field"); + } + + @Test + public void qualifyAttribute_protoMessageLite_unknownMapField_throwsUnsupportedOperation() + throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeString(999, "unknown_map_entry"); + cos.flush(); + ProtoMessageLiteValue message = + createProtoMessageWithUnknowns(TestAllTypes.getDefaultInstance(), baos.toByteArray()); + ImmutableList qualifiers = + ImmutableList.of(ImmutableList.of(999, "unknown_map", -1, ImmutableMap.of())); + + UnsupportedOperationException thrown = + assertThrows( + UnsupportedOperationException.class, + () -> LiteAttributeStep.qualifyAttribute(message, qualifiers, CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("unknown_map"); + } + + @Test + public void qualifyAttribute_rawProtoMessageLite_unknownMapField_throwsUnsupportedOperation() + throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeString(10, "unknown_map_entry"); + cos.flush(); + RawProtoMessageLiteValue rawMessage = + RawProtoMessageLiteValue.create(ByteString.copyFrom(baos.toByteArray())); + ImmutableList qualifiers = + ImmutableList.of(ImmutableList.of(10, "unknown_map", -1, ImmutableMap.of())); + + UnsupportedOperationException thrown = + assertThrows( + UnsupportedOperationException.class, + () -> LiteAttributeStep.qualifyAttribute(rawMessage, qualifiers, CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("unknown_map"); + } + + @Test + public void qualifyAttribute_selectableValue_present() { + TestSelectableValue selectable = + new TestSelectableValue(ImmutableMap.of("field", "selectable_val")); + + Object result = + LiteAttributeStep.qualifyAttribute( + selectable, + ImmutableList.of(ImmutableList.of(1, "field", 9, "default_val")), + CONVERTER); + + assertThat(result).isEqualTo("selectable_val"); + } + + @Test + public void qualifyAttribute_selectableValue_absentReturnsDefault() { + TestSelectableValue selectable = new TestSelectableValue(ImmutableMap.of()); + + Object result = + LiteAttributeStep.qualifyAttribute( + selectable, + ImmutableList.of(ImmutableList.of(1, "missing", 9, "default_val")), + CONVERTER); + + assertThat(result).isEqualTo("default_val"); + } + + @Test + public void qualifyAttribute_map_present() { + ImmutableMap map = ImmutableMap.of("key", "map_val"); + + Object result = + LiteAttributeStep.qualifyAttribute( + map, ImmutableList.of(ImmutableList.of(1, "key", 9, "default_val")), CONVERTER); + + assertThat(result).isEqualTo("map_val"); + } + + @Test + public void qualifyAttribute_map_nullValueReturnsNullValue() { + ImmutableMap map = ImmutableMap.of("key", NullValue.NULL_VALUE); + + Object result = + LiteAttributeStep.qualifyAttribute( + map, ImmutableList.of(ImmutableList.of(1, "key", 9, "default_val")), CONVERTER); + + assertThat(result).isEqualTo(NullValue.NULL_VALUE); + } + + @Test + public void qualifyAttribute_map_missingKeyThrowsException() { + ImmutableMap map = ImmutableMap.of(); + ImmutableList qualifiers = + ImmutableList.of(ImmutableList.of(1, "missing", 9, "default_val")); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> LiteAttributeStep.qualifyAttribute(map, qualifiers, CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("missing"); + } + + @Test + public void qualifyAttribute_unsupportedTargetThrowsException() { + int unsupportedTarget = 12345; + ImmutableList qualifiers = ImmutableList.of(ImmutableList.of(1, "field", 9, "default_val")); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> LiteAttributeStep.qualifyAttribute(unsupportedTarget, qualifiers, CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("field"); + } + + @Test + public void qualifyAttribute_invalidQualifierElementThrowsException() { + ImmutableMap target = ImmutableMap.of("field", "val"); + ImmutableList invalidQualifiers = ImmutableList.of("invalid_non_list_qualifier"); + + IllegalArgumentException thrown = + assertThrows( + IllegalArgumentException.class, + () -> LiteAttributeStep.qualifyAttribute(target, invalidQualifiers, CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("Expected qualifier list"); + } + + @Test + public void qualifyAttribute_malformedQualifierFormatThrowsException() { + ImmutableMap target = ImmutableMap.of("field", "val"); + ImmutableList malformedQualifiers = ImmutableList.of(ImmutableList.of(1, "field")); + + IllegalArgumentException thrown = + assertThrows( + IllegalArgumentException.class, + () -> LiteAttributeStep.qualifyAttribute(target, malformedQualifiers, CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("Invalid qualifier format"); + } + + @Test + public void qualifyAttribute_unsupportedTargetType_throwsCelAttributeNotFoundException() { + Object target = new Object(); + ImmutableList qualifiers = + ImmutableList.of(ImmutableList.of(1, "field", 9, NullValue.NULL_VALUE)); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> LiteAttributeStep.qualifyAttribute(target, qualifiers, CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("field"); + } + + @Test + public void hasField_unsupportedTargetType_throwsCelAttributeNotFoundException() { + Object target = new Object(); + ImmutableList qualifiers = ImmutableList.of(ImmutableList.of(1, "field")); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> LiteAttributeStep.hasField(target, qualifiers, CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("field"); + } + + @Test + public void qualifyAttribute_multiStepChaining() throws Exception { + ByteArrayOutputStream subBaos = new ByteArrayOutputStream(); + CodedOutputStream subCos = CodedOutputStream.newInstance(subBaos); + subCos.writeString(20, "nested_val"); + subCos.flush(); + ByteString subBytes = ByteString.copyFrom(subBaos.toByteArray()); + ByteArrayOutputStream rootBaos = new ByteArrayOutputStream(); + CodedOutputStream rootCos = CodedOutputStream.newInstance(rootBaos); + rootCos.writeBytes(999, subBytes); + rootCos.flush(); + ProtoMessageLiteValue rootMessage = + createProtoMessageWithUnknowns(TestAllTypes.getDefaultInstance(), rootBaos.toByteArray()); + + Object result = + LiteAttributeStep.qualifyAttribute( + rootMessage, + ImmutableList.of( + ImmutableList.of(999, "unknown_submessage", 11, NullValue.NULL_VALUE), + ImmutableList.of(20, "nested_field", 9, "default")), + CONVERTER); + + assertThat(result).isEqualTo("nested_val"); + } + + @Test + public void hasField_nullTarget_throwsCelAttributeNotFoundException() { + ImmutableList qualifiers = ImmutableList.of(ImmutableList.of(1, "field")); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> LiteAttributeStep.hasField(null, qualifiers, CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("field"); + } + + @Test + public void hasField_nullValueTarget_throwsCelAttributeNotFoundException() { + ImmutableList qualifiers = ImmutableList.of(ImmutableList.of(1, "field")); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> LiteAttributeStep.hasField(NullValue.NULL_VALUE, qualifiers, CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("field"); + } + + @Test + public void hasField_emptyOptional_throwsUnsupportedOperationException() { + ImmutableList qualifiers = ImmutableList.of(ImmutableList.of(1, "field")); + + UnsupportedOperationException thrown = + assertThrows( + UnsupportedOperationException.class, + () -> LiteAttributeStep.hasField(OptionalValue.EMPTY, qualifiers, CONVERTER)); + + assertThat(thrown) + .hasMessageThat() + .contains("Optional operands are not yet supported by the lite select-optimized runtime"); + } + + @Test + public void hasField_optionalContainingNullValue_throwsUnsupportedOperationException() { + OptionalValue target = OptionalValue.create(NullValue.NULL_VALUE); + ImmutableList qualifiers = ImmutableList.of(ImmutableList.of(1, "field")); + + UnsupportedOperationException thrown = + assertThrows( + UnsupportedOperationException.class, + () -> LiteAttributeStep.hasField(target, qualifiers, CONVERTER)); + + assertThat(thrown) + .hasMessageThat() + .contains("Optional operands are not yet supported by the lite select-optimized runtime"); + } + + @Test + public void hasField_optionalPresent_throwsUnsupportedOperationException() { + OptionalValue target = OptionalValue.create(ImmutableMap.of("field", "val")); + ImmutableList qualifiers = ImmutableList.of(ImmutableList.of(1, "field")); + + UnsupportedOperationException thrown = + assertThrows( + UnsupportedOperationException.class, + () -> LiteAttributeStep.hasField(target, qualifiers, CONVERTER)); + + assertThat(thrown) + .hasMessageThat() + .contains("Optional operands are not yet supported by the lite select-optimized runtime"); + } + + @Test + public void hasField_protoMessageLite_renamedField_returnsTrue() { + TestAllTypes proto = TestAllTypes.newBuilder().setSingleString("val").build(); + ProtoMessageLiteValue message = + ProtoMessageLiteValue.create(proto, "cel.expr.conformance.proto3.TestAllTypes", CONVERTER); + // Field 14 is "single_string" in descriptor, but qualifier carries renamed AST name. + ImmutableList qualifiers = ImmutableList.of(ImmutableList.of(14, "renamed_single_string")); + + Object result = LiteAttributeStep.hasField(message, qualifiers, CONVERTER); + + assertThat(result).isEqualTo(true); + } + + @Test + public void hasField_protoMessageLite_renamedIntermediateSubmessage_returnsTrue() { + TestAllTypes.NestedMessage nested = TestAllTypes.NestedMessage.newBuilder().setBb(42).build(); + TestAllTypes proto = TestAllTypes.newBuilder().setSingleNestedMessage(nested).build(); + ProtoMessageLiteValue message = + ProtoMessageLiteValue.create(proto, "cel.expr.conformance.proto3.TestAllTypes", CONVERTER); + // Field 21 is "single_nested_message" in descriptor, but qualifier carries renamed AST name. + ImmutableList qualifiers = + ImmutableList.of( + ImmutableList.of(21, "renamed_single_nested_message"), ImmutableList.of(1, "bb")); + + Object result = LiteAttributeStep.hasField(message, qualifiers, CONVERTER); + + assertThat(result).isEqualTo(true); + } + + @Test + public void hasField_protoMessageLite_knownFieldReturnsTrue() { + TestAllTypes proto = TestAllTypes.newBuilder().setSingleString("val").build(); + ProtoMessageLiteValue message = + ProtoMessageLiteValue.create(proto, "cel.expr.conformance.proto3.TestAllTypes", CONVERTER); + + Object result = + LiteAttributeStep.hasField( + message, ImmutableList.of(ImmutableList.of(14, "single_string")), CONVERTER); + + assertThat(result).isEqualTo(true); + } + + @Test + public void hasField_protoMessageLite_unknownFieldReturnsTrue() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeInt64(999, 12345L); + cos.flush(); + ProtoMessageLiteValue message = + createProtoMessageWithUnknowns(TestAllTypes.getDefaultInstance(), baos.toByteArray()); + + Object result = + LiteAttributeStep.hasField( + message, ImmutableList.of(ImmutableList.of(999, "unknown_field")), CONVERTER); + + assertThat(result).isEqualTo(true); + } + + @Test + public void hasField_protoMessageLite_absentReturnsFalse() { + ProtoMessageLiteValue message = + ProtoMessageLiteValue.create( + TestAllTypes.getDefaultInstance(), + "cel.expr.conformance.proto3.TestAllTypes", + CONVERTER); + + Object result = + LiteAttributeStep.hasField( + message, ImmutableList.of(ImmutableList.of(9999, "missing_field")), CONVERTER); + + assertThat(result).isEqualTo(false); + } + + @Test + public void hasField_protoMessageLite_emptyRepeatedFieldReturnsFalse() { + ProtoMessageLiteValue message = + ProtoMessageLiteValue.create( + TestAllTypes.getDefaultInstance(), + "cel.expr.conformance.proto3.TestAllTypes", + CONVERTER); + ImmutableList qualifiers = ImmutableList.of(ImmutableList.of(44, "repeated_string")); + + Object result = LiteAttributeStep.hasField(message, qualifiers, CONVERTER); + + assertThat(result).isEqualTo(false); + } + + @Test + public void hasField_protoMessageLite_populatedRepeatedFieldReturnsTrue() { + TestAllTypes proto = TestAllTypes.newBuilder().addRepeatedString("val").build(); + ProtoMessageLiteValue message = + ProtoMessageLiteValue.create(proto, "cel.expr.conformance.proto3.TestAllTypes", CONVERTER); + ImmutableList qualifiers = ImmutableList.of(ImmutableList.of(44, "repeated_string")); + + Object result = LiteAttributeStep.hasField(message, qualifiers, CONVERTER); + + assertThat(result).isEqualTo(true); + } + + @Test + public void hasField_protoMessageLite_emptyMapFieldReturnsFalse() { + ProtoMessageLiteValue message = + ProtoMessageLiteValue.create( + TestAllTypes.getDefaultInstance(), + "cel.expr.conformance.proto3.TestAllTypes", + CONVERTER); + ImmutableList qualifiers = ImmutableList.of(ImmutableList.of(64, "map_bool_string")); + + Object result = LiteAttributeStep.hasField(message, qualifiers, CONVERTER); + + assertThat(result).isEqualTo(false); + } + + @Test + public void hasField_protoMessageLite_populatedMapFieldReturnsTrue() { + TestAllTypes proto = TestAllTypes.newBuilder().putMapBoolString(true, "val").build(); + ProtoMessageLiteValue message = + ProtoMessageLiteValue.create(proto, "cel.expr.conformance.proto3.TestAllTypes", CONVERTER); + ImmutableList qualifiers = ImmutableList.of(ImmutableList.of(64, "map_bool_string")); + + Object result = LiteAttributeStep.hasField(message, qualifiers, CONVERTER); + + assertThat(result).isEqualTo(true); + } + + @Test + public void hasField_rawProtoMessageLite_unknownFieldReturnsTrue() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeInt64(10, 42L); + cos.flush(); + RawProtoMessageLiteValue rawMessage = + RawProtoMessageLiteValue.create(ByteString.copyFrom(baos.toByteArray())); + + Object result = + LiteAttributeStep.hasField( + rawMessage, ImmutableList.of(ImmutableList.of(10, "field")), CONVERTER); + + assertThat(result).isEqualTo(true); + } + + @Test + public void hasField_rawProtoMessageLite_absentReturnsFalse() { + RawProtoMessageLiteValue rawMessage = RawProtoMessageLiteValue.create(ByteString.EMPTY); + + Object result = + LiteAttributeStep.hasField( + rawMessage, ImmutableList.of(ImmutableList.of(99, "missing_field")), CONVERTER); + + assertThat(result).isEqualTo(false); + } + + @Test + public void hasField_selectableValue_presentReturnsTrue() { + TestSelectableValue selectable = new TestSelectableValue(ImmutableMap.of("field", "val")); + + Object result = + LiteAttributeStep.hasField( + selectable, ImmutableList.of(ImmutableList.of(1, "field")), CONVERTER); + + assertThat(result).isEqualTo(true); + } + + @Test + public void hasField_selectableValue_absentReturnsFalse() { + TestSelectableValue selectable = new TestSelectableValue(ImmutableMap.of()); + + Object result = + LiteAttributeStep.hasField( + selectable, ImmutableList.of(ImmutableList.of(1, "field")), CONVERTER); + + assertThat(result).isEqualTo(false); + } + + @Test + public void hasField_map_presentReturnsTrue() { + ImmutableMap map = ImmutableMap.of("key", "val"); + + Object result = + LiteAttributeStep.hasField(map, ImmutableList.of(ImmutableList.of(1, "key")), CONVERTER); + + assertThat(result).isEqualTo(true); + } + + @Test + public void hasField_map_absentReturnsFalse() { + ImmutableMap map = ImmutableMap.of(); + + Object result = + LiteAttributeStep.hasField( + map, ImmutableList.of(ImmutableList.of(1, "missing")), CONVERTER); + + assertThat(result).isEqualTo(false); + } + + @Test + public void hasField_protoMessageLite_knownMapFieldPresent_returnsTrue() { + TestAllTypes proto = TestAllTypes.newBuilder().putMapStringString("key_1", "val_1").build(); + ProtoMessageLiteValue message = + ProtoMessageLiteValue.create(proto, "cel.expr.conformance.proto3.TestAllTypes", CONVERTER); + + Object result = + LiteAttributeStep.hasField( + message, ImmutableList.of(ImmutableList.of(61, "map_string_string")), CONVERTER); + + assertThat(result).isEqualTo(true); + } + + @Test + public void hasField_protoMessageLite_unsetMapField_returnsFalse() { + ProtoMessageLiteValue message = + ProtoMessageLiteValue.create( + TestAllTypes.getDefaultInstance(), + "cel.expr.conformance.proto3.TestAllTypes", + CONVERTER); + + Object result = + LiteAttributeStep.hasField( + message, ImmutableList.of(ImmutableList.of(61, "map_string_string")), CONVERTER); + + assertThat(result).isEqualTo(false); + } + + @Test + public void hasField_unsupportedTarget_throwsException() { + String unsupportedTarget = "unsupported_string"; + ImmutableList qualifiers = ImmutableList.of(ImmutableList.of(1, "field")); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> LiteAttributeStep.hasField(unsupportedTarget, qualifiers, CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("field"); + } + + @Test + public void hasField_intermediateUnsupportedTarget_throwsException() { + String unsupportedTarget = "unsupported_string"; + ImmutableList qualifiers = + ImmutableList.of(ImmutableList.of(1, "submessage"), ImmutableList.of(2, "field")); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> LiteAttributeStep.hasField(unsupportedTarget, qualifiers, CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("submessage"); + } + + @Test + public void hasField_invalidQualifierThrowsException() { + ImmutableMap target = ImmutableMap.of("field", "val"); + ImmutableList invalidQualifiers = ImmutableList.of("invalid_non_list_qualifier"); + + IllegalArgumentException thrown = + assertThrows( + IllegalArgumentException.class, + () -> LiteAttributeStep.hasField(target, invalidQualifiers, CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("Expected qualifier list"); + } + + @Test + public void hasField_malformedQualifierFormatThrowsException() { + ImmutableMap target = ImmutableMap.of("field", "val"); + ImmutableList malformedQualifiers = ImmutableList.of(ImmutableList.of(1)); + + IllegalArgumentException thrown = + assertThrows( + IllegalArgumentException.class, + () -> LiteAttributeStep.hasField(target, malformedQualifiers, CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("Invalid qualifier format"); + } + + @Test + public void hasField_emptyQualifiersReturnsFalse() { + ImmutableMap target = ImmutableMap.of("field", "val"); + + Object result = LiteAttributeStep.hasField(target, ImmutableList.of(), CONVERTER); + + assertThat(result).isEqualTo(false); + } + + @Test + public void hasField_multiStepChaining_intermediateRawProto_present() throws Exception { + ByteArrayOutputStream leafBaos = new ByteArrayOutputStream(); + CodedOutputStream leafCos = CodedOutputStream.newInstance(leafBaos); + leafCos.writeInt64(20, 100L); + leafCos.flush(); + ByteString leafBytes = ByteString.copyFrom(leafBaos.toByteArray()); + ByteArrayOutputStream childBaos = new ByteArrayOutputStream(); + CodedOutputStream childCos = CodedOutputStream.newInstance(childBaos); + childCos.writeBytes(15, leafBytes); + childCos.flush(); + ByteString childBytes = ByteString.copyFrom(childBaos.toByteArray()); + ByteArrayOutputStream parentBaos = new ByteArrayOutputStream(); + CodedOutputStream parentCos = CodedOutputStream.newInstance(parentBaos); + parentCos.writeBytes(10, childBytes); + parentCos.flush(); + RawProtoMessageLiteValue parent = + RawProtoMessageLiteValue.create(ByteString.copyFrom(parentBaos.toByteArray())); + + Object result = + LiteAttributeStep.hasField( + parent, + ImmutableList.of( + ImmutableList.of(10, "child"), + ImmutableList.of(15, "leaf"), + ImmutableList.of(20, "val_field")), + CONVERTER); + + assertThat(result).isEqualTo(true); + } + + @Test + public void hasField_multiStepChaining_intermediateRawProto_absent() throws Exception { + ByteArrayOutputStream childBaos = new ByteArrayOutputStream(); + CodedOutputStream childCos = CodedOutputStream.newInstance(childBaos); + childCos.writeString(99, "other"); + childCos.flush(); + ByteString childBytes = ByteString.copyFrom(childBaos.toByteArray()); + ByteArrayOutputStream parentBaos = new ByteArrayOutputStream(); + CodedOutputStream parentCos = CodedOutputStream.newInstance(parentBaos); + parentCos.writeBytes(10, childBytes); + parentCos.flush(); + RawProtoMessageLiteValue parent = + RawProtoMessageLiteValue.create(ByteString.copyFrom(parentBaos.toByteArray())); + + Object result = + LiteAttributeStep.hasField( + parent, + ImmutableList.of( + ImmutableList.of(10, "child"), + ImmutableList.of(15, "missing_leaf"), + ImmutableList.of(20, "val_field")), + CONVERTER); + + assertThat(result).isEqualTo(false); + } + + @Test + public void hasField_multiStepChaining_intermediateProtoMessageLite_absent() { + ProtoMessageLiteValue rootMessage = + ProtoMessageLiteValue.create( + TestAllTypes.getDefaultInstance(), + "cel.expr.conformance.proto3.TestAllTypes", + CONVERTER); + + Object result = + LiteAttributeStep.hasField( + rootMessage, + ImmutableList.of( + ImmutableList.of(9999, "missing_sub_message"), ImmutableList.of(20, "field")), + CONVERTER); + + assertThat(result).isEqualTo(false); + } + + @Test + public void hasField_multiStepChaining_intermediateSelectableValue_present() { + TestSelectableValue child = new TestSelectableValue(ImmutableMap.of("leaf", "val")); + TestSelectableValue parent = new TestSelectableValue(ImmutableMap.of("child", child)); + + Object result = + LiteAttributeStep.hasField( + parent, + ImmutableList.of(ImmutableList.of(1, "child"), ImmutableList.of(2, "leaf")), + CONVERTER); + + assertThat(result).isEqualTo(true); + } + + @Test + public void hasField_multiStepChaining_intermediateSelectableValue_absent() { + TestSelectableValue parent = new TestSelectableValue(ImmutableMap.of()); + + Object result = + LiteAttributeStep.hasField( + parent, + ImmutableList.of(ImmutableList.of(1, "missing_child"), ImmutableList.of(2, "leaf")), + CONVERTER); + + assertThat(result).isEqualTo(false); + } + + @Test + public void hasField_multiStepChaining_intermediateMap_present() { + ImmutableMap child = ImmutableMap.of("leaf", "val"); + ImmutableMap parent = ImmutableMap.of("child", child); + + Object result = + LiteAttributeStep.hasField( + parent, + ImmutableList.of(ImmutableList.of(1, "child"), ImmutableList.of(2, "leaf")), + CONVERTER); + + assertThat(result).isEqualTo(true); + } + + @Test + public void hasField_multiStepChaining_intermediateMap_absent() { + ImmutableMap parent = ImmutableMap.of(); + + Object result = + LiteAttributeStep.hasField( + parent, + ImmutableList.of(ImmutableList.of(1, "missing_child"), ImmutableList.of(2, "leaf")), + CONVERTER); + + assertThat(result).isEqualTo(false); + } + + @Test + public void qualifyAttribute_rawProto_withKnownTypeName_resolvesChildType() throws Exception { + ByteArrayOutputStream subBaos = new ByteArrayOutputStream(); + CodedOutputStream subCos = CodedOutputStream.newInstance(subBaos); + subCos.writeInt32(1, 42); + subCos.flush(); + ByteArrayOutputStream rootBaos = new ByteArrayOutputStream(); + CodedOutputStream rootCos = CodedOutputStream.newInstance(rootBaos); + rootCos.writeBytes(21, ByteString.copyFrom(subBaos.toByteArray())); + rootCos.flush(); + RawProtoMessageLiteValue rawParent = + RawProtoMessageLiteValue.create( + ByteString.copyFrom(rootBaos.toByteArray()), + "cel.expr.conformance.proto3.TestAllTypes", + CONVERTER); + + Object result = + LiteAttributeStep.qualifyAttribute( + rawParent, + ImmutableList.of(ImmutableList.of(21, "single_nested_message", 11)), + CONVERTER); + + assertThat(result).isInstanceOf(RawProtoMessageLiteValue.class); + RawProtoMessageLiteValue child = (RawProtoMessageLiteValue) result; + assertThat(child.celType().name()) + .isEqualTo("cel.expr.conformance.proto3.TestAllTypes.NestedMessage"); + } + + @Test + public void qualifyAttribute_celUnknownSetTarget_propagatesUnknown() { + CelUnknownSet unknown = CelUnknownSet.create(1L); + ImmutableList qualifiers = ImmutableList.of(ImmutableList.of(1, "field", 9, "default")); + + Object result = LiteAttributeStep.qualifyAttribute(unknown, qualifiers, CONVERTER); + + assertThat(result).isSameInstanceAs(unknown); + } + + @Test + public void qualifyAttribute_errorValueTarget_propagatesError() { + ErrorValue error = ErrorValue.create(1L, new RuntimeException("test error")); + ImmutableList qualifiers = ImmutableList.of(ImmutableList.of(1, "field", 9, "default")); + + Object result = LiteAttributeStep.qualifyAttribute(error, qualifiers, CONVERTER); + + assertThat(result).isSameInstanceAs(error); + } + + @Test + public void qualifyAttribute_floatingPointFieldNumber_throwsIllegalArgumentException() { + ImmutableList qualifiers = ImmutableList.of(ImmutableList.of(1.5, "field", 9, "default")); + + IllegalArgumentException thrown = + assertThrows( + IllegalArgumentException.class, + () -> LiteAttributeStep.qualifyAttribute(ImmutableMap.of(), qualifiers, CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("Invalid qualifier format"); + } + + @Test + public void hasField_intermediateMapWithNullValue_throwsCelAttributeNotFoundException() { + ImmutableMap map = ImmutableMap.of("sub", NullValue.NULL_VALUE); + ImmutableList qualifiers = + ImmutableList.of(ImmutableList.of(1, "sub"), ImmutableList.of(2, "leaf")); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> LiteAttributeStep.hasField(map, qualifiers, CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("leaf"); + } + + @Test + public void hasField_celUnknownSetTarget_propagatesUnknown() { + CelUnknownSet unknown = CelUnknownSet.create(1L); + ImmutableList qualifiers = ImmutableList.of(ImmutableList.of(1, "field")); + + Object result = LiteAttributeStep.hasField(unknown, qualifiers, CONVERTER); + + assertThat(result).isSameInstanceAs(unknown); + } + + @Test + public void hasField_errorValueTarget_propagatesError() { + ErrorValue error = ErrorValue.create(1L, new RuntimeException("test error")); + ImmutableList qualifiers = ImmutableList.of(ImmutableList.of(1, "field")); + + Object result = LiteAttributeStep.hasField(error, qualifiers, CONVERTER); + + assertThat(result).isSameInstanceAs(error); + } + + @Test + public void qualifyAttribute_zeroFieldNumber_throwsIllegalArgumentException() { + ImmutableList qualifiers = ImmutableList.of(ImmutableList.of(0, "field", 9, "default")); + + IllegalArgumentException thrown = + assertThrows( + IllegalArgumentException.class, + () -> LiteAttributeStep.qualifyAttribute(ImmutableMap.of(), qualifiers, CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("Invalid protobuf field number"); + } + + @Test + public void qualifyAttribute_outOfRangeFieldNumber_throwsIllegalArgumentException() { + ImmutableList qualifiers = + ImmutableList.of(ImmutableList.of(536870912L, "field", 9, "default")); + + IllegalArgumentException thrown = + assertThrows( + IllegalArgumentException.class, + () -> LiteAttributeStep.qualifyAttribute(ImmutableMap.of(), qualifiers, CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("Invalid protobuf field number"); + } + + @Test + public void hasField_zeroFieldNumber_throwsIllegalArgumentException() { + ImmutableList qualifiers = ImmutableList.of(ImmutableList.of(0, "field")); + + IllegalArgumentException thrown = + assertThrows( + IllegalArgumentException.class, + () -> LiteAttributeStep.hasField(ImmutableMap.of(), qualifiers, CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("Invalid protobuf field number"); + } + + @Test + public void hasField_outOfRangeFieldNumber_throwsIllegalArgumentException() { + ImmutableList qualifiers = ImmutableList.of(ImmutableList.of(536870912L, "field")); + + IllegalArgumentException thrown = + assertThrows( + IllegalArgumentException.class, + () -> LiteAttributeStep.hasField(ImmutableMap.of(), qualifiers, CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("Invalid protobuf field number"); + } + + @Test + public void qualifyAttribute_mapContainingProtoMessage_resolvesField() { + TestAllTypes proto = TestAllTypes.newBuilder().setSingleInt64(42L).build(); + ImmutableMap map = ImmutableMap.of("proto", proto); + ImmutableList qualifiers = + ImmutableList.of( + ImmutableList.of(1, "proto", 11), ImmutableList.of(2, "single_int64", 3, 0L)); + + Object result = LiteAttributeStep.qualifyAttribute(map, qualifiers, CONVERTER); + + assertThat(result).isEqualTo(42L); + } + + @Test + public void hasField_mapContainingProtoMessage_resolvesPresence() { + TestAllTypes proto = TestAllTypes.newBuilder().setSingleInt64(42L).build(); + ImmutableMap map = ImmutableMap.of("proto", proto); + ImmutableList presentQualifiers = + ImmutableList.of(ImmutableList.of(1, "proto"), ImmutableList.of(2, "single_int64")); + ImmutableList absentQualifiers = + ImmutableList.of(ImmutableList.of(1, "proto"), ImmutableList.of(14, "single_string")); + + Object presentResult = LiteAttributeStep.hasField(map, presentQualifiers, CONVERTER); + Object absentResult = LiteAttributeStep.hasField(map, absentQualifiers, CONVERTER); + + assertThat(presentResult).isEqualTo(true); + assertThat(absentResult).isEqualTo(false); + } + + @Test + public void qualifyAttribute_protoMessageContainingMap_resolvesMapKey() { + TestAllTypes proto = TestAllTypes.newBuilder().putMapStringString("k", "v").build(); + ImmutableList qualifiers = + ImmutableList.of( + ImmutableList.of(61, "map_string_string", -1, ImmutableMap.of()), + ImmutableList.of(1, "k", 9, "")); + + Object result = LiteAttributeStep.qualifyAttribute(proto, qualifiers, CONVERTER); + + assertThat(result).isEqualTo("v"); + } + + @Test + public void hasField_protoMessageContainingMap_resolvesPresence() { + TestAllTypes proto = TestAllTypes.newBuilder().putMapStringString("k", "v").build(); + ImmutableList presentQualifiers = + ImmutableList.of(ImmutableList.of(61, "map_string_string"), ImmutableList.of(1, "k")); + ImmutableList absentQualifiers = + ImmutableList.of(ImmutableList.of(61, "map_string_string"), ImmutableList.of(1, "missing")); + + Object presentResult = LiteAttributeStep.hasField(proto, presentQualifiers, CONVERTER); + Object absentResult = LiteAttributeStep.hasField(proto, absentQualifiers, CONVERTER); + + assertThat(presentResult).isEqualTo(true); + assertThat(absentResult).isEqualTo(false); + } + + @Test + public void hasField_emptyPackedRepeated_withoutDescriptor_returnsFalse() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeBytes(31, ByteString.EMPTY); + cos.flush(); + RawProtoMessageLiteValue raw = + RawProtoMessageLiteValue.create( + ByteString.copyFrom(baos.toByteArray()), "cel.expr.conformance.proto3.TestAllTypes"); + ImmutableList qualifiers = + ImmutableList.of(ImmutableList.of(31, "repeated_int32", 5, ImmutableList.of())); + + Object result = + LiteAttributeStep.hasField(raw, qualifiers, ProtoLiteCelValueConverter.newInstance()); + + assertThat(result).isEqualTo(false); + } + + @Test + public void hasField_intermediateMapWithJavaNullValue_throwsCelAttributeNotFoundException() { + Map map = Collections.singletonMap("sub", null); + ImmutableList qualifiers = + ImmutableList.of(ImmutableList.of(1, "sub"), ImmutableList.of(2, "leaf")); + + CelAttributeNotFoundException thrown = + assertThrows( + CelAttributeNotFoundException.class, + () -> LiteAttributeStep.hasField(map, qualifiers, CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("leaf"); + } + + @Test + public void qualifyAttribute_typeCodeOutOfIntRange_throwsIllegalArgumentException() { + ImmutableList qualifiers = + ImmutableList.of(ImmutableList.of(1, "field", (1L << 32) + 9, "default")); + + IllegalArgumentException thrown = + assertThrows( + IllegalArgumentException.class, + () -> LiteAttributeStep.qualifyAttribute(ImmutableMap.of(), qualifiers, CONVERTER)); + + assertThat(thrown).hasMessageThat().contains("Invalid protobuf type code"); + } +}