Skip to content

Commit

Permalink
Fix packed reflection handling bug in edition 2023.
Browse files Browse the repository at this point in the history
This is a very narrow edge case where touching a packed extension via generated APIs first, and then doing so reflectively will trigger a DCHECK.  Otherwise, reflective APIs will work but not use packed encoding for the extension.  This was likely a pre-existing bug dating back to proto3, where it would only be visible on custom options (the only extensions allowed in proto3).

To help qualify this and uncover similar issues, unittest.proto was migrated to editions.  This turned up some other minor issues in DebugString and python.

PiperOrigin-RevId: 675785611
  • Loading branch information
mkruskal-google authored and copybara-github committed Sep 18, 2024
1 parent 7dfbf79 commit 4c92328
Show file tree
Hide file tree
Showing 16 changed files with 1,822 additions and 846 deletions.
6 changes: 0 additions & 6 deletions compatibility/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,6 @@ java_runtime_conformance(
gencode_version = "main",
)

# Generates a build_test named "conformance_v3.25.0"
java_runtime_conformance(
name = "java_conformance_v3.25.0",
gencode_version = "3.25.0",
)

# Breaking change detection for well-known types and descriptor.proto.
buf_breaking_test(
name = "any_proto_breaking",
Expand Down
125 changes: 106 additions & 19 deletions java/core/src/test/java/com/google/protobuf/DescriptorsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.google.protobuf.DescriptorProtos.FeatureSetDefaults;
import com.google.protobuf.DescriptorProtos.FeatureSetDefaults.FeatureSetEditionDefault;
import com.google.protobuf.DescriptorProtos.FieldDescriptorProto;
import com.google.protobuf.DescriptorProtos.FieldOptions;
import com.google.protobuf.DescriptorProtos.FileDescriptorProto;
import com.google.protobuf.DescriptorProtos.FileOptions;
import com.google.protobuf.DescriptorProtos.MethodDescriptorProto;
Expand Down Expand Up @@ -53,7 +54,6 @@
import protobuf_unittest.UnittestProto.TestReservedFields;
import protobuf_unittest.UnittestProto.TestService;
import protobuf_unittest.UnittestRetention;
import proto3_unittest.UnittestProto3;
import protobuf_unittest.UnittestProto3Extensions.Proto3FileExtensions;
import java.util.Collections;
import java.util.List;
Expand Down Expand Up @@ -1256,32 +1256,106 @@ public void testLegacyGroupTransform() {
}

@Test
public void testLegacyInferRequired() {
FieldDescriptor field = UnittestProto.TestRequired.getDescriptor().findFieldByName("a");
public void testLegacyInferRequired() throws Exception {
FileDescriptor file =
FileDescriptor.buildFrom(
FileDescriptorProto.newBuilder()
.setName("some/filename/some.proto")
.setSyntax("proto2")
.addMessageType(
DescriptorProto.newBuilder()
.setName("Foo")
.addField(
FieldDescriptorProto.newBuilder()
.setName("a")
.setNumber(1)
.setType(FieldDescriptorProto.Type.TYPE_INT32)
.setLabel(FieldDescriptorProto.Label.LABEL_REQUIRED)
.build())
.build())
.build(),
new FileDescriptor[0]);
FieldDescriptor field = file.findMessageTypeByName("Foo").findFieldByName("a");
assertThat(field.features.getFieldPresence())
.isEqualTo(DescriptorProtos.FeatureSet.FieldPresence.LEGACY_REQUIRED);
}

@Test
public void testLegacyInferGroup() {
FieldDescriptor field =
UnittestProto.TestAllTypes.getDescriptor().findFieldByName("optionalgroup");
public void testLegacyInferGroup() throws Exception {
FileDescriptor file =
FileDescriptor.buildFrom(
FileDescriptorProto.newBuilder()
.setName("some/filename/some.proto")
.setSyntax("proto2")
.addMessageType(
DescriptorProto.newBuilder()
.setName("Foo")
.addNestedType(
DescriptorProto.newBuilder().setName("OptionalGroup").build())
.addField(
FieldDescriptorProto.newBuilder()
.setName("optionalgroup")
.setNumber(1)
.setType(FieldDescriptorProto.Type.TYPE_GROUP)
.setLabel(FieldDescriptorProto.Label.LABEL_OPTIONAL)
.setTypeName("Foo.OptionalGroup")
.build())
.build())
.build(),
new FileDescriptor[0]);
FieldDescriptor field = file.findMessageTypeByName("Foo").findFieldByName("optionalgroup");
assertThat(field.features.getMessageEncoding())
.isEqualTo(DescriptorProtos.FeatureSet.MessageEncoding.DELIMITED);
}

@Test
public void testLegacyInferProto2Packed() {
FieldDescriptor field =
UnittestProto.TestPackedTypes.getDescriptor().findFieldByName("packed_int32");
public void testLegacyInferProto2Packed() throws Exception {
FileDescriptor file =
FileDescriptor.buildFrom(
FileDescriptorProto.newBuilder()
.setName("some/filename/some.proto")
.setSyntax("proto2")
.addMessageType(
DescriptorProto.newBuilder()
.setName("Foo")
.addField(
FieldDescriptorProto.newBuilder()
.setName("a")
.setNumber(1)
.setLabel(FieldDescriptorProto.Label.LABEL_REPEATED)
.setType(FieldDescriptorProto.Type.TYPE_INT32)
.setOptions(FieldOptions.newBuilder().setPacked(true).build())
.build())
.build())
.build(),
new FileDescriptor[0]);
FieldDescriptor field = file.findMessageTypeByName("Foo").findFieldByName("a");
assertThat(field.features.getRepeatedFieldEncoding())
.isEqualTo(DescriptorProtos.FeatureSet.RepeatedFieldEncoding.PACKED);
}

@Test
public void testLegacyInferProto3Expanded() {
FieldDescriptor field =
UnittestProto3.TestUnpackedTypes.getDescriptor().findFieldByName("repeated_int32");
public void testLegacyInferProto3Expanded() throws Exception {
FileDescriptor file =
FileDescriptor.buildFrom(
FileDescriptorProto.newBuilder()
.setName("some/filename/some.proto")
.setSyntax("proto3")
.addMessageType(
DescriptorProto.newBuilder()
.setName("Foo")
.addField(
FieldDescriptorProto.newBuilder()
.setName("a")
.setNumber(1)
.setType(FieldDescriptorProto.Type.TYPE_INT32)
.setLabel(FieldDescriptorProto.Label.LABEL_REPEATED)
.setOptions(FieldOptions.newBuilder().setPacked(false).build())
.build())
.build())
.build(),
new FileDescriptor[0]);
FieldDescriptor field = file.findMessageTypeByName("Foo").findFieldByName("a");
assertThat(field.features.getRepeatedFieldEncoding())
.isEqualTo(DescriptorProtos.FeatureSet.RepeatedFieldEncoding.EXPANDED);
}
Expand All @@ -1302,9 +1376,16 @@ public void testLegacyInferProto2Utf8Validation() throws Exception {
}

@Test
public void testProto2Defaults() {
FieldDescriptor proto2Field = TestAllTypes.getDescriptor().findFieldByName("optional_int32");
DescriptorProtos.FeatureSet features = proto2Field.features;
public void testProto2Defaults() throws Exception {
FileDescriptor proto2File =
FileDescriptor.buildFrom(
FileDescriptorProto.newBuilder()
.setName("some/filename/some.proto")
.setPackage("protobuf_unittest")
.setSyntax("proto2")
.build(),
new FileDescriptor[0]);
DescriptorProtos.FeatureSet features = proto2File.features;
assertThat(features.getFieldPresence())
.isEqualTo(DescriptorProtos.FeatureSet.FieldPresence.EXPLICIT);
assertThat(features.getEnumType()).isEqualTo(DescriptorProtos.FeatureSet.EnumType.CLOSED);
Expand All @@ -1323,10 +1404,16 @@ public void testProto2Defaults() {
}

@Test
public void testProto3Defaults() {
FieldDescriptor proto3Field =
UnittestProto3.TestAllTypes.getDescriptor().findFieldByName("optional_int32");
DescriptorProtos.FeatureSet features = proto3Field.features;
public void testProto3Defaults() throws Exception {
FileDescriptor proto3File =
FileDescriptor.buildFrom(
FileDescriptorProto.newBuilder()
.setName("some/filename/some.proto")
.setPackage("proto3_unittest")
.setSyntax("proto3")
.build(),
new FileDescriptor[0]);
DescriptorProtos.FeatureSet features = proto3File.features;
assertThat(features.getFieldPresence())
.isEqualTo(DescriptorProtos.FeatureSet.FieldPresence.IMPLICIT);
assertThat(features.getEnumType()).isEqualTo(DescriptorProtos.FeatureSet.EnumType.OPEN);
Expand Down
6 changes: 6 additions & 0 deletions python/descriptor.c
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,11 @@ static PyObject* PyUpb_FieldDescriptor_GetIsExtension(
return PyBool_FromLong(upb_FieldDef_IsExtension(self->def));
}

static PyObject* PyUpb_FieldDescriptor_GetIsPacked(PyUpb_DescriptorBase* self,
void* closure) {
return PyBool_FromLong(upb_FieldDef_IsPacked(self->def));
}

static PyObject* PyUpb_FieldDescriptor_GetNumber(PyUpb_DescriptorBase* self,
void* closure) {
return PyLong_FromLong(upb_FieldDef_Number(self->def));
Expand Down Expand Up @@ -1164,6 +1169,7 @@ static PyGetSetDef PyUpb_FieldDescriptor_Getters[] = {
"Default Value"},
{"has_default_value", (getter)PyUpb_FieldDescriptor_HasDefaultValue},
{"is_extension", (getter)PyUpb_FieldDescriptor_GetIsExtension, NULL, "ID"},
{"is_packed", (getter)PyUpb_FieldDescriptor_GetIsPacked, NULL, "Is Packed"},
// TODO
//{ "id", (getter)GetID, NULL, "ID"},
{"message_type", (getter)PyUpb_FieldDescriptor_GetMessageType, NULL,
Expand Down
7 changes: 4 additions & 3 deletions python/google/protobuf/internal/descriptor_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,18 @@
from google.protobuf import symbol_database
from google.protobuf import text_format
from google.protobuf.internal import api_implementation
from google.protobuf.internal import test_proto2_pb2
from google.protobuf.internal import test_util
from google.protobuf.internal import testing_refleaks

from google.protobuf.internal import _parameterized
from google.protobuf import unittest_legacy_features_pb2
from google.protobuf import unittest_custom_options_pb2
from google.protobuf import unittest_features_pb2
from google.protobuf import unittest_import_pb2
from google.protobuf import unittest_legacy_features_pb2
from google.protobuf import unittest_pb2
from google.protobuf import unittest_proto3_pb2
from google.protobuf import unittest_proto3_extensions_pb2
from google.protobuf import unittest_proto3_pb2


TEST_EMPTY_MESSAGE_DESCRIPTOR_ASCII = """
Expand Down Expand Up @@ -1325,7 +1326,7 @@ def testLegacyInferProto3Expanded(self):
)

def testProto2Defaults(self):
features = unittest_pb2.TestAllTypes.DESCRIPTOR.fields_by_name[
features = test_proto2_pb2.TestProto2.DESCRIPTOR.fields_by_name[
'optional_int32'
]._GetFeatures()
fs = descriptor_pb2.FeatureSet
Expand Down
22 changes: 12 additions & 10 deletions python/google/protobuf/internal/message_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1408,18 +1408,20 @@ def testDel(self):
del msg.repeated_nested_message

def testAssignInvalidEnum(self):
"""Assigning an invalid enum number is not allowed in proto2."""
"""Assigning an invalid enum number is not allowed for closed enums."""
m = unittest_pb2.TestAllTypes()

# Proto2 can not assign unknown enum.
with self.assertRaises(ValueError) as _:
m.optional_nested_enum = 1234567
self.assertRaises(ValueError, m.repeated_nested_enum.append, 1234567)
# Assignment is a different code path than append for the C++ impl.
m.repeated_nested_enum.append(2)
m.repeated_nested_enum[0] = 2
with self.assertRaises(ValueError):
m.repeated_nested_enum[0] = 123456
# TODO Enable these once upb's behavior is made conformant.
if api_implementation.Type() != 'upb':
# Can not assign unknown enum to closed enums.
with self.assertRaises(ValueError) as _:
m.optional_nested_enum = 1234567
self.assertRaises(ValueError, m.repeated_nested_enum.append, 1234567)
# Assignment is a different code path than append for the C++ impl.
m.repeated_nested_enum.append(2)
m.repeated_nested_enum[0] = 2
with self.assertRaises(ValueError):
m.repeated_nested_enum[0] = 123456

# Unknown enum value can be parsed but is ignored.
m2 = unittest_proto3_arena_pb2.TestAllTypes()
Expand Down
4 changes: 2 additions & 2 deletions python/google/protobuf/internal/reflection_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3261,13 +3261,13 @@ def testPackedOptions(self):
proto.optional_int32 = 1
proto.optional_double = 3.0
for field_descriptor, _ in proto.ListFields():
self.assertEqual(False, field_descriptor.GetOptions().packed)
self.assertEqual(False, field_descriptor.is_packed)

proto = unittest_pb2.TestPackedTypes()
proto.packed_int32.append(1)
proto.packed_double.append(3.0)
for field_descriptor, _ in proto.ListFields():
self.assertEqual(True, field_descriptor.GetOptions().packed)
self.assertEqual(True, field_descriptor.is_packed)
self.assertEqual(descriptor.FieldDescriptor.LABEL_REPEATED,
field_descriptor.label)

Expand Down
5 changes: 5 additions & 0 deletions python/google/protobuf/pyext/descriptor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,10 @@ static PyObject* IsExtension(PyBaseDescriptor *self, void *closure) {
return PyBool_FromLong(_GetDescriptor(self)->is_extension());
}

static PyObject* IsPacked(PyBaseDescriptor* self, void* closure) {
return PyBool_FromLong(_GetDescriptor(self)->is_packed());
}

static PyObject* HasDefaultValue(PyBaseDescriptor *self, void *closure) {
return PyBool_FromLong(_GetDescriptor(self)->has_default_value());
}
Expand Down Expand Up @@ -1050,6 +1054,7 @@ static PyGetSetDef Getters[] = {
{"default_value", (getter)GetDefaultValue, nullptr, "Default Value"},
{"has_default_value", (getter)HasDefaultValue},
{"is_extension", (getter)IsExtension, nullptr, "ID"},
{"is_packed", (getter)IsPacked, nullptr, "Is Packed"},
{"id", (getter)GetID, nullptr, "ID"},
{"_cdescriptor", (getter)GetCDescriptor, nullptr, "HAACK REMOVE ME"},

Expand Down
2 changes: 2 additions & 0 deletions src/google/protobuf/compiler/cpp/file_unittest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ TEST(FileTest, TopologicallyOrderedDescriptors) {
"TestUnpackedTypes",
"TestUnpackedExtensions",
"TestReservedFields",
"TestRequiredOpenEnum",
"TestRequiredOneof.NestedMessage",
"TestRequiredNoMaskMulti",
"TestRequiredEnumNoMask",
Expand Down Expand Up @@ -111,6 +112,7 @@ TEST(FileTest, TopologicallyOrderedDescriptors) {
"RedactedFields.MapUnredactedStringEntry",
"RedactedFields.MapRedactedStringEntry",
"OptionalGroup_extension",
"OpenEnumMessage",
"OneString",
"OneBytes",
"MoreString",
Expand Down
12 changes: 8 additions & 4 deletions src/google/protobuf/descriptor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3599,8 +3599,10 @@ void Descriptor::DebugString(int depth, std::string* contents,
if (reserved_name_count() > 0) {
absl::SubstituteAndAppend(contents, "$0 reserved ", prefix);
for (int i = 0; i < reserved_name_count(); i++) {
absl::SubstituteAndAppend(contents, "\"$0\", ",
absl::CEscape(reserved_name(i)));
absl::SubstituteAndAppend(
contents,
file()->edition() < Edition::EDITION_2023 ? "\"$0\", " : "$0, ",
absl::CEscape(reserved_name(i)));
}
contents->replace(contents->size() - 2, 2, ";\n");
}
Expand Down Expand Up @@ -3819,8 +3821,10 @@ void EnumDescriptor::DebugString(
if (reserved_name_count() > 0) {
absl::SubstituteAndAppend(contents, "$0 reserved ", prefix);
for (int i = 0; i < reserved_name_count(); i++) {
absl::SubstituteAndAppend(contents, "\"$0\", ",
absl::CEscape(reserved_name(i)));
absl::SubstituteAndAppend(
contents,
file()->edition() < Edition::EDITION_2023 ? "\"$0\", " : "$0, ",
absl::CEscape(reserved_name(i)));
}
contents->replace(contents->size() - 2, 2, ";\n");
}
Expand Down
Loading

0 comments on commit 4c92328

Please sign in to comment.