| |
| #include <cstring> |
| #include <string> |
| #include <string_view> |
| |
| #include "google/protobuf/descriptor.pb.h" |
| #include "google/protobuf/descriptor.upb.h" |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| #include "absl/log/absl_check.h" |
| #include "absl/status/status.h" |
| #include "absl/status/statusor.h" |
| #include "absl/strings/str_format.h" |
| #include "absl/strings/str_join.h" |
| #include "google/protobuf/unittest.upbdefs.h" |
| #include "upb/base/status.hpp" |
| #include "upb/mem/alloc.h" |
| #include "upb/mem/arena.hpp" |
| #include "upb/reflection/def.h" |
| #include "upb/reflection/def.hpp" |
| #include "upb/reflection/internal/def_pool.h" |
| #include "upb/test/parse_text_proto.h" |
| |
| namespace upb_test { |
| namespace { |
| |
| using ::testing::HasSubstr; |
| using ::testing::NotNull; |
| |
| google_protobuf_FileDescriptorProto* ToUpbDescriptorSet( |
| const google::protobuf::FileDescriptorProto& proto, upb::Arena& arena) { |
| std::string serialized; |
| (void)proto.SerializeToString(&serialized); |
| return google_protobuf_FileDescriptorProto_parse(serialized.data(), serialized.size(), |
| arena.ptr()); |
| } |
| |
| absl::StatusOr<upb::DefPool> LoadDescriptorSetFromProto( |
| const google::protobuf::FileDescriptorSet& set) { |
| upb::Arena arena; |
| upb::DefPool defpool; |
| upb::Status status; |
| for (const auto& file : set.file()) { |
| google_protobuf_FileDescriptorProto* upb_proto = ToUpbDescriptorSet(file, arena); |
| ABSL_CHECK(upb_proto); |
| upb::FileDefPtr file_def = defpool.AddFile(upb_proto, &status); |
| if (!file_def) return absl::InternalError(status.error_message()); |
| } |
| return defpool; |
| } |
| |
| absl::StatusOr<upb::DefPool> LoadDescriptorProto(absl::string_view proto_text) { |
| google::protobuf::FileDescriptorProto proto = ParseTextProtoOrDie(proto_text); |
| google::protobuf::FileDescriptorSet set; |
| *set.add_file() = proto; |
| return LoadDescriptorSetFromProto(set); |
| } |
| |
| TEST(ReflectionTest, OpenEnumWithNonZeroDefault) { |
| absl::Status status = LoadDescriptorProto( |
| R"pb( |
| syntax: "proto3" |
| name: "F" |
| enum_type { |
| name: "BadEnum" |
| value { name: "v1" number: 1 } |
| } |
| )pb") |
| .status(); |
| EXPECT_EQ(std::string_view(status.message()), |
| "for open enums, the first value must be zero (BadEnum)"); |
| } |
| |
| TEST(ReflectionTest, FailedAddFileDoesNotRetainExtensions) { |
| // Add a valid message with one available extension number. |
| google::protobuf::FileDescriptorProto base; |
| base.set_name("base.proto"); |
| base.set_package("base"); |
| base.set_syntax("proto2"); |
| auto* target = base.add_message_type(); |
| target->set_name("Target"); |
| auto* range = target->add_extension_range(); |
| range->set_start(100); |
| range->set_end(101); |
| |
| // This file is rejected because both extensions use that number. |
| google::protobuf::FileDescriptorProto duplicate; |
| duplicate.set_name("duplicate.proto"); |
| duplicate.set_package("duplicate"); |
| duplicate.set_syntax("proto2"); |
| duplicate.add_dependency("base.proto"); |
| for (const char* name : {"first", "second"}) { |
| auto* extension = duplicate.add_extension(); |
| extension->set_name(name); |
| extension->set_extendee(".base.Target"); |
| extension->set_number(100); |
| extension->set_label(google::protobuf::FieldDescriptorProto::LABEL_OPTIONAL); |
| extension->set_type(google::protobuf::FieldDescriptorProto::TYPE_INT32); |
| } |
| |
| upb::Arena arena; |
| upb::DefPool pool; |
| upb::Status status; |
| ASSERT_TRUE(pool.AddFile(ToUpbDescriptorSet(base, arena), &status)); |
| status.Clear(); |
| EXPECT_FALSE(pool.AddFile(ToUpbDescriptorSet(duplicate, arena), &status)); |
| |
| upb::MessageDefPtr message = pool.FindMessageByName("base.Target"); |
| ASSERT_TRUE(message); |
| // A failed addition must not leave extensions in the pool. |
| size_t count; |
| const upb_FieldDef** extensions = |
| upb_DefPool_GetAllExtensions(pool.ptr(), message.ptr(), &count); |
| EXPECT_EQ(count, 0); |
| upb_gfree(extensions); |
| } |
| |
| TEST(ReflectionTest, EnumDefault) { |
| upb::DefPool pool = LoadDescriptorProto( |
| R"pb( |
| syntax: "proto2" |
| name: "F" |
| enum_type { |
| name: "FooEnum" |
| value { name: "v1" number: 1 } |
| } |
| )pb") |
| .value(); |
| upb::EnumDefPtr e = pool.FindEnumByName("FooEnum"); |
| EXPECT_EQ(e.default_value(), 1); |
| } |
| |
| TEST(ReflectionTest, ImplicitPresenceWithDefault) { |
| absl::Status status = |
| LoadDescriptorProto( |
| R"pb( |
| syntax: "editions" |
| edition: EDITION_2023 |
| name: "F" |
| message_type { |
| name: "FooMessage" |
| field { |
| name: "f1" |
| number: 1 |
| type: TYPE_INT32 |
| default_value: "1" |
| options { features { field_presence: IMPLICIT } } |
| } |
| } |
| )pb") |
| .status(); |
| EXPECT_EQ(std::string_view(status.message()), |
| "fields with implicit presence cannot have explicit defaults " |
| "(FooMessage.f1)"); |
| } |
| |
| TEST(ReflectionTest, ImplicitPresenceWithNonZeroDefaultEnum) { |
| absl::Status status = |
| LoadDescriptorProto( |
| R"pb( |
| syntax: "editions" |
| edition: EDITION_2023 |
| name: "F" |
| enum_type { |
| name: "FooEnum" |
| value { name: "v1" number: 1 } |
| options { features { enum_type: CLOSED } } |
| } |
| message_type { |
| name: "FooMessage" |
| field { |
| name: "f1" |
| number: 1 |
| type: TYPE_ENUM |
| type_name: "FooEnum" |
| options { features { field_presence: IMPLICIT } } |
| } |
| } |
| )pb") |
| .status(); |
| EXPECT_EQ(std::string_view(status.message()), |
| "Implicit presence field (FooMessage.f1) cannot use an enum type " |
| "with a non-zero default (FooEnum)"); |
| } |
| |
| TEST(ReflectionTest, EditionWithoutSyntax) { |
| absl::Status status = LoadDescriptorProto( |
| R"pb( |
| edition: EDITION_2023 |
| )pb") |
| .status(); |
| EXPECT_EQ( |
| status.message(), |
| R"(Setting edition requires that syntax="editions", but syntax is "")"); |
| } |
| |
| TEST(ReflectionTest, EditionWithWrongSyntax) { |
| absl::Status status = LoadDescriptorProto( |
| R"pb( |
| edition: EDITION_2023 syntax: "proto2" |
| )pb") |
| .status(); |
| EXPECT_EQ( |
| status.message(), |
| R"(Setting edition requires that syntax="editions", but syntax is "proto2")"); |
| } |
| |
| TEST(ReflectionTest, SyntaxEditionsWithNoEdition) { |
| absl::Status status = LoadDescriptorProto( |
| R"pb( |
| syntax: "editions" |
| )pb") |
| .status(); |
| EXPECT_EQ(status.message(), |
| R"(File has syntax="editions", but no edition is specified)"); |
| } |
| |
| TEST(ReflectionTest, InvalidSyntax) { |
| absl::Status status = LoadDescriptorProto( |
| R"pb( |
| syntax: "abc123" |
| )pb") |
| .status(); |
| EXPECT_EQ(status.message(), R"(Invalid syntax 'abc123')"); |
| } |
| |
| TEST(ReflectionTest, ExplicitFeatureOnProto2File) { |
| absl::Status status = LoadDescriptorProto( |
| R"pb( |
| syntax: "proto2" |
| options { features { field_presence: EXPLICIT } } |
| )pb") |
| .status(); |
| EXPECT_EQ(status.message(), R"(Features can only be specified for editions)"); |
| } |
| |
| TEST(ReflectionTest, ExplicitFeatureOnProto2Message) { |
| absl::Status status = |
| LoadDescriptorProto( |
| R"pb( |
| syntax: "proto2" |
| message_type { |
| name: "M" |
| options { features { field_presence: EXPLICIT } } |
| } |
| )pb") |
| .status(); |
| EXPECT_EQ(status.message(), R"(Features can only be specified for editions)"); |
| } |
| |
| TEST(ReflectionTest, ExplicitFeatureOnProto2Enum) { |
| absl::Status status = |
| LoadDescriptorProto( |
| R"pb( |
| syntax: "proto2" |
| enum_type { |
| name: "E" |
| options { features { field_presence: EXPLICIT } } |
| } |
| )pb") |
| .status(); |
| EXPECT_EQ(status.message(), R"(Features can only be specified for editions)"); |
| } |
| |
| TEST(ReflectionTest, ExplicitFeatureOnProto2EnumValue) { |
| absl::Status status = |
| LoadDescriptorProto( |
| R"pb( |
| syntax: "proto2" |
| enum_type { |
| name: "E" |
| value { |
| name: "V" |
| options { features { field_presence: EXPLICIT } } |
| } |
| } |
| )pb") |
| .status(); |
| EXPECT_EQ(status.message(), R"(Features can only be specified for editions)"); |
| } |
| |
| TEST(ReflectionTest, TooManyRequiredFieldsFailGracefully) { |
| const auto make_desc = [](int n) { |
| std::vector<std::string> fields; |
| for (int i = 1; i <= n; ++i) { |
| fields.push_back(absl::StrFormat(R"pb( |
| field { |
| name: "f%d" |
| number: %d |
| type: TYPE_INT32 |
| label: LABEL_REQUIRED |
| })pb", |
| i, i)); |
| } |
| return absl::StrFormat( |
| R"pb( |
| syntax: "proto2" |
| name: "F" |
| message_type { name: "FooMessage" %s } |
| )pb", |
| absl::StrJoin(fields, "\n")); |
| }; |
| // 63 required fields is ok. |
| upb::DefPool good = LoadDescriptorProto(make_desc(63)).value(); |
| auto m = good.FindMessageByName("FooMessage"); |
| auto f = m.FindFieldByNumber(63); |
| EXPECT_STREQ(f.full_name(), "FooMessage.f63"); |
| |
| // 64 is too much. |
| EXPECT_THAT(LoadDescriptorProto(make_desc(64)).status().message(), |
| HasSubstr("Too many required fields")); |
| } |
| |
| #define STRING_AND_SIZE(string) string, strlen(string) |
| |
| TEST(ReflectionTest, FindMethodByName) { |
| upb::Arena arena; |
| upb::DefPool defpool; |
| upb::Status status; |
| ASSERT_TRUE(_upb_DefPool_LoadDefInit( |
| defpool.ptr(), &google_protobuf_unittest_proto_upbdefinit)); |
| const upb_ServiceDef* service_def = upb_DefPool_FindServiceByName( |
| defpool.ptr(), "proto2_unittest.TestService"); |
| ASSERT_THAT(service_def, NotNull()); |
| EXPECT_STREQ(upb_ServiceDef_Name(service_def), "TestService"); |
| EXPECT_STREQ(upb_ServiceDef_FullName(service_def), |
| "proto2_unittest.TestService"); |
| EXPECT_EQ(upb_DefPool_FindServiceByNameWithSize( |
| defpool.ptr(), STRING_AND_SIZE("proto2_unittest.TestService")), |
| service_def); |
| const upb_MethodDef* method_def = |
| upb_ServiceDef_FindMethodByName(service_def, "Bar"); |
| ASSERT_THAT(method_def, NotNull()); |
| EXPECT_STREQ(upb_MethodDef_Name(method_def), "Bar"); |
| EXPECT_STREQ(upb_MethodDef_FullName(method_def), |
| "proto2_unittest.TestService.Bar"); |
| EXPECT_EQ(upb_ServiceDef_FindMethodByNameWithSize(service_def, |
| STRING_AND_SIZE("Bar")), |
| method_def); |
| } |
| |
| TEST(ReflectionTest, FindEnumByName) { |
| upb::Arena arena; |
| upb::DefPool defpool; |
| upb::Status status; |
| ASSERT_TRUE(_upb_DefPool_LoadDefInit( |
| defpool.ptr(), &google_protobuf_unittest_proto_upbdefinit)); |
| const upb_EnumDef* enum_def = upb_DefPool_FindEnumByName( |
| defpool.ptr(), "proto2_unittest.TestAllTypes.NestedEnum"); |
| ASSERT_THAT(enum_def, NotNull()); |
| EXPECT_STREQ(upb_EnumDef_Name(enum_def), "NestedEnum"); |
| EXPECT_STREQ(upb_EnumDef_FullName(enum_def), |
| "proto2_unittest.TestAllTypes.NestedEnum"); |
| EXPECT_EQ(upb_DefPool_FindEnumByNameWithSize( |
| defpool.ptr(), |
| STRING_AND_SIZE("proto2_unittest.TestAllTypes.NestedEnum")), |
| enum_def); |
| } |
| |
| TEST(ReflectionTest, FindEnumValueByName) { |
| upb::Arena arena; |
| upb::DefPool defpool; |
| upb::Status status; |
| ASSERT_TRUE(_upb_DefPool_LoadDefInit( |
| defpool.ptr(), &google_protobuf_unittest_proto_upbdefinit)); |
| const upb_EnumValueDef* enum_value_def = upb_DefPool_FindEnumValueByName( |
| defpool.ptr(), "proto2_unittest.TestAllTypes.BAR"); |
| ASSERT_THAT(enum_value_def, NotNull()); |
| EXPECT_STREQ(upb_EnumValueDef_Name(enum_value_def), "BAR"); |
| EXPECT_STREQ(upb_EnumValueDef_FullName(enum_value_def), |
| "proto2_unittest.TestAllTypes.BAR"); |
| EXPECT_EQ( |
| upb_DefPool_FindEnumValueByNameWithSize( |
| defpool.ptr(), STRING_AND_SIZE("proto2_unittest.TestAllTypes.BAR")), |
| enum_value_def); |
| const upb_EnumDef* enum_def = upb_DefPool_FindEnumByName( |
| defpool.ptr(), "proto2_unittest.TestAllTypes.NestedEnum"); |
| ASSERT_THAT(enum_def, NotNull()); |
| EXPECT_EQ(upb_EnumDef_FindValueByName(enum_def, "BAR"), enum_value_def); |
| EXPECT_EQ( |
| upb_EnumDef_FindValueByNameWithSize(enum_def, STRING_AND_SIZE("BAR")), |
| enum_value_def); |
| } |
| |
| TEST(ReflectionTest, NegativePublicDependencyIndex) { |
| // Verify that a negative public_dependency index is rejected rather than |
| // causing an out-of-bounds read when the index is later used to access the |
| // deps array. |
| absl::Status status = LoadDescriptorProto( |
| R"pb( |
| name: "test.proto" public_dependency: -1 |
| )pb") |
| .status(); |
| EXPECT_FALSE(status.ok()); |
| EXPECT_THAT(std::string(status.message()), HasSubstr("out of range")); |
| } |
| |
| TEST(ReflectionTest, NegativeWeakDependencyIndex) { |
| absl::Status status = LoadDescriptorProto( |
| R"pb( |
| name: "test.proto" weak_dependency: -1 |
| )pb") |
| .status(); |
| EXPECT_FALSE(status.ok()); |
| EXPECT_THAT(std::string(status.message()), HasSubstr("out of range")); |
| } |
| |
| TEST(ReflectionTest, ZeroPublicDependencyIndexWithNoDeps) { |
| // Index 0 is out of range when there are no dependencies (dep_count=0). |
| absl::Status status = LoadDescriptorProto( |
| R"pb( |
| name: "test.proto" public_dependency: 0 |
| )pb") |
| .status(); |
| EXPECT_FALSE(status.ok()); |
| EXPECT_THAT(std::string(status.message()), HasSubstr("out of range")); |
| } |
| |
| TEST(ReflectionTest, ZeroWeakDependencyIndexWithNoDeps) { |
| absl::Status status = LoadDescriptorProto( |
| R"pb( |
| name: "test.proto" weak_dependency: 0 |
| )pb") |
| .status(); |
| EXPECT_FALSE(status.ok()); |
| EXPECT_THAT(std::string(status.message()), HasSubstr("out of range")); |
| } |
| |
| // Sets (pb.enumvalue.json).string option (extension field 998) on |
| // EnumValueOptions. |
| static void SetCustomJsonOption(google::protobuf::EnumValueOptions* options, |
| absl::string_view json_name) { |
| // Wire format for JsonEnumValueOptions { string string = 1 }: |
| // Field 1 (string, length-delimited): tag = (1 << 3) | 2 = 0x0a. |
| std::string payload; |
| payload.push_back('\x0a'); |
| payload.push_back(static_cast<char>(json_name.size())); |
| payload.append(json_name); |
| // Field 998 is extension (pb.enumvalue.json) on EnumValueOptions. |
| options->mutable_unknown_fields()->AddLengthDelimited(998, payload); |
| } |
| |
| struct TestEnumValueSpec { |
| std::string name; |
| int number; |
| std::string json_name; |
| }; |
| |
| static absl::StatusOr<upb::DefPool> LoadEnumDescriptorWithValues( |
| absl::Span<const TestEnumValueSpec> values) { |
| google::protobuf::FileDescriptorProto file_proto; |
| file_proto.set_name("test.proto"); |
| file_proto.set_syntax("editions"); |
| file_proto.set_edition(google::protobuf::EDITION_2026); |
| |
| google::protobuf::EnumDescriptorProto* enum_proto = file_proto.add_enum_type(); |
| enum_proto->set_name("TestEnum"); |
| |
| for (const TestEnumValueSpec& v : values) { |
| google::protobuf::EnumValueDescriptorProto* val_proto = enum_proto->add_value(); |
| val_proto->set_name(v.name); |
| val_proto->set_number(v.number); |
| if (!v.json_name.empty()) { |
| SetCustomJsonOption(val_proto->mutable_options(), v.json_name); |
| } |
| } |
| |
| google::protobuf::FileDescriptorSet set; |
| *set.add_file() = file_proto; |
| return LoadDescriptorSetFromProto(set); |
| } |
| |
| TEST(ReflectionTest, EnumCustomJsonNameConflictDifferentNumberFails) { |
| // Two enum values with DIFFERENT numbers (1 and 2) sharing the same custom |
| // JSON name must fail descriptor validation. |
| absl::Status status = |
| LoadEnumDescriptorWithValues({ |
| {"VAL_ZERO", 0, ""}, |
| {"VAL_A", 1, "custom_name"}, |
| {"VAL_B", 2, "custom_name"}, |
| }) |
| .status(); |
| EXPECT_FALSE(status.ok()); |
| EXPECT_THAT(std::string(status.message()), |
| HasSubstr("duplicate custom json_name (custom_name) in enum")); |
| } |
| |
| TEST(ReflectionTest, EnumCustomJsonNameAliasedSameNumberSucceeds) { |
| // Two aliased enum values with the SAME number (1 and 1) sharing the same |
| // custom JSON name must succeed descriptor validation. |
| absl::Status status = |
| LoadEnumDescriptorWithValues({ |
| {"VAL_ZERO", 0, ""}, |
| {"VAL_A", 1, "custom_name"}, |
| {"VAL_B", 1, "custom_name"}, |
| }) |
| .status(); |
| EXPECT_TRUE(status.ok()) << status.message(); |
| } |
| |
| TEST(ReflectionTest, DefPoolFindMethodsRespectSize) { |
| upb::DefPool pool = LoadDescriptorProto(R"pb( |
| syntax: "proto2" |
| name: "test.proto" |
| package: "pkg" |
| message_type { |
| name: "TestMessage" |
| extension_range { start: 1 end: 1000 } |
| } |
| enum_type { |
| name: "TestEnum" |
| value { name: "VAL" number: 1 } |
| } |
| extension { |
| name: "test_ext" |
| number: 100 |
| label: LABEL_OPTIONAL |
| type: TYPE_INT32 |
| extendee: ".pkg.TestMessage" |
| } |
| )pb") |
| .value(); |
| |
| // Test FindMessageByName |
| { |
| absl::string_view full_name = "pkg.TestMessage"; |
| EXPECT_TRUE(pool.FindMessageByName(full_name)); |
| EXPECT_FALSE( |
| pool.FindMessageByName(full_name.substr(0, 12))); // "pkg.TestMess" |
| } |
| |
| // Test FindEnumByName |
| { |
| absl::string_view full_name = "pkg.TestEnum"; |
| EXPECT_TRUE(pool.FindEnumByName(full_name)); |
| EXPECT_FALSE(pool.FindEnumByName(full_name.substr(0, 10))); // "pkg.TestEn" |
| } |
| |
| // Test FindFileByName |
| { |
| absl::string_view full_name = "test.proto"; |
| EXPECT_TRUE(pool.FindFileByName(full_name)); |
| EXPECT_FALSE(pool.FindFileByName(full_name.substr(0, 7))); // "test.pr" |
| } |
| |
| // Test FindExtensionByName |
| { |
| absl::string_view full_name = "pkg.test_ext"; |
| EXPECT_TRUE(pool.FindExtensionByName(full_name)); |
| EXPECT_FALSE( |
| pool.FindExtensionByName(full_name.substr(0, 11))); // "pkg.test_ex" |
| } |
| } |
| |
| } // namespace |
| } // namespace upb_test |