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..e3a25c849 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,10 @@ CEL_VALUES_SOURCES = [
"ErrorValue.java",
"NullValue.java",
"OpaqueValue.java",
+ "OptimizedSelectTraversal.java",
+ "OptimizedSelectable.java",
"OptionalValue.java",
+ "SelectField.java",
"SelectableValue.java",
"StructValue.java",
]
@@ -167,6 +170,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 +222,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 +322,8 @@ java_library(
srcs = [
"ProtoLiteCelValueConverter.java",
"ProtoMessageLiteValue.java",
+ "RawProtoMessageLiteValue.java",
+ "UnknownFieldResolver.java",
],
tags = [
],
@@ -325,6 +332,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 +341,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 +351,8 @@ cel_android_library(
srcs = [
"ProtoLiteCelValueConverter.java",
"ProtoMessageLiteValue.java",
+ "RawProtoMessageLiteValue.java",
+ "UnknownFieldResolver.java",
],
tags = [
],
@@ -350,6 +361,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 +370,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/OptimizedSelectTraversal.java b/common/src/main/java/dev/cel/common/values/OptimizedSelectTraversal.java
new file mode 100644
index 000000000..b57bf4186
--- /dev/null
+++ b/common/src/main/java/dev/cel/common/values/OptimizedSelectTraversal.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 dev.cel.common.annotations.Internal;
+import dev.cel.common.exceptions.CelAttributeNotFoundException;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Walks a sequence of {@link SelectField} selections, dispatching each field over {@link
+ * OptimizedSelectable}, {@link SelectableValue}, or {@link Map}.
+ *
+ *
CEL Library Internals. Do Not Use.
+ */
+@Internal
+public final class OptimizedSelectTraversal {
+
+ /**
+ * Qualifies {@code target} through every field in {@code fields} and returns the terminal value.
+ *
+ * @param celValueConverter Converter for unadapted entries encountered in a root {@link Map}.
+ */
+ public static Object qualify(
+ Object target, ImmutableList fields, CelValueConverter celValueConverter) {
+ Object current = target;
+ for (int i = 0; i < fields.size(); i++) {
+ current = qualifyField(current, fields.get(i), celValueConverter);
+ }
+ return current;
+ }
+
+ /**
+ * Presence tests the terminal field of {@code fields}, navigating through all preceding fields.
+ *
+ * Absence of any intermediate field short-circuits to {@code false}.
+ */
+ public static boolean hasField(
+ Object target, ImmutableList fields, CelValueConverter celValueConverter) {
+ if (fields.isEmpty()) {
+ return false;
+ }
+ Object current = target;
+ int terminalIndex = fields.size() - 1;
+ for (int i = 0; i < terminalIndex; i++) {
+ Optional next = navigateField(current, fields.get(i), celValueConverter);
+ if (!next.isPresent()) {
+ return false;
+ }
+ current = next.get();
+ }
+ return hasTerminalField(current, fields.get(terminalIndex));
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Object qualifyField(
+ Object target, SelectField field, CelValueConverter celValueConverter) {
+ if (target instanceof ErrorValue) {
+ return target;
+ }
+ if (target instanceof OptimizedSelectable) {
+ return ((OptimizedSelectable) target).selectByFieldNumber(field);
+ }
+ if (target instanceof SelectableValue) {
+ SelectableValue selectable = (SelectableValue) target;
+ if (field.defaultValue() != null) {
+ return selectable.find(field.fieldName()).map(Object.class::cast).orElse(field.defaultValue());
+ }
+ return selectable.select(field.fieldName());
+ }
+ if (target instanceof Map) {
+ return getMapEntry((Map, ?>) target, field.fieldName(), celValueConverter);
+ }
+ throw CelAttributeNotFoundException.forFieldResolution(field.fieldName());
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Optional navigateField(
+ Object target, SelectField field, CelValueConverter celValueConverter) {
+ if (target instanceof ErrorValue) {
+ return Optional.of(target);
+ }
+ if (target instanceof OptimizedSelectable) {
+ return ((OptimizedSelectable) target).findByFieldNumber(field);
+ }
+ if (target instanceof SelectableValue) {
+ return ((SelectableValue) target).find(field.fieldName()).map(Object.class::cast);
+ }
+ if (target instanceof Map) {
+ return findMapEntry((Map, ?>) target, field.fieldName(), celValueConverter);
+ }
+ throw CelAttributeNotFoundException.forFieldResolution(field.fieldName());
+ }
+
+ @SuppressWarnings("unchecked")
+ private static boolean hasTerminalField(Object target, SelectField field) {
+ if (target instanceof ErrorValue) {
+ return false;
+ }
+ if (target instanceof OptimizedSelectable) {
+ return ((OptimizedSelectable) target).hasFieldByNumber(field);
+ }
+ if (target instanceof SelectableValue) {
+ return ((SelectableValue) target).find(field.fieldName()).isPresent();
+ }
+ if (target instanceof Map) {
+ return ((Map, ?>) target).containsKey(field.fieldName());
+ }
+ throw CelAttributeNotFoundException.forFieldResolution(field.fieldName());
+ }
+
+ private static Object getMapEntry(
+ Map, ?> map, String key, CelValueConverter celValueConverter) {
+ return findMapEntry(map, key, celValueConverter)
+ .orElseThrow(() -> CelAttributeNotFoundException.forMissingMapKey(key));
+ }
+
+ private static Optional findMapEntry(
+ Map, ?> map, String key, CelValueConverter celValueConverter) {
+ Object mapValue = map.get(key);
+ if (mapValue != null) {
+ return Optional.of(toStepTarget(mapValue, celValueConverter));
+ }
+ if (!map.containsKey(key)) {
+ return Optional.empty();
+ }
+ throw CelAttributeNotFoundException.of(
+ String.format("Map value cannot be null for key: %s", key));
+ }
+
+ static Object toStepTarget(Object value, CelValueConverter celValueConverter) {
+ if (value instanceof Map) {
+ return value;
+ }
+ return celValueConverter.toRuntimeValue(value);
+ }
+
+ private OptimizedSelectTraversal() {}
+}
diff --git a/common/src/main/java/dev/cel/common/values/OptimizedSelectable.java b/common/src/main/java/dev/cel/common/values/OptimizedSelectable.java
new file mode 100644
index 000000000..ee4113420
--- /dev/null
+++ b/common/src/main/java/dev/cel/common/values/OptimizedSelectable.java
@@ -0,0 +1,45 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.common.values;
+
+import com.google.errorprone.annotations.Immutable;
+import dev.cel.common.annotations.Internal;
+import java.util.Optional;
+
+/**
+ * Resolves an optimized field selection within a selection chain rewritten by the select optimizer.
+ *
+ * Implementations resolve individual field selections against themselves by protobuf field
+ * number. Walking the chain across multiple fields and heterogeneous values belongs to {@link
+ * OptimizedSelectTraversal}.
+ *
+ *
CEL Library Internals. Do Not Use.
+ */
+@Internal
+@Immutable
+public interface OptimizedSelectable {
+
+ /** Selects {@code field}, falling back to its default value or an empty submessage if absent. */
+ Object selectByFieldNumber(SelectField field);
+
+ /** Returns whether {@code field} is present. */
+ boolean hasFieldByNumber(SelectField field);
+
+ /**
+ * Returns the submessage at {@code field} for an intermediate step of a presence test, or empty
+ * if absent.
+ */
+ Optional findByFieldNumber(SelectField field);
+}
diff --git a/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java b/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java
index 64d6ec1d4..f2d1a7912 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,6 +64,31 @@
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) {
return new ProtoLiteCelValueConverter(celLiteDescriptorPool);
@@ -80,7 +107,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 +187,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 +253,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 +332,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 +406,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 +426,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 +450,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..2e90220bb 100644
--- a/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java
+++ b/common/src/main/java/dev/cel/common/values/ProtoMessageLiteValue.java
@@ -14,16 +14,21 @@
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.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}.
@@ -32,12 +37,26 @@
*
* If the codebase has access to full protobuf messages with descriptors, use {@code
* ProtoMessageValue} instead.
+ *
+ *
Implements {@link OptimizedSelectable} so that select chains can address fields by number:
+ *
+ *
+ * Field renames: If a protobuf field is renamed in schema after an AST was compiled,
+ * resolving by {@link SelectField#fieldNumber()} maps the number to the runtime descriptor's
+ * current field name, preventing {@code CelAttributeNotFoundException}.
+ * Version skew / unknown fields: When evaluating payloads serialized by a newer binary
+ * containing fields absent from the local {@code CelLiteDescriptor}, the unknown wire bytes
+ * are preserved in {@link #unknownFields()} and decoded on demand using the compile-time
+ * wire type and default metadata in {@link SelectField}.
+ *
*/
@AutoValue
@Immutable
-public abstract class ProtoMessageLiteValue extends StructValue {
+public abstract class ProtoMessageLiteValue extends StructValue
+ implements OptimizedSelectable {
@Override
+ @SuppressWarnings("Immutable") // MessageLite is immutable in practice
public abstract MessageLite value();
@Override
@@ -46,14 +65,23 @@ public abstract class ProtoMessageLiteValue extends StructValue fieldValues() {
+ MessageFields messageFields() {
try {
- return protoLiteCelValueConverter().readAllFields(value(), celType().name());
+ return protoLiteCelValueConverter()
+ .readAllFields(value().toByteArray(), celType().name());
} catch (IOException e) {
- throw new IllegalStateException("Unable to read message fields for " + celType().name(), e);
+ throw new IllegalStateException(e);
}
}
+ ImmutableMap fieldValues() {
+ return messageFields().values();
+ }
+
+ ImmutableListMultimap unknownFields() {
+ return messageFields().unknowns();
+ }
+
@Override
public boolean isZeroValue() {
return value().getDefaultInstanceForType().equals(value());
@@ -72,11 +100,57 @@ public Optional find(String field) {
.map(value -> protoLiteCelValueConverter().toRuntimeValue(fieldValue));
}
+ @Override
+ public Object selectByFieldNumber(SelectField field) {
+ Optional fd = findFieldDescriptor(field);
+ Object known = findKnownFieldValue(field, fd);
+ if (known != null) {
+ return protoLiteCelValueConverter().toRuntimeValue(known);
+ }
+ return UnknownFieldResolver.selectUnknownOrDefault(
+ field, fd, unknownFields().get(field.fieldNumber()), protoLiteCelValueConverter());
+ }
+
+ @Override
+ public boolean hasFieldByNumber(SelectField field) {
+ Optional fd = findFieldDescriptor(field);
+ if (findKnownFieldValue(field, fd) != null) {
+ return true;
+ }
+ return UnknownFieldResolver.isPresentInUnknowns(
+ field, fd, unknownFields().get(field.fieldNumber()), protoLiteCelValueConverter());
+ }
+
+ @Override
+ public Optional findByFieldNumber(SelectField field) {
+ Optional fd = findFieldDescriptor(field);
+ Object known = findKnownFieldValue(field, fd);
+ if (known != null) {
+ return Optional.of(protoLiteCelValueConverter().toRuntimeValue(known));
+ }
+ return UnknownFieldResolver.navigateUnknown(
+ field, fd, unknownFields().get(field.fieldNumber()), protoLiteCelValueConverter());
+ }
+
+ private Optional findFieldDescriptor(SelectField field) {
+ return protoLiteCelValueConverter().findFieldDescriptor(celType().name(), field.fieldNumber());
+ }
+
+ private @Nullable Object findKnownFieldValue(
+ SelectField field, Optional fieldDescriptor) {
+ String currentFieldName =
+ fieldDescriptor.map(FieldLiteDescriptor::getFieldName).orElse(field.fieldName());
+ return fieldValues().get(currentFieldName);
+ }
+
public static ProtoMessageLiteValue create(
MessageLite value, String typeName, ProtoLiteCelValueConverter protoLiteCelValueConverter) {
- Preconditions.checkNotNull(value);
- Preconditions.checkNotNull(typeName);
+ checkNotNull(value);
+ checkNotNull(typeName);
+ checkNotNull(protoLiteCelValueConverter);
return new AutoValue_ProtoMessageLiteValue(
value, StructTypeReference.create(typeName), protoLiteCelValueConverter);
}
+
+ 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..159c37845
--- /dev/null
+++ b/common/src/main/java/dev/cel/common/values/RawProtoMessageLiteValue.java
@@ -0,0 +1,355 @@
+// 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.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
+ implements OptimizedSelectable {
+
+ abstract ByteString rawWireBytes();
+
+ @Override
+ public abstract CelType celType();
+
+ abstract ProtoLiteCelValueConverter protoLiteCelValueConverter();
+
+ /**
+ * Returns {@code this} so that {@link CelValueConverter#maybeUnwrap} preserves the {@link
+ * RawProtoMessageLiteValue} wrapper when a classless submessage is returned as a terminal
+ * expression result, rather than unwrapping it into a raw {@link ByteString}.
+ */
+ @Override
+ public RawProtoMessageLiteValue value() {
+ return this;
+ }
+
+ @Memoized
+ ImmutableListMultimap unknownFields() {
+ try {
+ CodedInputStream inputStream = rawWireBytes().newCodedInput();
+ Multimap fields = Multimaps.newMultimap(new TreeMap<>(), ArrayList::new);
+ for (int tag = inputStream.readTag(); tag != 0; tag = inputStream.readTag()) {
+ int tagWireType = WireFormat.getTagWireType(tag);
+ int fieldNumber = WireFormat.getTagFieldNumber(tag);
+ fields.put(
+ fieldNumber, ProtoLiteCelValueConverter.readUnknownField(tagWireType, inputStream));
+ }
+ return ImmutableListMultimap.copyOf(fields);
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to parse raw proto message wire bytes", e);
+ }
+ }
+
+ @Override
+ public boolean isZeroValue() {
+ return rawWireBytes().isEmpty();
+ }
+
+ /**
+ * Direct field selection by name is unsupported on {@link RawProtoMessageLiteValue} because raw
+ * wire bytes lack message descriptors, and field names are not preserved on the protobuf wire.
+ *
+ * Field traversal on classless messages must be performed via optimized attribute steps
+ * ({@code cel.@attribute} and {@code cel.@hasField}), where the AST optimizer supplies the
+ * pre-resolved protobuf field numbers.
+ *
+ * @throws CelAttributeNotFoundException always, indicating the field cannot be resolved by name.
+ */
+ @Override
+ public Object select(String field) {
+ throw CelAttributeNotFoundException.forFieldResolution(field);
+ }
+
+ /**
+ * Direct field presence testing by name is unsupported on {@link RawProtoMessageLiteValue}
+ * because raw wire bytes lack message descriptors and field names.
+ *
+ * @throws CelAttributeNotFoundException always, indicating the field cannot be resolved by name.
+ */
+ @Override
+ public Optional find(String field) {
+ throw CelAttributeNotFoundException.forFieldResolution(field);
+ }
+
+ @Override
+ public Object selectByFieldNumber(SelectField field) {
+ int fieldNumber = field.fieldNumber();
+ Optional fieldDescriptor =
+ protoLiteCelValueConverter().findFieldDescriptor(celType().name(), fieldNumber);
+ return UnknownFieldResolver.selectUnknownOrDefault(
+ field, fieldDescriptor, unknownFields().get(fieldNumber), protoLiteCelValueConverter());
+ }
+
+ @VisibleForTesting
+ boolean hasField(int fieldNumber) {
+ return unknownFields().containsKey(fieldNumber);
+ }
+
+ @Override
+ public boolean hasFieldByNumber(SelectField field) {
+ int fieldNumber = field.fieldNumber();
+ Optional fieldDescriptor =
+ protoLiteCelValueConverter().findFieldDescriptor(celType().name(), fieldNumber);
+ return UnknownFieldResolver.isPresentInUnknowns(
+ field, fieldDescriptor, unknownFields().get(fieldNumber), protoLiteCelValueConverter());
+ }
+
+ @Override
+ public Optional findByFieldNumber(SelectField field) {
+ int fieldNumber = field.fieldNumber();
+ Optional fieldDescriptor =
+ protoLiteCelValueConverter().findFieldDescriptor(celType().name(), fieldNumber);
+ return UnknownFieldResolver.navigateUnknown(
+ field, fieldDescriptor, unknownFields().get(fieldNumber), protoLiteCelValueConverter());
+ }
+
+ public 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);
+ }
+
+ 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) {
+ return decodeWireValue(raw, fieldType, protoTypeName, ProtoLiteCelValueConverter.newInstance());
+ }
+
+ 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);
+ }
+
+ 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..6ecc103ed
--- /dev/null
+++ b/common/src/main/java/dev/cel/common/values/SelectField.java
@@ -0,0 +1,97 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.common.values;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.auto.value.AutoValue;
+import com.google.errorprone.annotations.Immutable;
+import dev.cel.common.annotations.Internal;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Represents a single field selection in an optimized selection chain.
+ *
+ * CEL Library Internals. Do Not Use.
+ */
+@Internal
+@AutoValue
+@AutoValue.CopyAnnotations
+@Immutable
+@SuppressWarnings("Immutable") // Default value is an immutable CEL literal or null
+public abstract class SelectField {
+
+ public static final long MAX_FIELD_NUMBER = 536870911L;
+
+ /**
+ * Type code for protobuf maps, shared by the optimizer that emits it and the runtime that reads
+ * it. Maps need a sentinel outside the {@code FieldDescriptorProto.Type} range because on the
+ * wire they are indistinguishable from repeated {@code MapEntry} submessages.
+ */
+ public static final int CEL_MAP_TYPE_CODE = -1;
+
+ /** Sentinel for a presence-test qualifier, whose 2-tuple carries no type code. */
+ public static final int NO_TYPE_CODE = 0;
+
+ /** Protobuf field type code for {@code TYPE_MESSAGE} ({@code FieldDescriptorProto.Type}). */
+ public static final int MESSAGE_TYPE_CODE = 11;
+
+ // Mirrors FieldDescriptorProto.Type. Not validated against a protobuf enum because the :values
+ // target is deliberately protobuf-free; keep in sync with CelLiteDescriptor.FieldLiteDescriptor.
+ private static final int MIN_PROTO_TYPE_CODE = 1; // TYPE_DOUBLE
+ private static final int MAX_PROTO_TYPE_CODE = 18; // TYPE_SINT64
+ private static final int GROUP_PROTO_TYPE_CODE = 10; // Unsupported by CEL.
+
+ 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, long typeCode, @Nullable Object defaultValue) {
+ checkArgument(
+ fieldNumber >= 1 && fieldNumber <= MAX_FIELD_NUMBER,
+ "Field number out of protobuf range: %s",
+ fieldNumber);
+ checkNotNull(fieldName);
+ checkArgument(isSupportedTypeCode(typeCode), "Invalid protobuf type code: %s", typeCode);
+ return new AutoValue_SelectField((int) fieldNumber, fieldName, (int) typeCode, defaultValue);
+ }
+
+ private static boolean isSupportedTypeCode(long typeCode) {
+ if (typeCode == CEL_MAP_TYPE_CODE) {
+ return true;
+ }
+ return typeCode >= MIN_PROTO_TYPE_CODE
+ && typeCode <= MAX_PROTO_TYPE_CODE
+ && typeCode != GROUP_PROTO_TYPE_CODE;
+ }
+
+ SelectField() {}
+}
diff --git a/common/src/main/java/dev/cel/common/values/UnknownFieldResolver.java b/common/src/main/java/dev/cel/common/values/UnknownFieldResolver.java
new file mode 100644
index 000000000..4d280987f
--- /dev/null
+++ b/common/src/main/java/dev/cel/common/values/UnknownFieldResolver.java
@@ -0,0 +1,193 @@
+// 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.common.collect.ImmutableMap;
+import com.google.protobuf.ByteString;
+import com.google.protobuf.WireFormat;
+import dev.cel.common.exceptions.CelAttributeNotFoundException;
+import dev.cel.protobuf.CelLiteDescriptor.FieldLiteDescriptor;
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Resolves field selections, presence tests, and intermediate navigations over raw protobuf wire
+ * fields shared by {@link ProtoMessageLiteValue} and {@link RawProtoMessageLiteValue}.
+ */
+final class UnknownFieldResolver {
+
+ static final String UNKNOWN_MESSAGE_TYPE_NAME = "cel.@unknownMessage";
+
+ 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 RawProtoMessageLiteValue.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 RawProtoMessageLiteValue.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 Optional navigateUnknown(
+ SelectField field,
+ Optional fieldDescriptor,
+ ImmutableList unknowns,
+ ProtoLiteCelValueConverter converter) {
+ if (!isPresentInUnknowns(field, fieldDescriptor, unknowns, converter)) {
+ return Optional.empty();
+ }
+ if (fieldDescriptor.isPresent() || field.typeCode() != SelectField.NO_TYPE_CODE) {
+ return Optional.ofNullable(
+ selectUnknownOrDefault(field, fieldDescriptor, unknowns, converter));
+ }
+ Object lastEntry = unknowns.get(unknowns.size() - 1);
+ if (lastEntry instanceof ByteString) {
+ return Optional.of(
+ RawProtoMessageLiteValue.decodeWireEntries(
+ unknowns,
+ FieldLiteDescriptor.Type.MESSAGE.getNumber(),
+ UNKNOWN_MESSAGE_TYPE_NAME,
+ /* isRepeated= */ false,
+ converter));
+ }
+ return Optional.of(lastEntry);
+ }
+
+ private static ImmutableMap decodeMapEntries(
+ ImmutableList unknowns,
+ FieldLiteDescriptor mapFieldDescriptor,
+ ProtoLiteCelValueConverter converter) {
+ String entryTypeName = mapFieldDescriptor.getFieldProtoTypeName();
+ Map resultMap = new LinkedHashMap<>();
+ for (Object raw : unknowns) {
+ ByteString bytes =
+ RawProtoMessageLiteValue.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);
+ }
+
+ private UnknownFieldResolver() {}
+}
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/OptimizedSelectTraversalTest.java b/common/src/test/java/dev/cel/common/values/OptimizedSelectTraversalTest.java
new file mode 100644
index 000000000..076211b45
--- /dev/null
+++ b/common/src/test/java/dev/cel/common/values/OptimizedSelectTraversalTest.java
@@ -0,0 +1,422 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.common.values;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.testing.junit.testparameterinjector.TestParameter;
+import com.google.testing.junit.testparameterinjector.TestParameterInjector;
+import dev.cel.common.exceptions.CelAttributeNotFoundException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(TestParameterInjector.class)
+public final class OptimizedSelectTraversalTest {
+
+ private static final CelValueConverter DEFAULT_CONVERTER = CelValueConverter.getDefaultInstance();
+
+ private enum TargetType {
+ MAP {
+ @Override
+ Object createTarget(Map data) {
+ return ImmutableMap.copyOf(data);
+ }
+
+ @Override
+ Object createNestedTarget(Map innerData) {
+ return ImmutableMap.of("outer_key", ImmutableMap.copyOf(innerData));
+ }
+ },
+ OPTIMIZED_SELECTABLE {
+ @Override
+ Object createTarget(Map data) {
+ return new FakeOptimizedSelectable(data);
+ }
+
+ @Override
+ Object createNestedTarget(Map innerData) {
+ return new FakeOptimizedSelectable(
+ ImmutableMap.of("outer_key", new FakeOptimizedSelectable(innerData)));
+ }
+ },
+ SELECTABLE_VALUE {
+ @Override
+ Object createTarget(Map data) {
+ return new FakeSelectableValue(data);
+ }
+
+ @Override
+ Object createNestedTarget(Map innerData) {
+ return new FakeSelectableValue(
+ ImmutableMap.of("outer_key", new FakeSelectableValue(innerData)));
+ }
+ };
+
+ abstract Object createTarget(Map data);
+
+ abstract Object createNestedTarget(Map innerData);
+ }
+
+ private enum NestedPresenceTestCase {
+ ALL_PRESENT(
+ ImmutableMap.of("inner_key", "nested_val"), "outer_key", "inner_key", /* expected= */ true),
+ INTERMEDIATE_MISSING(
+ ImmutableMap.of("inner_key", "nested_val"),
+ "missing_outer",
+ "inner_key",
+ /* expected= */ false),
+ TERMINAL_MISSING(
+ ImmutableMap.of("other_key", "nested_val"),
+ "outer_key",
+ "missing_terminal",
+ /* expected= */ false);
+
+ final ImmutableMap innerData;
+ final String outerField;
+ final String innerField;
+ final boolean expected;
+
+ NestedPresenceTestCase(
+ ImmutableMap innerData,
+ String outerField,
+ String innerField,
+ boolean expected) {
+ this.innerData = innerData;
+ this.outerField = outerField;
+ this.innerField = innerField;
+ this.expected = expected;
+ }
+ }
+
+ @Test
+ public void qualify_emptyFields_returnsTargetInstance() {
+ Object target = new Object();
+
+ Object result = OptimizedSelectTraversal.qualify(target, ImmutableList.of(), DEFAULT_CONVERTER);
+
+ assertThat(result).isSameInstanceAs(target);
+ }
+
+ @Test
+ public void qualify_singleField_success(@TestParameter TargetType targetType) {
+ Object target = targetType.createTarget(ImmutableMap.of("key", "value"));
+ ImmutableList fields = ImmutableList.of(SelectField.create(1L, "key", 9, ""));
+
+ Object result = OptimizedSelectTraversal.qualify(target, fields, DEFAULT_CONVERTER);
+
+ assertThat(result).isEqualTo("value");
+ }
+
+ @Test
+ public void qualify_nested_success(@TestParameter TargetType targetType) {
+ Object target = targetType.createNestedTarget(ImmutableMap.of("inner_key", "nested_value"));
+ ImmutableList fields =
+ ImmutableList.of(
+ SelectField.create(1L, "outer_key", SelectField.CEL_MAP_TYPE_CODE, ImmutableMap.of()),
+ SelectField.create(2L, "inner_key", 9, ""));
+
+ Object result = OptimizedSelectTraversal.qualify(target, fields, DEFAULT_CONVERTER);
+
+ assertThat(result).isEqualTo("nested_value");
+ }
+
+ @Test
+ public void qualify_singleField_missingThrowsException(@TestParameter TargetType targetType) {
+ Object target = targetType.createTarget(ImmutableMap.of("present", "value"));
+ ImmutableList fields =
+ ImmutableList.of(SelectField.create(1L, "missing", 9, null));
+
+ CelAttributeNotFoundException thrown =
+ assertThrows(
+ CelAttributeNotFoundException.class,
+ () -> OptimizedSelectTraversal.qualify(target, fields, DEFAULT_CONVERTER));
+
+ assertThat(thrown).hasMessageThat().contains("missing");
+ }
+
+ @Test
+ public void qualify_map_nullValue_throwsException() {
+ Map map = new HashMap<>();
+ map.put("null_key", null);
+ ImmutableList fields = ImmutableList.of(SelectField.create(1L, "null_key"));
+
+ CelAttributeNotFoundException thrown =
+ assertThrows(
+ CelAttributeNotFoundException.class,
+ () -> OptimizedSelectTraversal.qualify(map, fields, DEFAULT_CONVERTER));
+
+ assertThat(thrown).hasMessageThat().contains("Map value cannot be null for key: null_key");
+ }
+
+ @Test
+ public void qualify_optimizedSelectable_absentWithDefaultValue_returnsDefault() {
+ FakeOptimizedSelectable selectable = new FakeOptimizedSelectable(ImmutableMap.of());
+ ImmutableList fields =
+ ImmutableList.of(SelectField.create(1L, "absent", 9, "default_fallback"));
+
+ Object result = OptimizedSelectTraversal.qualify(selectable, fields, DEFAULT_CONVERTER);
+
+ assertThat(result).isEqualTo("default_fallback");
+ }
+
+ @Test
+ public void qualify_selectableValue_absentWithDefaultValue_returnsDefault() {
+ FakeSelectableValue selectable = new FakeSelectableValue(ImmutableMap.of());
+ ImmutableList fields =
+ ImmutableList.of(SelectField.create(1L, "absent", 9, "default_fallback"));
+
+ Object result = OptimizedSelectTraversal.qualify(selectable, fields, DEFAULT_CONVERTER);
+
+ assertThat(result).isEqualTo("default_fallback");
+ }
+
+ @Test
+ public void qualify_rootMap_convertsUnadaptedEntryWithConverter() {
+ TrackingConverter customConverter = new TrackingConverter();
+ ImmutableMap rootMap = ImmutableMap.of("step1", "adapt_to_selectable");
+ ImmutableList fields =
+ ImmutableList.of(SelectField.create(1L, "step1"), SelectField.create(2L, "leaf", 9, ""));
+
+ Object result = OptimizedSelectTraversal.qualify(rootMap, fields, customConverter);
+
+ assertThat(result).isEqualTo("custom_adapted");
+ assertThat(customConverter.callCount.get()).isAtLeast(1);
+ }
+
+ @Test
+ public void qualify_unsupportedTarget_throwsException() {
+ ImmutableList fields = ImmutableList.of(SelectField.create(1L, "invalid_field"));
+
+ CelAttributeNotFoundException thrown =
+ assertThrows(
+ CelAttributeNotFoundException.class,
+ () -> OptimizedSelectTraversal.qualify(12345L, fields, DEFAULT_CONVERTER));
+
+ assertThat(thrown).hasMessageThat().contains("invalid_field");
+ }
+
+ @Test
+ public void qualify_intermediateUnsupportedTarget_throwsException() {
+ ImmutableMap map = ImmutableMap.of("scalar", 999L);
+ ImmutableList fields =
+ ImmutableList.of(
+ SelectField.create(1L, "scalar", 3, 0L), SelectField.create(2L, "unreachable", 9, ""));
+
+ CelAttributeNotFoundException thrown =
+ assertThrows(
+ CelAttributeNotFoundException.class,
+ () -> OptimizedSelectTraversal.qualify(map, fields, DEFAULT_CONVERTER));
+
+ assertThat(thrown).hasMessageThat().contains("unreachable");
+ }
+
+ @Test
+ public void hasField_emptyFields_returnsFalse() {
+ Object target = ImmutableMap.of("key", "value");
+
+ boolean hasField =
+ OptimizedSelectTraversal.hasField(target, ImmutableList.of(), DEFAULT_CONVERTER);
+
+ assertThat(hasField).isFalse();
+ }
+
+ @Test
+ public void hasField_singleField(
+ @TestParameter TargetType targetType,
+ @TestParameter({"present_key", "missing_key"}) String queryKey) {
+ Object target = targetType.createTarget(ImmutableMap.of("present_key", "val"));
+ ImmutableList fields = ImmutableList.of(SelectField.create(1L, queryKey));
+
+ boolean hasField = OptimizedSelectTraversal.hasField(target, fields, DEFAULT_CONVERTER);
+
+ assertThat(hasField).isEqualTo(queryKey.equals("present_key"));
+ }
+
+ @Test
+ public void hasField_nestedFields(
+ @TestParameter TargetType targetType, @TestParameter NestedPresenceTestCase testCase) {
+ Object target = targetType.createNestedTarget(testCase.innerData);
+ ImmutableList fields =
+ ImmutableList.of(
+ SelectField.create(1L, testCase.outerField),
+ SelectField.create(2L, testCase.innerField));
+
+ boolean hasField = OptimizedSelectTraversal.hasField(target, fields, DEFAULT_CONVERTER);
+
+ assertThat(hasField).isEqualTo(testCase.expected);
+ }
+
+ @Test
+ public void hasField_map_terminalNullValue_returnsTrue() {
+ Map map = new HashMap<>();
+ map.put("null_key", null);
+ ImmutableList fields = ImmutableList.of(SelectField.create(1L, "null_key"));
+
+ boolean hasField = OptimizedSelectTraversal.hasField(map, fields, DEFAULT_CONVERTER);
+
+ assertThat(hasField).isTrue();
+ }
+
+ @Test
+ public void hasField_map_intermediateNullValue_throwsException() {
+ Map map = new HashMap<>();
+ map.put("child", null);
+ ImmutableList fields =
+ ImmutableList.of(SelectField.create(1L, "child"), SelectField.create(2L, "leaf"));
+
+ CelAttributeNotFoundException thrown =
+ assertThrows(
+ CelAttributeNotFoundException.class,
+ () -> OptimizedSelectTraversal.hasField(map, fields, DEFAULT_CONVERTER));
+
+ assertThat(thrown).hasMessageThat().contains("Map value cannot be null for key: child");
+ }
+
+ @Test
+ public void hasField_intermediateUnsupportedTarget_throwsException() {
+ ImmutableMap map = ImmutableMap.of("scalar_key", 100L);
+ ImmutableList fields =
+ ImmutableList.of(SelectField.create(1L, "scalar_key"), SelectField.create(2L, "child_key"));
+
+ CelAttributeNotFoundException thrown =
+ assertThrows(
+ CelAttributeNotFoundException.class,
+ () -> OptimizedSelectTraversal.hasField(map, fields, DEFAULT_CONVERTER));
+
+ assertThat(thrown).hasMessageThat().contains("child_key");
+ }
+
+ @Test
+ public void hasField_rootMap_convertsUnadaptedEntryWithConverter() {
+ TrackingConverter customConverter = new TrackingConverter();
+ ImmutableMap rootMap = ImmutableMap.of("step1", "adapt_to_selectable");
+ ImmutableList fields =
+ ImmutableList.of(SelectField.create(1L, "step1"), SelectField.create(2L, "leaf"));
+
+ boolean hasField = OptimizedSelectTraversal.hasField(rootMap, fields, customConverter);
+
+ assertThat(hasField).isTrue();
+ assertThat(customConverter.callCount.get()).isAtLeast(1);
+ }
+
+ @Test
+ public void hasField_unsupportedTarget_throwsException() {
+ ImmutableList fields = ImmutableList.of(SelectField.create(1L, "invalid_field"));
+
+ CelAttributeNotFoundException thrown =
+ assertThrows(
+ CelAttributeNotFoundException.class,
+ () -> OptimizedSelectTraversal.hasField(12345L, fields, DEFAULT_CONVERTER));
+
+ assertThat(thrown).hasMessageThat().contains("invalid_field");
+ }
+
+ @Test
+ public void qualify_errorValue_propagatesError() {
+ ErrorValue error = ErrorValue.create(1L, new RuntimeException("test error"));
+ ImmutableList fields =
+ ImmutableList.of(SelectField.create(1L, "field1"), SelectField.create(2L, "field2"));
+
+ Object result = OptimizedSelectTraversal.qualify(error, fields, DEFAULT_CONVERTER);
+
+ assertThat(result).isSameInstanceAs(error);
+ }
+
+ @Test
+ public void hasField_errorValue_returnsFalse() {
+ ErrorValue error = ErrorValue.create(1L, new RuntimeException("test error"));
+ ImmutableList fields =
+ ImmutableList.of(SelectField.create(1L, "field1"), SelectField.create(2L, "field2"));
+
+ boolean result = OptimizedSelectTraversal.hasField(error, fields, DEFAULT_CONVERTER);
+
+ assertThat(result).isFalse();
+ }
+
+ @SuppressWarnings("Immutable")
+ private static class FakeOptimizedSelectable implements OptimizedSelectable {
+ private final ImmutableMap values;
+
+ @Override
+ public Object selectByFieldNumber(SelectField field) {
+ Object value = values.get(field.fieldName());
+ if (value != null) {
+ return value;
+ }
+ if (field.defaultValue() != null) {
+ return field.defaultValue();
+ }
+ throw CelAttributeNotFoundException.forFieldResolution(field.fieldName());
+ }
+
+ @Override
+ public boolean hasFieldByNumber(SelectField field) {
+ return values.containsKey(field.fieldName());
+ }
+
+ @Override
+ public Optional findByFieldNumber(SelectField field) {
+ return Optional.ofNullable(values.get(field.fieldName()));
+ }
+
+ FakeOptimizedSelectable(Map values) {
+ this.values = ImmutableMap.copyOf(values);
+ }
+ }
+
+ @SuppressWarnings("Immutable")
+ private static final class FakeSelectableValue implements SelectableValue {
+ private final ImmutableMap values;
+
+ @Override
+ public Object select(String field) {
+ Object value = values.get(field);
+ if (value != null) {
+ return value;
+ }
+ throw CelAttributeNotFoundException.forFieldResolution(field);
+ }
+
+ @Override
+ public Optional find(String field) {
+ return Optional.ofNullable(values.get(field));
+ }
+
+ FakeSelectableValue(Map values) {
+ this.values = ImmutableMap.copyOf(values);
+ }
+ }
+
+ @SuppressWarnings("Immutable")
+ private static final class TrackingConverter extends CelValueConverter {
+ private final AtomicInteger callCount = new AtomicInteger();
+
+ @Override
+ public Object toRuntimeValue(Object value) {
+ callCount.incrementAndGet();
+ if ("adapt_to_selectable".equals(value)) {
+ return new FakeOptimizedSelectable(ImmutableMap.of("leaf", "custom_adapted"));
+ }
+ return super.toRuntimeValue(value);
+ }
+ }
+}
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..8481df0c8 100644
--- a/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java
+++ b/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java
@@ -15,35 +15,45 @@
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 java.util.Optional;
import org.junit.Test;
import org.junit.runner.RunWith;
@RunWith(TestParameterInjector.class)
-public class ProtoMessageLiteValueTest {
+public final class ProtoMessageLiteValueTest {
private static final CelLiteDescriptorPool DESCRIPTOR_POOL =
DefaultLiteDescriptorPool.newInstance(
ImmutableSet.of(TestAllTypesCelDescriptor.getDescriptor()));
@@ -153,19 +163,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 +261,311 @@ 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 selectByFieldNumber_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.selectByFieldNumber(SelectField.create(14L, "single_string", 9, "default"));
+
+ assertThat(result).isEqualTo("foo");
+ }
+
+ @Test
+ public void selectByFieldNumber_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.selectByFieldNumber(SelectField.create(999L, "unknown_field", 3, 0L));
+
+ assertThat(result).isEqualTo(42L);
+ }
+
+ @Test
+ public void selectByFieldNumber_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.selectByFieldNumber(
+ SelectField.create(999L, "unknown_repeated", 3, ImmutableList.of()));
+
+ assertThat((Iterable>) result).containsExactly(10L, 20L).inOrder();
+ }
+
+ @Test
+ public void selectByFieldNumber_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.selectByFieldNumber(SelectField.create(14L, "renamed_string", 9, "default"));
+
+ assertThat(result).isEqualTo("foo");
+ }
+
+ @Test
+ public void selectByFieldNumber_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.selectByFieldNumber(SelectField.create(61L, "renamed_map", -1, ImmutableMap.of()));
+
+ assertThat(result).isEqualTo(ImmutableMap.of("k", "v"));
+ }
+
+ @Test
+ public void selectByFieldNumber_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.selectByFieldNumber(
+ SelectField.create(32L, "renamed_repeated", 3, ImmutableList.of()));
+
+ assertThat(result).isEqualTo(ImmutableList.of(10L, 20L));
+ }
+
+ @Test
+ public void findByFieldNumber_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);
+
+ Optional nav = val.findByFieldNumber(SelectField.create(998L, "unknown_submessage"));
+
+ assertThat(nav.map(v -> v instanceof RawProtoMessageLiteValue)).hasValue(true);
+ }
+
+ @Test
+ public void hasFieldByNumber_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.hasFieldByNumber(SelectField.create(14L, "single_string"))).isTrue();
+ }
+
+ @Test
+ public void hasFieldByNumber_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.hasFieldByNumber(SelectField.create(999L, "unknown_present"))).isTrue();
+ }
+
+ @Test
+ public void hasFieldByNumber_unknownFieldAbsent_returnsFalse() {
+ ProtoMessageLiteValue val =
+ ProtoMessageLiteValue.create(
+ TestAllTypes.getDefaultInstance(),
+ "cel.expr.conformance.proto3.TestAllTypes",
+ PROTO_LITE_CEL_VALUE_CONVERTER);
+
+ assertThat(val.hasFieldByNumber(SelectField.create(888L, "unknown_absent"))).isFalse();
+ }
+
+ @Test
+ public void hasFieldByNumber_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.hasFieldByNumber(SelectField.create(14L, "renamed_string"))).isTrue();
+ }
+
+ @Test
+ public void hasFieldByNumber_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.hasFieldByNumber(SelectField.create(61L, "renamed_map"))).isTrue();
+ }
+
+ @Test
+ public void hasFieldByNumber_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.hasFieldByNumber(SelectField.create(32L, "renamed_repeated"))).isTrue();
+ }
+
+ @Test
+ public void qualify_emptyList_returnsSameInstance() {
+ ProtoMessageLiteValue val =
+ ProtoMessageLiteValue.create(
+ TestAllTypes.getDefaultInstance(),
+ "cel.expr.conformance.proto3.TestAllTypes",
+ PROTO_LITE_CEL_VALUE_CONVERTER);
+
+ assertThat(
+ OptimizedSelectTraversal.qualify(
+ val, ImmutableList.of(), PROTO_LITE_CEL_VALUE_CONVERTER))
+ .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(
+ OptimizedSelectTraversal.hasField(
+ val, ImmutableList.of(), PROTO_LITE_CEL_VALUE_CONVERTER))
+ .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);
+ ImmutableList fields =
+ ImmutableList.of(
+ SelectField.create(
+ 61L, "map_string_string", SelectField.CEL_MAP_TYPE_CODE, ImmutableMap.of()),
+ SelectField.create(1L, "k", 9, ""));
+
+ Object result = OptimizedSelectTraversal.qualify(val, fields, PROTO_LITE_CEL_VALUE_CONVERTER);
+
+ 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(
+ OptimizedSelectTraversal.hasField(
+ val,
+ ImmutableList.of(
+ SelectField.create(61L, "map_string_string"), SelectField.create(1L, "k")),
+ PROTO_LITE_CEL_VALUE_CONVERTER))
+ .isTrue();
+ assertThat(
+ OptimizedSelectTraversal.hasField(
+ val,
+ ImmutableList.of(
+ SelectField.create(61L, "map_string_string"),
+ SelectField.create(1L, "missing")),
+ PROTO_LITE_CEL_VALUE_CONVERTER))
+ .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);
+ ImmutableList fields =
+ ImmutableList.of(
+ SelectField.create(2L, "single_int64", 3, 0L),
+ SelectField.create(3L, "leaf_field", 9, ""));
+
+ CelAttributeNotFoundException thrown =
+ assertThrows(
+ CelAttributeNotFoundException.class,
+ () -> OptimizedSelectTraversal.qualify(val, fields, PROTO_LITE_CEL_VALUE_CONVERTER));
+
+ 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);
+ ImmutableList fields =
+ ImmutableList.of(
+ SelectField.create(2L, "single_int64"), SelectField.create(3L, "leaf_field"));
+
+ CelAttributeNotFoundException thrown =
+ assertThrows(
+ CelAttributeNotFoundException.class,
+ () -> OptimizedSelectTraversal.hasField(val, fields, PROTO_LITE_CEL_VALUE_CONVERTER));
+
+ 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..bbb28b15f
--- /dev/null
+++ b/common/src/test/java/dev/cel/common/values/RawProtoMessageLiteValueTest.java
@@ -0,0 +1,1021 @@
+// 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.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 java.util.Optional;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class RawProtoMessageLiteValueTest {
+
+ private static final ProtoLiteCelValueConverter CONVERTER =
+ ProtoLiteCelValueConverter.newInstance(
+ DefaultLiteDescriptorPool.newInstance(
+ ImmutableSet.of(TestAllTypesCelDescriptor.getDescriptor())));
+
+ @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_throwsCelAttributeNotFoundException() {
+ RawProtoMessageLiteValue value =
+ RawProtoMessageLiteValue.create(ByteString.EMPTY, "custom.Message");
+
+ assertThrows(CelAttributeNotFoundException.class, () -> value.find("field"));
+ }
+
+ @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 selectByFieldNumber_presentOnWire_decoded() throws Exception {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ CodedOutputStream cos = CodedOutputStream.newInstance(baos);
+ cos.writeString(14, "hello");
+ cos.flush();
+ RawProtoMessageLiteValue raw =
+ RawProtoMessageLiteValue.create(
+ ByteString.copyFrom(baos.toByteArray()), "cel.expr.conformance.proto3.TestAllTypes");
+
+ Object val = raw.selectByFieldNumber(SelectField.create(14L, "single_string", 9, ""));
+
+ assertThat(val).isEqualTo("hello");
+ }
+
+ @Test
+ public void selectByFieldNumber_absentWithDefaultValue_returnsDefault() {
+ RawProtoMessageLiteValue raw =
+ RawProtoMessageLiteValue.create(
+ ByteString.EMPTY, "cel.expr.conformance.proto3.TestAllTypes");
+
+ Object val = raw.selectByFieldNumber(SelectField.create(14L, "single_string", 9, "default"));
+
+ assertThat(val).isEqualTo("default");
+ }
+
+ @Test
+ public void selectByFieldNumber_withConverter_resolvesDescriptor() {
+ RawProtoMessageLiteValue raw =
+ RawProtoMessageLiteValue.create(
+ ByteString.EMPTY, "cel.expr.conformance.proto3.TestAllTypes", CONVERTER);
+
+ Object val = raw.selectByFieldNumber(SelectField.create(14L, "single_string"));
+
+ assertThat(val).isEqualTo("");
+ }
+
+ @Test
+ public void hasFieldByNumber_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.hasFieldByNumber(SelectField.create(14L, "single_string"))).isTrue();
+ }
+
+ @Test
+ public void hasFieldByNumber_wireAbsent_returnsFalse() {
+ RawProtoMessageLiteValue raw =
+ RawProtoMessageLiteValue.create(
+ ByteString.EMPTY, "cel.expr.conformance.proto3.TestAllTypes");
+
+ assertThat(raw.hasFieldByNumber(SelectField.create(15L, "other_field"))).isFalse();
+ }
+
+ @Test
+ public void hasFieldByNumber_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.hasFieldByNumber(SelectField.create(31L, "repeated_int32"))).isFalse();
+ }
+
+ @Test
+ public void hasFieldByNumber_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.hasFieldByNumber(SelectField.create(31L, "repeated_int32", 5, ImmutableList.of())))
+ .isFalse();
+ }
+
+ @Test
+ public void hasFieldByNumber_nonEmptyPackedRepeated_returnsTrue() throws Exception {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ CodedOutputStream cos = CodedOutputStream.newInstance(baos);
+ ByteArrayOutputStream packed = new ByteArrayOutputStream();
+ CodedOutputStream packedCos = CodedOutputStream.newInstance(packed);
+ packedCos.writeInt32NoTag(42);
+ packedCos.flush();
+ cos.writeBytes(31, ByteString.copyFrom(packed.toByteArray()));
+ cos.flush();
+ RawProtoMessageLiteValue raw =
+ RawProtoMessageLiteValue.create(
+ ByteString.copyFrom(baos.toByteArray()),
+ "cel.expr.conformance.proto3.TestAllTypes",
+ CONVERTER);
+
+ assertThat(raw.hasFieldByNumber(SelectField.create(31L, "repeated_int32"))).isTrue();
+ }
+
+ @Test
+ public void findByFieldNumber_intermediatePresent_returnsSubmessage() 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");
+
+ Optional nav = raw.findByFieldNumber(SelectField.create(21L, "single_nested_message"));
+
+ assertThat(nav.map(v -> v instanceof RawProtoMessageLiteValue)).hasValue(true);
+ }
+
+ @Test
+ public void findByFieldNumber_intermediateAbsent_returnsEmpty() {
+ RawProtoMessageLiteValue raw =
+ RawProtoMessageLiteValue.create(
+ ByteString.EMPTY, "cel.expr.conformance.proto3.TestAllTypes");
+
+ Optional nav = raw.findByFieldNumber(SelectField.create(999L, "absent"));
+
+ assertThat(nav).isEmpty();
+ }
+
+ @Test
+ public void selectByFieldNumber_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.selectByFieldNumber(SelectField.create(61L, "map_string_string", -1, ImmutableMap.of()));
+
+ assertThat(selected).isEqualTo(ImmutableMap.of("k1", "v1", "k2", "v2"));
+ }
+
+ @Test
+ public void selectByFieldNumber_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.selectByFieldNumber(SelectField.create(101L, "single_duration", 11, Duration.ZERO));
+
+ assertThat(selected).isEqualTo(Duration.ofSeconds(10L, 500L));
+ }
+
+ @Test
+ public void selectByFieldNumber_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.selectByFieldNumber(
+ SelectField.create(
+ TestAllTypes.SINGLE_INT64_WRAPPER_FIELD_NUMBER,
+ "single_int64_wrapper",
+ 11,
+ NullValue.NULL_VALUE));
+
+ assertThat(selected).isEqualTo(12345L);
+ }
+
+ @Test
+ public void findByFieldNumber_scalarFieldWithDescriptor_returnsScalar() {
+ TestAllTypes proto = TestAllTypes.newBuilder().setSingleInt64(99L).build();
+ RawProtoMessageLiteValue raw =
+ RawProtoMessageLiteValue.create(
+ proto.toByteString(), "cel.expr.conformance.proto3.TestAllTypes", CONVERTER);
+
+ Optional nav = raw.findByFieldNumber(SelectField.create(2L, "single_int64"));
+
+ assertThat(nav).hasValue(99L);
+ }
+
+ @Test
+ public void findByFieldNumber_scalarFieldWithoutDescriptor_returnsScalar() {
+ TestAllTypes proto = TestAllTypes.newBuilder().setSingleInt64(99L).build();
+ RawProtoMessageLiteValue raw =
+ RawProtoMessageLiteValue.create(
+ proto.toByteString(), "cel.expr.conformance.proto3.TestAllTypes");
+
+ Optional nav = raw.findByFieldNumber(SelectField.create(2L, "single_int64"));
+
+ assertThat(nav).hasValue(99L);
+ }
+
+ @Test
+ public void
+ selectByFieldNumber_mapInt32Bytes_withOmittedAndPresentKeyAndValue_decodesDefaultsAndConvertsTypes() {
+ TestAllTypes proto =
+ TestAllTypes.newBuilder()
+ .putMapInt32Bytes(0, ByteString.copyFromUtf8("val_for_default_key"))
+ .putMapInt32Bytes(42, ByteString.EMPTY)
+ .build();
+ RawProtoMessageLiteValue raw =
+ RawProtoMessageLiteValue.create(
+ proto.toByteString(), "cel.expr.conformance.proto3.TestAllTypes", CONVERTER);
+
+ Object selected =
+ raw.selectByFieldNumber(
+ SelectField.create(
+ TestAllTypes.MAP_INT32_BYTES_FIELD_NUMBER,
+ "map_int32_bytes",
+ -1,
+ ImmutableMap.of()));
+
+ assertThat(selected)
+ .isEqualTo(
+ ImmutableMap.of(
+ 0L, CelByteString.copyFromUtf8("val_for_default_key"), 42L, CelByteString.EMPTY));
+ }
+}
diff --git a/common/src/test/java/dev/cel/common/values/SelectFieldTest.java b/common/src/test/java/dev/cel/common/values/SelectFieldTest.java
new file mode 100644
index 000000000..ba9dc7008
--- /dev/null
+++ b/common/src/test/java/dev/cel/common/values/SelectFieldTest.java
@@ -0,0 +1,133 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package dev.cel.common.values;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import com.google.common.testing.EqualsTester;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public final class SelectFieldTest {
+
+ @Test
+ public void create_twoArguments_success() {
+ SelectField field = SelectField.create(1L, "foo");
+
+ assertThat(field.fieldNumber()).isEqualTo(1);
+ assertThat(field.fieldName()).isEqualTo("foo");
+ assertThat(field.typeCode()).isEqualTo(SelectField.NO_TYPE_CODE);
+ assertThat(field.defaultValue()).isNull();
+ }
+
+ @Test
+ public void create_fourArguments_success() {
+ SelectField field = SelectField.create(2L, "bar", 9, "default_str");
+
+ assertThat(field.fieldNumber()).isEqualTo(2);
+ assertThat(field.fieldName()).isEqualTo("bar");
+ assertThat(field.typeCode()).isEqualTo(9);
+ assertThat(field.defaultValue()).isEqualTo("default_str");
+ }
+
+ @Test
+ public void create_mapTypeCode_success() {
+ SelectField field = SelectField.create(3L, "map_field", -1, null);
+
+ assertThat(field.typeCode()).isEqualTo(-1);
+ }
+
+ @Test
+ public void create_twoArgNullFieldName_throwsNullPointerException() {
+ assertThrows(NullPointerException.class, () -> SelectField.create(1L, null));
+ }
+
+ @Test
+ public void create_fourArgNullFieldName_throwsNullPointerException() {
+ assertThrows(NullPointerException.class, () -> SelectField.create(1L, null, 9, null));
+ }
+
+ @Test
+ public void create_fieldNumberBelowMinimum_throwsIllegalArgumentException() {
+ IllegalArgumentException thrown =
+ assertThrows(IllegalArgumentException.class, () -> SelectField.create(0L, "foo"));
+
+ assertThat(thrown).hasMessageThat().contains("Field number out of protobuf range: 0");
+ }
+
+ @Test
+ public void create_fieldNumberNegative_throwsIllegalArgumentException() {
+ IllegalArgumentException thrown =
+ assertThrows(IllegalArgumentException.class, () -> SelectField.create(-1L, "foo"));
+
+ assertThat(thrown).hasMessageThat().contains("Field number out of protobuf range: -1");
+ }
+
+ @Test
+ public void create_fieldNumberAboveMaximum_throwsIllegalArgumentException() {
+ IllegalArgumentException thrown =
+ assertThrows(IllegalArgumentException.class, () -> SelectField.create(536870912L, "foo"));
+
+ assertThat(thrown).hasMessageThat().contains("Field number out of protobuf range: 536870912");
+ }
+
+ @Test
+ public void create_typeCodeZero_throwsIllegalArgumentException() {
+ IllegalArgumentException thrown =
+ assertThrows(IllegalArgumentException.class, () -> SelectField.create(1L, "foo", 0, null));
+
+ assertThat(thrown).hasMessageThat().contains("Invalid protobuf type code: 0");
+ }
+
+ @Test
+ public void create_typeCodeAboveMaximum_throwsIllegalArgumentException() {
+ IllegalArgumentException thrown =
+ assertThrows(IllegalArgumentException.class, () -> SelectField.create(1L, "foo", 19, null));
+
+ assertThat(thrown).hasMessageThat().contains("Invalid protobuf type code: 19");
+ }
+
+ @Test
+ public void create_typeCodeBelowSentinel_throwsIllegalArgumentException() {
+ IllegalArgumentException thrown =
+ assertThrows(IllegalArgumentException.class, () -> SelectField.create(1L, "foo", -2, null));
+
+ assertThat(thrown).hasMessageThat().contains("Invalid protobuf type code: -2");
+ }
+
+ @Test
+ public void create_typeCodeGroupProto_throwsIllegalArgumentException() {
+ IllegalArgumentException thrown =
+ assertThrows(IllegalArgumentException.class, () -> SelectField.create(1L, "foo", 10, null));
+
+ assertThat(thrown).hasMessageThat().contains("Invalid protobuf type code: 10");
+ }
+
+ @Test
+ public void equalsAndHashCode_testedProperly() {
+ new EqualsTester()
+ .addEqualityGroup(SelectField.create(1L, "foo"), SelectField.create(1L, "foo"))
+ .addEqualityGroup(SelectField.create(2L, "foo"), SelectField.create(2L, "foo"))
+ .addEqualityGroup(SelectField.create(1L, "bar"), SelectField.create(1L, "bar"))
+ .addEqualityGroup(
+ SelectField.create(1L, "foo", 9, "default"),
+ SelectField.create(1L, "foo", 9, "default"))
+ .addEqualityGroup(SelectField.create(1L, "foo", 9, "other_default"))
+ .testEquals();
+ }
+}