Skip to content

Commit 6d36950

Browse files
Java protobuf verified lazy message extension field prototype
PiperOrigin-RevId: 823639590
1 parent 0bc4192 commit 6d36950

File tree

10 files changed

+718
-22
lines changed

10 files changed

+718
-22
lines changed

java/core/BUILD.bazel

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
load("@bazel_skylib//rules:build_test.bzl", "build_test")
22
load("@rules_java//java:java_library.bzl", "java_library")
3-
load("@rules_pkg//pkg:mappings.bzl", "pkg_files", "strip_prefix")
43
load("//:protobuf.bzl", "internal_gen_well_known_protos_java")
54
load("//:protobuf_version.bzl", "PROTOBUF_JAVA_VERSION")
65
load("//bazel:cc_proto_library.bzl", "cc_proto_library")
@@ -563,6 +562,7 @@ LITE_TEST_EXCLUSIONS = [
563562
"src/test/java/com/google/protobuf/LegacyUnredactedTextFormatTest.java",
564563
"src/test/java/com/google/protobuf/MapForProto2Test.java",
565564
"src/test/java/com/google/protobuf/MapTest.java",
565+
"src/test/java/com/google/protobuf/LazyExtensionFieldsTest.java",
566566
"src/test/java/com/google/protobuf/MessageTest.java",
567567
"src/test/java/com/google/protobuf/NestedBuildersTest.java",
568568
"src/test/java/com/google/protobuf/PackedFieldTest.java",
@@ -654,6 +654,7 @@ junit_tests(
654654
"src/test/java/com/google/protobuf/TestUtil.java",
655655
"src/test/java/com/google/protobuf/TestUtilLite.java",
656656
"src/test/java/com/google/protobuf/RuntimeVersionTest.java",
657+
"src/test/java/com/google/protobuf/LazyExtensionFieldsTest.java",
657658
],
658659
),
659660
test_prefix = "v25SrcJar",
@@ -708,6 +709,7 @@ junit_tests(
708709
"src/test/java/com/google/protobuf/TestUtil.java",
709710
"src/test/java/com/google/protobuf/TestUtilLite.java",
710711
"src/test/java/com/google/protobuf/RuntimeVersionTest.java",
712+
"src/test/java/com/google/protobuf/LazyExtensionFieldsTest.java",
711713
],
712714
),
713715
test_prefix = "v25Jar",

java/core/src/main/java/com/google/protobuf/CodedInputStream.java

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,22 @@ private CodedInputStream() {}
189189
*/
190190
public abstract boolean skipField(final int tag) throws IOException;
191191

192+
/**
193+
* Reads and discards a single varint field.
194+
*
195+
* @throws InvalidProtocolBufferException if the varint is malformed.
196+
*/
197+
abstract void skipRawVarint() throws IOException;
198+
199+
/**
200+
* Reads and discards a single UTF-8 string field.
201+
*
202+
* @throws InvalidProtocolBufferException if the string is not valid UTF-8.
203+
*/
204+
void skipUtf8String(int length) throws IOException {
205+
throw new UnsupportedOperationException("Utf8 string skipping is not implemented.");
206+
}
207+
192208
/**
193209
* Reads a single field and writes it to output in wire format, given its tag value.
194210
*
@@ -1061,14 +1077,31 @@ public int readRawVarint32() throws IOException {
10611077
return (int) readRawVarint64SlowPath();
10621078
}
10631079

1064-
private void skipRawVarint() throws IOException {
1080+
@Override
1081+
void skipRawVarint() throws IOException {
10651082
if (limit - pos >= MAX_VARINT_SIZE) {
10661083
skipRawVarintFastPath();
10671084
} else {
10681085
skipRawVarintSlowPath();
10691086
}
10701087
}
10711088

1089+
@Override
1090+
void skipUtf8String(int length) throws IOException {
1091+
if (length >= 0 && length <= (limit - pos)) {
1092+
if (!Utf8.isValidUtf8(buffer, pos, pos + length)) {
1093+
throw InvalidProtocolBufferException.invalidUtf8();
1094+
}
1095+
pos += length;
1096+
return;
1097+
}
1098+
1099+
if (length < 0) {
1100+
throw InvalidProtocolBufferException.negativeSize();
1101+
}
1102+
throw InvalidProtocolBufferException.truncatedMessage();
1103+
}
1104+
10721105
private void skipRawVarintFastPath() throws IOException {
10731106
for (int i = 0; i < MAX_VARINT_SIZE; i++) {
10741107
if (buffer[pos++] >= 0) {
@@ -1834,7 +1867,8 @@ public int readRawVarint32() throws IOException {
18341867
return (int) readRawVarint64SlowPath();
18351868
}
18361869

1837-
private void skipRawVarint() throws IOException {
1870+
@Override
1871+
void skipRawVarint() throws IOException {
18381872
if (bufferSize - pos >= MAX_VARINT_SIZE) {
18391873
skipRawVarintFastPath();
18401874
} else {

java/core/src/main/java/com/google/protobuf/ExtensionRegistryLite.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,59 @@ public class ExtensionRegistryLite {
5353
// applications. Need to support this feature on smaller granularity.
5454
private static volatile boolean eagerlyParseMessageSets = false;
5555

56+
enum LazyExtensionFieldsExperimentMode {
57+
EAGER("EAGER"),
58+
// Throw exceptions on malformed data, but will defer parsing until first access.
59+
VERIFIED_LAZY("VERIFIED_LAZY"),
60+
// Caution: This mode is unsafe as it postpone parsing errors such as required fields missing
61+
// until first access.
62+
UNVERIFIED_LAZY("UNVERIFIED_LAZY");
63+
64+
private final String name;
65+
66+
LazyExtensionFieldsExperimentMode(String name) {
67+
this.name = name;
68+
}
69+
70+
String getName() {
71+
return name;
72+
}
73+
74+
@SuppressWarnings("StatementSwitchToExpressionSwitch")
75+
static LazyExtensionFieldsExperimentMode fromString(String name) {
76+
switch (name) {
77+
case "EAGER":
78+
return EAGER;
79+
case "VERIFIED_LAZY":
80+
return VERIFIED_LAZY;
81+
case "UNVERIFIED_LAZY":
82+
return UNVERIFIED_LAZY;
83+
default:
84+
throw new IllegalArgumentException("Unknown LazyExtensionFieldsExperimentMode: " + name);
85+
}
86+
}
87+
}
88+
89+
private static volatile LazyExtensionFieldsExperimentMode lazyExtensionFieldsExperimentMode =
90+
LazyExtensionFieldsExperimentMode.EAGER;
91+
92+
static void setLazyExtensionFieldsExperimentMode(LazyExtensionFieldsExperimentMode mode) {
93+
lazyExtensionFieldsExperimentMode = mode;
94+
}
95+
96+
static LazyExtensionFieldsExperimentMode getLazyExtensionFieldsExperimentMode() {
97+
return lazyExtensionFieldsExperimentMode;
98+
}
99+
100+
static boolean lazyExtensionFieldEnabled() {
101+
return !lazyExtensionFieldsExperimentMode.equals(LazyExtensionFieldsExperimentMode.EAGER);
102+
}
103+
104+
static boolean lazyExtensionFieldValidationEnabled() {
105+
return lazyExtensionFieldsExperimentMode.equals(
106+
LazyExtensionFieldsExperimentMode.VERIFIED_LAZY);
107+
}
108+
56109
// Visible for testing.
57110
static final String EXTENSION_CLASS_NAME = "com.google.protobuf.Extension";
58111

java/core/src/main/java/com/google/protobuf/FieldSet.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,15 @@ public Object getField(final T descriptor) {
273273
return o;
274274
}
275275

276+
@SuppressWarnings({"ReturnMissingNullable", "PatternMatchingInstanceof"})
277+
LazyField getLazyField(final T descriptor) {
278+
Object o = fields.get(descriptor);
279+
if (o instanceof LazyField) {
280+
return (LazyField) o;
281+
}
282+
return null;
283+
}
284+
276285
/** Returns true if the field is a lazy field and it is corrupted. */
277286
boolean lazyFieldCorrupted(final T descriptor) {
278287
Object o = fields.get(descriptor);
@@ -493,6 +502,9 @@ private static boolean isMessageFieldValueInitialized(Object value) {
493502
// this method is used by FieldSet.Builder.isInitialized.
494503
return ((MessageLiteOrBuilder) value).isInitialized();
495504
} else if (value instanceof LazyField) {
505+
if (ExtensionRegistryLite.lazyExtensionFieldValidationEnabled()) {
506+
return !((LazyField) value).containsMissingRequiredFields();
507+
}
496508
return true;
497509
} else {
498510
throw new IllegalArgumentException(
@@ -1137,6 +1149,15 @@ public Object getField(final T descriptor) {
11371149
return replaceBuilders(descriptor, value, true);
11381150
}
11391151

1152+
@SuppressWarnings({"ReturnMissingNullable", "PatternMatchingInstanceof"})
1153+
public LazyField getLazyField(final T descriptor) {
1154+
Object value = fields.get(descriptor);
1155+
if (value instanceof LazyField) {
1156+
return (LazyField) value;
1157+
}
1158+
return null;
1159+
}
1160+
11401161
/** Same as {@link #getField(F)}, but allow a {@link MessageLite.Builder} to be returned. */
11411162
Object getFieldAllowBuilders(final T descriptor) {
11421163
Object o = fields.get(descriptor);

java/core/src/main/java/com/google/protobuf/GeneratedMessage.java

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1180,14 +1180,14 @@ public void writeUntil(final int end, final CodedOutputStream output) throws IOE
11801180
output.writeMessageSetExtension(descriptor.getNumber(), (Message) next.getValue());
11811181
}
11821182
} else {
1183-
// TODO: Taken care of following code, it may cause
1184-
// problem when we use LazyField for normal fields/extensions.
1185-
// Due to the optional field can be duplicated at the end of
1186-
// serialized bytes, which will make the serialized size change
1187-
// after lazy field parsed. So when we use LazyField globally,
1188-
// we need to change the following write method to write cached
1189-
// bytes directly rather than write the parsed message.
1190-
FieldSet.writeField(descriptor, next.getValue(), output);
1183+
if (descriptor.getLiteJavaType() == WireFormat.JavaType.MESSAGE
1184+
&& next instanceof LazyField.LazyEntry<?>) {
1185+
output.writeBytes(
1186+
descriptor.getNumber(),
1187+
((LazyField.LazyEntry<?>) next).getField().toByteString());
1188+
} else {
1189+
FieldSet.writeField(descriptor, next.getValue(), output);
1190+
}
11911191
}
11921192
if (iter.hasNext()) {
11931193
next = iter.next();
@@ -2014,13 +2014,21 @@ protected Object fromReflectionType(final Object value) {
20142014
* single element.
20152015
*/
20162016
@Override
2017+
@SuppressWarnings("PatternMatchingInstanceof")
20172018
protected Object singularFromReflectionType(final Object value) {
20182019
FieldDescriptor descriptor = getDescriptor();
20192020
switch (descriptor.getJavaType()) {
20202021
case MESSAGE:
20212022
if (singularType.isInstance(value)) {
20222023
return value;
20232024
} else {
2025+
if (value instanceof LazyField) {
2026+
LazyField lazyField = (LazyField) value;
2027+
return messageDefaultInstance
2028+
.newBuilderForType()
2029+
.mergeFrom(lazyField.getValue())
2030+
.build();
2031+
}
20242032
return messageDefaultInstance.newBuilderForType().mergeFrom((Message) value).build();
20252033
}
20262034
case ENUM:

java/core/src/main/java/com/google/protobuf/LazyFieldLite.java

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@ public class LazyFieldLite {
9191

9292
private volatile boolean corrupted;
9393

94+
/**
95+
* Required field presence is usually checked by calling `isInitialized` on the parsed message
96+
* instead of at decoding phase (when LazyField is initialized), so we need to defer the check
97+
* until it is actually needed. For example, `builder.mergeFrom` will not trigger the check, but
98+
* the following `builder.build()` will.
99+
*/
100+
private volatile boolean containsMissingRequiredFields;
101+
94102
/**
95103
* Carry a message's default instance which is used by {@code hashCode()}, {@code equals()}, and
96104
* {@code toString()}. Can be null.
@@ -118,6 +126,14 @@ public LazyFieldLite() {
118126
defaultInstance = null;
119127
}
120128

129+
public boolean containsMissingRequiredFields() {
130+
return containsMissingRequiredFields;
131+
}
132+
133+
public void setContainsMissingRequiredFields(boolean containsMissingRequiredFields) {
134+
this.containsMissingRequiredFields = containsMissingRequiredFields;
135+
}
136+
121137
/**
122138
* Constructs a LazyFieldLite instance with a value. The LazyFieldLite may not be able to parse
123139
* the extensions in the value as it has no ExtensionRegistry.
@@ -331,10 +347,9 @@ public void merge(LazyFieldLite other) {
331347
* <p>LazyField is not thread-safe for write access. Synchronizations are needed under read/write
332348
* situations.
333349
*/
334-
public void mergeFrom(CodedInputStream input, ExtensionRegistryLite extensionRegistry)
335-
throws IOException {
350+
void mergeFrom(ByteString bytes, ExtensionRegistryLite extensionRegistry) throws IOException {
336351
if (this.containsDefaultInstance()) {
337-
setByteString(input.readBytes(), extensionRegistry);
352+
setByteString(bytes, extensionRegistry);
338353
return;
339354
}
340355

@@ -350,21 +365,29 @@ public void mergeFrom(CodedInputStream input, ExtensionRegistryLite extensionReg
350365
// to outway the benefits of combining the extension registries, which is not normally done for
351366
// lite protos anyways.
352367
if (this.delayedBytes != null) {
353-
setByteString(this.delayedBytes.concat(input.readBytes()), this.extensionRegistry);
368+
setByteString(this.delayedBytes.concat(bytes), this.extensionRegistry);
354369
return;
355370
}
356371

357372
// We are parsed and both contain data. We won't drop any extensions here directly, but in the
358373
// case that the extension registries are not the same then we might in the future if we
359374
// need to serialize and parse a message again.
360375
try {
361-
setValue(value.toBuilder().mergeFrom(input, extensionRegistry).build());
376+
setValue(value.toBuilder().mergeFrom(bytes, extensionRegistry).build());
362377
} catch (InvalidProtocolBufferException e) {
363378
// Nothing is logged and no exceptions are thrown. Clients will be unaware that a proto
364379
// was invalid.
365380
}
366381
}
367382

383+
/**
384+
* Merges another instance's contents from a stream.
385+
*/
386+
public void mergeFrom(CodedInputStream input, ExtensionRegistryLite extensionRegistry)
387+
throws IOException {
388+
mergeFrom(input.readBytes(), extensionRegistry);
389+
}
390+
368391
private static MessageLite mergeValueAndBytes(
369392
MessageLite value, ByteString otherBytes, ExtensionRegistryLite extensionRegistry) {
370393
try {
@@ -477,8 +500,15 @@ protected void ensureInitialized(MessageLite defaultInstance) {
477500
try {
478501
if (delayedBytes != null) {
479502
// The extensionRegistry shouldn't be null here since we have delayedBytes.
503+
// We don't enforce required field presence in lazy mode here, as `isInitialized()` at
504+
// `build()` or `parseFrom()` has already checked missing required fields before this
505+
// step, so exception has already been thrown or `parsePartialFrom()` is used.
480506
MessageLite parsedValue =
481-
defaultInstance.getParserForType().parseFrom(delayedBytes, extensionRegistry);
507+
ExtensionRegistryLite.lazyExtensionFieldValidationEnabled()
508+
? defaultInstance
509+
.getParserForType()
510+
.parsePartialFrom(delayedBytes, extensionRegistry)
511+
: defaultInstance.getParserForType().parseFrom(delayedBytes, extensionRegistry);
482512
this.value = parsedValue;
483513
this.memoizedBytes = delayedBytes;
484514
} else {

0 commit comments

Comments
 (0)