Skip to content

Commit b94cd10

Browse files
Dave MacLachlancopybara-github
authored andcommitted
Refactor: Improve map entry parsing in GeneratedMessage.mm
This change adds validation for map key types and ensures that default values are used for missing key or value fields when parsing map entries from a stream. Error handling is also improved by checking the return value of `ReadMapEntryField`. By adding a couple of judicious autoreleases, it also protects us from memory leaks in case of bad maps. PiperOrigin-RevId: 895935026
1 parent 83dcb8c commit b94cd10

File tree

5 files changed

+249
-30
lines changed

5 files changed

+249
-30
lines changed

protobuf/runtime/src/com/google/protobuf/GeneratedMessage.mm

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -831,8 +831,7 @@ static BOOL AddMapGetWithKeyMethod(Class cls, SEL sel, CGPFieldDescriptor *field
831831
key.CGPValueField_##KEY_NAME = pKey; \
832832
CGPValue value; \
833833
value.CGPValueField_##VALUE_NAME = pValue; \
834-
CGPMapFieldPut(MAP_FIELD_PTR(msg, offset), key, keyType, value, valueType, \
835-
/* retainedKeyAndValue */ false); \
834+
CGPMapFieldPut(MAP_FIELD_PTR(msg, offset), key, keyType, value, valueType); \
836835
return msg; \
837836
}); \
838837
}
@@ -1674,28 +1673,37 @@ static BOOL MergeMapEntryFromStream(CGPMapField *field, CGPCodedInputStream *str
16741673
if (!CGPReadInt32(stream, &length)) return NO;
16751674
CGPCodedInputStream::Limit limit = stream->PushLimit(length);
16761675
CGPFieldDescriptor *keyField = entry->fields_->buffer_[0];
1676+
CGPFieldJavaType keyType = CGPFieldGetJavaType(keyField);
16771677
CGPFieldDescriptor *valueField = entry->fields_->buffer_[1];
1678-
BOOL hasKey = NO;
1679-
BOOL hasValue = NO;
1678+
CGPFieldJavaType valueType = CGPFieldGetJavaType(valueField);
16801679
CGPValue key;
1680+
key.valueId = nil;
16811681
CGPValue value;
1682+
if (CGPJavaTypeIsEnum(valueType)) {
1683+
// Note that even though value.valueId is an id, in the case of enums it is not retained.
1684+
// It is a "constant" value from the value descriptor that owns it.
1685+
value.valueId =
1686+
((ComGoogleProtobufDescriptors_EnumValueDescriptor *)[valueField getDefaultValue])->enum_;
1687+
} else {
1688+
value.valueId = nil;
1689+
}
16821690
while (YES) {
16831691
uint32_t tag = stream->ReadTag();
16841692
if (tag == 0) break;
16851693
switch (CGPWireFormatGetTagFieldNumber(tag)) {
16861694
case 1:
1687-
if (hasKey && CGPIsRetainedType(CGPFieldGetJavaType(keyField))) {
1688-
RELEASE_(key.valueId);
1695+
if (!ReadMapEntryField(stream, keyField, tag, registry, &key)) {
1696+
return NO;
1697+
} else if (CGPIsRetainedType(keyType)) {
1698+
AUTORELEASE(key.valueId);
16891699
}
1690-
ReadMapEntryField(stream, keyField, tag, registry, &key);
1691-
hasKey = YES;
16921700
break;
16931701
case 2:
1694-
if (hasValue && CGPIsRetainedType(CGPFieldGetJavaType(valueField))) {
1695-
RELEASE_(value.valueId);
1702+
if (!ReadMapEntryField(stream, valueField, tag, registry, &value)) {
1703+
return NO;
1704+
} else if (CGPIsRetainedType(valueType)) {
1705+
AUTORELEASE(value.valueId);
16961706
}
1697-
ReadMapEntryField(stream, valueField, tag, registry, &value);
1698-
hasValue = YES;
16991707
break;
17001708
default:
17011709
if (!CGPWireFormatSkipField(stream, tag)) return NO;
@@ -1704,8 +1712,33 @@ static BOOL MergeMapEntryFromStream(CGPMapField *field, CGPCodedInputStream *str
17041712
}
17051713
if (!stream->ConsumedEntireMessage()) return NO;
17061714
stream->PopLimit(limit);
1707-
CGPMapFieldPut(field, key, CGPFieldGetJavaType(keyField), value, CGPFieldGetJavaType(valueField),
1708-
/* retainedKeyAndValue */ true);
1715+
if ((keyType == ComGoogleProtobufDescriptors_FieldDescriptor_JavaType_Enum_STRING) &&
1716+
(key.valueId == nil)) {
1717+
key.valueId = @"";
1718+
}
1719+
if ((CGPIsRetainedType(valueType)) && (value.valueId == nil)) {
1720+
switch (valueType) {
1721+
case ComGoogleProtobufDescriptors_FieldDescriptor_JavaType_Enum_STRING:
1722+
value.valueId = @"";
1723+
break;
1724+
case ComGoogleProtobufDescriptors_FieldDescriptor_JavaType_Enum_BYTE_STRING:
1725+
value.valueId = [CGPByteString empty];
1726+
break;
1727+
case ComGoogleProtobufDescriptors_FieldDescriptor_JavaType_Enum_MESSAGE:
1728+
if (valueField->valueType_ == nil) {
1729+
// Should not happen, but protect against crash.
1730+
return NO;
1731+
}
1732+
value.valueId = AUTORELEASE(CGPNewMessage(valueField->valueType_));
1733+
break;
1734+
default:
1735+
// Should not happen, but we don't trust potential undefined behavior
1736+
// in CGPIsRetainedType.
1737+
return NO;
1738+
break;
1739+
}
1740+
}
1741+
CGPMapFieldPut(field, key, keyType, value, valueType);
17091742
return YES;
17101743
}
17111744

protobuf/runtime/src/com/google/protobuf/MapField.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,10 @@ CGP_ALWAYS_INLINE uint32_t CGPMapFieldMapSize(
106106
CGPMapFieldEntry *CGPMapFieldGetWithKey(
107107
CGPMapField *field, CGPValue key, CGPFieldJavaType keyType, CGPFieldJavaType valueType);
108108

109-
// The caller will indicate whether the key and value are already retained.
109+
// The key and value are assumed to not be retained.
110110
void CGPMapFieldPut(
111111
CGPMapField *field, CGPValue key, CGPFieldJavaType keyType, CGPValue value,
112-
CGPFieldJavaType valueType, bool retainedKeyAndValue);
112+
CGPFieldJavaType valueType);
113113

114114
void CGPMapFieldRemove(
115115
CGPMapField *field, CGPValue key, CGPFieldJavaType keyType, CGPFieldJavaType valueType);

protobuf/runtime/src/com/google/protobuf/MapField.m

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -322,11 +322,11 @@ static void EnsureAdditionalHashMapCapacity(
322322

323323
void CGPMapFieldPut(
324324
CGPMapField *field, CGPValue key, CGPFieldJavaType keyType, CGPValue value,
325-
CGPFieldJavaType valueType, bool retainedKeyAndValue) {
325+
CGPFieldJavaType valueType) {
326326
BOOL keyTypeIsRetainable = CGPIsRetainedType(keyType);
327327
BOOL valueTypeIsRetainable = CGPIsRetainedType(valueType);
328328
// The value is always added to the map so make sure it's retained.
329-
if (valueTypeIsRetainable && !retainedKeyAndValue) {
329+
if (valueTypeIsRetainable) {
330330
RETAIN_(value.valueId);
331331
}
332332
uint32_t hash = Hash(key, keyType);
@@ -337,18 +337,14 @@ void CGPMapFieldPut(
337337
entry = GetFromHashArray(data, key, keyType, hash);
338338
}
339339
if (entry) {
340-
// Existing entry so the key is not added to the map and must not be retained.
341-
if (keyTypeIsRetainable && retainedKeyAndValue) {
342-
[key.valueId autorelease];
343-
}
344340
// Release the previous value.
345341
if (valueTypeIsRetainable) {
346342
[entry->value.valueId autorelease];
347343
}
348344
entry->value = value;
349345
} else {
350346
// Creating a new entry using the passed in key which must be retained.
351-
if (keyTypeIsRetainable && !retainedKeyAndValue) {
347+
if (keyTypeIsRetainable) {
352348
RETAIN_(key.valueId);
353349
}
354350
EnsureAdditionalHashMapCapacity(field, 1, keyType, valueType);

protobuf/tests/MapsTest.java

Lines changed: 130 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*/
1414

1515
import com.google.j2objc.annotations.AutoreleasePool;
16+
import com.google.protobuf.ByteString;
1617
import com.google.protobuf.Descriptors.Descriptor;
1718
import com.google.protobuf.Descriptors.FieldDescriptor;
1819
import com.google.protobuf.Descriptors.FieldDescriptor.Type;
@@ -23,13 +24,25 @@
2324
import java.util.ArrayList;
2425
import java.util.List;
2526
import java.util.Map;
27+
import protos.FakeScalarBytesMap;
28+
import protos.FakeScalarBytesMapFieldEntry;
29+
import protos.FakeScalarEnumMap;
30+
import protos.FakeScalarEnumMapFieldEntry;
31+
import protos.FakeScalarMsgMap;
32+
import protos.FakeScalarMsgMapFieldEntry;
33+
import protos.FakeStringStringMap;
34+
import protos.FakeStringStringMapFieldEntry;
2635
import protos.MapMsg;
2736
import protos.MapMsgOrBuilder;
2837
import protos.MapValue;
29-
30-
/**
31-
* Tests for correct serialization and deserialization of map fields.
32-
*/
38+
import protos.RandomMessage;
39+
import protos.RealScalarBytesMap;
40+
import protos.RealScalarEnumMap;
41+
import protos.RealScalarMsgMap;
42+
import protos.RealStringBytesMap;
43+
import protos.RealStringStringMap;
44+
45+
/** Tests for correct serialization and deserialization of map fields. */
3346
public class MapsTest extends ProtobufTest {
3447

3548
@AutoreleasePool
@@ -272,6 +285,119 @@ public void testEquals() throws Exception {
272285
assertEquals(msg1.hashCode(), msg2.hashCode());
273286
}
274287

288+
public void testStringStringRepeatedFieldsToMapConversions() throws Exception {
289+
// According to https://protobuf.dev/programming-guides/proto3/#backwards repeated fields are
290+
// converted to maps by using the first field as the key and the second field as the value.
291+
// This also verifies that we can parse maps with missing fields by using default values.
292+
FakeStringStringMap fakeMap =
293+
FakeStringStringMap.newBuilder()
294+
.addMapField(FakeStringStringMapFieldEntry.newBuilder().setKey("duck").build())
295+
.addMapField(FakeStringStringMapFieldEntry.newBuilder().setValue("quack").build())
296+
.addMapField(
297+
FakeStringStringMapFieldEntry.newBuilder().setKey("cat").setValue("meow").build())
298+
.build();
299+
byte[] bytes = fakeMap.toByteArray();
300+
RealStringStringMap realMap = RealStringStringMap.parseFrom(bytes);
301+
assertEquals("", realMap.getMapFieldOrThrow("duck"));
302+
assertEquals("quack", realMap.getMapFieldOrThrow(""));
303+
assertEquals("meow", realMap.getMapFieldOrThrow("cat"));
304+
}
305+
306+
public void testScalarBytesRepeatedFieldsToMapConversions() throws Exception {
307+
// According to https://protobuf.dev/programming-guides/proto3/#backwards repeated fields are
308+
// converted to maps by using the first field as the key and the second field as the value.
309+
// This also verifies that we can parse maps with missing fields by using default values.
310+
FakeScalarBytesMap fakeScalarBytesMap =
311+
FakeScalarBytesMap.newBuilder()
312+
.addMapField(FakeScalarBytesMapFieldEntry.newBuilder().setKey(42).build())
313+
.addMapField(
314+
FakeScalarBytesMapFieldEntry.newBuilder().setValue(ByteString.EMPTY).build())
315+
.build();
316+
byte[] bytes = fakeScalarBytesMap.toByteArray();
317+
RealScalarBytesMap realScalarBytesMap = RealScalarBytesMap.parseFrom(bytes);
318+
assertEquals(ByteString.EMPTY, realScalarBytesMap.getMapFieldOrThrow(42));
319+
assertEquals(ByteString.EMPTY, realScalarBytesMap.getMapFieldOrThrow(0));
320+
}
321+
322+
public void testScalarMsgRepeatedFieldsToMapConversions() throws Exception {
323+
// According to https://protobuf.dev/programming-guides/proto3/#backwards repeated fields are
324+
// converted to maps by using the first field as the key and the second field as the value.
325+
// This also verifies that we can parse maps with missing fields by using default values.
326+
FakeScalarMsgMap fakeScalarMsgMap =
327+
FakeScalarMsgMap.newBuilder()
328+
.addMapField(FakeScalarMsgMapFieldEntry.newBuilder().setKey(777).build())
329+
.addMapField(
330+
FakeScalarMsgMapFieldEntry.newBuilder()
331+
.setValue(RandomMessage.getDefaultInstance())
332+
.build())
333+
.build();
334+
byte[] bytes = fakeScalarMsgMap.toByteArray();
335+
RealScalarMsgMap realScalarMsgMap = RealScalarMsgMap.parseFrom(bytes);
336+
assertEquals(RandomMessage.getDefaultInstance(), realScalarMsgMap.getMapFieldOrThrow(777));
337+
assertEquals(RandomMessage.getDefaultInstance(), realScalarMsgMap.getMapFieldOrThrow(0));
338+
}
339+
340+
public void testScalarEnumRepeatedFieldsToMapConversions() throws Exception {
341+
// According to https://protobuf.dev/programming-guides/proto3/#backwards repeated fields are
342+
// converted to maps by using the first field as the key and the second field as the value.
343+
// This also verifies that we can parse maps with missing fields by using default values.
344+
// Note that the default value for an enum is the first enum value.
345+
FakeScalarEnumMap fakeScalarEnumMap =
346+
FakeScalarEnumMap.newBuilder()
347+
.addMapField(FakeScalarEnumMapFieldEntry.newBuilder().setKey(123).build())
348+
.addMapField(
349+
FakeScalarEnumMapFieldEntry.newBuilder()
350+
.setKey(456)
351+
.setValue(MapMsg.Color.YELLOW)
352+
.build())
353+
.build();
354+
byte[] bytes = fakeScalarEnumMap.toByteArray();
355+
RealScalarEnumMap realScalarEnumMap = RealScalarEnumMap.parseFrom(bytes);
356+
assertEquals(MapMsg.Color.GREEN, realScalarEnumMap.getMapFieldOrThrow(123));
357+
assertEquals(MapMsg.Color.YELLOW, realScalarEnumMap.getMapFieldOrThrow(456));
358+
}
359+
360+
public void testBadValueType() throws Exception {
361+
// Verifies that maps fail to parse if the value type is not the expected type.
362+
FakeScalarBytesMap fakeMap =
363+
FakeScalarBytesMap.newBuilder()
364+
.addMapField(
365+
FakeScalarBytesMapFieldEntry.newBuilder()
366+
.setKey(7)
367+
.setValue(ByteString.copyFromUtf8("hello"))
368+
.build())
369+
.build();
370+
byte[] bytes = fakeMap.toByteArray();
371+
try {
372+
RealScalarMsgMap realMap = RealScalarMsgMap.parseFrom(bytes);
373+
fail("Expected exception instead of map: " + realMap);
374+
} catch (Exception e) {
375+
// Expected.
376+
}
377+
}
378+
379+
// This test is disabled because the java runtime doesn't throw an exception when the key type is
380+
// not the expected type. Instead, it has undefined behavior with regards to what map you actually
381+
// get. The j2objc runtime throws an exception.
382+
public void disabledTestBadKeyType() throws Exception {
383+
// Verifies that maps fail to parse if the key type is not the expected type.
384+
FakeScalarBytesMap fakeMap =
385+
FakeScalarBytesMap.newBuilder()
386+
.addMapField(
387+
FakeScalarBytesMapFieldEntry.newBuilder()
388+
.setKey(7)
389+
.setValue(ByteString.copyFromUtf8("hello"))
390+
.build())
391+
.build();
392+
byte[] bytes = fakeMap.toByteArray();
393+
try {
394+
RealStringBytesMap realMap = RealStringBytesMap.parseFrom(bytes);
395+
fail("Expected exception instead of map: " + realMap);
396+
} catch (Exception e) {
397+
// Expected.
398+
}
399+
}
400+
275401
public void testToString() throws Exception {
276402
String result = getFilledMessage().toString();
277403
assertTrue(result.contains("int_int"));

protobuf/tests/protos/map_fields.proto

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,71 @@ message MapMsg {
3838
}
3939

4040
message MapValue {
41-
string foo = 1 [
42-
features.field_presence = LEGACY_REQUIRED
43-
];
41+
string foo = 1 [features.field_presence = LEGACY_REQUIRED];
42+
}
43+
44+
// Verifying conversion from RepeatedFields to Maps for string, string.
45+
message FakeStringStringMapFieldEntry {
46+
string key = 1;
47+
string value = 2;
48+
}
49+
50+
message FakeStringStringMap {
51+
repeated FakeStringStringMapFieldEntry map_field = 1;
52+
}
53+
54+
message RealStringStringMap {
55+
map<string, string> map_field = 1;
56+
}
57+
58+
// Verifying conversion from RepeatedFields to Maps for scalar, bytes.
59+
message FakeScalarBytesMapFieldEntry {
60+
int32 key = 1;
61+
bytes value = 2;
62+
}
63+
64+
message FakeScalarBytesMap {
65+
repeated FakeScalarBytesMapFieldEntry map_field = 1;
66+
}
67+
68+
message RealScalarBytesMap {
69+
map<int32, bytes> map_field = 1;
70+
}
71+
72+
// Verifying conversion from RepeatedFields to Maps for scalar, msg.
73+
message RandomMessage {
74+
string foo = 1;
75+
}
76+
77+
message FakeScalarMsgMapFieldEntry {
78+
int32 key = 1;
79+
RandomMessage value = 2;
80+
}
81+
82+
message FakeScalarMsgMap {
83+
repeated FakeScalarMsgMapFieldEntry map_field = 1;
84+
}
85+
86+
message RealScalarMsgMap {
87+
map<int32, RandomMessage> map_field = 1;
88+
}
89+
90+
// Verifying conversion from RepeatedFields to Maps for scalar, enum.
91+
// Specifically making sure the default value is used when the value is
92+
// missing.
93+
message FakeScalarEnumMapFieldEntry {
94+
int32 key = 1;
95+
MapMsg.Color value = 2;
96+
}
97+
98+
message FakeScalarEnumMap {
99+
repeated FakeScalarEnumMapFieldEntry map_field = 1;
100+
}
101+
102+
message RealScalarEnumMap {
103+
map<int32, MapMsg.Color> map_field = 1;
104+
}
105+
106+
message RealStringBytesMap {
107+
map<string, bytes> map_field = 1;
44108
}

0 commit comments

Comments
 (0)