diff --git a/java/core/BUILD.bazel b/java/core/BUILD.bazel index 43053bd0d91bf..8da283ee6ed09 100644 --- a/java/core/BUILD.bazel +++ b/java/core/BUILD.bazel @@ -1,6 +1,5 @@ load("@bazel_skylib//rules:build_test.bzl", "build_test") load("@rules_java//java:java_library.bzl", "java_library") -load("@rules_pkg//pkg:mappings.bzl", "pkg_files", "strip_prefix") load("//:protobuf.bzl", "internal_gen_well_known_protos_java") load("//:protobuf_version.bzl", "PROTOBUF_JAVA_VERSION") load("//bazel:cc_proto_library.bzl", "cc_proto_library") @@ -55,6 +54,7 @@ LITE_SRCS = [ "src/main/java/com/google/protobuf/Internal.java", "src/main/java/com/google/protobuf/InternalLazyField.java", "src/main/java/com/google/protobuf/InvalidProtocolBufferException.java", + "src/main/java/com/google/protobuf/InvalidProtobufRuntimeException.java", "src/main/java/com/google/protobuf/IterableByteBufferInputStream.java", "src/main/java/com/google/protobuf/Java8Compatibility.java", "src/main/java/com/google/protobuf/JavaType.java", diff --git a/java/core/src/main/java/com/google/protobuf/ExtensionRegistryLite.java b/java/core/src/main/java/com/google/protobuf/ExtensionRegistryLite.java index 951575e5032d1..61af0afd3ff96 100644 --- a/java/core/src/main/java/com/google/protobuf/ExtensionRegistryLite.java +++ b/java/core/src/main/java/com/google/protobuf/ExtensionRegistryLite.java @@ -53,6 +53,27 @@ public class ExtensionRegistryLite { // applications. Need to support this feature on smaller granularity. private static volatile boolean eagerlyParseMessageSets = false; + enum LazyExtensionMode { + EAGER, + // Caution: This mode is unsafe as it postpone parsing errors such as required fields missing + // until first access. + UNVERIFIED_LAZY; + } + + private static volatile LazyExtensionMode lazyExtensionMode = LazyExtensionMode.EAGER; + + static void setLazyExtensionMode(LazyExtensionMode mode) { + lazyExtensionMode = mode; + } + + static LazyExtensionMode getLazyExtensionMode() { + return lazyExtensionMode; + } + + static boolean lazyExtensionEnabled() { + return lazyExtensionMode == LazyExtensionMode.UNVERIFIED_LAZY; + } + // Visible for testing. static final String EXTENSION_CLASS_NAME = "com.google.protobuf.Extension"; diff --git a/java/core/src/main/java/com/google/protobuf/InternalLazyField.java b/java/core/src/main/java/com/google/protobuf/InternalLazyField.java index f4377d6782c1b..05a3c439643ed 100644 --- a/java/core/src/main/java/com/google/protobuf/InternalLazyField.java +++ b/java/core/src/main/java/com/google/protobuf/InternalLazyField.java @@ -1,3 +1,10 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + package com.google.protobuf; import java.io.IOException; @@ -29,7 +36,9 @@ class InternalLazyField { protected volatile MessageLite value; // Whether the lazy field (i.e. {@link bytes}) was corrupted, and if so, {@link value} must be - // `null`. + // `null`. This is used to avoid repeat parsing attempts on invalid bytes. + // TODO: b/473034710 - Can we drop this field as it might be unreal for people to re-parse the + // bytes, especially an exception has already been thrown? private volatile boolean corrupted; /** Constructor for InternalLazyField. All arguments cannot be null. */ @@ -65,42 +74,178 @@ class InternalLazyField { this.corrupted = false; } - private boolean containsEmptyBytes() { - return bytes == null || bytes.isEmpty(); + /** + * Merges the InternalLazyField from the given CodedInputStream with the given extension registry. + * + *

Precondition: the input stream should have already read the tag and be expected to read the + * size of the bytes next. + * + * @throws IOException only if an error occurs while reading the raw bytes from the input stream, + * not for failures in parsing the read bytes. + * @throws InvalidProtobufRuntimeException if the lazy field is corrupted and cannot be merged + * with a different extension registry. + */ + static InternalLazyField mergeFrom( + InternalLazyField lazyField, CodedInputStream input, ExtensionRegistryLite extensionRegistry) + throws IOException { + ByteString inputBytes = input.readBytes(); + if (lazyField.isEmpty()) { + return new InternalLazyField(lazyField.defaultInstance, extensionRegistry, inputBytes); + } + + if (inputBytes.isEmpty()) { + return lazyField; + } + + if (lazyField.hasBytes()) { + if (lazyField.extensionRegistry == extensionRegistry) { + return new InternalLazyField( + lazyField.defaultInstance, extensionRegistry, lazyField.bytes.concat(inputBytes)); + } + // The extension registries are different, so we need to parse the bytes first to "consume" + // the extension registry of this lazy field. + try { + lazyField.ensureInitialized(); + } catch (InvalidProtocolBufferException e) { + throw new InvalidProtobufRuntimeException( + "Cannot merge invalid lazy field from bytes that is absent or with a different" + + " extension registry.", + e); + } + } + + // TODO: b/473034710 - Consider avoiding the copy as it's parsing into a message. + return mergeValueFromBytes(lazyField, inputBytes, extensionRegistry); } - private void initializeValue() { + /** + * Merges a InternalLazyField from another InternalLazyField. + * + * @throws InvalidProtobufRuntimeException if either lazy field is corrupted and cannot be merged + * with a different extension registry. + */ + static InternalLazyField mergeFrom(InternalLazyField self, InternalLazyField other) { + if (self.defaultInstance != other.defaultInstance) { + throw new IllegalArgumentException( + "LazyFields with different default instances cannot be merged."); + } + + // If either InternalLazyField is empty, return the other InternalLazyField. + if (self.isEmpty()) { + return other; + } + if (other.isEmpty()) { + return self; + } + + // Fast path: concatenate the bytes if both LazyFields contain bytes and have the same extension + // registry, even if one or both are corrupted. + if (self.hasBytes() && other.hasBytes() && self.extensionRegistry == other.extensionRegistry) { + return new InternalLazyField( + self.defaultInstance, self.extensionRegistry, self.bytes.concat(other.bytes)); + } + + // Cannot concatenate the bytes right way. Try initializing the value first and merge from + // the other. + try { + self.ensureInitialized(); + } catch (InvalidProtocolBufferException e) { + // The self lazy field is corrupted, meaning that it contains invalid bytes to be + // concatenated. However, we should only do so if other has bytes and the extension registries + // are the same. Which has already been handled above. + throw new InvalidProtobufRuntimeException( + "Cannot merge invalid lazy field from bytes that is absent or with a different" + + " extension registry.", + e); + } + + // Merge from the other depending on whether the other contains bytes or value. + if (other.value != null) { + return new InternalLazyField(self.value.toBuilder().mergeFrom(other.value).build()); + } + + // Since other.value is null, other.bytes must be non-null. + return mergeValueFromBytes(self, other.bytes, other.extensionRegistry); + } + + /** + * Merges the InternalLazyField from the given ByteString with the given extension registry. + * + *

It can throw a runtime exception if it cannot parse the bytes. + */ + private static InternalLazyField mergeValueFromBytes( + InternalLazyField self, ByteString inputBytes, ExtensionRegistryLite extensionRegistry) { + try { + return new InternalLazyField( + self.value.toBuilder().mergeFrom(inputBytes, extensionRegistry).build()); + } catch (InvalidProtocolBufferException e) { + // If the input bytes is corrupted, we should cancat bytes. However, we should only do so if + // the extension registries are the same AND self.bytes is present. This should have been + // handled in the mergeFrom function, so we just throw an exception here. + throw new InvalidProtobufRuntimeException( + "Cannot merge lazy field from invalid bytes with a different extension registry.", e); + } + } + + private boolean hasBytes() { + return bytes != null; + } + + private boolean isEmpty() { + // Assumes that bytes and value cannot be null at the same time. + return bytes != null ? bytes.isEmpty() : value.equals(defaultInstance); + } + + /** + * Guarantees that `this.value` is non-null or throws. + * + * @throws {InvalidProtocolBufferException} If `bytes` cannot be parsed. + */ + private void ensureInitialized() throws InvalidProtocolBufferException { if (value != null) { return; } synchronized (this) { if (corrupted) { - return; + throw new InvalidProtocolBufferException("Repeat access to corrupted lazy field"); } try { - if (!containsEmptyBytes()) { - value = defaultInstance.getParserForType().parseFrom(bytes, extensionRegistry); - } else { - value = defaultInstance; - } + // `Bytes` is guaranteed to be non-null since `value` was null. + value = defaultInstance.getParserForType().parseFrom(bytes, extensionRegistry); } catch (InvalidProtocolBufferException e) { corrupted = true; - value = null; + throw e; } } } + /** + * Returns the parsed message value. + * + *

If the lazy field is corrupted, it will throw a runtime exception if {@link + * ExtensionRegistryLite#lazyExtensionEnabled()} is true, otherwise it will return the default + * instance. + */ MessageLite getValue() { - initializeValue(); - return value == null ? defaultInstance : value; + try { + ensureInitialized(); + return value; + } catch (InvalidProtocolBufferException e) { + if (ExtensionRegistryLite.lazyExtensionEnabled()) { + // New behavior: runtime exception on corrupted extensions. + throw new InvalidProtobufRuntimeException(e); + } else { + // Old behavior: silently return the default instance. + return defaultInstance; + } + } } + // TODO: b/473034710 - Consider returning `toByteString().size()` as an optimization to + // materialize the bytes expecting `toByteString()` to be called right after. int getSerializedSize() { - if (bytes != null) { - return bytes.size(); - } - return value.getSerializedSize(); + return bytes != null ? bytes.size() : value.getSerializedSize(); } ByteString toByteString() { diff --git a/java/core/src/main/java/com/google/protobuf/InvalidProtobufRuntimeException.java b/java/core/src/main/java/com/google/protobuf/InvalidProtobufRuntimeException.java new file mode 100644 index 0000000000000..572c6eaeb346d --- /dev/null +++ b/java/core/src/main/java/com/google/protobuf/InvalidProtobufRuntimeException.java @@ -0,0 +1,28 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +package com.google.protobuf; + +/** + * Thrown when a protocol message being parsed is invalid in some way, but a checked exception is + * not allowed. For instance, in a lazy field value getter. + */ +public class InvalidProtobufRuntimeException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public InvalidProtobufRuntimeException(String description) { + super(description); + } + + public InvalidProtobufRuntimeException(Exception e) { + super(e.getMessage(), e); + } + + public InvalidProtobufRuntimeException(String description, Exception e) { + super(description, e); + } +} diff --git a/java/core/src/test/java/com/google/protobuf/InternalLazyFieldTest.java b/java/core/src/test/java/com/google/protobuf/InternalLazyFieldTest.java index de5a62b9ffe7b..023cfd97925d1 100644 --- a/java/core/src/test/java/com/google/protobuf/InternalLazyFieldTest.java +++ b/java/core/src/test/java/com/google/protobuf/InternalLazyFieldTest.java @@ -1,18 +1,61 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2026 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + package com.google.protobuf; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import proto2_unittest.UnittestProto; import proto2_unittest.UnittestProto.TestAllExtensions; import proto2_unittest.UnittestProto.TestAllTypes; +import proto2_unittest.UnittestProto.TestRequired; +import java.io.ByteArrayOutputStream; +import java.util.Arrays; +import java.util.List; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; -@RunWith(JUnit4.class) +@RunWith(Parameterized.class) public final class InternalLazyFieldTest { private static final ExtensionRegistryLite EXTENSION_REGISTRY = TestUtil.getExtensionRegistry(); + private final ExtensionRegistryLite.LazyExtensionMode mode; + private final ExtensionRegistryLite.LazyExtensionMode defaultMode; + + public InternalLazyFieldTest(ExtensionRegistryLite.LazyExtensionMode mode) { + this.defaultMode = ExtensionRegistryLite.getLazyExtensionMode(); + this.mode = mode; + } + + @Parameters + public static List data() { + return Arrays.asList( + new Object[][] { + {ExtensionRegistryLite.LazyExtensionMode.EAGER}, + {ExtensionRegistryLite.LazyExtensionMode.UNVERIFIED_LAZY} + }); + } + + @Before + public void setUp() { + ExtensionRegistryLite.setLazyExtensionMode(mode); + } + + @After + public void tearDown() { + ExtensionRegistryLite.setLazyExtensionMode(defaultMode); + } + @Test public void testGetValue() { MessageLite message = TestUtil.getAllSet(); @@ -41,14 +84,54 @@ public void testGetValueMemoized() { } @Test - public void testGetValueOnInvalidData_defaultInstance() { + public void testGetValueOnInvalidData() { + InternalLazyField lazyField = + new InternalLazyField( + TestAllTypes.getDefaultInstance(), + EXTENSION_REGISTRY, + ByteString.copyFromUtf8("invalid")); + + if (mode == ExtensionRegistryLite.LazyExtensionMode.UNVERIFIED_LAZY) { + assertThrows(InvalidProtobufRuntimeException.class, () -> lazyField.getValue()); + } else { + assertThat(lazyField.getValue()).isEqualTo(TestAllTypes.getDefaultInstance()); + } + } + + @Test + public void testGetValueDataWtihInvalidExtension() { + TestAllExtensions message = + TestAllExtensions.newBuilder() + .setExtension(TestRequired.single, TestRequired.getDefaultInstance()) + .buildPartial(); + InternalLazyField lazyField = createLazyFieldWithBytesFromMessage(message); + + if (mode == ExtensionRegistryLite.LazyExtensionMode.UNVERIFIED_LAZY) { + assertThrows(InvalidProtobufRuntimeException.class, () -> lazyField.getValue()); + } else { + assertThat(lazyField.getValue()).isEqualTo(TestAllExtensions.getDefaultInstance()); + } + } + + @Test + public void testGetValueOnInvalidDataTwice() { InternalLazyField lazyField = new InternalLazyField( TestAllTypes.getDefaultInstance(), EXTENSION_REGISTRY, ByteString.copyFromUtf8("invalid")); - assertThat(lazyField.getValue()).isEqualTo(TestAllTypes.getDefaultInstance()); + if (mode == ExtensionRegistryLite.LazyExtensionMode.UNVERIFIED_LAZY) { + assertThrows(InvalidProtobufRuntimeException.class, () -> lazyField.getValue()); + Throwable exception = + assertThrows(InvalidProtobufRuntimeException.class, () -> lazyField.getValue()); + assertThat(exception).hasMessageThat().contains("Repeat access to corrupted lazy field"); + } else { + MessageLite firstValue = lazyField.getValue(); + MessageLite secondValue = lazyField.getValue(); + assertThat(firstValue).isEqualTo(TestAllTypes.getDefaultInstance()); + assertThat(firstValue).isSameInstanceAs(secondValue); + } } @Test @@ -127,8 +210,389 @@ public void testEqualsObjectContainingExtensions() throws Exception { assertThat(lazyField).isEqualTo(message); } + // Tests for mergeFrom(InternalLazyField lazyField, CodedInputStream input, + // ExtensionRegistryLite extensionRegistry) + + @Test + public void testEmptyLazyFieldMergeFromBytes() throws Exception { + MessageLite message = TestUtil.getAllSet(); + InternalLazyField emptyLazyField = + new InternalLazyField( + TestAllTypes.getDefaultInstance(), EXTENSION_REGISTRY, ByteString.EMPTY); + CodedInputStream input = createCodedInputStreamFromMessage(message); + + InternalLazyField mergedLazyField = + InternalLazyField.mergeFrom(emptyLazyField, input, EXTENSION_REGISTRY); + + assertThat(mergedLazyField.getValue()).isEqualTo(message); + } + + @Test + public void testMergeFromEmptyBytes() throws Exception { + MessageLite message = TestUtil.getAllSet(); + InternalLazyField lazyField = createLazyFieldWithBytesFromMessage(message); + CodedInputStream input = createCodedInputStreamFromBytes(ByteString.EMPTY); + + InternalLazyField mergedLazyField = + InternalLazyField.mergeFrom(lazyField, input, EXTENSION_REGISTRY); + + assertThat(mergedLazyField.getValue()).isEqualTo(message); + assertThat(mergedLazyField).isSameInstanceAs(lazyField); + } + + @Test + public void testMergeFromBytesSameExtensionRegistry() throws Exception { + InternalLazyField lazyField = + createLazyFieldWithBytesFromMessage(TestAllTypes.newBuilder().setOptionalInt32(1).build()); + CodedInputStream input = + createCodedInputStreamFromMessage(TestAllTypes.newBuilder().setOptionalInt64(2).build()); + + InternalLazyField mergedLazyField = + InternalLazyField.mergeFrom(lazyField, input, EXTENSION_REGISTRY); + + assertThat(mergedLazyField.getValue()) + .isEqualTo(TestAllTypes.newBuilder().setOptionalInt32(1).setOptionalInt64(2).build()); + } + + @Test + public void testMergeExtensionsFromBytesSameExtensionRegistry() throws Exception { + InternalLazyField lazyField = + createLazyFieldWithBytesFromMessage( + TestAllExtensions.newBuilder() + .setExtension(UnittestProto.optionalInt32Extension, 1) + .build()); + CodedInputStream input = + createCodedInputStreamFromMessage( + TestAllExtensions.newBuilder() + .setExtension(UnittestProto.optionalInt64Extension, 2L) + .build()); + + InternalLazyField mergedLazyField = + InternalLazyField.mergeFrom(lazyField, input, EXTENSION_REGISTRY); + + assertThat(mergedLazyField.getValue()) + .isEqualTo( + TestAllExtensions.newBuilder() + .setExtension(UnittestProto.optionalInt32Extension, 1) + .setExtension(UnittestProto.optionalInt64Extension, 2L) + .build()); + } + + @Test + public void testMergeFromBytesDifferentExtensionRegistry() throws Exception { + InternalLazyField lazyField = + createLazyFieldWithBytesFromMessage(TestAllTypes.newBuilder().setOptionalInt32(1).build()); + CodedInputStream input = + createCodedInputStreamFromMessage(TestAllTypes.newBuilder().setOptionalInt64(2).build()); + + InternalLazyField mergedLazyField = + InternalLazyField.mergeFrom(lazyField, input, TestUtil.getExtensionRegistry()); + + assertThat(mergedLazyField.getValue()) + .isEqualTo(TestAllTypes.newBuilder().setOptionalInt32(1).setOptionalInt64(2).build()); + } + + @Test + public void testMergeExtensionsFromBytesDifferentExtensionRegistry() throws Exception { + ExtensionRegistryLite extensionRegistry1 = ExtensionRegistryLite.newInstance(); + extensionRegistry1.add(UnittestProto.optionalInt32Extension); + InternalLazyField lazyField = + createLazyFieldWithBytesFromMessage( + TestAllExtensions.newBuilder() + .setExtension(UnittestProto.optionalInt32Extension, 1) + .build(), + extensionRegistry1); + ExtensionRegistryLite extensionRegistry2 = ExtensionRegistryLite.newInstance(); + extensionRegistry2.add(UnittestProto.optionalInt64Extension); + CodedInputStream input = + createCodedInputStreamFromMessage( + TestAllExtensions.newBuilder() + .setExtension(UnittestProto.optionalInt64Extension, 2L) + .build()); + + InternalLazyField mergedLazyField = + InternalLazyField.mergeFrom(lazyField, input, extensionRegistry2); + + assertThat(mergedLazyField.getValue()) + .isEqualTo( + TestAllExtensions.newBuilder() + .setExtension(UnittestProto.optionalInt32Extension, 1) + .setExtension(UnittestProto.optionalInt64Extension, 2L) + .build()); + } + + @Test + public void testInvalidFieldMergeFromBytesBecomesValid() throws Exception { + // missing c + InternalLazyField lazyField = + createLazyFieldWithBytesFromMessage( + TestRequired.newBuilder().setA(1).setB(2).buildPartial()); + // missing a + CodedInputStream input = + createCodedInputStreamFromMessage( + TestRequired.newBuilder().setB(123).setC(3).buildPartial()); + + InternalLazyField mergedLazyField = + InternalLazyField.mergeFrom(lazyField, input, EXTENSION_REGISTRY); + + assertThat(mergedLazyField.getValue()) + .isEqualTo(TestRequired.newBuilder().setA(1).setB(123).setC(3).build()); + } + + @Test + public void testInvalidFieldMergeFromBytesException() throws Exception { + InternalLazyField lazyField = + new InternalLazyField( + TestAllTypes.getDefaultInstance(), + EXTENSION_REGISTRY, + ByteString.copyFromUtf8("invalid")); + CodedInputStream input = createCodedInputStreamFromMessage(TestUtil.getAllSet()); + ExtensionRegistryLite extensionRegistry = TestUtil.getExtensionRegistry(); + + Throwable exception = + assertThrows( + InvalidProtobufRuntimeException.class, + () -> InternalLazyField.mergeFrom(lazyField, input, extensionRegistry)); + + assertThat(exception).hasMessageThat().contains("Cannot merge invalid lazy field"); + } + + @Test + public void testInvalidFieldMergeFromBytesKeepsInvalid() throws Exception { + InternalLazyField lazyField = + new InternalLazyField( + TestAllTypes.getDefaultInstance(), + EXTENSION_REGISTRY, + ByteString.copyFromUtf8("invalidinvalid")); + CodedInputStream input = + createCodedInputStreamFromMessage( + TestAllTypes.newBuilder().setOptionalString("and valid").build()); + + InternalLazyField mergedLazyField = + InternalLazyField.mergeFrom(lazyField, input, EXTENSION_REGISTRY); + + if (mode == ExtensionRegistryLite.LazyExtensionMode.UNVERIFIED_LAZY) { + assertThrows(InvalidProtobufRuntimeException.class, () -> mergedLazyField.getValue()); + } else { + assertThat(mergedLazyField.getValue()).isEqualTo(TestAllTypes.getDefaultInstance()); + } + } + + @Test + public void testMergeFromInvalidBytesException() throws Exception { + InternalLazyField lazyField = createLazyFieldWithBytesFromMessage(TestUtil.getAllSet()); + CodedInputStream input = createCodedInputStreamFromBytes(ByteString.copyFromUtf8("invalid")); + ExtensionRegistryLite extensionRegistry = TestUtil.getExtensionRegistry(); + + Throwable exception = + assertThrows( + InvalidProtobufRuntimeException.class, + () -> InternalLazyField.mergeFrom(lazyField, input, extensionRegistry)); + assertThat(exception).hasMessageThat().contains("Cannot merge lazy field from invalid bytes"); + } + + // Tests for mergeFrom(InternalLazyField self, InternalLazyField other) + + @Test + public void testMergeFromDifferentDefaultInstances() throws Exception { + InternalLazyField lazyField1 = + new InternalLazyField( + TestAllTypes.getDefaultInstance(), EXTENSION_REGISTRY, ByteString.EMPTY); + InternalLazyField lazyField2 = + new InternalLazyField( + TestAllExtensions.getDefaultInstance(), EXTENSION_REGISTRY, ByteString.EMPTY); + + Throwable exception = + assertThrows( + IllegalArgumentException.class, + () -> InternalLazyField.mergeFrom(lazyField1, lazyField2)); + assertThat(exception) + .hasMessageThat() + .contains("LazyFields with different default instances cannot be merged."); + } + + @Test + public void testEmptyFromOtherLazyField() { + InternalLazyField lazyField1 = + new InternalLazyField( + TestAllTypes.getDefaultInstance(), EXTENSION_REGISTRY, ByteString.EMPTY); + InternalLazyField lazyField2 = createLazyFieldWithBytesFromMessage(TestUtil.getAllSet()); + + InternalLazyField mergedLazyField = InternalLazyField.mergeFrom(lazyField1, lazyField2); + + assertThat(mergedLazyField.getValue()).isEqualTo(TestUtil.getAllSet()); + assertThat(mergedLazyField).isSameInstanceAs(lazyField2); + } + + @Test + public void testMergeFromEmptyOtherLazyField() throws Exception { + InternalLazyField lazyField1 = createLazyFieldWithBytesFromMessage(TestUtil.getAllSet()); + InternalLazyField lazyField2 = + new InternalLazyField( + TestAllTypes.getDefaultInstance(), EXTENSION_REGISTRY, ByteString.EMPTY); + + InternalLazyField mergedLazyField = InternalLazyField.mergeFrom(lazyField1, lazyField2); + + assertThat(mergedLazyField.getValue()).isEqualTo(TestUtil.getAllSet()); + assertThat(mergedLazyField).isSameInstanceAs(lazyField1); + } + + @Test + public void testMergeFromOtherBytesSameExtensionRegistryConcat() throws Exception { + InternalLazyField lazyField1 = + createLazyFieldWithBytesFromMessage( + TestAllTypes.newBuilder().setOptionalInt32(123).build()); + InternalLazyField lazyField2 = + createLazyFieldWithBytesFromMessage( + TestAllTypes.newBuilder().setOptionalInt64(456).build()); + + InternalLazyField mergedLazyField = InternalLazyField.mergeFrom(lazyField1, lazyField2); + + assertThat(mergedLazyField.getValue()) + .isEqualTo(TestAllTypes.newBuilder().setOptionalInt32(123).setOptionalInt64(456).build()); + } + + @Test + public void testInvalidMergeFromOtherDifferentExtRegException() throws Exception { + InternalLazyField lazyField1 = + new InternalLazyField( + TestAllTypes.getDefaultInstance(), + EXTENSION_REGISTRY, + ByteString.copyFromUtf8("invalid")); + InternalLazyField lazyField2 = + createLazyFieldWithBytesFromMessage( + TestAllTypes.newBuilder().setOptionalInt64(456).build(), + TestUtil.getExtensionRegistry()); + + Throwable exception = + assertThrows( + InvalidProtobufRuntimeException.class, + () -> InternalLazyField.mergeFrom(lazyField1, lazyField2)); + + assertThat(exception).hasMessageThat().contains("Cannot merge invalid lazy field"); + } + + @Test + public void testInvalidMergeFromOtherSameExtRegException() throws Exception { + InternalLazyField lazyField1 = + new InternalLazyField( + TestAllTypes.getDefaultInstance(), + ExtensionRegistryLite.getEmptyRegistry(), + ByteString.copyFromUtf8("invalid")); + // Construct lazyField2 with bytes==null. + InternalLazyField lazyField2 = + new InternalLazyField(TestAllTypes.newBuilder().setOptionalInt64(456).build()); + + Throwable exception = + assertThrows( + InvalidProtobufRuntimeException.class, + () -> InternalLazyField.mergeFrom(lazyField1, lazyField2)); + + assertThat(exception).hasMessageThat().contains("Cannot merge invalid lazy field"); + } + + @Test + public void testInvalidMergeFromOtherBecomesValid() throws Exception { + // Construct lazyField1 with bytes==null. + InternalLazyField lazyField1 = + new InternalLazyField(TestRequired.newBuilder().setA(1).setB(2).buildPartial()); + InternalLazyField lazyField2 = + createLazyFieldWithBytesFromMessage( + TestRequired.newBuilder().setB(123).setC(3).buildPartial()); + + InternalLazyField mergedLazyField = InternalLazyField.mergeFrom(lazyField1, lazyField2); + + assertThat(mergedLazyField.getValue()) + .isEqualTo(TestRequired.newBuilder().setA(1).setB(123).setC(3).build()); + } + + @Test + public void testMergeFromOtherValue() { + InternalLazyField lazyField1 = + new InternalLazyField(TestAllTypes.newBuilder().setOptionalInt32(123).build()); + InternalLazyField lazyField2 = + new InternalLazyField(TestAllTypes.newBuilder().setOptionalInt64(456).build()); + + InternalLazyField mergedLazyField = InternalLazyField.mergeFrom(lazyField1, lazyField2); + + assertThat(mergedLazyField.getValue()) + .isEqualTo(TestAllTypes.newBuilder().setOptionalInt32(123).setOptionalInt64(456).build()); + } + + @Test + public void testMergeFromOtherBytes() { + InternalLazyField lazyField1 = + new InternalLazyField(TestAllTypes.newBuilder().setOptionalInt32(123).build()); + InternalLazyField lazyField2 = + createLazyFieldWithBytesFromMessage( + TestAllTypes.newBuilder().setOptionalInt64(456).build()); + + InternalLazyField mergedLazyField = InternalLazyField.mergeFrom(lazyField1, lazyField2); + + assertThat(mergedLazyField.getValue()) + .isEqualTo(TestAllTypes.newBuilder().setOptionalInt32(123).setOptionalInt64(456).build()); + } + + // Tests for combinations of getValue(), mergeFrom() and toByteString() + + @Test + public void testGetValueAndMergeFromBytes() throws Exception { + InternalLazyField lazyField = + createLazyFieldWithBytesFromMessage( + TestAllTypes.newBuilder().setOptionalInt32(123).build()); + MessageLite value = lazyField.getValue(); + + assertThat(value).isEqualTo(TestAllTypes.newBuilder().setOptionalInt32(123).build()); + + InternalLazyField mergedLazyField = + InternalLazyField.mergeFrom( + lazyField, + createCodedInputStreamFromMessage( + TestAllTypes.newBuilder().setOptionalInt64(456).build()), + EXTENSION_REGISTRY); + + assertThat(mergedLazyField.getValue()) + .isEqualTo(TestAllTypes.newBuilder().setOptionalInt32(123).setOptionalInt64(456).build()); + } + + @Test + public void testToByteStringAndMergeFromBytes() throws Exception { + InternalLazyField lazyField = + new InternalLazyField(TestAllTypes.newBuilder().setOptionalInt32(123).build()); + ByteString bytes = lazyField.toByteString(); + CodedInputStream input = createCodedInputStreamFromBytes(ByteString.copyFromUtf8("invalid")); + + // lazyField contains non-empty bytes, so merging from other bytes with the same extension + // registry will concatenate the bytes. + InternalLazyField mergedLazyField = + InternalLazyField.mergeFrom(lazyField, input, ExtensionRegistryLite.getEmptyRegistry()); + + assertThat(mergedLazyField.toByteString()) + .isEqualTo(bytes.concat(ByteString.copyFromUtf8("invalid"))); + } + private InternalLazyField createLazyFieldWithBytesFromMessage(MessageLite message) { + return createLazyFieldWithBytesFromMessage(message, EXTENSION_REGISTRY); + } + + private InternalLazyField createLazyFieldWithBytesFromMessage( + MessageLite message, ExtensionRegistryLite extensionRegistry) { return new InternalLazyField( - message.getDefaultInstanceForType(), EXTENSION_REGISTRY, message.toByteString()); + message.getDefaultInstanceForType(), extensionRegistry, message.toByteString()); + } + + private CodedInputStream createCodedInputStreamFromMessage(MessageLite message) throws Exception { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + message.writeDelimitedTo(output); + return CodedInputStream.newInstance(output.toByteArray()); + } + + private CodedInputStream createCodedInputStreamFromBytes(ByteString bytes) throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + CodedOutputStream output = CodedOutputStream.newInstance(baos); + // Skip the tag. + output.writeBytesNoTag(bytes); + output.flush(); + return CodedInputStream.newInstance(baos.toByteArray()); } }