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..c39eaaa73 100644 --- a/common/src/main/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/values/BUILD.bazel @@ -317,6 +317,7 @@ java_library( srcs = [ "ProtoLiteCelValueConverter.java", "ProtoMessageLiteValue.java", + "RawProtoMessageLiteValue.java", ], tags = [ ], @@ -325,6 +326,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 +335,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 +345,7 @@ cel_android_library( srcs = [ "ProtoLiteCelValueConverter.java", "ProtoMessageLiteValue.java", + "RawProtoMessageLiteValue.java", ], tags = [ ], @@ -350,6 +354,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 +363,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/ProtoLiteCelValueConverter.java b/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java index 64d6ec1d4..093819198 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java @@ -20,6 +20,7 @@ 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; @@ -45,6 +46,7 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Optional; import java.util.TreeMap; /** @@ -80,7 +82,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 +162,17 @@ Object getDefaultCelValue(String protoTypeName, String fieldName) { return toRuntimeValue(defaultValue); } + public Optional findFieldDescriptor(String protoTypeName, int fieldNumber) { + return descriptorPool + .findDescriptor(protoTypeName) + .flatMap(desc -> desc.findByFieldNumber(fieldNumber)); + } + + public Optional findDefaultCelValue(String protoTypeName, int fieldNumber) { + return findFieldDescriptor(protoTypeName, fieldNumber) + .map(fieldDescriptor -> toRuntimeValue(getDefaultValue(fieldDescriptor))); + } + @Override @SuppressWarnings("LiteProtoToString") // No alternative identifier to use. Debug only info is OK. public Object toRuntimeValue(Object value) { @@ -193,7 +206,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); } @@ -367,13 +383,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 +407,19 @@ private static Object readUnknownField(int tagWireType, CodedInputStream inputSt } @AutoValue - @SuppressWarnings("AutoValueImmutableFields") // Unknowns are inaccessible to users. + @AutoValue.CopyAnnotations + @Immutable + @SuppressWarnings("Immutable") // Safe immutable fields abstract static class MessageFields { abstract ImmutableMap values(); - abstract Multimap unknowns(); + abstract ImmutableListMultimap unknowns(); static MessageFields create( ImmutableMap fieldValues, Multimap unknownFields) { - return new AutoValue_ProtoLiteCelValueConverter_MessageFields(fieldValues, unknownFields); + return new AutoValue_ProtoLiteCelValueConverter_MessageFields( + fieldValues, ImmutableListMultimap.copyOf(unknownFields)); } } diff --git a/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java b/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java index 2e4d980c7..99e95ebd3 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java +++ b/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java @@ -17,11 +17,14 @@ import com.google.auto.value.AutoValue; import com.google.auto.value.extension.memoized.Memoized; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.errorprone.annotations.Immutable; import com.google.protobuf.MessageLite; +import dev.cel.common.annotations.Internal; import dev.cel.common.types.CelType; import dev.cel.common.types.StructTypeReference; +import dev.cel.common.values.ProtoLiteCelValueConverter.MessageFields; import java.io.IOException; import java.util.Optional; @@ -46,14 +49,23 @@ public abstract class ProtoMessageLiteValue extends StructValue fieldValues() { + MessageFields messageFields() { try { - return protoLiteCelValueConverter().readAllFields(value(), celType().name()); + return protoLiteCelValueConverter().readMessageFields(value(), celType().name()); } catch (IOException e) { throw new IllegalStateException("Unable to read message fields for " + celType().name(), e); } } + @Internal + public ImmutableMap fieldValues() { + return messageFields().values(); + } + + public ImmutableListMultimap unknownFields() { + return messageFields().unknowns(); + } + @Override public boolean isZeroValue() { return value().getDefaultInstanceForType().equals(value()); diff --git a/common/src/main/java/dev/cel/common/values/RawProtoMessageLiteValue.java b/common/src/main/java/dev/cel/common/values/RawProtoMessageLiteValue.java new file mode 100644 index 000000000..bb50cd013 --- /dev/null +++ b/common/src/main/java/dev/cel/common/values/RawProtoMessageLiteValue.java @@ -0,0 +1,284 @@ +// 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.collect.ImmutableCollection; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +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.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 { + + abstract ByteString rawWireBytes(); + + @Override + public RawProtoMessageLiteValue value() { + return this; + } + + @Override + public abstract CelType celType(); + + @Memoized + public 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); + } + } + + public 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(); + } + + public static @Nullable Object decodeWireEntries( + ImmutableCollection entries, int typeCode, String protoTypeName, boolean isRepeated) { + 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)); + } + } + 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); + } + // Protobuf "last one wins" semantics for non-repeated scalar fields + return decodeWireValue(Iterables.getLast(entries), fieldType, protoTypeName); + } + + static Object decodeWireValue(Object raw, int typeCode, String protoTypeName) { + return decodeWireValue( + raw, FieldLiteDescriptor.Type.forNumber(typeCode).toWireFormatFieldType(), protoTypeName); + } + + static Object decodeWireValue(Object raw, WireFormat.FieldType fieldType, String protoTypeName) { + 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: + return RawProtoMessageLiteValue.create( + requireType(raw, ByteString.class, fieldType), protoTypeName); + 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, ""); + } + + public static RawProtoMessageLiteValue create(ByteString rawWireBytes, String protoTypeName) { + checkNotNull(rawWireBytes); + checkNotNull(protoTypeName); + return new AutoValue_RawProtoMessageLiteValue( + rawWireBytes, StructTypeReference.create(protoTypeName)); + } + + RawProtoMessageLiteValue() {} +} diff --git a/common/src/test/java/dev/cel/common/values/BUILD.bazel b/common/src/test/java/dev/cel/common/values/BUILD.bazel index 76c761567..baa33ebc3 100644 --- a/common/src/test/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/test/java/dev/cel/common/values/BUILD.bazel @@ -15,6 +15,7 @@ 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", @@ -32,6 +33,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..88799878e 100644 --- a/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java +++ b/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java @@ -21,11 +21,17 @@ import com.google.common.collect.ImmutableSet; import com.google.common.primitives.UnsignedLong; import com.google.protobuf.Any; +import com.google.protobuf.BoolValue; import com.google.protobuf.ByteString; +import com.google.protobuf.BytesValue; +import com.google.protobuf.CodedOutputStream; +import com.google.protobuf.DoubleValue; import com.google.protobuf.DynamicMessage; +import com.google.protobuf.ExtensionRegistryLite; import com.google.protobuf.FloatValue; import com.google.protobuf.Int32Value; import com.google.protobuf.Int64Value; +import com.google.protobuf.StringValue; import com.google.protobuf.Timestamp; import com.google.protobuf.UInt32Value; import com.google.protobuf.UInt64Value; @@ -37,6 +43,7 @@ import dev.cel.expr.conformance.proto3.TestAllTypes.NestedEnum; import dev.cel.expr.conformance.proto3.TestAllTypes.NestedMessage; import dev.cel.expr.conformance.proto3.TestAllTypesCelDescriptor; +import java.io.ByteArrayOutputStream; import java.time.Duration; import java.time.Instant; import org.junit.Test; @@ -153,19 +160,17 @@ public void selectField_success(@TestParameter SelectFieldTestCase testCase) { .setSingleDouble(2.5d) .setSingleString("test") .setSingleBytes(ByteString.copyFrom(new byte[] {0x01})) - .setSingleAny( - Any.pack(DynamicMessage.newBuilder(com.google.protobuf.BoolValue.of(true)).build())) + .setSingleAny(Any.pack(DynamicMessage.newBuilder(BoolValue.of(true)).build())) .setSingleDuration(com.google.protobuf.Duration.newBuilder().setSeconds(100)) .setSingleTimestamp(Timestamp.newBuilder().setSeconds(100)) .setSingleInt32Wrapper(Int32Value.of(5)) .setSingleInt64Wrapper(Int64Value.of(10L)) .setSingleUint32Wrapper(UInt32Value.of(1)) .setSingleUint64Wrapper(UInt64Value.of(UnsignedLong.MAX_VALUE.longValue())) - .setSingleStringWrapper(com.google.protobuf.StringValue.of("hello")) + .setSingleStringWrapper(StringValue.of("hello")) .setSingleFloatWrapper(FloatValue.of(7.5f)) - .setSingleDoubleWrapper(com.google.protobuf.DoubleValue.of(8.5d)) - .setSingleBytesWrapper( - com.google.protobuf.BytesValue.of(ByteString.copyFrom(new byte[] {0x02}))) + .setSingleDoubleWrapper(DoubleValue.of(8.5d)) + .setSingleBytesWrapper(BytesValue.of(ByteString.copyFrom(new byte[] {0x02}))) .addRepeatedInt64(5L) .addRepeatedInt64(6L) .addRepeatedUint64(7L) @@ -253,4 +258,26 @@ public void selectField_defaultValue(@TestParameter DefaultValueTestCase testCas assertThat(selectedValue).isEqualTo(testCase.value); } + + @Test + public void unknownFields_retainsUnknownWireFields() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream cos = CodedOutputStream.newInstance(baos); + cos.writeInt64(999, 12345L); + cos.writeString(1000, "hello unknown"); + cos.flush(); + + TestAllTypes msgWithUnknown = + TestAllTypes.parseFrom(baos.toByteArray(), ExtensionRegistryLite.getEmptyRegistry()); + ProtoMessageLiteValue messageLiteValue = + ProtoMessageLiteValue.create( + msgWithUnknown, + "cel.expr.conformance.proto3.TestAllTypes", + PROTO_LITE_CEL_VALUE_CONVERTER); + + assertThat(messageLiteValue.unknownFields()).valuesForKey(999).containsExactly(12345L); + assertThat(messageLiteValue.unknownFields()) + .valuesForKey(1000) + .containsExactly(ByteString.copyFromUtf8("hello unknown")); + } } diff --git a/common/src/test/java/dev/cel/common/values/RawProtoMessageLiteValueTest.java b/common/src/test/java/dev/cel/common/values/RawProtoMessageLiteValueTest.java new file mode 100644 index 000000000..8f5ac623a --- /dev/null +++ b/common/src/test/java/dev/cel/common/values/RawProtoMessageLiteValueTest.java @@ -0,0 +1,766 @@ +// 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.primitives.UnsignedLong; +import com.google.protobuf.ByteString; +import com.google.protobuf.CodedOutputStream; +import com.google.protobuf.WireFormat; +import dev.cel.common.exceptions.CelAttributeNotFoundException; +import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor; +import java.io.ByteArrayOutputStream; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class RawProtoMessageLiteValueTest { + + @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"); + } +} diff --git a/optimizer/src/main/java/dev/cel/optimizer/optimizers/SelectOptimizer.java b/optimizer/src/main/java/dev/cel/optimizer/optimizers/SelectOptimizer.java index 3c6097180..6154a5672 100644 --- a/optimizer/src/main/java/dev/cel/optimizer/optimizers/SelectOptimizer.java +++ b/optimizer/src/main/java/dev/cel/optimizer/optimizers/SelectOptimizer.java @@ -59,6 +59,8 @@ import dev.cel.common.types.CelTypes; import dev.cel.common.types.ListType; import dev.cel.common.types.SimpleType; +import dev.cel.common.types.TypeParamType; +import dev.cel.common.types.TypeType; import dev.cel.common.values.CelByteString; import dev.cel.optimizer.AstMutator; import dev.cel.optimizer.CelAstOptimizer; @@ -103,9 +105,9 @@ *

Expressions are rewritten into the following forms: * *

- *   // Selection chains (user message is 3-tuple, leaf scalar is 4-tuple)
+ *   // Selection chains (user message is 3-tuple, leaf scalar is 4-tuple, leaf type is 3rd argument)
  *   request.user.age -> cel.@attribute(request,
- *       [[user_num, "user", type_code], [age_num, "age", type_code, default_val]])
+ *       [[user_num, "user", type_code], [age_num, "age", type_code, default_val]], int)
  *
  *   // Presence tests (2-tuples)
  *   has(request.user.age) -> cel.@hasField(request,
@@ -127,15 +129,18 @@ public final class SelectOptimizer implements CelAstOptimizer {
   private static final String CEL_ATTRIBUTE_FUNCTION_NAME = "cel.@attribute";
   private static final String CEL_HAS_FIELD_FUNCTION_NAME = "cel.@hasField";
 
+  private static final TypeParamType TYPE_PARAM_T = TypeParamType.create("T");
+
   @VisibleForTesting
   static final CelFunctionDecl CEL_ATTRIBUTE_FUNCTION_DECL =
       CelFunctionDecl.newFunctionDeclaration(
           CEL_ATTRIBUTE_FUNCTION_NAME,
           CelOverloadDecl.newGlobalOverload(
               "cel_attribute_list",
+              TYPE_PARAM_T,
               SimpleType.DYN,
-              SimpleType.DYN,
-              ListType.create(SimpleType.DYN)));
+              ListType.create(SimpleType.DYN),
+              TypeType.create(TYPE_PARAM_T)));
 
   @VisibleForTesting
   static final CelFunctionDecl CEL_HAS_FIELD_FUNCTION_DECL =
@@ -295,8 +300,19 @@ private void rewriteSelectChain(
 
     CelMutableExpr qualifiersExpr =
         CelMutableExpr.ofList(idGenerator.nextExprId(), CelMutableList.create(qualifierLists));
-    String functionName = isHasField ? CEL_HAS_FIELD_FUNCTION_NAME : CEL_ATTRIBUTE_FUNCTION_NAME;
-    topNode.expr().setCall(CelMutableCall.create(functionName, currentExpr, qualifiersExpr));
+    if (isHasField) {
+      topNode
+          .expr()
+          .setCall(CelMutableCall.create(CEL_HAS_FIELD_FUNCTION_NAME, currentExpr, qualifiersExpr));
+    } else {
+      CelMutableExpr typeExpr =
+          CelMutableExpr.ofIdent(idGenerator.nextExprId(), resolveTypeIdent(topField));
+      topNode
+          .expr()
+          .setCall(
+              CelMutableCall.create(
+                  CEL_ATTRIBUTE_FUNCTION_NAME, currentExpr, qualifiersExpr, typeExpr));
+    }
   }
 
   private static long resolveTypeCode(FieldDescriptor field) {
@@ -306,6 +322,43 @@ private static long resolveTypeCode(FieldDescriptor field) {
     return field.getType().toProto().getNumber();
   }
 
+  private static String resolveTypeIdent(FieldDescriptor field) {
+    if (field.isMapField()) {
+      return "map";
+    }
+    if (field.isRepeated()) {
+      return "list";
+    }
+    switch (field.getType()) {
+      case DOUBLE:
+      case FLOAT:
+        return "double";
+      case INT64:
+      case SINT64:
+      case SFIXED64:
+      case INT32:
+      case SINT32:
+      case SFIXED32:
+      case ENUM:
+        return "int";
+      case UINT64:
+      case FIXED64:
+      case UINT32:
+      case FIXED32:
+        return "uint";
+      case BOOL:
+        return "bool";
+      case STRING:
+        return "string";
+      case BYTES:
+        return "bytes";
+      case MESSAGE:
+        return field.getMessageType().getFullName();
+      default:
+        throw new IllegalArgumentException("Unsupported protobuf field type: " + field.getType());
+    }
+  }
+
   private boolean isTopOfSelectChain(CelNavigableMutableAst navAst, CelNavigableMutableExpr node) {
     return getOptimizableField(navAst, node).isPresent()
         && !node.parent().flatMap(parent -> getOptimizableField(navAst, parent)).isPresent();
@@ -414,13 +467,6 @@ private static CelAbstractSyntaxTree tagAstExtension(CelAbstractSyntaxTree ast)
     return CelAbstractSyntaxTree.newParsedAst(ast.getExpr(), celSourceBuilder.build());
   }
 
-  private SelectOptimizer(
-      SelectOptimizerOptions options, Iterable fileDescriptors) {
-    this.options = checkNotNull(options);
-    this.astMutator = AstMutator.newInstance(options.iterationLimit());
-    this.descriptorPool = newDescriptorPool(options, checkNotNull(fileDescriptors));
-  }
-
   private static CelDescriptorPool newDescriptorPool(
       SelectOptimizerOptions options, Iterable fileDescriptors) {
     CelDescriptors celDescriptors =
@@ -432,6 +478,13 @@ private static CelDescriptorPool newDescriptorPool(
     return CombinedDescriptorPool.create(descriptorPools.build());
   }
 
+  private SelectOptimizer(
+      SelectOptimizerOptions options, Iterable fileDescriptors) {
+    this.options = checkNotNull(options);
+    this.astMutator = AstMutator.newInstance(options.iterationLimit());
+    this.descriptorPool = newDescriptorPool(options, checkNotNull(fileDescriptors));
+  }
+
   /** Options configuring the behavior of {@link SelectOptimizer}. */
   @AutoValue
   public abstract static class SelectOptimizerOptions {
diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel b/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel
index 787012466..1fd34709a 100644
--- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel
+++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/BUILD.bazel
@@ -26,6 +26,7 @@ java_library(
         "//extensions:optional_library",
         #         "//java/com/google/testing/testsize:annotations",
         "//optimizer",
+        "//optimizer:ast_optimizer",
         "//optimizer:optimization_exception",
         "//optimizer:optimizer_builder",
         "//optimizer/optimizers:common_subexpression_elimination",
diff --git a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SelectOptimizerTest.java b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SelectOptimizerTest.java
index 7740319fe..f08d01990 100644
--- a/optimizer/src/test/java/dev/cel/optimizer/optimizers/SelectOptimizerTest.java
+++ b/optimizer/src/test/java/dev/cel/optimizer/optimizers/SelectOptimizerTest.java
@@ -34,8 +34,10 @@
 import dev.cel.common.CelFunctionDecl;
 import dev.cel.common.CelMutableAst;
 import dev.cel.common.CelOptions;
+import dev.cel.common.CelOverloadDecl;
 import dev.cel.common.CelProtoAbstractSyntaxTree;
 import dev.cel.common.CelValidationException;
+import dev.cel.common.ast.CelReference;
 import dev.cel.common.navigation.CelNavigableMutableAst;
 import dev.cel.common.types.MapType;
 import dev.cel.common.types.SimpleType;
@@ -43,6 +45,7 @@
 import dev.cel.expr.conformance.proto2.NestedTestAllTypes;
 import dev.cel.expr.conformance.proto2.TestAllTypesProto;
 import dev.cel.expr.conformance.proto3.TestAllTypes;
+import dev.cel.optimizer.CelAstOptimizer;
 import dev.cel.optimizer.CelOptimizer;
 import dev.cel.optimizer.CelOptimizerFactory;
 import dev.cel.optimizer.optimizers.SelectOptimizer.SelectOptimizerOptions;
@@ -119,27 +122,32 @@ private static Cel setupEnv(CelBuilder celBuilder) {
   private enum RewriteTestCase {
     // === Selection & Traversal ===
     PROTO3_SINGLE_FIELD_SELECT(
-        "msg.single_int64", "cel.@attribute(msg, [[2, \"single_int64\", 3, 0]])"),
+        "msg.single_int64", "cel.@attribute(msg, [[2, \"single_int64\", 3, 0]], int)"),
     PROTO3_SINGLE_MESSAGE_FIELD_SELECT(
-        "msg.single_nested_message", "cel.@attribute(msg, [[21, \"single_nested_message\", 11]])"),
+        "msg.single_nested_message",
+        "cel.@attribute(msg, [[21, \"single_nested_message\", 11]],"
+            + " cel.expr.conformance.proto3.TestAllTypes.NestedMessage)"),
     PROTO3_CHAINED_FIELD_SELECT(
         "msg.single_nested_message.bb",
-        "cel.@attribute(msg, [[21, \"single_nested_message\", 11], [1, \"bb\", 5, 0]])"),
+        "cel.@attribute(msg, [[21, \"single_nested_message\", 11], [1, \"bb\", 5, 0]], int)"),
     PROTO2_SINGLE_MESSAGE_FIELD_SELECT(
         "proto2_msg.single_nested_message",
-        "cel.@attribute(proto2_msg, [[21, \"single_nested_message\", 11]])"),
+        "cel.@attribute(proto2_msg, [[21, \"single_nested_message\", 11]],"
+            + " cel.expr.conformance.proto2.TestAllTypes.NestedMessage)"),
     PROTO2_CHAINED_FIELD_SELECT(
         "proto2_msg.single_nested_message.bb",
-        "cel.@attribute(proto2_msg, [[21, \"single_nested_message\", 11], [1, \"bb\", 5, 0]])"),
+        "cel.@attribute(proto2_msg, [[21, \"single_nested_message\", 11], [1, \"bb\", 5, 0]],"
+            + " int)"),
     PROTO2_TRIPLE_CHAINED_FIELD_SELECT(
         "nested_msg.child.payload.single_int64",
         "cel.@attribute(nested_msg, "
             + "[[1, \"child\", 11], "
             + "[2, \"payload\", 11], "
-            + "[2, \"single_int64\", 3, -64]])"),
+            + "[2, \"single_int64\", 3, -64]], int)"),
     PROTO2_CHAINED_MESSAGE_FIELD_SELECT(
         "nested_msg.child.payload",
-        "cel.@attribute(nested_msg, [[1, \"child\", 11], [2, \"payload\", 11]])"),
+        "cel.@attribute(nested_msg, [[1, \"child\", 11], [2, \"payload\", 11]],"
+            + " cel.expr.conformance.proto2.TestAllTypes)"),
 
     // === Presence Tests: Proto2 (Explicit Presence) vs Proto3 (Implicit/Explicit Presence) ===
     // In proto2, scalar fields have explicit presence (has-bit).
@@ -178,102 +186,122 @@ private enum RewriteTestCase {
     // === Default Value Divergence: Proto2 Custom Defaults vs Proto3 Zero Defaults ===
     // Int32: proto2 has custom default -32, proto3 has 0
     PROTO2_CUSTOM_INT32(
-        "proto2_msg.single_int32", "cel.@attribute(proto2_msg, [[1, \"single_int32\", 5, -32]])"),
-    PROTO3_ZERO_INT32("msg.single_int32", "cel.@attribute(msg, [[1, \"single_int32\", 5, 0]])"),
+        "proto2_msg.single_int32",
+        "cel.@attribute(proto2_msg, [[1, \"single_int32\", 5, -32]], int)"),
+    PROTO3_ZERO_INT32(
+        "msg.single_int32", "cel.@attribute(msg, [[1, \"single_int32\", 5, 0]], int)"),
 
     // Int64: proto2 has custom default -64, proto3 has 0
     PROTO2_CUSTOM_INT64(
-        "proto2_msg.single_int64", "cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]])"),
-    PROTO3_ZERO_INT64("msg.single_int64", "cel.@attribute(msg, [[2, \"single_int64\", 3, 0]])"),
+        "proto2_msg.single_int64",
+        "cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]], int)"),
+    PROTO3_ZERO_INT64(
+        "msg.single_int64", "cel.@attribute(msg, [[2, \"single_int64\", 3, 0]], int)"),
 
     // Uint32: proto2 has custom default 32, proto3 has 0
     PROTO2_CUSTOM_UINT32(
         "proto2_msg.single_uint32",
-        "cel.@attribute(proto2_msg, [[3, \"single_uint32\", 13, 32u]])"),
+        "cel.@attribute(proto2_msg, [[3, \"single_uint32\", 13, 32u]], uint)"),
     PROTO3_ZERO_UINT32(
-        "msg.single_uint32", "cel.@attribute(msg, [[3, \"single_uint32\", 13, 0u]])"),
+        "msg.single_uint32", "cel.@attribute(msg, [[3, \"single_uint32\", 13, 0u]], uint)"),
 
     // Uint64: proto2 has custom default 64, proto3 has 0
     PROTO2_CUSTOM_UINT64(
-        "proto2_msg.single_uint64", "cel.@attribute(proto2_msg, [[4, \"single_uint64\", 4, 64u]])"),
-    PROTO3_ZERO_UINT64("msg.single_uint64", "cel.@attribute(msg, [[4, \"single_uint64\", 4, 0u]])"),
+        "proto2_msg.single_uint64",
+        "cel.@attribute(proto2_msg, [[4, \"single_uint64\", 4, 64u]], uint)"),
+    PROTO3_ZERO_UINT64(
+        "msg.single_uint64", "cel.@attribute(msg, [[4, \"single_uint64\", 4, 0u]], uint)"),
 
     // String: proto2 has custom default "empty", proto3 has ""
     PROTO2_CUSTOM_STRING(
         "proto2_msg.single_string",
-        "cel.@attribute(proto2_msg, [[14, \"single_string\", 9, \"empty\"]])"),
+        "cel.@attribute(proto2_msg, [[14, \"single_string\", 9, \"empty\"]], string)"),
     PROTO3_ZERO_STRING(
-        "msg.single_string", "cel.@attribute(msg, [[14, \"single_string\", 9, \"\"]])"),
+        "msg.single_string", "cel.@attribute(msg, [[14, \"single_string\", 9, \"\"]], string)"),
 
     // Bool: proto2 has custom default true, proto3 has false
     PROTO2_CUSTOM_BOOL(
-        "proto2_msg.single_bool", "cel.@attribute(proto2_msg, [[13, \"single_bool\", 8, true]])"),
-    PROTO3_ZERO_BOOL("msg.single_bool", "cel.@attribute(msg, [[13, \"single_bool\", 8, false]])"),
+        "proto2_msg.single_bool",
+        "cel.@attribute(proto2_msg, [[13, \"single_bool\", 8, true]], bool)"),
+    PROTO3_ZERO_BOOL(
+        "msg.single_bool", "cel.@attribute(msg, [[13, \"single_bool\", 8, false]], bool)"),
 
     // Float: proto2 has custom default 3.0, proto3 has 0.0
     PROTO2_CUSTOM_FLOAT(
-        "proto2_msg.single_float", "cel.@attribute(proto2_msg, [[11, \"single_float\", 2, 3.0]])"),
-    PROTO3_ZERO_FLOAT("msg.single_float", "cel.@attribute(msg, [[11, \"single_float\", 2, 0.0]])"),
+        "proto2_msg.single_float",
+        "cel.@attribute(proto2_msg, [[11, \"single_float\", 2, 3.0]], double)"),
+    PROTO3_ZERO_FLOAT(
+        "msg.single_float", "cel.@attribute(msg, [[11, \"single_float\", 2, 0.0]], double)"),
 
     // Double: proto2 has custom default 6.4, proto3 has 0.0
     PROTO2_CUSTOM_DOUBLE(
         "proto2_msg.single_double",
-        "cel.@attribute(proto2_msg, [[12, \"single_double\", 1, 6.4]])"),
+        "cel.@attribute(proto2_msg, [[12, \"single_double\", 1, 6.4]], double)"),
     PROTO3_ZERO_DOUBLE(
-        "msg.single_double", "cel.@attribute(msg, [[12, \"single_double\", 1, 0.0]])"),
+        "msg.single_double", "cel.@attribute(msg, [[12, \"single_double\", 1, 0.0]], double)"),
 
     // Bytes: proto2 has custom default "none", proto3 has ""
     PROTO2_CUSTOM_BYTES(
         "proto2_msg.single_bytes",
-        "cel.@attribute(proto2_msg, [[15, \"single_bytes\", 12, b\"\\156\\157\\156\\145\"]])"),
+        "cel.@attribute(proto2_msg, [[15, \"single_bytes\", 12, b\"\\156\\157\\156\\145\"]],"
+            + " bytes)"),
     PROTO3_ZERO_BYTES(
-        "msg.single_bytes", "cel.@attribute(msg, [[15, \"single_bytes\", 12, b\"\"]])"),
+        "msg.single_bytes", "cel.@attribute(msg, [[15, \"single_bytes\", 12, b\"\"]], bytes)"),
 
     // Enum: proto2 has custom default 1 (BAR), proto3 has 0 (FOO)
     PROTO2_CUSTOM_ENUM(
         "proto2_msg.single_nested_enum",
-        "cel.@attribute(proto2_msg, [[22, \"single_nested_enum\", 14, 1]])"),
+        "cel.@attribute(proto2_msg, [[22, \"single_nested_enum\", 14, 1]], int)"),
     PROTO3_ZERO_ENUM(
-        "msg.single_nested_enum", "cel.@attribute(msg, [[22, \"single_nested_enum\", 14, 0]])"),
-
-    // Fixed / sfixed fields
+        "msg.single_nested_enum",
+        "cel.@attribute(msg, [[22, \"single_nested_enum\", 14, 0]], int)"),
+
+    // Fixed / sfixed / sint fields
+    PROTO3_FIXED32(
+        "msg.single_fixed32", "cel.@attribute(msg, [[7, \"single_fixed32\", 7, 0u]], uint)"),
+    PROTO3_FIXED64(
+        "msg.single_fixed64", "cel.@attribute(msg, [[8, \"single_fixed64\", 6, 0u]], uint)"),
     PROTO3_SFIXED32(
-        "msg.single_sfixed32", "cel.@attribute(msg, [[9, \"single_sfixed32\", 15, 0]])"),
+        "msg.single_sfixed32", "cel.@attribute(msg, [[9, \"single_sfixed32\", 15, 0]], int)"),
     PROTO3_SFIXED64(
-        "msg.single_sfixed64", "cel.@attribute(msg, [[10, \"single_sfixed64\", 16, 0]])"),
+        "msg.single_sfixed64", "cel.@attribute(msg, [[10, \"single_sfixed64\", 16, 0]], int)"),
+    PROTO3_SINT32("msg.single_sint32", "cel.@attribute(msg, [[5, \"single_sint32\", 17, 0]], int)"),
+    PROTO3_SINT64("msg.single_sint64", "cel.@attribute(msg, [[6, \"single_sint64\", 18, 0]], int)"),
 
     // Repeated fields: empty list default
     PROTO2_REPEATED_PRIMITIVE(
         "proto2_msg.repeated_int64",
-        "cel.@attribute(proto2_msg, [[32, \"repeated_int64\", 3, []]])"),
+        "cel.@attribute(proto2_msg, [[32, \"repeated_int64\", 3, []]], list)"),
     PROTO3_REPEATED_PRIMITIVE(
-        "msg.repeated_int64", "cel.@attribute(msg, [[32, \"repeated_int64\", 3, []]])"),
+        "msg.repeated_int64", "cel.@attribute(msg, [[32, \"repeated_int64\", 3, []]], list)"),
     PROTO3_REPEATED_MESSAGE(
         "msg.repeated_nested_message",
-        "cel.@attribute(msg, [[51, \"repeated_nested_message\", 11, []]])"),
+        "cel.@attribute(msg, [[51, \"repeated_nested_message\", 11, []]], list)"),
 
     // Well-known types
     PROTO3_TIMESTAMP(
         "msg.single_timestamp",
-        "cel.@attribute(msg, [[102, \"single_timestamp\", 11, timestamp(0)]])"),
+        "cel.@attribute(msg, [[102, \"single_timestamp\", 11, timestamp(0)]],"
+            + " google.protobuf.Timestamp)"),
     PROTO3_DURATION(
         "msg.single_duration",
-        "cel.@attribute(msg, [[101, \"single_duration\", 11, duration(\"0s\")]])"),
+        "cel.@attribute(msg, [[101, \"single_duration\", 11, duration(\"0s\")]],"
+            + " google.protobuf.Duration)"),
 
     // Map selects
     MAP_FIELD_INDEXING(
         "msg.map_int64_message[1].bb",
         "cel.@attribute("
-            + "cel.@attribute(msg, [[95, \"map_int64_message\", 20, {}]])[1], "
-            + "[[1, \"bb\", 5, 0]])"),
+            + "cel.@attribute(msg, [[95, \"map_int64_message\", 20, {}]], map)[1], "
+            + "[[1, \"bb\", 5, 0]], int)"),
     MAP_FIELD_SELECT_CHAIN_STOPS_AT_MAP_BOUNDARY(
         "map_var_msg.key.single_nested_message.bb",
         "cel.@attribute(map_var_msg.key, "
             + "[[21, \"single_nested_message\", 11], "
-            + "[1, \"bb\", 5, 0]])"),
+            + "[1, \"bb\", 5, 0]], int)"),
     MAP_FIELD_SELECT_STOPS_AT_MAP_BOUNDARY(
         "map_var_msg.key.single_int64",
-        "cel.@attribute(map_var_msg.key, [[2, \"single_int64\", 3, 0]])"),
+        "cel.@attribute(map_var_msg.key, [[2, \"single_int64\", 3, 0]], int)"),
     MAP_FIELD_HAS_STOPS_AT_MAP_BOUNDARY(
         "has(map_var_msg.key.single_nested_message)",
         "cel.@hasField(map_var_msg.key, [[21, \"single_nested_message\"]])"),
@@ -283,17 +311,17 @@ private enum RewriteTestCase {
     PROTO_MAP_FIELD_SELECT_STOPS_AT_MAP_BOUNDARY(
         "msg.map_string_message.key.bb",
         "cel.@attribute("
-            + "cel.@attribute(msg, [[227, \"map_string_message\", 20, {}]]).key, "
-            + "[[1, \"bb\", 5, 0]])"),
+            + "cel.@attribute(msg, [[227, \"map_string_message\", 20, {}]], map).key, "
+            + "[[1, \"bb\", 5, 0]], int)"),
     PROTO_MAP_FIELD_HAS_STOPS_AT_MAP_BOUNDARY(
         "has(msg.map_string_message.key.bb)",
         "cel.@hasField("
-            + "cel.@attribute(msg, [[227, \"map_string_message\", 20, {}]]).key, "
+            + "cel.@attribute(msg, [[227, \"map_string_message\", 20, {}]], map).key, "
             + "[[1, \"bb\"]])"),
 
     MIXED_BOOLEAN_EXPRESSION(
         "msg.single_int64 > 0 && has(msg.single_nested_message)",
-        "cel.@attribute(msg, [[2, \"single_int64\", 3, 0]]) > 0 "
+        "cel.@attribute(msg, [[2, \"single_int64\", 3, 0]], int) > 0 "
             + "&& cel.@hasField(msg, [[21, \"single_nested_message\"]])");
 
     private final String expression;
@@ -376,7 +404,7 @@ public void optimize_withFileDescriptors_success() throws Exception {
     CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast);
 
     assertThat(CEL_UNPARSER.unparse(optimizedAst))
-        .isEqualTo("cel.@attribute(msg, [[2, \"single_int64\", 3, 0]])");
+        .isEqualTo("cel.@attribute(msg, [[2, \"single_int64\", 3, 0]], int)");
   }
 
   @Test
@@ -394,7 +422,7 @@ public void optimize_withFileDescriptorsIterable_success() throws Exception {
     CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast);
 
     assertThat(CEL_UNPARSER.unparse(optimizedAst))
-        .isEqualTo("cel.@attribute(msg, [[2, \"single_int64\", 3, 0]])");
+        .isEqualTo("cel.@attribute(msg, [[2, \"single_int64\", 3, 0]], int)");
   }
 
   @Test
@@ -409,7 +437,7 @@ public void newInstance_withOptionsAndFileDescriptors_preservesAddedDescriptors(
     CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast, cel).optimizedAst();
 
     assertThat(CEL_UNPARSER.unparse(optimizedAst))
-        .isEqualTo("cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]])");
+        .isEqualTo("cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]], int)");
   }
 
   @Test
@@ -459,7 +487,9 @@ public void optimizeAndEvaluate_withAttributeFunctionBinding_evaluatesSuccessful
             .addFunctionDeclarations(SelectOptimizer.CEL_ATTRIBUTE_FUNCTION_DECL)
             .addFunctionBindings(
                 CelFunctionBinding.from(
-                    "cel_attribute_list", Object.class, List.class, (target, path) -> 42L))
+                    "cel_attribute_list",
+                    ImmutableList.of(Object.class, List.class, Object.class),
+                    args -> 42L))
             .build();
     CelOptimizer optimizer =
         CelOptimizerFactory.standardCelOptimizerBuilder(celWithBinding)
@@ -487,7 +517,9 @@ public void optimizeAndEvaluate_withChainedMessageSelect_unpacksTuplesSuccessful
             .addFunctionDeclarations(SelectOptimizer.CEL_ATTRIBUTE_FUNCTION_DECL)
             .addFunctionBindings(
                 CelFunctionBinding.from(
-                    "cel_attribute_list", Object.class, List.class, (target, path) -> path))
+                    "cel_attribute_list",
+                    ImmutableList.of(Object.class, List.class, Object.class),
+                    args -> args[1]))
             .build();
     CelOptimizer optimizer =
         CelOptimizerFactory.standardCelOptimizerBuilder(celWithBinding)
@@ -545,7 +577,9 @@ public void optimizeAndEvaluate_withSelectOnMapValue_evaluatesSuccessfully() thr
             .addFunctionDeclarations(SelectOptimizer.CEL_ATTRIBUTE_FUNCTION_DECL)
             .addFunctionBindings(
                 CelFunctionBinding.from(
-                    "cel_attribute_list", Object.class, List.class, (target, path) -> 42L))
+                    "cel_attribute_list",
+                    ImmutableList.of(Object.class, List.class, Object.class),
+                    args -> 42L))
             .build();
     CelOptimizer optimizer =
         CelOptimizerFactory.standardCelOptimizerBuilder(celWithBinding)
@@ -608,7 +642,9 @@ public void optimizeAndEvaluate_withMissingMapKey_throwsEvaluationException() th
             .addFunctionDeclarations(SelectOptimizer.CEL_ATTRIBUTE_FUNCTION_DECL)
             .addFunctionBindings(
                 CelFunctionBinding.from(
-                    "cel_attribute_list", Object.class, List.class, (target, path) -> 42L))
+                    "cel_attribute_list",
+                    ImmutableList.of(Object.class, List.class, Object.class),
+                    args -> 42L))
             .build();
     CelOptimizer optimizer =
         CelOptimizerFactory.standardCelOptimizerBuilder(celWithBinding)
@@ -744,7 +780,7 @@ public void newInstance_fileDescriptorsVarargs_defaultOptions_success() throws E
     CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast, cel).optimizedAst();
 
     assertThat(CEL_UNPARSER.unparse(optimizedAst))
-        .isEqualTo("cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]])");
+        .isEqualTo("cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]], int)");
   }
 
   @Test
@@ -756,7 +792,7 @@ public void newInstance_fileDescriptorsIterable_defaultOptions_success() throws
     CelAbstractSyntaxTree optimizedAst = optimizer.optimize(ast, cel).optimizedAst();
 
     assertThat(CEL_UNPARSER.unparse(optimizedAst))
-        .isEqualTo("cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]])");
+        .isEqualTo("cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]], int)");
   }
 
   @Test
@@ -774,7 +810,7 @@ public void newInstance_fileDescriptorsIterable_defaultOptions_success() throws
     CelAbstractSyntaxTree proto3Optimized = optimizer.optimize(proto3Ast, cel).optimizedAst();
 
     assertThat(CEL_UNPARSER.unparse(proto2Optimized))
-        .isEqualTo("cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]])");
+        .isEqualTo("cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]], int)");
     assertThat(CEL_UNPARSER.unparse(proto3Optimized)).isEqualTo("msg.single_int64");
   }
 
@@ -794,18 +830,18 @@ public void newInstance_fileDescriptorsIterable_defaultOptions_success() throws
     CelAbstractSyntaxTree proto3Optimized = optimizer.optimize(proto3Ast, cel).optimizedAst();
 
     assertThat(CEL_UNPARSER.unparse(proto2Optimized))
-        .isEqualTo("cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]])");
+        .isEqualTo("cel.@attribute(proto2_msg, [[2, \"single_int64\", 3, -64]], int)");
     assertThat(CEL_UNPARSER.unparse(proto3Optimized)).isEqualTo("msg.single_int64");
   }
 
   private enum CompilerRejectionTestCase {
     ATTRIBUTE_AT_SIGN(
         SelectOptimizer.CEL_ATTRIBUTE_FUNCTION_DECL,
-        "cel.@attribute(msg, [])",
+        "cel.@attribute(msg, [], int)",
         "token recognition error at: '@'"),
     ATTRIBUTE_OVERLOAD(
         SelectOptimizer.CEL_ATTRIBUTE_FUNCTION_DECL,
-        "cel_attribute_list(msg, [])",
+        "cel_attribute_list(msg, [], int)",
         "undeclared reference to 'cel_attribute_list'"),
     HAS_FIELD_AT_SIGN(
         SelectOptimizer.CEL_HAS_FIELD_FUNCTION_DECL,
@@ -912,6 +948,12 @@ public void optimize_toParsedExpr_matchesExpectedSerializedProto() throws Except
                 + "        }\n"
                 + "      }\n"
                 + "    }\n"
+                + "    args {\n"
+                + "      id: 13\n"
+                + "      ident_expr {\n"
+                + "        name: \"int\"\n"
+                + "      }\n"
+                + "    }\n"
                 + "  }\n"
                 + "}\n"
                 + "source_info {\n"
@@ -931,4 +973,70 @@ public void optimize_toParsedExpr_matchesExpectedSerializedProto() throws Except
 
     assertThat(parsedExpr).isEqualTo(expectedParsedExpr);
   }
+
+  @Test
+  public void
+      optimize_binaryOperationOnOptimizedSelect_resolvesOverloadAndPreservesConcreteResultType()
+          throws Exception {
+    CelAbstractSyntaxTree ast = cel.compile("msg.single_int64 + 1").getAst();
+
+    CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast);
+
+    assertThat(optimizedAst.getResultType()).isEqualTo(SimpleType.INT);
+    assertThat(CEL_UNPARSER.unparse(optimizedAst))
+        .isEqualTo("cel.@attribute(msg, [[2, \"single_int64\", 3, 0]], int) + 1");
+  }
+
+  @Test
+  public void
+      optimize_stringOperationOnOptimizedSelect_resolvesOverloadAndPreservesConcreteResultType()
+          throws Exception {
+    CelAbstractSyntaxTree ast = cel.compile("msg.single_string + 'suffix'").getAst();
+
+    CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast);
+
+    assertThat(optimizedAst.getResultType()).isEqualTo(SimpleType.STRING);
+    assertThat(CEL_UNPARSER.unparse(optimizedAst))
+        .isEqualTo("cel.@attribute(msg, [[14, \"single_string\", 9, \"\"]], string) + \"suffix\"");
+  }
+
+  @Test
+  public void optimize_resultFunctionDeclarations_containsOnlySingularAttributeAndHasField()
+      throws Exception {
+    SelectOptimizer optimizer = SelectOptimizer.newInstance(TestAllTypes.getDescriptor().getFile());
+    CelAbstractSyntaxTree ast = cel.compile("msg.single_int64").getAst();
+
+    CelAstOptimizer.OptimizationResult result = optimizer.optimize(ast, cel);
+
+    assertThat(result.newFunctionDecls())
+        .containsExactly(
+            SelectOptimizer.CEL_ATTRIBUTE_FUNCTION_DECL,
+            SelectOptimizer.CEL_HAS_FIELD_FUNCTION_DECL);
+    assertThat(
+            SelectOptimizer.CEL_ATTRIBUTE_FUNCTION_DECL.overloads().stream()
+                .map(CelOverloadDecl::overloadId))
+        .containsExactly("cel_attribute_list");
+  }
+
+  @Test
+  public void optimize_referenceMap_containsSingleOverloadIdForAttributeCall() throws Exception {
+    CelAbstractSyntaxTree ast = cel.compile("msg.single_int64").getAst();
+
+    CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast);
+
+    CelReference reference = optimizedAst.getReferenceOrThrow(optimizedAst.getExpr().id());
+    assertThat(reference.overloadIds()).containsExactly("cel_attribute_list");
+  }
+
+  @Test
+  public void optimize_binaryOperationBetweenOptimizedSelects_resolvesSingleOverloadInReferenceMap()
+      throws Exception {
+    CelAbstractSyntaxTree ast = cel.compile("msg.single_int64 + msg.single_sint64").getAst();
+
+    CelAbstractSyntaxTree optimizedAst = celOptimizer.optimize(ast);
+
+    assertThat(optimizedAst.getResultType()).isEqualTo(SimpleType.INT);
+    CelReference addReference = optimizedAst.getReferenceOrThrow(optimizedAst.getExpr().id());
+    assertThat(addReference.overloadIds()).containsExactly("add_int64");
+  }
 }
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, + "")); + } }