diff --git a/.gitignore b/.gitignore index d8b0bd3842..cc881f3f2c 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,23 @@ m4/ltversion.m4 m4/lt~obsolete.m4 autom4te.cache +# CMake-generated files +.ninja_deps +.ninja_logs +cmake/protobuf/*.cmake +cmake_install.cmake +CMakeCache.txt +CTestTestfile.cmake +CMakeFiles/* +Testing/Temporary/* + +/core +/protoc +/test_plugin +/tests +/lite-test +/protoc-*.* + # downloaded files /gmock @@ -41,6 +58,7 @@ stamp-h1 *.la src/.libs *.so +*.a .dirstamp diff --git a/CHANGES.txt b/CHANGES.txt index c49a53a502..a8e7fd19a9 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 2022-07-01 Unreleased version + C++ * Reduced .pb.o object file size slightly by explicitly instantiating InternalMetadata templates in the runtime. @@ -14,6 +15,12 @@ * Performance improvement for repeated use of FieldMaskUtil#merge by caching constructed FieldMaskTrees. +2022-07-25 version 21.4 (C++/Java/Python/PHP/Objective-C/C#/Ruby) + + C++ + * Reduce the required alignment of ArenaString from 8 to 4 (#10298) + + 2022-07-19 version 21.3 (C++/Java/Python/PHP/Objective-C/C#/Ruby) C++ * Add header search paths to Protobuf-C++.podspec (#10024) diff --git a/Protobuf.podspec b/Protobuf.podspec index 140a142ef5..e41f39a704 100644 --- a/Protobuf.podspec +++ b/Protobuf.podspec @@ -5,7 +5,7 @@ # dependent projects use the :git notation to refer to the library. Pod::Spec.new do |s| s.name = 'Protobuf' - s.version = '3.21.3' + s.version = '3.21.4' s.summary = 'Protocol Buffers v.3 runtime library for Objective-C.' s.homepage = 'https://github.com/protocolbuffers/protobuf' s.license = 'BSD-3-Clause' diff --git a/csharp/Google.Protobuf.Tools.nuspec b/csharp/Google.Protobuf.Tools.nuspec index 3c2f345431..90033cfae8 100644 --- a/csharp/Google.Protobuf.Tools.nuspec +++ b/csharp/Google.Protobuf.Tools.nuspec @@ -5,7 +5,7 @@ Google Protocol Buffers tools Tools for Protocol Buffers - Google's data interchange format. See project site for more info. - 3.21.3 + 3.21.4 Google Inc. protobuf-packages https://github.com/protocolbuffers/protobuf/blob/main/LICENSE diff --git a/csharp/src/Google.Protobuf.Test/JsonFormatterSettingsTest.cs b/csharp/src/Google.Protobuf.Test/JsonFormatterSettingsTest.cs new file mode 100644 index 0000000000..f7ea97c135 --- /dev/null +++ b/csharp/src/Google.Protobuf.Test/JsonFormatterSettingsTest.cs @@ -0,0 +1,111 @@ +#region Copyright notice and license + +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#endregion + +using Google.Protobuf.Reflection; +using NUnit.Framework; + +// For WrapInQuotes + +namespace Google.Protobuf +{ + public class JsonFormatterSettingsTest + { + [Test] + public void WithIndentation() + { + var settings = JsonFormatter.Settings.Default.WithIndentation("\t"); + Assert.AreEqual("\t", settings.Indentation); + } + + [Test] + public void WithTypeRegistry() + { + var typeRegistry = TypeRegistry.Empty; + var settings = JsonFormatter.Settings.Default.WithTypeRegistry(typeRegistry); + Assert.AreEqual(typeRegistry, settings.TypeRegistry); + } + + [Test] + public void WithFormatDefaultValues() + { + var settingsWith = JsonFormatter.Settings.Default.WithFormatDefaultValues(true); + Assert.AreEqual(true, settingsWith.FormatDefaultValues); + + var settingsWithout = JsonFormatter.Settings.Default.WithFormatDefaultValues(false); + Assert.AreEqual(false, settingsWithout.FormatDefaultValues); + } + + [Test] + public void WithFormatEnumsAsIntegers() + { + var settingsWith = JsonFormatter.Settings.Default.WithFormatEnumsAsIntegers(true); + Assert.AreEqual(true, settingsWith.FormatEnumsAsIntegers); + + var settingsWithout = JsonFormatter.Settings.Default.WithFormatEnumsAsIntegers(false); + Assert.AreEqual(false, settingsWithout.FormatEnumsAsIntegers); + } + + [Test] + public void WithMethodsPreserveExistingSettings() + { + var typeRegistry = TypeRegistry.Empty; + var baseSettings = JsonFormatter.Settings.Default + .WithIndentation("\t") + .WithFormatDefaultValues(true) + .WithFormatEnumsAsIntegers(true) + .WithTypeRegistry(typeRegistry) + .WithPreserveProtoFieldNames(true); + + var settings1 = baseSettings.WithIndentation("\t"); + var settings2 = baseSettings.WithFormatDefaultValues(true); + var settings3 = baseSettings.WithFormatEnumsAsIntegers(true); + var settings4 = baseSettings.WithTypeRegistry(typeRegistry); + var settings5 = baseSettings.WithPreserveProtoFieldNames(true); + + AssertAreEqual(baseSettings, settings1); + AssertAreEqual(baseSettings, settings2); + AssertAreEqual(baseSettings, settings3); + AssertAreEqual(baseSettings, settings4); + AssertAreEqual(baseSettings, settings5); + } + + private static void AssertAreEqual(JsonFormatter.Settings settings, JsonFormatter.Settings other) + { + Assert.AreEqual(settings.Indentation, other.Indentation); + Assert.AreEqual(settings.FormatDefaultValues, other.FormatDefaultValues); + Assert.AreEqual(settings.FormatEnumsAsIntegers, other.FormatEnumsAsIntegers); + Assert.AreEqual(settings.TypeRegistry, other.TypeRegistry); + } + } +} diff --git a/csharp/src/Google.Protobuf.Test/JsonFormatterTest.cs b/csharp/src/Google.Protobuf.Test/JsonFormatterTest.cs index 714c78cbeb..f4dfde2435 100644 --- a/csharp/src/Google.Protobuf.Test/JsonFormatterTest.cs +++ b/csharp/src/Google.Protobuf.Test/JsonFormatterTest.cs @@ -674,6 +674,200 @@ namespace Google.Protobuf AssertWriteValue(value, "[ 1, 2, 3 ]"); } + [Test] + public void WriteValueWithIndentation_EmptyMessage() + { + var value = new TestEmptyMessage(); + + AssertWriteValue(value, "{}", JsonFormatter.Settings.Default.WithIndentation()); + } + + [Test] + public void WriteValueWithIndentation_NestedTestAllTypes() + { + var value = new NestedTestAllTypes + { + Payload = new TestAllTypes + { + SingleBool = true, + SingleInt32 = 100, + SingleString = "multiple fields", + RepeatedString = { "string1", "string2" }, + }, + Child = new NestedTestAllTypes + { + Payload = new TestAllTypes + { + SingleString = "single field", + }, + }, + RepeatedChild = + { + new NestedTestAllTypes { Payload = new TestAllTypes { SingleString = "child 1", RepeatedString = { "string" } } }, + new NestedTestAllTypes { Payload = new TestAllTypes { SingleString = "child 2" } }, + }, + }; + + const string expectedJson = @" +{ + 'child': { + 'payload': { + 'singleString': 'single field' + } + }, + 'payload': { + 'singleInt32': 100, + 'singleBool': true, + 'singleString': 'multiple fields', + 'repeatedString': [ + 'string1', + 'string2' + ] + }, + 'repeatedChild': [ + { + 'payload': { + 'singleString': 'child 1', + 'repeatedString': [ + 'string' + ] + } + }, + { + 'payload': { + 'singleString': 'child 2' + } + } + ] +}"; + AssertWriteValue(value, expectedJson, JsonFormatter.Settings.Default.WithIndentation()); + } + + [Test] + public void WriteValueWithIndentation_WellKnownTypes() + { + var value = new TestWellKnownTypes + { + StructField = new Struct + { + Fields = + { + { "string", Value.ForString("foo") }, + { "numbers", Value.ForList(Value.ForNumber(1), Value.ForNumber(2), Value.ForNumber(3)) }, + { "emptyList", Value.ForList() }, + { "emptyStruct", Value.ForStruct(new Struct()) }, + }, + }, + }; + + const string expectedJson = @" +{ + 'structField': { + 'string': 'foo', + 'numbers': [ + 1, + 2, + 3 + ], + 'emptyList': [], + 'emptyStruct': {} + } +}"; + AssertWriteValue(value, expectedJson, JsonFormatter.Settings.Default.WithIndentation()); + } + + [Test] + public void WriteValueWithIndentation_StructSingleField() + { + var value = new Struct { Fields = { { "structField1", Value.ForString("structFieldValue1") } } }; + + const string expectedJson = @" +{ + 'structField1': 'structFieldValue1' +}"; + AssertWriteValue(value, expectedJson, JsonFormatter.Settings.Default.WithIndentation()); + } + + [Test] + public void WriteValueWithIndentation_StructMultipleFields() + { + var value = new Struct + { + Fields = + { + { "structField1", Value.ForString("structFieldValue1") }, + { "structField2", Value.ForString("structFieldValue2") }, + { "structField3", Value.ForString("structFieldValue3") }, + }, + }; + + const string expectedJson = @" +{ + 'structField1': 'structFieldValue1', + 'structField2': 'structFieldValue2', + 'structField3': 'structFieldValue3' +}"; + AssertWriteValue(value, expectedJson, JsonFormatter.Settings.Default.WithIndentation()); + } + + [Test] + public void FormatWithIndentation_EmbeddedMessage() + { + var value = new TestAllTypes { SingleInt32 = 100, SingleInt64 = 3210987654321L }; + var formatter = new JsonFormatter(JsonFormatter.Settings.Default.WithIndentation()); + var valueJson = formatter.Format(value, indentationLevel: 1); + + var actualJson = $@" +{{ + ""data"": {valueJson} +}}"; + const string expectedJson = @" +{ + 'data': { + 'singleInt32': 100, + 'singleInt64': '3210987654321' + } +}"; + AssertJson(expectedJson, actualJson.Trim()); + } + + [Test] + public void WriteValueWithIndentation_Map() + { + var value = new TestMap + { + MapStringString = + { + { "key1", "value1" }, + { "key2", "value2" }, + }, + }; + + const string expectedJson = @" +{ + 'mapStringString': { + 'key1': 'value1', + 'key2': 'value2' + } +}"; + + AssertWriteValue(value, expectedJson, JsonFormatter.Settings.Default.WithIndentation()); + } + + [Test] + public void WriteValueWithIndentation_List() + { + var value = new RepeatedField { 1, 2, 3 }; + AssertWriteValue(value, "[\n 1,\n 2,\n 3\n]", JsonFormatter.Settings.Default.WithIndentation()); + } + + [Test] + public void WriteValueWithIndentation_CustomIndentation() + { + var value = new RepeatedField { 1, 2, 3 }; + AssertWriteValue(value, "[\n\t1,\n\t2,\n\t3\n]", JsonFormatter.Settings.Default.WithIndentation("\t")); + } + [Test] public void Proto2_DefaultValuesWritten() { @@ -683,7 +877,7 @@ namespace Google.Protobuf private static void AssertWriteValue(object value, string expectedJson, JsonFormatter.Settings settings = null) { - var writer = new StringWriter(); + var writer = new StringWriter { NewLine = "\n" }; new JsonFormatter(settings ?? JsonFormatter.Settings.Default).WriteValue(writer, value); string actual = writer.ToString(); AssertJson(expectedJson, actual); @@ -691,13 +885,17 @@ namespace Google.Protobuf /// /// Checks that the actual JSON is the same as the expected JSON - but after replacing - /// all apostrophes in the expected JSON with double quotes. This basically makes the tests easier - /// to read. + /// all apostrophes in the expected JSON with double quotes, trimming leading whitespace and normalizing new lines. + /// This basically makes the tests easier to read. /// + /// + /// Line endings are normalized because indented JSON strings are generated with system-specific line endings, + /// while line endings in the test cases are hard-coded, but may be converted during source checkout, depending + /// on git settings, causing unpredictability in the test results otherwise. private static void AssertJson(string expectedJsonWithApostrophes, string actualJson) { - var expectedJson = expectedJsonWithApostrophes.Replace("'", "\""); - Assert.AreEqual(expectedJson, actualJson); + var expectedJson = expectedJsonWithApostrophes.Replace("'", "\"").Replace("\r\n", "\n").TrimStart(); + Assert.AreEqual(expectedJson, actualJson.Replace("\r\n", "\n")); } } } diff --git a/csharp/src/Google.Protobuf/Google.Protobuf.csproj b/csharp/src/Google.Protobuf/Google.Protobuf.csproj index 3efeabba67..c95ecad94e 100644 --- a/csharp/src/Google.Protobuf/Google.Protobuf.csproj +++ b/csharp/src/Google.Protobuf/Google.Protobuf.csproj @@ -4,7 +4,7 @@ C# runtime library for Protocol Buffers - Google's data interchange format. Copyright 2015, Google Inc. Google Protocol Buffers - 3.21.3 + 3.21.4 10.0 Google Inc. netstandard1.1;netstandard2.0;net45;net50 diff --git a/csharp/src/Google.Protobuf/JsonFormatter.cs b/csharp/src/Google.Protobuf/JsonFormatter.cs index 2ef10ee7e9..4482c8790b 100644 --- a/csharp/src/Google.Protobuf/JsonFormatter.cs +++ b/csharp/src/Google.Protobuf/JsonFormatter.cs @@ -63,7 +63,12 @@ namespace Google.Protobuf internal const string AnyDiagnosticValueField = "@value"; internal const string AnyWellKnownTypeValueField = "value"; private const string NameValueSeparator = ": "; - private const string PropertySeparator = ", "; + private const string ValueSeparator = ", "; + private const string MultilineValueSeparator = ","; + private const char ObjectOpenBracket = '{'; + private const char ObjectCloseBracket = '}'; + private const char ListBracketOpen = '['; + private const char ListBracketClose = ']'; /// /// Returns a formatter using the default settings. @@ -140,11 +145,26 @@ namespace Google.Protobuf /// Formats the specified message as JSON. /// /// The message to format. + /// This method delegates to Format(IMessage, int) with indentationLevel = 0. /// The formatted message. - public string Format(IMessage message) + public string Format(IMessage message) => Format(message, indentationLevel: 0); + + /// + /// Formats the specified message as JSON. + /// + /// The message to format. + /// Indentation level to start at. + /// To keep consistent indentation when embedding a message inside another JSON string, set . E.g: + /// + /// var response = $@"{{ + /// ""data"": { Format(message, indentationLevel: 1) } + /// }}" + /// + /// The formatted message. + public string Format(IMessage message, int indentationLevel) { var writer = new StringWriter(); - Format(message, writer); + Format(message, writer, indentationLevel); return writer.ToString(); } @@ -153,19 +173,29 @@ namespace Google.Protobuf /// /// The message to format. /// The TextWriter to write the formatted message to. + /// This method delegates to Format(IMessage, TextWriter, int) with indentationLevel = 0. /// The formatted message. - public void Format(IMessage message, TextWriter writer) + public void Format(IMessage message, TextWriter writer) => Format(message, writer, indentationLevel: 0); + + /// + /// Formats the specified message as JSON. When is not null, start indenting at the specified . + /// + /// The message to format. + /// The TextWriter to write the formatted message to. + /// Indentation level to start at. + /// To keep consistent indentation when embedding a message inside another JSON string, set . + public void Format(IMessage message, TextWriter writer, int indentationLevel) { ProtoPreconditions.CheckNotNull(message, nameof(message)); ProtoPreconditions.CheckNotNull(writer, nameof(writer)); if (message.Descriptor.IsWellKnownType) { - WriteWellKnownTypeValue(writer, message.Descriptor, message); + WriteWellKnownTypeValue(writer, message.Descriptor, message, indentationLevel); } else { - WriteMessage(writer, message); + WriteMessage(writer, message, indentationLevel); } } @@ -192,7 +222,7 @@ namespace Google.Protobuf return diagnosticFormatter.Format(message); } - private void WriteMessage(TextWriter writer, IMessage message) + private void WriteMessage(TextWriter writer, IMessage message, int indentationLevel) { if (message == null) { @@ -207,12 +237,13 @@ namespace Google.Protobuf return; } } - writer.Write("{ "); - bool writtenFields = WriteMessageFields(writer, message, false); - writer.Write(writtenFields ? " }" : "}"); + + WriteBracketOpen(writer, ObjectOpenBracket); + bool writtenFields = WriteMessageFields(writer, message, false, indentationLevel + 1); + WriteBracketClose(writer, ObjectCloseBracket, writtenFields, indentationLevel); } - private bool WriteMessageFields(TextWriter writer, IMessage message, bool assumeFirstFieldWritten) + private bool WriteMessageFields(TextWriter writer, IMessage message, bool assumeFirstFieldWritten, int indentationLevel) { var fields = message.Descriptor.Fields; bool first = !assumeFirstFieldWritten; @@ -226,10 +257,8 @@ namespace Google.Protobuf continue; } - if (!first) - { - writer.Write(PropertySeparator); - } + MaybeWriteValueSeparator(writer, first); + MaybeWriteValueWhitespace(writer, indentationLevel); if (settings.PreserveProtoFieldNames) { @@ -240,13 +269,23 @@ namespace Google.Protobuf WriteString(writer, accessor.Descriptor.JsonName); } writer.Write(NameValueSeparator); - WriteValue(writer, value); + WriteValue(writer, value, indentationLevel); first = false; } return !first; } + private void MaybeWriteValueSeparator(TextWriter writer, bool first) + { + if (first) + { + return; + } + + writer.Write(settings.Indentation == null ? ValueSeparator : MultilineValueSeparator); + } + /// /// Determines whether or not a field value should be serialized according to the field, /// its value in the message, and the settings of this formatter. @@ -342,7 +381,19 @@ namespace Google.Protobuf /// /// The writer to write the value to. Must not be null. /// The value to write. May be null. - public void WriteValue(TextWriter writer, object value) + /// Delegates to WriteValue(TextWriter, object, int) with indentationLevel = 0. + public void WriteValue(TextWriter writer, object value) => WriteValue(writer, value, 0); + + /// + /// Writes a single value to the given writer as JSON. Only types understood by + /// Protocol Buffers can be written in this way. This method is only exposed for + /// advanced use cases; most users should be using + /// or . + /// + /// The writer to write the value to. Must not be null. + /// The value to write. May be null. + /// The current indentationLevel. Not used when is null. + public void WriteValue(TextWriter writer, object value, int indentationLevel) { if (value == null || value is NullValue) { @@ -365,11 +416,11 @@ namespace Google.Protobuf } else if (value is IDictionary dictionary) { - WriteDictionary(writer, dictionary); + WriteDictionary(writer, dictionary, indentationLevel); } else if (value is IList list) { - WriteList(writer, list); + WriteList(writer, list, indentationLevel); } else if (value is int || value is uint) { @@ -418,7 +469,7 @@ namespace Google.Protobuf } else if (value is IMessage message) { - Format(message, writer); + Format(message, writer, indentationLevel); } else { @@ -432,7 +483,7 @@ namespace Google.Protobuf /// values are using the embedded well-known types, in order to allow for dynamic messages /// in the future. /// - private void WriteWellKnownTypeValue(TextWriter writer, MessageDescriptor descriptor, object value) + private void WriteWellKnownTypeValue(TextWriter writer, MessageDescriptor descriptor, object value, int indentationLevel) { // Currently, we can never actually get here, because null values are always handled by the caller. But if we *could*, // this would do the right thing. @@ -472,26 +523,26 @@ namespace Google.Protobuf } if (descriptor.FullName == Struct.Descriptor.FullName) { - WriteStruct(writer, (IMessage)value); + WriteStruct(writer, (IMessage)value, indentationLevel); return; } if (descriptor.FullName == ListValue.Descriptor.FullName) { var fieldAccessor = descriptor.Fields[ListValue.ValuesFieldNumber].Accessor; - WriteList(writer, (IList)fieldAccessor.GetValue((IMessage)value)); + WriteList(writer, (IList)fieldAccessor.GetValue((IMessage)value), indentationLevel); return; } if (descriptor.FullName == Value.Descriptor.FullName) { - WriteStructFieldValue(writer, (IMessage)value); + WriteStructFieldValue(writer, (IMessage)value, indentationLevel); return; } if (descriptor.FullName == Any.Descriptor.FullName) { - WriteAny(writer, (IMessage)value); + WriteAny(writer, (IMessage)value, indentationLevel); return; } - WriteMessage(writer, (IMessage)value); + WriteMessage(writer, (IMessage)value, indentationLevel); } private void WriteTimestamp(TextWriter writer, IMessage value) @@ -519,7 +570,7 @@ namespace Google.Protobuf writer.Write(FieldMask.ToJson(paths, DiagnosticOnly)); } - private void WriteAny(TextWriter writer, IMessage value) + private void WriteAny(TextWriter writer, IMessage value, int indentationLevel) { if (DiagnosticOnly) { @@ -536,23 +587,23 @@ namespace Google.Protobuf throw new InvalidOperationException($"Type registry has no descriptor for type name '{typeName}'"); } IMessage message = descriptor.Parser.ParseFrom(data); - writer.Write("{ "); + WriteBracketOpen(writer, ObjectOpenBracket); WriteString(writer, AnyTypeUrlField); writer.Write(NameValueSeparator); WriteString(writer, typeUrl); if (descriptor.IsWellKnownType) { - writer.Write(PropertySeparator); + writer.Write(ValueSeparator); WriteString(writer, AnyWellKnownTypeValueField); writer.Write(NameValueSeparator); - WriteWellKnownTypeValue(writer, descriptor, message); + WriteWellKnownTypeValue(writer, descriptor, message, indentationLevel); } else { - WriteMessageFields(writer, message, true); + WriteMessageFields(writer, message, true, indentationLevel); } - writer.Write(" }"); + WriteBracketClose(writer, ObjectCloseBracket, true, indentationLevel); } private void WriteDiagnosticOnlyAny(TextWriter writer, IMessage value) @@ -563,7 +614,7 @@ namespace Google.Protobuf WriteString(writer, AnyTypeUrlField); writer.Write(NameValueSeparator); WriteString(writer, typeUrl); - writer.Write(PropertySeparator); + writer.Write(ValueSeparator); WriteString(writer, AnyDiagnosticValueField); writer.Write(NameValueSeparator); writer.Write('"'); @@ -572,9 +623,9 @@ namespace Google.Protobuf writer.Write(" }"); } - private void WriteStruct(TextWriter writer, IMessage message) + private void WriteStruct(TextWriter writer, IMessage message, int indentationLevel) { - writer.Write("{ "); + WriteBracketOpen(writer, ObjectOpenBracket); IDictionary fields = (IDictionary) message.Descriptor.Fields[Struct.FieldsFieldNumber].Accessor.GetValue(message); bool first = true; foreach (DictionaryEntry entry in fields) @@ -586,19 +637,17 @@ namespace Google.Protobuf throw new InvalidOperationException("Struct fields cannot have an empty key or a null value."); } - if (!first) - { - writer.Write(PropertySeparator); - } + MaybeWriteValueSeparator(writer, first); + MaybeWriteValueWhitespace(writer, indentationLevel + 1); WriteString(writer, key); writer.Write(NameValueSeparator); - WriteStructFieldValue(writer, value); + WriteStructFieldValue(writer, value, indentationLevel + 1); first = false; } - writer.Write(first ? "}" : " }"); + WriteBracketClose(writer, ObjectCloseBracket, !first, indentationLevel); } - private void WriteStructFieldValue(TextWriter writer, IMessage message) + private void WriteStructFieldValue(TextWriter writer, IMessage message, int indentationLevel) { var specifiedField = message.Descriptor.Oneofs[0].Accessor.GetCaseFieldDescriptor(message); if (specifiedField == null) @@ -619,7 +668,7 @@ namespace Google.Protobuf case Value.ListValueFieldNumber: // Structs and ListValues are nested messages, and already well-known types. var nestedMessage = (IMessage) specifiedField.Accessor.GetValue(message); - WriteWellKnownTypeValue(writer, nestedMessage.Descriptor, nestedMessage); + WriteWellKnownTypeValue(writer, nestedMessage.Descriptor, nestedMessage, indentationLevel); return; case Value.NullValueFieldNumber: WriteNull(writer); @@ -629,33 +678,30 @@ namespace Google.Protobuf } } - internal void WriteList(TextWriter writer, IList list) + internal void WriteList(TextWriter writer, IList list, int indentationLevel = 0) { - writer.Write("[ "); + WriteBracketOpen(writer, ListBracketOpen); + bool first = true; foreach (var value in list) { - if (!first) - { - writer.Write(PropertySeparator); - } - WriteValue(writer, value); + MaybeWriteValueSeparator(writer, first); + MaybeWriteValueWhitespace(writer, indentationLevel + 1); + WriteValue(writer, value, indentationLevel + 1); first = false; } - writer.Write(first ? "]" : " ]"); + + WriteBracketClose(writer, ListBracketClose, !first, indentationLevel); } - internal void WriteDictionary(TextWriter writer, IDictionary dictionary) + internal void WriteDictionary(TextWriter writer, IDictionary dictionary, int indentationLevel = 0) { - writer.Write("{ "); + WriteBracketOpen(writer, ObjectOpenBracket); + bool first = true; // This will box each pair. Could use IDictionaryEnumerator, but that's ugly in terms of disposal. foreach (DictionaryEntry pair in dictionary) { - if (!first) - { - writer.Write(PropertySeparator); - } string keyText; if (pair.Key is string s) { @@ -677,12 +723,16 @@ namespace Google.Protobuf } throw new ArgumentException("Unhandled dictionary key type: " + pair.Key.GetType()); } + + MaybeWriteValueSeparator(writer, first); + MaybeWriteValueWhitespace(writer, indentationLevel + 1); WriteString(writer, keyText); writer.Write(NameValueSeparator); WriteValue(writer, pair.Value); first = false; } - writer.Write(first ? "}" : " }"); + + WriteBracketClose(writer, ObjectCloseBracket, !first, indentationLevel); } /// @@ -766,6 +816,49 @@ namespace Google.Protobuf writer.Write(Hex[(c >> 0) & 0xf]); } + private void WriteBracketOpen(TextWriter writer, char openChar) + { + writer.Write(openChar); + if (settings.Indentation == null) + { + writer.Write(' '); + } + } + + private void WriteBracketClose(TextWriter writer, char closeChar, bool hasFields, int indentationLevel) + { + if (hasFields) + { + if (settings.Indentation != null) + { + writer.WriteLine(); + WriteIndentation(writer, indentationLevel); + } + else + { + writer.Write(" "); + } + } + + writer.Write(closeChar); + } + + private void MaybeWriteValueWhitespace(TextWriter writer, int indentationLevel) + { + if (settings.Indentation != null) { + writer.WriteLine(); + WriteIndentation(writer, indentationLevel); + } + } + + private void WriteIndentation(TextWriter writer, int indentationLevel) + { + for (int i = 0; i < indentationLevel; i++) + { + writer.Write(settings.Indentation); + } + } + /// /// Settings controlling JSON formatting. /// @@ -806,6 +899,10 @@ namespace Google.Protobuf /// public bool PreserveProtoFieldNames { get; } + /// + /// Indentation string, used for formatting. Setting null disables indentation. + /// + public string Indentation { get; } /// /// Creates a new object with the specified formatting of default values @@ -833,40 +930,54 @@ namespace Google.Protobuf /// The to use when formatting messages. TypeRegistry.Empty will be used if it is null. /// true to format the enums as integers; false to format enums as enum names. /// true to preserve proto field names; false to convert them to lowerCamelCase. + /// The indentation string to use for multi-line formatting. null to disable multi-line format. private Settings(bool formatDefaultValues, TypeRegistry typeRegistry, bool formatEnumsAsIntegers, - bool preserveProtoFieldNames) + bool preserveProtoFieldNames, + string indentation = null) { FormatDefaultValues = formatDefaultValues; TypeRegistry = typeRegistry ?? TypeRegistry.Empty; FormatEnumsAsIntegers = formatEnumsAsIntegers; PreserveProtoFieldNames = preserveProtoFieldNames; + Indentation = indentation; } /// /// Creates a new object with the specified formatting of default values and the current settings. /// /// true if default values (0, empty strings etc) should be formatted; false otherwise. - public Settings WithFormatDefaultValues(bool formatDefaultValues) => new Settings(formatDefaultValues, TypeRegistry, FormatEnumsAsIntegers, PreserveProtoFieldNames); + public Settings WithFormatDefaultValues(bool formatDefaultValues) => new Settings(formatDefaultValues, TypeRegistry, FormatEnumsAsIntegers, PreserveProtoFieldNames, Indentation); /// /// Creates a new object with the specified type registry and the current settings. /// /// The to use when formatting messages. - public Settings WithTypeRegistry(TypeRegistry typeRegistry) => new Settings(FormatDefaultValues, typeRegistry, FormatEnumsAsIntegers, PreserveProtoFieldNames); + public Settings WithTypeRegistry(TypeRegistry typeRegistry) => new Settings(FormatDefaultValues, typeRegistry, FormatEnumsAsIntegers, PreserveProtoFieldNames, Indentation); /// /// Creates a new object with the specified enums formatting option and the current settings. /// /// true to format the enums as integers; false to format enums as enum names. - public Settings WithFormatEnumsAsIntegers(bool formatEnumsAsIntegers) => new Settings(FormatDefaultValues, TypeRegistry, formatEnumsAsIntegers, PreserveProtoFieldNames); + public Settings WithFormatEnumsAsIntegers(bool formatEnumsAsIntegers) => new Settings(FormatDefaultValues, TypeRegistry, formatEnumsAsIntegers, PreserveProtoFieldNames, Indentation); /// /// Creates a new object with the specified field name formatting option and the current settings. /// /// true to preserve proto field names; false to convert them to lowerCamelCase. - public Settings WithPreserveProtoFieldNames(bool preserveProtoFieldNames) => new Settings(FormatDefaultValues, TypeRegistry, FormatEnumsAsIntegers, preserveProtoFieldNames); + public Settings WithPreserveProtoFieldNames(bool preserveProtoFieldNames) => new Settings(FormatDefaultValues, TypeRegistry, FormatEnumsAsIntegers, preserveProtoFieldNames, Indentation); + + /// + /// Creates a new object with the specified indentation and the current settings. + /// + /// The string to output for each level of indentation (nesting). The default is two spaces per level. Use null to disable indentation entirely. + /// A non-null value for will insert additional line-breaks to the JSON output. + /// Each line will contain either a single value, or braces. The default line-break is determined by , + /// which is "\n" on Unix platforms, and "\r\n" on Windows. If seems to produce empty lines, + /// you need to pass a that uses a "\n" newline. See . + /// + public Settings WithIndentation(string indentation = " ") => new Settings(FormatDefaultValues, TypeRegistry, FormatEnumsAsIntegers, PreserveProtoFieldNames, indentation); } // Effectively a cache of mapping from enum values to the original name as specified in the proto file, diff --git a/java/README.md b/java/README.md index 3465f3aa23..5e3ded418f 100644 --- a/java/README.md +++ b/java/README.md @@ -23,7 +23,7 @@ If you are using Maven, use the following: com.google.protobuf protobuf-java - 3.21.3 + 3.21.4 ``` @@ -37,7 +37,7 @@ protobuf-java-util package: com.google.protobuf protobuf-java-util - 3.21.3 + 3.21.4 ``` @@ -45,7 +45,7 @@ protobuf-java-util package: If you are using Gradle, add the following to your `build.gradle` file's dependencies: ``` - implementation 'com.google.protobuf:protobuf-java:3.21.3' + implementation 'com.google.protobuf:protobuf-java:3.21.4' ``` Again, be sure to check that the version number matches (or is newer than) the version number of protoc that you are using. diff --git a/java/bom/pom.xml b/java/bom/pom.xml index 130c967f56..4cccfb3545 100644 --- a/java/bom/pom.xml +++ b/java/bom/pom.xml @@ -4,7 +4,7 @@ com.google.protobuf protobuf-bom - 3.21.3 + 3.21.4 pom Protocol Buffers [BOM] diff --git a/java/core/pom.xml b/java/core/pom.xml index 5a36e6ff0f..d5eacd6a10 100644 --- a/java/core/pom.xml +++ b/java/core/pom.xml @@ -4,7 +4,7 @@ com.google.protobuf protobuf-parent - 3.21.3 + 3.21.4 protobuf-java diff --git a/java/kotlin-lite/pom.xml b/java/kotlin-lite/pom.xml index feb51224c2..2dc430907e 100644 --- a/java/kotlin-lite/pom.xml +++ b/java/kotlin-lite/pom.xml @@ -4,7 +4,7 @@ com.google.protobuf protobuf-parent - 3.21.3 + 3.21.4 protobuf-kotlin-lite diff --git a/java/kotlin/pom.xml b/java/kotlin/pom.xml index f8a8efc700..7a350f7853 100644 --- a/java/kotlin/pom.xml +++ b/java/kotlin/pom.xml @@ -4,7 +4,7 @@ com.google.protobuf protobuf-parent - 3.21.3 + 3.21.4 protobuf-kotlin diff --git a/java/lite.md b/java/lite.md index 7243d3aabc..d4b4fc87b2 100644 --- a/java/lite.md +++ b/java/lite.md @@ -29,7 +29,7 @@ protobuf Java Lite runtime. If you are using Maven, include the following: com.google.protobuf protobuf-javalite - 3.21.3 + 3.21.4 ``` diff --git a/java/lite/pom.xml b/java/lite/pom.xml index d22d62f620..20381c65ba 100644 --- a/java/lite/pom.xml +++ b/java/lite/pom.xml @@ -4,7 +4,7 @@ com.google.protobuf protobuf-parent - 3.21.3 + 3.21.4 protobuf-javalite diff --git a/java/pom.xml b/java/pom.xml index 9f38fd0f95..99abcf86a9 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -4,7 +4,7 @@ com.google.protobuf protobuf-parent - 3.21.3 + 3.21.4 pom Protocol Buffers [Parent] diff --git a/java/util/pom.xml b/java/util/pom.xml index cb19a74e89..f634047c6d 100644 --- a/java/util/pom.xml +++ b/java/util/pom.xml @@ -4,7 +4,7 @@ com.google.protobuf protobuf-parent - 3.21.3 + 3.21.4 protobuf-java-util diff --git a/kokoro/linux/cmake/build.sh b/kokoro/linux/cmake/build.sh new file mode 100755 index 0000000000..1b0ebfc5fa --- /dev/null +++ b/kokoro/linux/cmake/build.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# +# Build file to set up and run tests based on distribution archive + +set -eux + +# Change to repo root +cd $(dirname $0)/../../.. +GIT_REPO_ROOT=`pwd` + +CONTAINER_IMAGE=gcr.io/protobuf-build/cmake/linux@sha256:79e6ed9d7f3f8e56167a3309a521e5b7e6a212bfb19855c65ee1cbb6f9099671 + +# Update git submodules +git submodule update --init --recursive + +tmpfile=$(mktemp -u) + +docker run \ + --cidfile $tmpfile \ + -v $GIT_REPO_ROOT:/workspace \ + $CONTAINER_IMAGE \ + /test.sh -Dprotobuf_BUILD_CONFORMANCE=ON + +# Save logs for Kokoro +docker cp \ + `cat $tmpfile`:/workspace/logs $KOKORO_ARTIFACTS_DIR diff --git a/kokoro/linux/cmake/continuous.cfg b/kokoro/linux/cmake/continuous.cfg new file mode 100644 index 0000000000..f03bd3945f --- /dev/null +++ b/kokoro/linux/cmake/continuous.cfg @@ -0,0 +1,11 @@ +# Config file for running tests in Kokoro + +# Location of the build script in repository +build_file: "protobuf/kokoro/linux/cmake/build.sh" +timeout_mins: 1440 + +action { + define_artifacts { + regex: "**/sponge_log.*" + } +} diff --git a/kokoro/linux/cmake/presubmit.cfg b/kokoro/linux/cmake/presubmit.cfg new file mode 100644 index 0000000000..f03bd3945f --- /dev/null +++ b/kokoro/linux/cmake/presubmit.cfg @@ -0,0 +1,11 @@ +# Config file for running tests in Kokoro + +# Location of the build script in repository +build_file: "protobuf/kokoro/linux/cmake/build.sh" +timeout_mins: 1440 + +action { + define_artifacts { + regex: "**/sponge_log.*" + } +} diff --git a/kokoro/linux/cmake_install/build.sh b/kokoro/linux/cmake_install/build.sh new file mode 100755 index 0000000000..6fdafa557e --- /dev/null +++ b/kokoro/linux/cmake_install/build.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# +# Build file to set up and run tests based on distribution archive + +set -eux + +# Change to repo root +cd $(dirname $0)/../../.. +GIT_REPO_ROOT=`pwd` + +CONTAINER_IMAGE=gcr.io/protobuf-build/cmake/linux@sha256:79e6ed9d7f3f8e56167a3309a521e5b7e6a212bfb19855c65ee1cbb6f9099671 + +# Update git submodules +git submodule update --init --recursive + +tmpfile=$(mktemp -u) + +docker run \ + --cidfile $tmpfile \ + -v $GIT_REPO_ROOT:/workspace \ + $CONTAINER_IMAGE \ + "/install.sh && /test.sh \ + -Dprotobuf_REMOVE_INSTALLED_HEADERS=ON \ + -Dprotobuf_BUILD_PROTOBUF_BINARIES=OFF \ + -Dprotobuf_BUILD_CONFORMANCE=ON" + + +# Save logs for Kokoro +docker cp \ + `cat $tmpfile`:/workspace/logs $KOKORO_ARTIFACTS_DIR diff --git a/kokoro/linux/cmake_install/continuous.cfg b/kokoro/linux/cmake_install/continuous.cfg new file mode 100644 index 0000000000..f1ae0b351f --- /dev/null +++ b/kokoro/linux/cmake_install/continuous.cfg @@ -0,0 +1,11 @@ +# Config file for running tests in Kokoro + +# Location of the build script in repository +build_file: "protobuf/kokoro/linux/cmake_install/build.sh" +timeout_mins: 1440 + +action { + define_artifacts { + regex: "**/sponge_log.*" + } +} diff --git a/kokoro/linux/cmake_install/presubmit.cfg b/kokoro/linux/cmake_install/presubmit.cfg new file mode 100644 index 0000000000..f1ae0b351f --- /dev/null +++ b/kokoro/linux/cmake_install/presubmit.cfg @@ -0,0 +1,11 @@ +# Config file for running tests in Kokoro + +# Location of the build script in repository +build_file: "protobuf/kokoro/linux/cmake_install/build.sh" +timeout_mins: 1440 + +action { + define_artifacts { + regex: "**/sponge_log.*" + } +} diff --git a/kokoro/linux/cmake_ninja/build.sh b/kokoro/linux/cmake_ninja/build.sh new file mode 100755 index 0000000000..d3a281f9ec --- /dev/null +++ b/kokoro/linux/cmake_ninja/build.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# +# Build file to set up and run tests based on distribution archive + +set -eux + +# Change to repo root +cd $(dirname $0)/../../.. +GIT_REPO_ROOT=`pwd` + +CONTAINER_IMAGE=gcr.io/protobuf-build/cmake/linux@sha256:79e6ed9d7f3f8e56167a3309a521e5b7e6a212bfb19855c65ee1cbb6f9099671 + +# Update git submodules +git submodule update --init --recursive + +tmpfile=$(mktemp -u) + +docker run \ + --cidfile $tmpfile \ + -v $GIT_REPO_ROOT:/workspace \ + $CONTAINER_IMAGE \ + /test.sh -G Ninja -Dprotobuf_BUILD_CONFORMANCE=ON + +# Save logs for Kokoro +docker cp \ + `cat $tmpfile`:/workspace/logs $KOKORO_ARTIFACTS_DIR diff --git a/kokoro/linux/cmake_ninja/continuous.cfg b/kokoro/linux/cmake_ninja/continuous.cfg new file mode 100644 index 0000000000..144fc90a82 --- /dev/null +++ b/kokoro/linux/cmake_ninja/continuous.cfg @@ -0,0 +1,11 @@ +# Config file for running tests in Kokoro + +# Location of the build script in repository +build_file: "protobuf/kokoro/linux/cmake_ninja/build.sh" +timeout_mins: 1440 + +action { + define_artifacts { + regex: "**/sponge_log.*" + } +} diff --git a/kokoro/linux/cmake_ninja/presubmit.cfg b/kokoro/linux/cmake_ninja/presubmit.cfg new file mode 100644 index 0000000000..144fc90a82 --- /dev/null +++ b/kokoro/linux/cmake_ninja/presubmit.cfg @@ -0,0 +1,11 @@ +# Config file for running tests in Kokoro + +# Location of the build script in repository +build_file: "protobuf/kokoro/linux/cmake_ninja/build.sh" +timeout_mins: 1440 + +action { + define_artifacts { + regex: "**/sponge_log.*" + } +} diff --git a/kokoro/windows/cmake/build.bat b/kokoro/windows/cmake/build.bat new file mode 100644 index 0000000000..52b83f4666 --- /dev/null +++ b/kokoro/windows/cmake/build.bat @@ -0,0 +1,4 @@ +@rem enter repo root +cd /d %~dp0\..\..\.. + +@rem TODO(mkruskal) Implement tests diff --git a/kokoro/windows/cmake/continuous.cfg b/kokoro/windows/cmake/continuous.cfg new file mode 100644 index 0000000000..37e89e068b --- /dev/null +++ b/kokoro/windows/cmake/continuous.cfg @@ -0,0 +1,5 @@ +# Config file for running tests in Kokoro + +# Location of the build script in repository +build_file: "protobuf/kokoro/windows/cmake/build.bat" +timeout_mins: 1440 diff --git a/kokoro/windows/cmake/presubmit.cfg b/kokoro/windows/cmake/presubmit.cfg new file mode 100644 index 0000000000..37e89e068b --- /dev/null +++ b/kokoro/windows/cmake/presubmit.cfg @@ -0,0 +1,5 @@ +# Config file for running tests in Kokoro + +# Location of the build script in repository +build_file: "protobuf/kokoro/windows/cmake/build.bat" +timeout_mins: 1440 diff --git a/kokoro/windows/cmake_install/build.bat b/kokoro/windows/cmake_install/build.bat new file mode 100644 index 0000000000..52b83f4666 --- /dev/null +++ b/kokoro/windows/cmake_install/build.bat @@ -0,0 +1,4 @@ +@rem enter repo root +cd /d %~dp0\..\..\.. + +@rem TODO(mkruskal) Implement tests diff --git a/kokoro/windows/cmake_install/continuous.cfg b/kokoro/windows/cmake_install/continuous.cfg new file mode 100644 index 0000000000..2efc0dced2 --- /dev/null +++ b/kokoro/windows/cmake_install/continuous.cfg @@ -0,0 +1,5 @@ +# Config file for running tests in Kokoro + +# Location of the build script in repository +build_file: "protobuf/kokoro/windows/cmake_install/build.bat" +timeout_mins: 1440 diff --git a/kokoro/windows/cmake_install/presubmit.cfg b/kokoro/windows/cmake_install/presubmit.cfg new file mode 100644 index 0000000000..2efc0dced2 --- /dev/null +++ b/kokoro/windows/cmake_install/presubmit.cfg @@ -0,0 +1,5 @@ +# Config file for running tests in Kokoro + +# Location of the build script in repository +build_file: "protobuf/kokoro/windows/cmake_install/build.bat" +timeout_mins: 1440 diff --git a/kokoro/windows/cmake_nmake/build.bat b/kokoro/windows/cmake_nmake/build.bat new file mode 100644 index 0000000000..52b83f4666 --- /dev/null +++ b/kokoro/windows/cmake_nmake/build.bat @@ -0,0 +1,4 @@ +@rem enter repo root +cd /d %~dp0\..\..\.. + +@rem TODO(mkruskal) Implement tests diff --git a/kokoro/windows/cmake_nmake/continuous.cfg b/kokoro/windows/cmake_nmake/continuous.cfg new file mode 100644 index 0000000000..3c279fe421 --- /dev/null +++ b/kokoro/windows/cmake_nmake/continuous.cfg @@ -0,0 +1,5 @@ +# Config file for running tests in Kokoro + +# Location of the build script in repository +build_file: "protobuf/kokoro/windows/cmake_nmake/build.bat" +timeout_mins: 1440 diff --git a/kokoro/windows/cmake_nmake/presubmit.cfg b/kokoro/windows/cmake_nmake/presubmit.cfg new file mode 100644 index 0000000000..3c279fe421 --- /dev/null +++ b/kokoro/windows/cmake_nmake/presubmit.cfg @@ -0,0 +1,5 @@ +# Config file for running tests in Kokoro + +# Location of the build script in repository +build_file: "protobuf/kokoro/windows/cmake_nmake/build.bat" +timeout_mins: 1440 diff --git a/php/ext/google/protobuf/package.xml b/php/ext/google/protobuf/package.xml index 7b99d916f5..c291714265 100644 --- a/php/ext/google/protobuf/package.xml +++ b/php/ext/google/protobuf/package.xml @@ -10,11 +10,11 @@ protobuf-packages@google.com yes - 2022-07-21 - + 2022-07-25 + - 3.21.3 - 3.21.3 + 3.21.4 + 3.21.4 stable @@ -1373,5 +1373,20 @@ G A release. + + + 3.21.4 + 3.21.4 + + + stable + stable + + 2022-07-25 + + BSD-3-Clause + + + diff --git a/php/ext/google/protobuf/protobuf.h b/php/ext/google/protobuf/protobuf.h index 0ecfb6f726..e63e9e8303 100644 --- a/php/ext/google/protobuf/protobuf.h +++ b/php/ext/google/protobuf/protobuf.h @@ -127,7 +127,7 @@ ZEND_BEGIN_ARG_INFO_EX(arginfo_setter, 0, 0, 1) ZEND_ARG_INFO(0, value) ZEND_END_ARG_INFO() -#define PHP_PROTOBUF_VERSION "3.21.3" +#define PHP_PROTOBUF_VERSION "3.21.4" // ptr -> PHP object cache. This is a weak map that caches lazily-created // wrapper objects around upb types: diff --git a/protobuf_version.bzl b/protobuf_version.bzl index c5d90299eb..edac50366c 100644 --- a/protobuf_version.bzl +++ b/protobuf_version.bzl @@ -1,3 +1,3 @@ -PROTOC_VERSION = '21.3' -PROTOBUF_JAVA_VERSION = '3.21.3' -PROTOBUF_PYTHON_VERSION = '4.21.3' +PROTOC_VERSION = '21.4' +PROTOBUF_JAVA_VERSION = '3.21.4' +PROTOBUF_PYTHON_VERSION = '4.21.4' diff --git a/protoc-artifacts/pom.xml b/protoc-artifacts/pom.xml index 4b462e2494..d816174f08 100644 --- a/protoc-artifacts/pom.xml +++ b/protoc-artifacts/pom.xml @@ -8,7 +8,7 @@ com.google.protobuf protoc - 3.21.3 + 3.21.4 pom Protobuf Compiler diff --git a/python/release.sh b/python/release.sh index 15a70db31f..87fcf8cfee 100755 --- a/python/release.sh +++ b/python/release.sh @@ -19,11 +19,24 @@ function run_install_test() { chmod +x test-venv/bin/protoc source test-venv/bin/activate - pip install -i ${PYPI} protobuf==${VERSION} --no-cache-dir + (pip install -i ${PYPI} protobuf==${VERSION} --no-cache-dir) || (retry_pip_install ${PYPI} ${VERSION}) deactivate rm -fr test-venv } +function retry_pip_install() { + local PYPI=$1 + local VERSION=$2 + + read -p "pip install failed, possibly due to delay between upload and availability on pip. Retry? [y/n]" -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi + + (pip install -i ${PYPI} protobuf==${VERSION} --no-cache-dir) || (retry_pip_install ${PYPI} ${VERSION}) +} + [ $# -lt 1 ] && { echo "Usage: $0 VERSION [" @@ -86,13 +99,16 @@ python3 setup.py test python3 setup.py sdist twine upload --skip-existing -r testpypi -u protobuf-wheel-test dist/* -# Test locally with different python versions. +# Sleep to allow time for distribution to be available on pip. +sleep 5m + +# Test locally. run_install_test ${TESTING_VERSION} python3 https://test.pypi.org/simple # Deploy egg/wheel packages to testing PyPI and test again. python3 setup.py clean build bdist_wheel twine upload --skip-existing -r testpypi -u protobuf-wheel-test dist/* - +sleep 5m run_install_test ${TESTING_VERSION} python3 https://test.pypi.org/simple echo "All install tests have passed using testing PyPI." diff --git a/ruby/google-protobuf.gemspec b/ruby/google-protobuf.gemspec index 1688735f45..e43665d447 100644 --- a/ruby/google-protobuf.gemspec +++ b/ruby/google-protobuf.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |s| s.name = "google-protobuf" - s.version = "3.21.3" + s.version = "3.21.4" git_tag = "v#{s.version.to_s.sub('.rc.', '-rc')}" # Converts X.Y.Z.rc.N to vX.Y.Z-rcN, used for the git tag s.licenses = ["BSD-3-Clause"] s.summary = "Protocol Buffers" diff --git a/ruby/pom.xml b/ruby/pom.xml index f1e4bef009..4264172772 100644 --- a/ruby/pom.xml +++ b/ruby/pom.xml @@ -9,7 +9,7 @@ com.google.protobuf.jruby protobuf-jruby - 3.21.3 + 3.21.4 Protocol Buffer JRuby native extension Protocol Buffers are a way of encoding structured data in an efficient yet @@ -76,7 +76,7 @@ com.google.protobuf protobuf-java-util - 3.21.3 + 3.21.4 org.jruby diff --git a/src/Makefile.am b/src/Makefile.am index 9875b1c575..4f4fc808a6 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -774,6 +774,7 @@ protobuf_test_SOURCES = \ google/protobuf/compiler/csharp/csharp_generator_unittest.cc \ google/protobuf/compiler/importer_unittest.cc \ google/protobuf/compiler/java/doc_comment_unittest.cc \ + google/protobuf/compiler/java/message_serialization_unittest.cc \ google/protobuf/compiler/java/plugin_unittest.cc \ google/protobuf/compiler/mock_code_generator.cc \ google/protobuf/compiler/mock_code_generator.h \ diff --git a/src/file_lists.cmake b/src/file_lists.cmake index 32f10b2705..67092cc39e 100644 --- a/src/file_lists.cmake +++ b/src/file_lists.cmake @@ -771,6 +771,7 @@ set(compiler_test_files ${protobuf_SOURCE_DIR}/src/google/protobuf/compiler/csharp/csharp_generator_unittest.cc ${protobuf_SOURCE_DIR}/src/google/protobuf/compiler/importer_unittest.cc ${protobuf_SOURCE_DIR}/src/google/protobuf/compiler/java/doc_comment_unittest.cc + ${protobuf_SOURCE_DIR}/src/google/protobuf/compiler/java/message_serialization_unittest.cc ${protobuf_SOURCE_DIR}/src/google/protobuf/compiler/java/plugin_unittest.cc ${protobuf_SOURCE_DIR}/src/google/protobuf/compiler/objectivec/objectivec_helpers_unittest.cc ${protobuf_SOURCE_DIR}/src/google/protobuf/compiler/parser_unittest.cc diff --git a/src/google/protobuf/arena_unittest.cc b/src/google/protobuf/arena_unittest.cc index 0987c9929e..1466376ed7 100644 --- a/src/google/protobuf/arena_unittest.cc +++ b/src/google/protobuf/arena_unittest.cc @@ -1419,7 +1419,7 @@ TEST(ArenaTest, BlockSizeDoubling) { ASSERT_GT(arena.SpaceAllocated(), first_block_size); auto second_block_size = (arena.SpaceAllocated() - first_block_size); - EXPECT_EQ(second_block_size, 2*first_block_size); + EXPECT_GE(second_block_size, 2*first_block_size); } TEST(ArenaTest, Alignment) { diff --git a/src/google/protobuf/compiler/command_line_interface.cc b/src/google/protobuf/compiler/command_line_interface.cc index 4dc5f96cf4..74910461ec 100644 --- a/src/google/protobuf/compiler/command_line_interface.cc +++ b/src/google/protobuf/compiler/command_line_interface.cc @@ -339,9 +339,12 @@ class CommandLineInterface::ErrorPrinter void AddErrorOrWarning(const std::string& filename, int line, int column, const std::string& message, const std::string& type, std::ostream& out) { - // Print full path when running under MSVS std::string dfile; - if (format_ == CommandLineInterface::ERROR_FORMAT_MSVS && + if ( +#ifndef PROTOBUF_OPENSOURCE + // Print full path when running under MSVS + format_ == CommandLineInterface::ERROR_FORMAT_MSVS && +#endif // !PROTOBUF_OPENSOURCE tree_ != nullptr && tree_->VirtualFileToDiskFile(filename, &dfile)) { out << dfile; } else { diff --git a/src/google/protobuf/compiler/cpp/field.cc b/src/google/protobuf/compiler/cpp/field.cc index cf4f14e8bf..90d20848b3 100644 --- a/src/google/protobuf/compiler/cpp/field.cc +++ b/src/google/protobuf/compiler/cpp/field.cc @@ -330,7 +330,6 @@ void FieldGenerator::GenerateCopyConstructorCode(io::Printer* printer) const { } } - void SetCommonOneofFieldVariables( const FieldDescriptor* descriptor, std::map* variables) { diff --git a/src/google/protobuf/compiler/cpp/field.h b/src/google/protobuf/compiler/cpp/field.h index 3903e79862..3fcbda371c 100644 --- a/src/google/protobuf/compiler/cpp/field.h +++ b/src/google/protobuf/compiler/cpp/field.h @@ -208,7 +208,6 @@ class FieldGenerator { virtual bool IsInlined() const { return false; } - virtual ArenaDtorNeeds NeedsArenaDestructor() const { return ArenaDtorNeeds::kNone; } diff --git a/src/google/protobuf/compiler/cpp/file.cc b/src/google/protobuf/compiler/cpp/file.cc index 838e0ab9b4..502d8c007e 100644 --- a/src/google/protobuf/compiler/cpp/file.cc +++ b/src/google/protobuf/compiler/cpp/file.cc @@ -495,12 +495,10 @@ void FileGenerator::GenerateSourceDefaultInstance(int idx, generator->GenerateInitDefaultSplitInstance(printer); format( "} {}\n" - " ~$1$() {}\n" " union {\n" - " $2$ _instance;\n" + " $1$ _instance;\n" " };\n" "};\n", - DefaultInstanceType(generator->descriptor_, options_, /*split=*/true), StrCat(generator->classname_, "::Impl_::Split")); // NO_DESTROY is not necessary for correctness. The empty destructor is // enough. However, the empty destructor fails to be elided in some @@ -508,7 +506,7 @@ void FileGenerator::GenerateSourceDefaultInstance(int idx, // there just to improve performance and binary size in these builds. format( "PROTOBUF_ATTRIBUTE_NO_DESTROY PROTOBUF_CONSTINIT " - "PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 $1$ $2$;\n", + "PROTOBUF_ATTRIBUTE_INIT_PRIORITY1 const $1$ $2$;\n", DefaultInstanceType(generator->descriptor_, options_, /*split=*/true), DefaultInstanceName(generator->descriptor_, options_, /*split=*/true)); } @@ -999,7 +997,7 @@ class FileGenerator::ForwardDeclarations { const Descriptor* class_desc = p.second; format( "struct $1$;\n" - "$dllexport_decl $extern $1$ $2$;\n", + "$dllexport_decl $extern const $1$ $2$;\n", DefaultInstanceType(class_desc, options, /*split=*/true), DefaultInstanceName(class_desc, options, /*split=*/true)); } diff --git a/src/google/protobuf/compiler/cpp/helpers.cc b/src/google/protobuf/compiler/cpp/helpers.cc index 4939aa533f..d182c8607f 100644 --- a/src/google/protobuf/compiler/cpp/helpers.cc +++ b/src/google/protobuf/compiler/cpp/helpers.cc @@ -176,7 +176,6 @@ static const char* const kKeywordList[] = { #endif // !PROTOBUF_FUTURE_BREAKING_CHANGES }; - static std::unordered_set* MakeKeywordsMap() { auto* result = new std::unordered_set(); for (const auto keyword : kKeywordList) { @@ -525,7 +524,6 @@ std::string FieldName(const FieldDescriptor* field) { return result; } - std::string FieldMemberName(const FieldDescriptor* field, bool split) { StringPiece prefix = IsMapEntryMessage(field->containing_type()) ? "" : "_impl_."; @@ -876,8 +874,6 @@ std::string SafeFunctionName(const Descriptor* descriptor, bool IsProfileDriven(const Options& options) { return options.access_info_map != nullptr; } - - bool IsStringInlined(const FieldDescriptor* descriptor, const Options& options) { (void)descriptor; diff --git a/src/google/protobuf/compiler/cpp/message.cc b/src/google/protobuf/compiler/cpp/message.cc index 3713b9cf2c..51a3e652f1 100644 --- a/src/google/protobuf/compiler/cpp/message.cc +++ b/src/google/protobuf/compiler/cpp/message.cc @@ -828,7 +828,6 @@ void MessageGenerator::GenerateFieldAccessorDeclarations(io::Printer* printer) { // Generate type-specific accessor declarations. field_generators_.get(field).GenerateAccessorDeclarations(printer); - format("\n"); } @@ -1238,41 +1237,41 @@ void MessageGenerator::GenerateFieldAccessorDefinitions(io::Printer* printer) { Formatter::SaveState saver(&format); format.AddMap(vars); - // Generate has_$name$() or $name$_size(). - if (field->is_repeated()) { - if (IsFieldStripped(field, options_)) { - format( - "inline int $classname$::$name$_size() const { " - "__builtin_trap(); }\n"); - } else { - format( - "inline int $classname$::_internal_$name$_size() const {\n" - " return $field$$1$.size();\n" - "}\n" - "inline int $classname$::$name$_size() const {\n" - "$annotate_size$" - " return _internal_$name$_size();\n" - "}\n", - IsImplicitWeakField(field, options_, scc_analyzer_) && - field->message_type() - ? ".weak" - : ""); - } - } else if (field->real_containing_oneof()) { - format.Set("field_name", UnderscoresToCamelCase(field->name(), true)); - format.Set("oneof_name", field->containing_oneof()->name()); - format.Set("oneof_index", - StrCat(field->containing_oneof()->index())); - GenerateOneofMemberHasBits(field, format); + + // Generate has_$name$() or $name$_size(). + if (field->is_repeated()) { + if (IsFieldStripped(field, options_)) { + format( + "inline int $classname$::$name$_size() const { " + "__builtin_trap(); }\n"); } else { - // Singular field. - GenerateSingularFieldHasBits(field, format); + format( + "inline int $classname$::_internal_$name$_size() const {\n" + " return $field$$1$.size();\n" + "}\n" + "inline int $classname$::$name$_size() const {\n" + "$annotate_size$" + " return _internal_$name$_size();\n" + "}\n", + IsImplicitWeakField(field, options_, scc_analyzer_) && + field->message_type() + ? ".weak" + : ""); } + } else if (field->real_containing_oneof()) { + format.Set("field_name", UnderscoresToCamelCase(field->name(), true)); + format.Set("oneof_name", field->containing_oneof()->name()); + format.Set("oneof_index", + StrCat(field->containing_oneof()->index())); + GenerateOneofMemberHasBits(field, format); + } else { + // Singular field. + GenerateSingularFieldHasBits(field, format); + } if (!IsCrossFileMaybeMap(field)) { GenerateFieldClear(field, true, format); } - // Generate type-specific accessors. if (!IsFieldStripped(field, options_)) { field_generators_.get(field).GenerateInlineAccessorDefinitions(printer); @@ -1760,7 +1759,7 @@ void MessageGenerator::GenerateClassDefinition(io::Printer* printer) { format( "private:\n" "inline bool IsSplitMessageDefault() const {\n" - " return $split$ == reinterpret_cast(&$1$);\n" + " return $split$ == reinterpret_cast(&$1$);\n" "}\n" "PROTOBUF_NOINLINE void PrepareSplitMessageForWrite();\n" "public:\n", @@ -2423,8 +2422,15 @@ void MessageGenerator::GenerateSharedConstructorCode(io::Printer* printer) { } if (ShouldSplit(descriptor_, options_)) { put_sep(); - format("decltype($split$){reinterpret_cast(&$1$)}", - DefaultInstanceName(descriptor_, options_, /*split=*/true)); + // We can't assign the default split to this->split without the const_cast + // because the former is a const. The const_cast is safe because we don't + // intend to modify the default split through this pointer, and we also + // expect the default split to be in the rodata section which is protected + // from mutation. + format( + "decltype($split$){const_cast" + "(reinterpret_cast(&$1$))}", + DefaultInstanceName(descriptor_, options_, /*split=*/true)); } for (auto oneof : OneOfRange(descriptor_)) { put_sep(); @@ -2683,7 +2689,7 @@ void MessageGenerator::GenerateConstexprConstructor(io::Printer* printer) { } if (ShouldSplit(descriptor_, options_)) { put_sep(); - format("/*decltype($split$)*/&$1$._instance", + format("/*decltype($split$)*/const_cast(&$1$._instance)", DefaultInstanceName(descriptor_, options_, /*split=*/true)); } @@ -2868,8 +2874,10 @@ void MessageGenerator::GenerateStructors(io::Printer* printer) { } if (ShouldSplit(descriptor_, options_)) { put_sep(); - format("decltype($split$){reinterpret_cast(&$1$)}", - DefaultInstanceName(descriptor_, options_, /*split=*/true)); + format( + "decltype($split$){const_cast" + "(reinterpret_cast(&$1$))}", + DefaultInstanceName(descriptor_, options_, /*split=*/true)); } for (auto oneof : OneOfRange(descriptor_)) { put_sep(); diff --git a/src/google/protobuf/compiler/java/message_serialization.h b/src/google/protobuf/compiler/java/message_serialization.h index 6145392f81..15dc515a85 100644 --- a/src/google/protobuf/compiler/java/message_serialization.h +++ b/src/google/protobuf/compiler/java/message_serialization.h @@ -32,6 +32,7 @@ #define GOOGLE_PROTOBUF_COMPILER_JAVA_MESSAGE_SERIALIZATION_H__ #include +#include #include #include @@ -66,20 +67,31 @@ void GenerateSerializeFieldsAndExtensions( std::sort(sorted_extensions.begin(), sorted_extensions.end(), ExtensionRangeOrdering()); + std::size_t range_idx = 0; + // Merge the fields and the extension ranges, both sorted by field number. - for (int i = 0, j = 0; - i < descriptor->field_count() || j < sorted_extensions.size();) { - if (i == descriptor->field_count()) { - GenerateSerializeExtensionRange(printer, sorted_extensions[j++]); - } else if (j == sorted_extensions.size()) { - field_generators.get(sorted_fields[i++]) - .GenerateSerializationCode(printer); - } else if (sorted_fields[i]->number() < sorted_extensions[j]->start) { - field_generators.get(sorted_fields[i++]) - .GenerateSerializationCode(printer); - } else { - GenerateSerializeExtensionRange(printer, sorted_extensions[j++]); + for (int i = 0; i < descriptor->field_count(); ++i) { + const FieldDescriptor* field = sorted_fields[i]; + + // Collapse all extension ranges up until the next field. This leads to + // shorter and more efficient codegen for messages containing a large + // number of extension ranges without fields in between them. + const Descriptor::ExtensionRange* range = nullptr; + while (range_idx < sorted_extensions.size() && + sorted_extensions[range_idx]->end <= field->number()) { + range = sorted_extensions[range_idx++]; + } + + if (range != nullptr) { + GenerateSerializeExtensionRange(printer, range); } + field_generators.get(field).GenerateSerializationCode(printer); + } + + // After serializing all fields, serialize any remaining extensions via a + // single writeUntil call. + if (range_idx < sorted_extensions.size()) { + GenerateSerializeExtensionRange(printer, sorted_extensions.back()); } } diff --git a/src/google/protobuf/compiler/java/message_serialization_unittest.cc b/src/google/protobuf/compiler/java/message_serialization_unittest.cc new file mode 100644 index 0000000000..b2e1fcaed8 --- /dev/null +++ b/src/google/protobuf/compiler/java/message_serialization_unittest.cc @@ -0,0 +1,124 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace google { +namespace protobuf { +namespace compiler { +namespace java { +namespace { + +using ::testing::ElementsAre; + +// Generates Java code for the specified Java proto, returning the compiler's +// exit status. +int CompileJavaProto(std::string proto_file_name) { + JavaGenerator java_generator; + + CommandLineInterface cli; + cli.RegisterGenerator("--java_out", &java_generator, /*help_text=*/""); + + std::string proto_path = StrCat( + "--proto_path=", + TestUtil::GetTestDataPath("third_party/protobuf/compiler/java")); + std::string java_out = StrCat("--java_out=", TestTempDir()); + + const char* argv[] = { + "protoc", + proto_path.c_str(), + java_out.c_str(), + proto_file_name.c_str(), + }; + + // Open-source codebase does not support ABSL_ARRAYSIZE. + return cli.Run(sizeof(argv) / sizeof(*argv), argv); +} + +TEST(MessageSerializationTest, CollapseAdjacentExtensionRanges) { + GOOGLE_CHECK_EQ(CompileJavaProto("message_serialization_unittest.proto"), 0); + + std::string java_source; + GOOGLE_CHECK_OK(File::GetContents( + // Open-source codebase does not support file::JoinPath, so we manually + // concatenate instead. + StrCat(TestTempDir(), + "/TestMessageWithManyExtensionRanges.java"), + &java_source, true)); + + // Open-source codebase does not support constexpr StringPiece. + static constexpr const char kWriteUntilCall[] = "extensionWriter.writeUntil("; + + std::vector range_ends; + + // Open-source codebase does not have Split overload taking a single + // char delimiter. + // + // NOLINTNEXTLINE(abseil-faster-strsplit-delimiter) + for (const auto& line : Split(java_source, "\n")) { + // Extract end position from writeUntil call. (Open-source codebase does not + // support RE2.) + std::size_t write_until_pos = line.find(kWriteUntilCall); + if (write_until_pos == std::string::npos) { + continue; + } + write_until_pos += (sizeof(kWriteUntilCall) - 1); + + std::size_t comma_pos = line.find(',', write_until_pos); + if (comma_pos == std::string::npos) { + continue; + } + + range_ends.push_back( + std::string(line.substr(write_until_pos, comma_pos - write_until_pos))); + } + + EXPECT_THAT(range_ends, ElementsAre("3", "13", "43")); +} + +} // namespace +} // namespace java +} // namespace compiler +} // namespace protobuf +} // namespace google diff --git a/src/google/protobuf/compiler/java/message_serialization_unittest.proto b/src/google/protobuf/compiler/java/message_serialization_unittest.proto new file mode 100644 index 0000000000..9cfdf42b32 --- /dev/null +++ b/src/google/protobuf/compiler/java/message_serialization_unittest.proto @@ -0,0 +1,56 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +syntax = "proto2"; + +package protobuf_unittest; + +option java_multiple_files = true; +option java_package = ""; + +// Each batch of extension ranges not separated by a non-extension field should +// be serialized using a single ExtensionWriter#writeUntil call. +message TestMessageWithManyExtensionRanges { + // First extension range: ends at field number 3 (exclusive) + extensions 1 to 2; + + optional int32 foo = 3; + optional int32 bar = 5; + + // Second extension range: ends at field number 13 (exclusive) + extensions 6; + extensions 8; + extensions 10 to 12; + + optional int32 baz = 23; + + // Third extension range: ends at field number 43 (exclusive) + extensions 42; +} diff --git a/src/google/protobuf/compiler/python/pyi_generator.cc b/src/google/protobuf/compiler/python/pyi_generator.cc index 0a28102a70..e954fd317f 100644 --- a/src/google/protobuf/compiler/python/pyi_generator.cc +++ b/src/google/protobuf/compiler/python/pyi_generator.cc @@ -44,25 +44,10 @@ namespace protobuf { namespace compiler { namespace python { -template -struct SortByName { - bool operator()(const DescriptorT* l, const DescriptorT* r) const { - return l->name() < r->name(); - } -}; - PyiGenerator::PyiGenerator() : file_(nullptr) {} PyiGenerator::~PyiGenerator() {} -void PyiGenerator::PrintItemMap( - const std::map& item_map) const { - for (const auto& entry : item_map) { - printer_->Print("$key$: $value$\n", "key", entry.first, "value", - entry.second); - } -} - template std::string PyiGenerator::ModuleLevelName(const DescriptorT& descriptor) const { std::string name = NamePrefixedWithNestedTypes(descriptor, "."); @@ -177,8 +162,7 @@ void PyiGenerator::PrintImportForDescriptor( seen_aliases->insert(alias); } -void PyiGenerator::PrintImports( - std::map* item_map) const { +void PyiGenerator::PrintImports() const { // Prints imported dependent _pb2 files. std::set seen_aliases; for (int i = 0; i < file_->dependency_count(); ++i) { @@ -250,7 +234,7 @@ void PyiGenerator::PrintImports( if (import_modules.has_union) { printer_->Print(", Union as _Union"); } - printer_->Print("\n\n"); + printer_->Print("\n"); // Public imports for (int i = 0; i < file_->public_dependency_count(); ++i) { @@ -268,17 +252,8 @@ void PyiGenerator::PrintImports( module_name, "enum_class", public_dep->enum_type(i)->name()); } - // Enum values for public imports - for (int i = 0; i < public_dep->enum_type_count(); ++i) { - const EnumDescriptor* enum_descriptor = public_dep->enum_type(i); - for (int j = 0; j < enum_descriptor->value_count(); ++j) { - (*item_map)[enum_descriptor->value(j)->name()] = - ModuleLevelName(*enum_descriptor); - } - } - // Top level extensions for public imports - AddExtensions(*public_dep, item_map); } +printer_->Print("\n"); } void PyiGenerator::PrintEnum(const EnumDescriptor& enum_descriptor) const { @@ -289,19 +264,18 @@ void PyiGenerator::PrintEnum(const EnumDescriptor& enum_descriptor) const { "enum_name", enum_name); } -// Adds enum value to item map which will be ordered and printed later. -void PyiGenerator::AddEnumValue( - const EnumDescriptor& enum_descriptor, - std::map* item_map) const { +void PyiGenerator::PrintEnumValues( + const EnumDescriptor& enum_descriptor) const { // enum values std::string module_enum_name = ModuleLevelName(enum_descriptor); for (int j = 0; j < enum_descriptor.value_count(); ++j) { const EnumValueDescriptor* value_descriptor = enum_descriptor.value(j); - (*item_map)[value_descriptor->name()] = module_enum_name; + printer_->Print("$name$: $module_enum_name$\n", + "name", value_descriptor->name(), + "module_enum_name", module_enum_name); } } -// Prints top level enums void PyiGenerator::PrintTopLevelEnums() const { for (int i = 0; i < file_->enum_type_count(); ++i) { printer_->Print("\n"); @@ -309,18 +283,16 @@ void PyiGenerator::PrintTopLevelEnums() const { } } -// Add top level extensions to item_map which will be ordered and -// printed later. template -void PyiGenerator::AddExtensions( - const DescriptorT& descriptor, - std::map* item_map) const { +void PyiGenerator::PrintExtensions(const DescriptorT& descriptor) const { for (int i = 0; i < descriptor.extension_count(); ++i) { const FieldDescriptor* extension_field = descriptor.extension(i); std::string constant_name = extension_field->name() + "_FIELD_NUMBER"; ToUpper(&constant_name); - (*item_map)[constant_name] = "_ClassVar[int]"; - (*item_map)[extension_field->name()] = "_descriptor.FieldDescriptor"; + printer_->Print("$constant_name$: _ClassVar[int]\n", + "constant_name", constant_name); + printer_->Print("$name$: _descriptor.FieldDescriptor\n", + "name", extension_field->name()); } } @@ -383,17 +355,11 @@ void PyiGenerator::PrintMessage( printer_->Indent(); printer_->Indent(); - std::vector fields; - fields.reserve(message_descriptor.field_count()); - for (int i = 0; i < message_descriptor.field_count(); ++i) { - fields.push_back(message_descriptor.field(i)); - } - std::sort(fields.begin(), fields.end(), SortByName()); - // Prints slots printer_->Print("__slots__ = [", "class_name", class_name); bool first_item = true; - for (const auto& field_des : fields) { + for (int i = 0; i < message_descriptor.field_count(); ++i) { + const FieldDescriptor* field_des = message_descriptor.field(i); if (IsPythonKeyword(field_des->name())) { continue; } @@ -406,48 +372,34 @@ void PyiGenerator::PrintMessage( } printer_->Print("]\n"); - std::map item_map; // Prints Extensions for extendable messages if (message_descriptor.extension_range_count() > 0) { - item_map["Extensions"] = "_python_message._ExtensionDict"; + printer_->Print("Extensions: _python_message._ExtensionDict\n"); } // Prints nested enums - std::vector nested_enums; - nested_enums.reserve(message_descriptor.enum_type_count()); for (int i = 0; i < message_descriptor.enum_type_count(); ++i) { - nested_enums.push_back(message_descriptor.enum_type(i)); - } - std::sort(nested_enums.begin(), nested_enums.end(), - SortByName()); - - for (const auto& entry : nested_enums) { - PrintEnum(*entry); - // Adds enum value to item_map which will be ordered and printed later - AddEnumValue(*entry, &item_map); + PrintEnum(*message_descriptor.enum_type(i)); + PrintEnumValues(*message_descriptor.enum_type(i)); } // Prints nested messages - std::vector nested_messages; - nested_messages.reserve(message_descriptor.nested_type_count()); for (int i = 0; i < message_descriptor.nested_type_count(); ++i) { - nested_messages.push_back(message_descriptor.nested_type(i)); + PrintMessage(*message_descriptor.nested_type(i), true); } - std::sort(nested_messages.begin(), nested_messages.end(), - SortByName()); - for (const auto& entry : nested_messages) { - PrintMessage(*entry, true); - } + PrintExtensions(message_descriptor); - // Adds extensions to item_map which will be ordered and printed later - AddExtensions(message_descriptor, &item_map); - - // Adds field number and field descriptor to item_map + // Prints field number + for (int i = 0; i < message_descriptor.field_count(); ++i) { + const FieldDescriptor& field_des = *message_descriptor.field(i); + printer_->Print( + "$field_number_name$: _ClassVar[int]\n", "field_number_name", + ToUpper(field_des.name()) + "_FIELD_NUMBER"); + } + // Prints field name and type for (int i = 0; i < message_descriptor.field_count(); ++i) { const FieldDescriptor& field_des = *message_descriptor.field(i); - item_map[ToUpper(field_des.name()) + "_FIELD_NUMBER"] = - "_ClassVar[int]"; if (IsPythonKeyword(field_des.name())) { continue; } @@ -473,12 +425,10 @@ void PyiGenerator::PrintMessage( if (field_des.is_repeated()) { field_type += "]"; } - item_map[field_des.name()] = field_type; + printer_->Print("$name$: $type$\n", + "name", field_des.name(), "type", field_type); } - // Prints all items in item_map - PrintItemMap(item_map); - // Prints __init__ printer_->Print("def __init__(self"); bool has_key_words = false; @@ -548,33 +498,19 @@ void PyiGenerator::PrintMessage( void PyiGenerator::PrintMessages() const { // Deterministically order the descriptors. - std::vector messages; - messages.reserve(file_->message_type_count()); for (int i = 0; i < file_->message_type_count(); ++i) { - messages.push_back(file_->message_type(i)); - } - std::sort(messages.begin(), messages.end(), SortByName()); - - for (const auto& entry : messages) { - PrintMessage(*entry, false); + PrintMessage(*file_->message_type(i), false); } } void PyiGenerator::PrintServices() const { - std::vector services; - services.reserve(file_->service_count()); - for (int i = 0; i < file_->service_count(); ++i) { - services.push_back(file_->service(i)); - } - std::sort(services.begin(), services.end(), SortByName()); - // Prints $Service$ and $Service$_Stub classes - for (const auto& entry : services) { + for (int i = 0; i < file_->service_count(); ++i) { printer_->Print("\n"); printer_->Print( "class $service_name$(_service.service): ...\n\n" "class $service_name$_Stub($service_name$): ...\n", - "service_name", entry->name()); + "service_name", file_->service(i)->name()); } } @@ -594,25 +530,28 @@ bool PyiGenerator::Generate(const FileDescriptor* file, io::Printer printer(output.get(), '$'); printer_ = &printer; - // item map will store "DESCRIPTOR", top level extensions, top level enum - // values. The items will be sorted and printed later. - std::map item_map; + PrintImports(); + printer_->Print("DESCRIPTOR: _descriptor.FileDescriptor\n"); - // Adds "DESCRIPTOR" into item_map. - item_map["DESCRIPTOR"] = "_descriptor.FileDescriptor"; + // Prints extensions and enums from imports. + for (int i = 0; i < file_->public_dependency_count(); ++i) { + const FileDescriptor* public_dep = file_->public_dependency(i); + PrintExtensions(*public_dep); + for (int i = 0; i < public_dep->enum_type_count(); ++i) { + const EnumDescriptor* enum_descriptor = public_dep->enum_type(i); + PrintEnumValues(*enum_descriptor); + } + } - PrintImports(&item_map); - // Adds top level enum values to item_map. + PrintTopLevelEnums(); + // Prints top level enum values for (int i = 0; i < file_->enum_type_count(); ++i) { - AddEnumValue(*file_->enum_type(i), &item_map); + PrintEnumValues(*file_->enum_type(i)); } - // Adds top level extensions to item_map. - AddExtensions(*file_, &item_map); - // Prints item map - PrintItemMap(item_map); - + // Prints top level Extensions + PrintExtensions(*file_); PrintMessages(); - PrintTopLevelEnums(); + if (HasGenericServices(file)) { PrintServices(); } diff --git a/src/google/protobuf/compiler/python/pyi_generator.h b/src/google/protobuf/compiler/python/pyi_generator.h index 6265593f11..40741e1e78 100644 --- a/src/google/protobuf/compiler/python/pyi_generator.h +++ b/src/google/protobuf/compiler/python/pyi_generator.h @@ -77,18 +77,15 @@ class PROTOC_EXPORT PyiGenerator : public google::protobuf::compiler::CodeGenera private: void PrintImportForDescriptor(const FileDescriptor& desc, std::set* seen_aliases) const; - void PrintImports(std::map* item_map) const; - void PrintEnum(const EnumDescriptor& enum_descriptor) const; - void AddEnumValue(const EnumDescriptor& enum_descriptor, - std::map* item_map) const; + void PrintImports() const; void PrintTopLevelEnums() const; + void PrintEnum(const EnumDescriptor& enum_descriptor) const; + void PrintEnumValues(const EnumDescriptor& enum_descriptor) const; template - void AddExtensions(const DescriptorT& descriptor, - std::map* item_map) const; + void PrintExtensions(const DescriptorT& descriptor) const; void PrintMessages() const; void PrintMessage(const Descriptor& message_descriptor, bool is_nested) const; void PrintServices() const; - void PrintItemMap(const std::map& item_map) const; std::string GetFieldType( const FieldDescriptor& field_des, const Descriptor& containing_des) const; template diff --git a/src/google/protobuf/generated_message_tctable_impl.h b/src/google/protobuf/generated_message_tctable_impl.h index 11ac5baf35..3fad18c7f7 100644 --- a/src/google/protobuf/generated_message_tctable_impl.h +++ b/src/google/protobuf/generated_message_tctable_impl.h @@ -38,8 +38,6 @@ #include #include #include -#include -#include #include #include #include diff --git a/src/google/protobuf/io/printer.h b/src/google/protobuf/io/printer.h index e30bfa6908..aa507cb014 100644 --- a/src/google/protobuf/io/printer.h +++ b/src/google/protobuf/io/printer.h @@ -251,7 +251,8 @@ class PROTOBUF_EXPORT Printer { template void Print(const char* text, const Args&... args) { std::map vars; - PrintInternal(&vars, text, args...); + FillMap(&vars, args...); + Print(vars, text); } // Indent text by two spaces. After calling Indent(), two spaces will be @@ -299,18 +300,13 @@ class PROTOBUF_EXPORT Printer { void Annotate(const char* begin_varname, const char* end_varname, const std::string& file_path, const std::vector& path); - // Base case - void PrintInternal(std::map* vars, - const char* text) { - Print(*vars, text); - } + void FillMap(std::map* vars) {} template - void PrintInternal(std::map* vars, const char* text, - const char* key, const std::string& value, - const Args&... args) { + void FillMap(std::map* vars, const std::string& key, + const std::string& value, const Args&... args) { (*vars)[key] = value; - PrintInternal(vars, text, args...); + FillMap(vars, args...); } // Copy size worth of bytes from data to buffer_. diff --git a/src/google/protobuf/map.h b/src/google/protobuf/map.h index d0aa010816..a3f79f1545 100644 --- a/src/google/protobuf/map.h +++ b/src/google/protobuf/map.h @@ -333,6 +333,11 @@ inline size_t SpaceUsedInValues(const void*) { return 0; } } // namespace internal +#ifdef PROTOBUF_FUTURE_MAP_PAIR_UPGRADE +// This is the class for Map's internal value_type. +template +using MapPair = std::pair; +#else // This is the class for Map's internal value_type. Instead of using // std::pair as value_type, we use this class which provides us more control of // its process of construction and destruction. @@ -363,6 +368,7 @@ struct PROTOBUF_ATTRIBUTE_STANDALONE_DEBUG MapPair { friend class Arena; friend class Map; }; +#endif // Map is an associative container type used to store protobuf map // fields. Each Map instance may or may not use a different hash function, a diff --git a/src/google/protobuf/map_test.cc b/src/google/protobuf/map_test.cc index f7c024cd16..fd895246b5 100644 --- a/src/google/protobuf/map_test.cc +++ b/src/google/protobuf/map_test.cc @@ -84,3 +84,5 @@ TEST(MapTest, Aligned8OnArena) { MapTest_Aligned(); } } // namespace internal } // namespace protobuf } // namespace google + +#include diff --git a/src/google/protobuf/map_test.inc b/src/google/protobuf/map_test.inc index 041ac824d4..1d55dd57e6 100644 --- a/src/google/protobuf/map_test.inc +++ b/src/google/protobuf/map_test.inc @@ -810,6 +810,8 @@ TEST_F(MapImplTest, Emplace) { m, UnorderedElementsAre(Pair(1, "one"), Pair(2, "two"), Pair(42, "aaa"))); } +#ifndef PROTOBUF_FUTURE_MAP_PAIR_UPGRADE + TEST_F(MapImplTest, EmplaceKeyOnly) { using ::testing::Pair; using ::testing::UnorderedElementsAre; @@ -824,6 +826,43 @@ TEST_F(MapImplTest, EmplaceKeyOnly) { EXPECT_THAT(m, UnorderedElementsAre(Pair(1, ""), Pair(42, ""))); } +#else + +TEST_F(MapImplTest, ValueTypeNoImplicitConversion) { + using vt = typename Map::value_type; + + EXPECT_FALSE((std::is_convertible< + vt, std::pair>>::value)); +} + +enum class ConstructorType { + kDefault, + kCopy, + kMove, +}; + +struct ConstructorTag { + ConstructorTag() : invoked_constructor(ConstructorType::kDefault) {} + ConstructorTag(const ConstructorTag&) + : invoked_constructor(ConstructorType::kCopy) {} + ConstructorTag(ConstructorTag&&) + : invoked_constructor(ConstructorType::kMove) {} + + ConstructorType invoked_constructor; +}; + +TEST_F(MapImplTest, ValueTypeHasMoveConstructor) { + using vt = typename Map::value_type; + ConstructorTag l, r; + + vt pair(l, std::move(r)); + + EXPECT_EQ(pair.first.invoked_constructor, ConstructorType::kCopy); + EXPECT_EQ(pair.second.invoked_constructor, ConstructorType::kMove); +} + +#endif // !PROTOBUF_FUTURE_MAP_PAIR_UPGRADE + struct CountedInstance { CountedInstance() { ++num_created; } CountedInstance(const CountedInstance&) : CountedInstance() {} diff --git a/src/google/protobuf/message.cc b/src/google/protobuf/message.cc index 724a6220f0..1279164619 100644 --- a/src/google/protobuf/message.cc +++ b/src/google/protobuf/message.cc @@ -214,7 +214,7 @@ uint64_t Message::GetInvariantPerBuild(uint64_t salt) { } namespace internal { -void* CreateSplitMessageGeneric(Arena* arena, void* default_split, +void* CreateSplitMessageGeneric(Arena* arena, const void* default_split, size_t size) { void* split = (arena == nullptr) ? ::operator new(size) : arena->AllocateAligned(size); diff --git a/src/google/protobuf/message.h b/src/google/protobuf/message.h index 7042a13342..b61fafb333 100644 --- a/src/google/protobuf/message.h +++ b/src/google/protobuf/message.h @@ -411,7 +411,8 @@ class PROTOBUF_EXPORT Message : public MessageLite { namespace internal { // Creates and returns an allocation for a split message. -void* CreateSplitMessageGeneric(Arena* arena, void* default_split, size_t size); +void* CreateSplitMessageGeneric(Arena* arena, const void* default_split, + size_t size); // Forward-declare interfaces used to implement RepeatedFieldRef. // These are protobuf internals that users shouldn't care about. diff --git a/src/google/protobuf/port_def.inc b/src/google/protobuf/port_def.inc index 2805a92aba..94d299d3f0 100644 --- a/src/google/protobuf/port_def.inc +++ b/src/google/protobuf/port_def.inc @@ -181,11 +181,15 @@ // Future versions of protobuf will include breaking changes to some APIs. // This macro can be set to enable these API changes ahead of time, so that // user code can be updated before upgrading versions of protobuf. -// PROTOBUF_FUTURE_FINAL is used on classes that are historically not marked as -// final, but that may be marked final in future (breaking) releases. #ifdef PROTOBUF_FUTURE_BREAKING_CHANGES -// Used on classes that are historically not marked as final. + +// Used to upgrade google::protobuf::MapPair to std::pair. +// Owner: mordberg@ +#define PROTOBUF_FUTURE_MAP_PAIR_UPGRADE 1 + +// Used on classes that are historically not marked as final, but that may be +// marked final in future (breaking) releases. // Owner: kfm@ #define PROTOBUF_FUTURE_FINAL final diff --git a/src/google/protobuf/port_undef.inc b/src/google/protobuf/port_undef.inc index 92ca8050c1..23eb789d1e 100644 --- a/src/google/protobuf/port_undef.inc +++ b/src/google/protobuf/port_undef.inc @@ -113,6 +113,7 @@ #ifdef PROTOBUF_FUTURE_BREAKING_CHANGES #undef PROTOBUF_FUTURE_BREAKING_CHANGES +#undef PROTOBUF_FUTURE_MAP_PAIR_UPGRADE #undef PROTOBUF_FUTURE_REMOVE_DEFAULT_FIELD_COMPARATOR #undef PROTOBUF_FUTURE_REMOVE_CLEARED_API #endif diff --git a/src/google/protobuf/repeated_field.h b/src/google/protobuf/repeated_field.h index e504274778..b4e441681f 100644 --- a/src/google/protobuf/repeated_field.h +++ b/src/google/protobuf/repeated_field.h @@ -233,12 +233,6 @@ class RepeatedField final { // copies data between each other. void Swap(RepeatedField* other); - // Swaps entire contents with "other". Should be called only if the caller can - // guarantee that both repeated fields are on the same arena or are on the - // heap. Swapping between different arenas is disallowed and caught by a - // GOOGLE_DCHECK (see API docs for details). - void UnsafeArenaSwap(RepeatedField* other); - // Swaps two elements. void SwapElements(int index1, int index2); @@ -321,6 +315,12 @@ class RepeatedField final { : rep()->arena; } + // Swaps entire contents with "other". Should be called only if the caller can + // guarantee that both repeated fields are on the same arena or are on the + // heap. Swapping between different arenas is disallowed and caught by a + // GOOGLE_DCHECK (see API docs for details). + void UnsafeArenaSwap(RepeatedField* other); + static constexpr int kInitialSize = 0; // A note on the representation here (see also comment below for // RepeatedPtrFieldBase's struct Rep): diff --git a/src/google/protobuf/util/json_util_test.cc b/src/google/protobuf/util/json_util_test.cc index ebe1b604d1..ca9fe731dd 100644 --- a/src/google/protobuf/util/json_util_test.cc +++ b/src/google/protobuf/util/json_util_test.cc @@ -307,6 +307,15 @@ TEST_P(JsonTest, Camels) { EXPECT_THAT(ToJson(m), IsOkAndHolds(R"({"StringField":"sTRINGfIELD"})")); } +TEST_P(JsonTest, EvilString) { + auto m = ToProto(R"json( + {"string_value": ")json" + "\n\r\b\f\1\2\3" + "\"}"); + ASSERT_OK(m); + EXPECT_EQ(m->string_value(), "\n\r\b\f\1\2\3"); +} + TEST_P(JsonTest, TestAlwaysPrintEnumsAsInts) { TestMessage orig; orig.set_enum_value(proto3::BAR); @@ -399,6 +408,7 @@ TEST_P(JsonTest, ParseMessage) { "repeatedEnumValue": [1, "FOO"], "repeatedMessageValue": [ {"value": 40}, + {}, {"value": 96} ] } @@ -427,9 +437,10 @@ TEST_P(JsonTest, ParseMessage) { EXPECT_THAT(m->repeated_string_value(), ElementsAre("foo", "bar ", "")); EXPECT_THAT(m->repeated_enum_value(), ElementsAre(proto3::BAR, proto3::FOO)); - ASSERT_THAT(m->repeated_message_value(), SizeIs(2)); + ASSERT_THAT(m->repeated_message_value(), SizeIs(3)); EXPECT_EQ(m->repeated_message_value(0).value(), 40); - EXPECT_EQ(m->repeated_message_value(1).value(), 96); + EXPECT_EQ(m->repeated_message_value(1).value(), 0); + EXPECT_EQ(m->repeated_message_value(2).value(), 96); EXPECT_THAT( ToJson(*m), @@ -440,7 +451,7 @@ TEST_P(JsonTest, ParseMessage) { R"("messageValue":{"value":2048},"repeatedBoolValue":[true],"repeatedInt32Value":[0,-42])" R"(,"repeatedUint64Value":["1","2"],"repeatedDoubleValue":[1.5,-2],)" R"("repeatedStringValue":["foo","bar ",""],"repeatedEnumValue":["BAR","FOO"],)" - R"("repeatedMessageValue":[{"value":40},{"value":96}]})")); + R"("repeatedMessageValue":[{"value":40},{},{"value":96}]})")); } TEST_P(JsonTest, CurseOfAtob) { @@ -761,6 +772,24 @@ TEST_P(JsonTest, TestFlatList) { )json"); ASSERT_OK(m); EXPECT_THAT(m->repeated_int32_value(), ElementsAre(5, 6)); + + // The above flatteing behavior is supressed for google::protobuf::ListValue. + auto m2 = ToProto(R"json( + { + "repeatedInt32Value": [[[5]], [6]] + } + )json"); + ASSERT_OK(m2); + auto fields = m2->struct_value().fields(); + auto list = fields["repeatedInt32Value"].list_value(); + EXPECT_EQ(list.values(0) + .list_value() + .values(0) + .list_value() + .values(0) + .number_value(), + 5); + EXPECT_EQ(list.values(1).list_value().values(0).number_value(), 6); } TEST_P(JsonTest, ParseWrappers) { @@ -1095,6 +1124,33 @@ TEST_P(JsonTest, TestLegalNullsInArray) { ASSERT_THAT(m2->repeated_value(), SizeIs(1)); EXPECT_TRUE(m2->repeated_value(0).has_null_value()); + + m2->Clear(); + m2->mutable_repeated_value(); // Materialize an empty singular Value. + m2->add_repeated_value(); + m2->add_repeated_value()->set_string_value("solitude"); + m2->add_repeated_value(); + EXPECT_THAT(ToJson(*m2), IsOkAndHolds(R"({"repeatedValue":["solitude"]})")); +} + +TEST_P(JsonTest, ListList) { + auto m = ToProto(R"json({ + "repeated_value": [["ayy", "lmao"]] + })json"); + ASSERT_OK(m); + + EXPECT_EQ(m->repeated_value(0).values(0).string_value(), "ayy"); + EXPECT_EQ(m->repeated_value(0).values(1).string_value(), "lmao"); + + m = ToProto(R"json({ + "repeated_value": [{ + "values": ["ayy", "lmao"] + }] + })json"); + ASSERT_OK(m); + + EXPECT_EQ(m->repeated_value(0).values(0).string_value(), "ayy"); + EXPECT_EQ(m->repeated_value(0).values(1).string_value(), "lmao"); } TEST_P(JsonTest, HtmlEscape) {