From b956512699d964bc245a782b83c95b577a1b7580 Mon Sep 17 00:00:00 2001 From: Sean Huh Date: Tue, 20 Oct 2026 17:44:10 -0700 Subject: [PATCH] Unpack google.protobuf.Any in the lite runtime's value converter. PiperOrigin-RevId: 1003531493 --- .../java/dev/cel/common/values/BUILD.bazel | 1 + .../values/ProtoLiteCelValueConverter.java | 33 +++++++ .../ProtoLiteCelValueConverterTest.java | 87 ++++++++++++++++++- .../values/ProtoMessageLiteValueTest.java | 5 +- .../CelLiteRuntimeVersionSkewTest.java | 24 ++++- 5 files changed, 145 insertions(+), 5 deletions(-) diff --git a/common/src/main/java/dev/cel/common/values/BUILD.bazel b/common/src/main/java/dev/cel/common/values/BUILD.bazel index 70e2d79da..774f720e4 100644 --- a/common/src/main/java/dev/cel/common/values/BUILD.bazel +++ b/common/src/main/java/dev/cel/common/values/BUILD.bazel @@ -345,6 +345,7 @@ java_library( "//protobuf:cel_lite_descriptor", "@maven//:com_google_errorprone_error_prone_annotations", "@maven//:com_google_guava_guava", + "@maven//:com_google_protobuf_protobuf_java", "@maven//:org_jspecify_jspecify", "@maven_android//:com_google_protobuf_protobuf_javalite", ], diff --git a/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java b/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java index 5653e3760..40d74924d 100644 --- a/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java +++ b/common/src/main/java/dev/cel/common/values/ProtoLiteCelValueConverter.java @@ -22,12 +22,14 @@ import com.google.common.collect.Iterables; import com.google.common.primitives.UnsignedLong; import com.google.errorprone.annotations.Immutable; +import com.google.protobuf.Any; import com.google.protobuf.ByteString; import com.google.protobuf.CodedInputStream; import com.google.protobuf.ExtensionRegistryLite; import com.google.protobuf.Internal.EnumLite; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.MessageLite; +import com.google.protobuf.MessageLiteOrBuilder; import com.google.protobuf.WireFormat; import dev.cel.common.annotations.Internal; import dev.cel.common.internal.CelLiteDescriptorPool; @@ -62,6 +64,9 @@ public final class ProtoLiteCelValueConverter extends BaseProtoCelValueConverter private static final String MAP_VALUE_FIELD_NAME = "value"; private static final int MAP_KEY_FIELD_NUMBER = 1; private static final int MAP_VALUE_FIELD_NUMBER = 2; + // Protobuf's default recursion limit. Nested Anys are unwrapped in a loop since each payload is + // parsed afresh, outside that limit. + private static final int MAX_ANY_NESTING_DEPTH = 100; private final CelLiteDescriptorPool descriptorPool; @@ -218,6 +223,34 @@ private Object toRuntimeValue(MessageLite msg, MessageLiteDescriptor descriptor) return fromWellKnownProto(msg, checkNotNull(wellKnownProto)); } + @Override + protected Object fromWellKnownProto(MessageLiteOrBuilder msg, WellKnownProto wellKnownProto) { + if (wellKnownProto != WellKnownProto.ANY_VALUE) { + return super.fromWellKnownProto(msg, wellKnownProto); + } + Any any = (Any) msg; + for (int depth = 0; depth < MAX_ANY_NESTING_DEPTH; depth++) { + String typeUrl = any.getTypeUrl(); + int pos = typeUrl.lastIndexOf('/'); + if (pos < 0 || pos == typeUrl.length() - 1) { + throw new IllegalArgumentException( + "Unpacking failed for message: google.protobuf.Any (malformed type URL: '" + + typeUrl + + "')"); + } + // Same path as a message field. + Object unpacked = readMessageField(any.getValue(), typeUrl.substring(pos + 1)); + if (!(unpacked instanceof Any)) { + return finalizeFieldValue(unpacked); + } + any = (Any) unpacked; + } + throw new IllegalArgumentException( + "Unpacking failed for message: google.protobuf.Any (nesting exceeds " + + MAX_ANY_NESTING_DEPTH + + ")"); + } + private Object getDefaultValue(FieldLiteDescriptor fieldDescriptor) { EncodingType encodingType = fieldDescriptor.getEncodingType(); switch (encodingType) { diff --git a/common/src/test/java/dev/cel/common/values/ProtoLiteCelValueConverterTest.java b/common/src/test/java/dev/cel/common/values/ProtoLiteCelValueConverterTest.java index ed8d68b7c..f4c80a862 100644 --- a/common/src/test/java/dev/cel/common/values/ProtoLiteCelValueConverterTest.java +++ b/common/src/test/java/dev/cel/common/values/ProtoLiteCelValueConverterTest.java @@ -22,6 +22,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.primitives.UnsignedLong; import com.google.common.truth.Expect; +import com.google.protobuf.Any; import com.google.protobuf.BoolValue; import com.google.protobuf.ByteString; import com.google.protobuf.BytesValue; @@ -33,6 +34,7 @@ import com.google.protobuf.FloatValue; import com.google.protobuf.Int32Value; import com.google.protobuf.Int64Value; +import com.google.protobuf.Message; import com.google.protobuf.MessageLite; import com.google.protobuf.StringValue; import com.google.protobuf.TextFormat; @@ -148,10 +150,10 @@ private enum WellKnownProtoTestCase { UINT64(UInt64Value.of(1L), UnsignedLong.valueOf(1L)), ; - private final MessageLite msg; + private final Message msg; private final Object value; - WellKnownProtoTestCase(MessageLite msg, Object value) { + WellKnownProtoTestCase(Message msg, Object value) { this.msg = msg; this.value = value; } @@ -176,6 +178,87 @@ public void fromProtoMessageToCelValue_fieldMask_returnsProtoMessageLiteValue() .isEqualTo(ImmutableList.of("foo", "bar")); } + @Test + public void toRuntimeValue_anyWithWellKnownProto_unpacksToCelValue( + @TestParameter WellKnownProtoTestCase testCase) { + Object adaptedValue = PROTO_LITE_CEL_VALUE_CONVERTER.toRuntimeValue(Any.pack(testCase.msg)); + + assertThat(adaptedValue).isEqualTo(testCase.value); + } + + @Test + public void toRuntimeValue_anyWithRegisteredMessage_returnsProtoMessageLiteValue() { + TestAllTypes msg = TestAllTypes.newBuilder().setSingleInt64(42L).build(); + + Object adaptedValue = PROTO_LITE_CEL_VALUE_CONVERTER.toRuntimeValue(Any.pack(msg)); + + assertThat(adaptedValue).isInstanceOf(ProtoMessageLiteValue.class); + assertThat(((ProtoMessageLiteValue) adaptedValue).value()).isEqualTo(msg); + } + + @Test + public void toRuntimeValue_anyWithUnregisteredMessage_returnsRawValueOfPackedType() { + ProtoLiteCelValueConverter wellKnownOnlyConverter = + ProtoLiteCelValueConverter.newInstance(DefaultLiteDescriptorPool.newInstance()); + TestAllTypes msg = TestAllTypes.newBuilder().setSingleInt64(42L).build(); + + Object adaptedValue = wellKnownOnlyConverter.toRuntimeValue(Any.pack(msg)); + + assertThat(adaptedValue).isInstanceOf(RawProtoMessageLiteValue.class); + RawProtoMessageLiteValue rawValue = (RawProtoMessageLiteValue) adaptedValue; + assertThat(rawValue.protoTypeName()).isEqualTo(PROTO3_TEST_ALL_TYPES); + assertThat(rawValue.toByteString()).isEqualTo(msg.toByteString()); + } + + @Test + public void toRuntimeValue_anyNestedToMaxDepth_unpacks() { + Object adaptedValue = PROTO_LITE_CEL_VALUE_CONVERTER.toRuntimeValue(nestedAny(100)); + + assertThat(adaptedValue).isEqualTo(42L); + } + + @Test + public void toRuntimeValue_anyNestedBeyondMaxDepth_throws() { + Any any = nestedAny(101); + + IllegalArgumentException thrown = + assertThrows( + IllegalArgumentException.class, + () -> PROTO_LITE_CEL_VALUE_CONVERTER.toRuntimeValue(any)); + + assertThat(thrown) + .hasMessageThat() + .isEqualTo("Unpacking failed for message: google.protobuf.Any (nesting exceeds 100)"); + } + + @Test + public void toRuntimeValue_anyWithMalformedTypeUrl_throws( + @TestParameter({"", "type.googleapis.com/"}) String typeUrl) { + Any any = + Any.newBuilder().setTypeUrl(typeUrl).setValue(Int64Value.of(1L).toByteString()).build(); + + IllegalArgumentException thrown = + assertThrows( + IllegalArgumentException.class, + () -> PROTO_LITE_CEL_VALUE_CONVERTER.toRuntimeValue(any)); + + assertThat(thrown) + .hasMessageThat() + .isEqualTo( + "Unpacking failed for message: google.protobuf.Any (malformed type URL: '" + + typeUrl + + "')"); + } + + /** Returns {@code layers} Anys nested around an {@code Int64Value} of 42. */ + private static Any nestedAny(int layers) { + Any any = Any.pack(Int64Value.of(42L)); + for (int i = 1; i < layers; i++) { + any = Any.pack(any); + } + return any; + } + /** Test cases for repeated_int64: 1L,2L,3L */ @SuppressWarnings("ImmutableEnumChecker") // Test only private enum RepeatedFieldBytesTestCase { 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 a5a7c1d47..9f4bb46b1 100644 --- a/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java +++ b/common/src/test/java/dev/cel/common/values/ProtoMessageLiteValueTest.java @@ -426,6 +426,9 @@ private enum SelectFieldTestCase { DOUBLE_WRAPPER("single_double_wrapper", 8.5d), STRING_WRAPPER("single_string_wrapper", "hello"), BYTES_WRAPPER("single_bytes_wrapper", CelByteString.of(new byte[] {0x02})), + ANY("single_any", true), + REPEATED_ANY("repeated_any", ImmutableList.of(true)), + MAP_STRING_ANY("map_string_any", ImmutableMap.of("key", true)), REPEATED_INT64("repeated_int64", ImmutableList.of(5L, 6L)), REPEATED_UINT64( "repeated_uint64", ImmutableList.of(UnsignedLong.valueOf(7L), UnsignedLong.valueOf(8L))), @@ -473,8 +476,6 @@ private enum SelectFieldTestCase { @Test public void selectField_success(@TestParameter SelectFieldTestCase testCase) { - // Converting Any isn't supported in lite yet, so it's set in a singular, a repeated and a map - // field to verify that selecting any other field doesn't attempt to convert it. Any any = Any.pack(DynamicMessage.newBuilder(BoolValue.of(true)).build()); TestAllTypes testAllTypes = TestAllTypes.newBuilder() diff --git a/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeVersionSkewTest.java b/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeVersionSkewTest.java index bfec9551c..9b33582c9 100644 --- a/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeVersionSkewTest.java +++ b/runtime/src/test/java/dev/cel/runtime/CelLiteRuntimeVersionSkewTest.java @@ -24,6 +24,7 @@ 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.ByteString; import com.google.protobuf.CodedOutputStream; import com.google.protobuf.DescriptorProtos.DescriptorProto; @@ -393,6 +394,14 @@ public void select_populatedUnknownScalar_decodesFromWireBytes( assertThat(result).isEqualTo(testCase.expectedPopulated); } + @Test + public void select_populatedUnknownScalarInRootAny_decodesFromPackedWireBytes( + @TestParameter UnknownScalarTestCase testCase) throws Exception { + Object result = eval(testCase.expression, Any.pack(POPULATED_SERVER_MESSAGE)); + + assertThat(result).isEqualTo(testCase.expectedPopulated); + } + @Test public void select_populatedUnknownOneofEnum_decodesFromWireBytes() throws Exception { TestAllTypes msg = TestAllTypes.newBuilder().setSingleNestedEnum(NestedEnum.BAR).build(); @@ -2318,6 +2327,19 @@ public void descriptorlessRuntime_zeroRegisteredDescriptors_evaluatesFromWireByt assertThat(result).isEqualTo(testCase.expectedResult); } + @Test + public void descriptorlessRuntime_rootAny_evaluatesFromPackedWireBytes( + @TestParameter DescriptorlessEvaluationTestCase testCase) throws Exception { + CelLiteRuntime descriptorlessRuntime = newDescriptorlessRuntime(); + CelAbstractSyntaxTree optimizedAst = + serverOptimizer.optimize(serverCompiler.compile(testCase.expression).getAst()); + Program program = descriptorlessRuntime.createProgram(optimizedAst); + + Object result = program.eval(ImmutableMap.of("msg", Any.pack(testCase.message))); + + assertThat(result).isEqualTo(testCase.expectedResult); + } + @Test public void partialDescriptorRuntime_missingChildDescriptor_evaluatesFromWireBytes( @TestParameter DescriptorlessEvaluationTestCase testCase) throws Exception { @@ -3002,7 +3024,7 @@ private static CelLateFunctionBindings newScoreModelLateBindings() { })); } - private Object eval(String expression, TestAllTypes message) throws Exception { + private Object eval(String expression, MessageLite message) throws Exception { CelAbstractSyntaxTree ast = serverCompiler.compile(expression).getAst(); CelAbstractSyntaxTree optimizedAst = serverOptimizer.optimize(ast); Program program = clientRuntime.createProgram(optimizedAst);