diff --git a/global.json b/global.json index 759654107..47a5edd25 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,4 @@ { - "sdk": { - "version": "2.1.500" - }, "msbuild-sdks": { "MSBuild.Sdk.Extras": "1.5.4", "__MSBuild.Sdk.Extras": "1.6.20-preview" diff --git a/src/protobuf-net.Test/Meta/Lists.cs b/src/protobuf-net.Test/Meta/Lists.cs index c41d3bdf7..d0f5d2531 100644 --- a/src/protobuf-net.Test/Meta/Lists.cs +++ b/src/protobuf-net.Test/Meta/Lists.cs @@ -124,6 +124,53 @@ public void RoundTripTypedList_String() Assert.True(obj.ListString.SequenceEqual(clone.ListString)); } + [Fact] + public void RoundTripTypedList_String_Null() + { + var model = TypeModel.Create(); + model.Add(typeof(TypeWithLists), false).Add(1, "ListString"); + TypeWithLists obj = new TypeWithLists(); + obj.ListString = null; + + TypeWithLists clone = (TypeWithLists)model.DeepClone(obj); + Assert.NotNull(clone); + Assert.Null(clone.ListString); + + model.CompileInPlace(); + clone = (TypeWithLists)model.DeepClone(obj); + Assert.NotNull(clone); + Assert.Null(clone.ListString); + + clone = (TypeWithLists)model.Compile().DeepClone(obj); + Assert.NotNull(clone); + Assert.Null(clone.ListString); + } + + [Fact] + public void RoundTripTypedList_String_Empty() + { + var model = TypeModel.Create(); + model.Add(typeof(TypeWithLists), false).Add(1, "ListString"); + TypeWithLists obj = new TypeWithLists(); + obj.ListString = new List(); + + TypeWithLists clone = (TypeWithLists)model.DeepClone(obj); + Assert.NotNull(clone); + Assert.NotNull(clone.ListString); + Assert.Empty(clone.ListString); + + model.CompileInPlace(); + clone = (TypeWithLists)model.DeepClone(obj); + Assert.NotNull(clone); + Assert.NotNull(clone.ListString); + Assert.Empty(clone.ListString); + + clone = (TypeWithLists)model.Compile().DeepClone(obj); + Assert.NotNull(clone); + Assert.NotNull(clone.ListString); + Assert.Empty(clone.ListString); + } + [Fact] public void RoundTripTypedIList_String() { @@ -151,6 +198,53 @@ public void RoundTripTypedIList_String() Assert.True(obj.IListStringTyped.SequenceEqual(clone.IListStringTyped)); } + [Fact] + public void RoundTripTypedIList_String_Null() + { + var model = TypeModel.Create(); + model.Add(typeof(TypeWithLists), false).Add(2, "IListStringTyped"); + TypeWithLists obj = new TypeWithLists(); + obj.IListStringTyped = null; + + TypeWithLists clone = (TypeWithLists)model.DeepClone(obj); + Assert.NotNull(clone); + Assert.Null(clone.IListStringTyped); + + model.CompileInPlace(); + clone = (TypeWithLists)model.DeepClone(obj); + Assert.NotNull(clone); + Assert.Null(clone.IListStringTyped); + + clone = (TypeWithLists)model.Compile().DeepClone(obj); + Assert.NotNull(clone); + Assert.Null(clone.IListStringTyped); + } + + [Fact] + public void RoundTripTypedIList_String_Empty() + { + var model = TypeModel.Create(); + model.Add(typeof(TypeWithLists), false).Add(2, "IListStringTyped"); + TypeWithLists obj = new TypeWithLists(); + obj.IListStringTyped = new List(); + + TypeWithLists clone = (TypeWithLists)model.DeepClone(obj); + Assert.NotNull(clone); + Assert.NotNull(clone.IListStringTyped); + Assert.True(obj.IListStringTyped.Count == 0); + + model.CompileInPlace(); + clone = (TypeWithLists)model.DeepClone(obj); + Assert.NotNull(clone); + Assert.NotNull(clone.IListStringTyped); + Assert.True(obj.IListStringTyped.Count == 0); + + clone = (TypeWithLists)model.Compile().DeepClone(obj); + Assert.NotNull(clone); + Assert.NotNull(clone.IListStringTyped); + Assert.True(obj.IListStringTyped.Count == 0); + } + [Fact] public void RoundTripArrayList_String() @@ -315,6 +409,84 @@ public void RoundTripIList_Int32() Assert.True(obj.IListInt32Untyped.Cast().SequenceEqual(clone.IListInt32Untyped.Cast())); } + [Fact] + public void RoundTripArray_String() + { + var model = TypeModel.Create(); + model.Add(typeof(Arrays), false).Add(4, "BasicArray"); + Arrays obj = new Arrays(); + obj.BasicArray = new[] { "abc", "def" }; + + Arrays clone = (Arrays)model.DeepClone(obj); + Assert.NotNull(clone); + Assert.NotNull(clone.BasicArray); + Assert.True(obj.BasicArray.SequenceEqual(clone.BasicArray)); + + model.CompileInPlace(); + clone = (Arrays)model.DeepClone(obj); + Assert.NotNull(clone); + Assert.NotNull(clone.BasicArray); + Assert.True(obj.BasicArray.SequenceEqual(clone.BasicArray)); + + clone = (Arrays)model.Compile().DeepClone(obj); + Assert.NotNull(clone); + Assert.NotNull(clone.BasicArray); + Assert.True(obj.BasicArray.SequenceEqual(clone.BasicArray)); + } + + + [Fact] + public void RoundTripArray_String_Null() + { + var model = TypeModel.Create(); + model.Add(typeof(Arrays), false).Add(4, "BasicArray"); + Arrays obj = new Arrays(); + obj.BasicArray = null; + + Arrays clone = (Arrays)model.DeepClone(obj); + Assert.NotNull(clone); + Assert.Null(clone.BasicArray); + + model.CompileInPlace(); + clone = (Arrays)model.DeepClone(obj); + Assert.NotNull(clone); + Assert.Null(clone.BasicArray); + + clone = (Arrays)model.Compile().DeepClone(obj); + Assert.NotNull(clone); + Assert.Null(clone.BasicArray); + } + + [Fact] + public void RoundTripArray_String_Empty() + { + var model = TypeModel.Create(); + model.Add(typeof(Arrays), false).Add(4, "BasicArray"); + Arrays obj = new Arrays(); + obj.BasicArray = new string[0]; + + Arrays clone = (Arrays)model.DeepClone(obj); + Assert.NotNull(clone); + Assert.NotNull(clone.BasicArray); + Assert.True(obj.BasicArray.Length == 0); + + model.CompileInPlace(); + clone = (Arrays)model.DeepClone(obj); + Assert.NotNull(clone); + Assert.NotNull(clone.BasicArray); + Assert.True(obj.BasicArray.Length == 0); + + clone = (Arrays)model.Compile().DeepClone(obj); + Assert.NotNull(clone); + Assert.NotNull(clone.BasicArray); + Assert.True(obj.BasicArray.Length == 0); + } + + public class Arrays + { + public string[] BasicArray { get; set; } + } + public class NastyType { diff --git a/src/protobuf-net.Test/PreviousVersions.Designer.cs b/src/protobuf-net.Test/PreviousVersions.Designer.cs new file mode 100644 index 000000000..7ff349897 --- /dev/null +++ b/src/protobuf-net.Test/PreviousVersions.Designer.cs @@ -0,0 +1,73 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace ProtoBuf { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class PreviousVersions { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal PreviousVersions() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ProtoBuf.PreviousVersions", typeof(PreviousVersions).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] protobuf_net_2_4_0_8641 { + get { + object obj = ResourceManager.GetObject("protobuf_net_2_4_0_8641", resourceCulture); + return ((byte[])(obj)); + } + } + } +} diff --git a/src/protobuf-net.Test/PreviousVersions.resx b/src/protobuf-net.Test/PreviousVersions.resx new file mode 100644 index 000000000..48be536f1 --- /dev/null +++ b/src/protobuf-net.Test/PreviousVersions.resx @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + protobuf-net_2_4_0_8641.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/src/protobuf-net.Test/Serializers/IReadOnlyCollectionTests.cs b/src/protobuf-net.Test/Serializers/IReadOnlyCollectionTests.cs index 771bfc0de..a9d64db05 100644 --- a/src/protobuf-net.Test/Serializers/IReadOnlyCollectionTests.cs +++ b/src/protobuf-net.Test/Serializers/IReadOnlyCollectionTests.cs @@ -9,7 +9,7 @@ public class IReadOnlyCollectionTests [Fact] public void BasicIReadOnlyCollectionTest() { - var orig = new TypeWithIReadOnlyCollection { Items = new List{"abc", "def"} }; + var orig = new TypeWithIReadOnlyCollection { Items = new List { "abc", "def" } }; var model = TypeModel.Create(); var clone = (TypeWithIReadOnlyCollection)model.DeepClone(orig); Assert.Equal(orig.Items, clone.Items); //, "Runtime"); @@ -22,6 +22,41 @@ public void BasicIReadOnlyCollectionTest() Assert.Equal(orig.Items, clone.Items); //, "Compile"); } + [Fact] + public void NullIReadOnlyCollectionTest() + { + var orig = new TypeWithIReadOnlyCollection { Items = null }; + var model = TypeModel.Create(); + var clone = (TypeWithIReadOnlyCollection)model.DeepClone(orig); + Assert.Null(clone.Items); + + model.CompileInPlace(); + clone = (TypeWithIReadOnlyCollection)model.DeepClone(orig); + Assert.Null(clone.Items); + + clone = (TypeWithIReadOnlyCollection)model.Compile().DeepClone(orig); + Assert.Null(clone.Items); + } + + [Fact] + public void EmptyIReadOnlyCollectionTest() + { + var orig = new TypeWithIReadOnlyCollection { Items = new List() }; + var model = TypeModel.Create(); + var clone = (TypeWithIReadOnlyCollection)model.DeepClone(orig); + Assert.NotNull(clone.Items); + Assert.True(clone.Items.Count == 0); + + model.CompileInPlace(); + clone = (TypeWithIReadOnlyCollection)model.DeepClone(orig); + Assert.NotNull(clone.Items); + Assert.True(clone.Items.Count == 0); + + clone = (TypeWithIReadOnlyCollection)model.Compile().DeepClone(orig); + Assert.NotNull(clone.Items); + Assert.True(clone.Items.Count == 0); + } + [ProtoContract] public class TypeWithIReadOnlyCollection { diff --git a/src/protobuf-net.Test/Serializers/KeyValuePairTests.cs b/src/protobuf-net.Test/Serializers/KeyValuePairTests.cs index 886dd300e..9eb0c4f1e 100644 --- a/src/protobuf-net.Test/Serializers/KeyValuePairTests.cs +++ b/src/protobuf-net.Test/Serializers/KeyValuePairTests.cs @@ -114,6 +114,47 @@ public void TypeWithDictionaryTest() Assert.Equal(123.45M, clone.Data["abc"]); //, "Runtime"); } + [Fact] + public void TypeWithDictionaryNullTest() + { + var orig = new TypeWithDictionary { Data = null }; + var model = TypeModel.Create(); + var clone = (TypeWithDictionary)model.DeepClone(orig); + Assert.Null(clone.Data); + + model.Compile("TypeWithDictionaryTest", "TypeWithDictionaryTest.dll"); + PEVerify.Verify("TypeWithDictionaryTest.dll"); + + model.CompileInPlace(); + clone = (TypeWithDictionary)model.DeepClone(orig); + Assert.Null(clone.Data); + + clone = (TypeWithDictionary)model.Compile().DeepClone(orig); + Assert.Null(clone.Data); + } + + [Fact] + public void TypeWithDictionaryEmptyTest() + { + var orig = new TypeWithDictionary { Data = new Dictionary() }; + var model = TypeModel.Create(); + var clone = (TypeWithDictionary)model.DeepClone(orig); + Assert.NotNull(clone.Data); + Assert.True(clone.Data.Count == 0); + + model.Compile("TypeWithDictionaryTest", "TypeWithDictionaryTest.dll"); + PEVerify.Verify("TypeWithDictionaryTest.dll"); + + model.CompileInPlace(); + clone = (TypeWithDictionary)model.DeepClone(orig); + Assert.NotNull(clone.Data); + Assert.True(clone.Data.Count == 0); + + clone = (TypeWithDictionary)model.Compile().DeepClone(orig); + Assert.NotNull(clone.Data); + Assert.True(clone.Data.Count == 0); + } + [Fact] public void ShouldWorkWithAutoLoadDisabledRuntime() { diff --git a/src/protobuf-net.Test/VersionStability/EmptyListTests.cs b/src/protobuf-net.Test/VersionStability/EmptyListTests.cs new file mode 100644 index 000000000..da30308de --- /dev/null +++ b/src/protobuf-net.Test/VersionStability/EmptyListTests.cs @@ -0,0 +1,241 @@ +using System; +using System.Collections.Generic; +using Xunit; +using System.IO; +using System.Reflection; +using System.Linq; + +namespace ProtoBuf.VersionStability +{ + public class EmptyListTests + { + private void TestRoundTrip(T source, Action assertBoth) + where T : IHaveOtherMembers + { + TestRoundTrip(source, assertBoth, assertBoth); + } + + private void TestRoundTrip(T source, Action assertOld, Action assertNew) + where T : IHaveOtherMembers + { + var oldVersion = Assembly.Load(PreviousVersions.protobuf_net_2_4_0_8641); + var oldSerializer = oldVersion.GetType("ProtoBuf.Serializer"); + var oldDeserializeMethod = oldSerializer.GetMethod("Deserialize", new Type[] { typeof(Type), typeof(Stream) }); + + source.PreOtherMember = "Pre" + Guid.NewGuid().ToString(); + source.PostOtherMember = "Post" + Guid.NewGuid().ToString(); + + using (var memoryStream = new MemoryStream()) + { + Serializer.Serialize(memoryStream, source); + memoryStream.Position = 0; + T oldClone = (T)oldDeserializeMethod.Invoke(null, new object[] { typeof(T), memoryStream }); + assertOld(oldClone); + Assert.Equal(source.PreOtherMember, oldClone.PreOtherMember); + Assert.Equal(source.PostOtherMember, oldClone.PostOtherMember); + + memoryStream.Position = 0; + T newClone = Serializer.Deserialize(memoryStream); + assertNew(newClone); + Assert.Equal(source.PreOtherMember, oldClone.PreOtherMember); + Assert.Equal(source.PostOtherMember, oldClone.PostOtherMember); + } + } + + [Fact] + public void NullListCanBeDeserializedWithOldVersion() + { + TestRoundTrip( + new ListTest { BasicList = null }, + clone => { + Assert.Null(clone.BasicList); + }); + } + + [Fact] + public void EmptyListCanBeDeserializedWithOldVersion() + { + TestRoundTrip( + new ListTest { BasicList = new List() }, + oldClone => { + Assert.Null(oldClone.BasicList); + }, + newClone => { + Assert.NotNull(newClone.BasicList); + Assert.Empty(newClone.BasicList); + }); + } + + [Fact] + public void ListCanBeDeserializedWithOldVersion() + { + TestRoundTrip( + new ListTest { BasicList = new List { "a", "b" } }, + clone => { + Assert.True(clone.BasicList.SequenceEqual(new[] { "a", "b" })); + }); + } + + [Fact] + public void NullArrayCanBeDeserializedWithOldVersion() + { + TestRoundTrip( + new ArrayTest { BasicArray = null }, + clone => { + Assert.Null(clone.BasicArray); + }); + } + + [Fact] + public void EmptyArrayCanBeDeserializedWithOldVersion() + { + TestRoundTrip( + new ArrayTest { BasicArray = new string[0] }, + oldClone => { + Assert.Null(oldClone.BasicArray); + }, + newClone => { + Assert.NotNull(newClone.BasicArray); + Assert.Empty(newClone.BasicArray); + }); + } + + [Fact] + public void ArrayCanBeDeserializedWithOldVersion() + { + TestRoundTrip( + new ArrayTest { BasicArray = new[] { "a", "b" } }, + clone => { + Assert.True(clone.BasicArray.SequenceEqual(new[] { "a", "b" })); + }); + } + + [Fact] + public void NullReadOnlyCollectionCanBeDeserializedWithOldVersion() + { + TestRoundTrip( + new ReadOnlyCollectionTest { BasicList = null }, + clone => { + Assert.Null(clone.BasicList); + }); + } + + [Fact] + public void EmptyReadOnlyCollectionCanBeDeserializedWithOldVersion() + { + TestRoundTrip( + new ReadOnlyCollectionTest { BasicList = new List() }, + oldClone => { + Assert.Null(oldClone.BasicList); + }, + newClone => { + Assert.NotNull(newClone.BasicList); + Assert.Empty(newClone.BasicList); + }); + } + + [Fact] + public void ReadOnlyCollectionCanBeDeserializedWithOldVersion() + { + TestRoundTrip( + new ReadOnlyCollectionTest { BasicList = new List { "a", "b" } }, + clone => { + Assert.True(clone.BasicList.SequenceEqual(new[] { "a", "b" })); + }); + } + + [Fact] + public void NullDictionaryCanBeDeserializedWithOldVersion() + { + TestRoundTrip( + new DictionaryTest { BasicDictionary = null }, + clone => { + Assert.Null(clone.BasicDictionary); + }); + } + + [Fact] + public void EmptyDictionaryCanBeDeserializedWithOldVersion() + { + TestRoundTrip( + new DictionaryTest { BasicDictionary = new Dictionary() }, + oldClone => { + Assert.Null(oldClone.BasicDictionary); + }, + newClone => { + Assert.NotNull(newClone.BasicDictionary); + Assert.Empty(newClone.BasicDictionary); + }); + } + + [Fact] + public void DictionaryCanBeDeserializedWithOldVersion() + { + TestRoundTrip( + new DictionaryTest { BasicDictionary = new Dictionary { { "a", "b" }, { "c", "d" } } }, + clone => { + Assert.Equal("b", clone.BasicDictionary["a"]); + Assert.Equal("d", clone.BasicDictionary["c"]); + }); + } + + public interface IHaveOtherMembers + { + string PreOtherMember { get; set; } + + string PostOtherMember { get; set; } + } + + [ProtoContract] + public class ListTest : IHaveOtherMembers + { + [ProtoMember(1)] + public string PreOtherMember { get; set; } + + [ProtoMember(2)] + public List BasicList { get; set; } + + [ProtoMember(3)] + public string PostOtherMember { get; set; } + } + + [ProtoContract] + public class ArrayTest : IHaveOtherMembers + { + [ProtoMember(1)] + public string PreOtherMember { get; set; } + + [ProtoMember(2)] + public string[] BasicArray { get; set; } + + [ProtoMember(3)] + public string PostOtherMember { get; set; } + } + + [ProtoContract] + public class ReadOnlyCollectionTest : IHaveOtherMembers + { + [ProtoMember(1)] + public string PreOtherMember { get; set; } + + [ProtoMember(2)] + public IReadOnlyCollection BasicList { get; set; } + + [ProtoMember(3)] + public string PostOtherMember { get; set; } + } + + [ProtoContract] + public class DictionaryTest : IHaveOtherMembers + { + [ProtoMember(1)] + public string PreOtherMember { get; set; } + + [ProtoMember(2)] + public Dictionary BasicDictionary { get; set; } + + [ProtoMember(3)] + public string PostOtherMember { get; set; } + } + } +} diff --git a/src/protobuf-net.Test/protobuf-net.Test.csproj b/src/protobuf-net.Test/protobuf-net.Test.csproj index adea41d4e..60b471f31 100644 --- a/src/protobuf-net.Test/protobuf-net.Test.csproj +++ b/src/protobuf-net.Test/protobuf-net.Test.csproj @@ -3,11 +3,11 @@ false - net452 + netcoreapp2.1 net Debug;Release;VS - + FEAT_COMPILER;NO_NHIBERNATE;COREFX;NO_INTERNAL_CONTEXT core @@ -53,6 +53,19 @@ + + + True + True + PreviousVersions.resx + + + + + ResXFileCodeGenerator + PreviousVersions.Designer.cs + + Always diff --git a/src/protobuf-net.Test/protobuf-net_2_4_0_8641.dll b/src/protobuf-net.Test/protobuf-net_2_4_0_8641.dll new file mode 100644 index 000000000..f090f1cae Binary files /dev/null and b/src/protobuf-net.Test/protobuf-net_2_4_0_8641.dll differ diff --git a/src/protobuf-net/Compiler/CompilerContext.cs b/src/protobuf-net/Compiler/CompilerContext.cs index 1fa5073da..1180e21a9 100644 --- a/src/protobuf-net/Compiler/CompilerContext.cs +++ b/src/protobuf-net/Compiler/CompilerContext.cs @@ -399,6 +399,12 @@ public void LoadReaderWriter() Emit(isStatic ? OpCodes.Ldarg_1 : OpCodes.Ldarg_2); } + public void SetTo1(Local local) + { + il.Emit(OpCodes.Ldc_I4_1); + StoreValue(local); + } + public void StoreValue(Local local) { if (local == this.InputValue) @@ -1237,6 +1243,11 @@ internal void Add() Emit(OpCodes.Add); } + internal void And() + { + Emit(OpCodes.And); + } + internal void LoadLength(Local arr, bool zeroIfNull) { Helpers.DebugAssert(arr.Type.IsArray && arr.Type.GetArrayRank() == 1); diff --git a/src/protobuf-net/Meta/TypeModel.cs b/src/protobuf-net/Meta/TypeModel.cs index 32e594d13..9ffce8080 100644 --- a/src/protobuf-net/Meta/TypeModel.cs +++ b/src/protobuf-net/Meta/TypeModel.cs @@ -1386,6 +1386,7 @@ public object DeepClone(object value, bool existingValue = false) writer.Close(); } ms.Position = 0; + ProtoReader reader = null; try { diff --git a/src/protobuf-net/ProtoReader.cs b/src/protobuf-net/ProtoReader.cs index ec3b52a82..97404346a 100644 --- a/src/protobuf-net/ProtoReader.cs +++ b/src/protobuf-net/ProtoReader.cs @@ -33,6 +33,11 @@ public sealed class ProtoReader : IDisposable /// public int FieldNumber => fieldNumber; + /// + /// Returns true if the field being processed is an empty list. + /// + public bool IsEmptyList => (fieldNumber & Serializer.EmptyListFlag) == Serializer.EmptyListFlag; + /// /// Indicates the underlying proto serialization format on the wire. /// @@ -659,6 +664,15 @@ public int ReadFieldHeader() { wireType = (WireType)(tag & 7); fieldNumber = (int)(tag >> 3); + + if (IsEmptyList) + { + // we emit an 'empty' flag as an empty string in the existing wire format, so that + // existing versions just skip the field. So we need to consume the empty string portion + // of that message + ReadString(); + } + if (fieldNumber < 1) throw new ProtoException("Invalid field in source data: " + fieldNumber.ToString()); } else diff --git a/src/protobuf-net/ProtoWriter.cs b/src/protobuf-net/ProtoWriter.cs index 20705f6c8..38db395ab 100644 --- a/src/protobuf-net/ProtoWriter.cs +++ b/src/protobuf-net/ProtoWriter.cs @@ -172,6 +172,12 @@ internal static void WriteHeaderCore(int fieldNumber, WireType wireType, ProtoWr WriteUInt32Variant(header, writer); } + public static void WriteEmptyList(int fieldNumber, ProtoWriter writer) + { + ProtoWriter.WriteFieldHeader(fieldNumber | Serializer.EmptyListFlag, WireType.String, writer); + ProtoWriter.WriteString("", writer); + } + /// /// Writes a byte-array to the stream; supported wire-types: String /// diff --git a/src/protobuf-net/Serializer.cs b/src/protobuf-net/Serializer.cs index 605dc93f8..7ae72aee0 100644 --- a/src/protobuf-net/Serializer.cs +++ b/src/protobuf-net/Serializer.cs @@ -18,6 +18,8 @@ namespace ProtoBuf /// public static class Serializer { + public const int EmptyListFlag = 0x01000000; + #if !NO_RUNTIME /// /// Suggest a .proto definition for the given type diff --git a/src/protobuf-net/Serializers/ArrayDecorator.cs b/src/protobuf-net/Serializers/ArrayDecorator.cs index e86f8f018..ea4d5bf3d 100644 --- a/src/protobuf-net/Serializers/ArrayDecorator.cs +++ b/src/protobuf-net/Serializers/ArrayDecorator.cs @@ -116,6 +116,19 @@ protected override void EmitWrite(ProtoBuf.Compiler.CompilerContext ctx, ProtoBu ctx.EmitCall(mappedWriter.GetMethod("EndSubItem")); } } + else + { + Compiler.CodeLabel allDone = ctx.DefineLabel(); + ctx.LoadLength(arr, false); + ctx.LoadValue(0); + ctx.BranchIfGreater(allDone, false); + + ctx.LoadValue(fieldNumber); + ctx.LoadReaderWriter(); + ctx.EmitCall(ctx.MapType(typeof(ProtoWriter)).GetMethod(nameof(ProtoWriter.WriteEmptyList))); + + ctx.MarkLabel(allDone); + } } } } @@ -204,11 +217,20 @@ public override void Write(object value, ProtoWriter dest) ProtoWriter.EndSubItem(token, dest); } } + else if (arr.Count == 0) + { + ProtoWriter.WriteEmptyList(fieldNumber, dest); + } } public override object Read(object value, ProtoReader source) { int field = source.FieldNumber; BasicList list = new BasicList(); + if (source.IsEmptyList) + { + return Array.CreateInstance(itemType, 0); + } + if (packedWireType != WireType.None && source.WireType == WireType.String) { SubItemToken token = ProtoReader.StartSubItem(source); @@ -244,6 +266,12 @@ protected override void EmitRead(ProtoBuf.Compiler.CompilerContext ctx, ProtoBuf { ctx.EmitCtor(listType); ctx.StoreValue(list); + + var createEmpty = ctx.DefineLabel(); + ctx.LoadReaderWriter(); + ctx.LoadValue(typeof(ProtoReader).GetProperty(nameof(ProtoReader.IsEmptyList))); + ctx.BranchIfTrue(createEmpty, false); + ListDecorator.EmitReadList(ctx, list, Tail, listType.GetMethod("Add"), packedWireType, false); // leave this "using" here, as it can share the "FieldNumber" local with EmitReadList @@ -299,6 +327,17 @@ protected override void EmitRead(ProtoBuf.Compiler.CompilerContext ctx, ProtoBuf } ctx.EmitCall(copyTo); } + + var complete = ctx.DefineLabel(); + ctx.Branch(complete, false); + + ctx.MarkLabel(createEmpty); + + ctx.LoadValue(0); + ctx.CreateArray(itemType, null); + ctx.StoreValue(newArr); + + ctx.MarkLabel(complete); ctx.LoadValue(newArr); } diff --git a/src/protobuf-net/Serializers/ListDecorator.cs b/src/protobuf-net/Serializers/ListDecorator.cs index 82bb128e8..d9a1b73ca 100644 --- a/src/protobuf-net/Serializers/ListDecorator.cs +++ b/src/protobuf-net/Serializers/ListDecorator.cs @@ -153,9 +153,15 @@ protected override void EmitRead(ProtoBuf.Compiler.CompilerContext ctx, ProtoBuf ctx.MarkLabel(notNull); } + var complete = ctx.DefineLabel(); + ctx.LoadReaderWriter(); + ctx.LoadValue(typeof(ProtoReader).GetProperty(nameof(ProtoReader.IsEmptyList))); + ctx.BranchIfTrue(complete, false); + bool castListForAdd = !add.DeclaringType.IsAssignableFrom(declaredType); EmitReadList(ctx, list, Tail, add, packedWireType, castListForAdd); + ctx.MarkLabel(complete); if (returnList) { if (AppendToCollection && origlist != null) @@ -406,7 +412,8 @@ internal static MethodInfo GetEnumeratorInfo(TypeModel model, Type expectedType, #if FEAT_COMPILER protected override void EmitWrite(ProtoBuf.Compiler.CompilerContext ctx, ProtoBuf.Compiler.Local valueFrom) { - using (Compiler.Local list = ctx.GetLocalWithValue(ExpectedType, valueFrom)) + using (Compiler.Local list = ctx.GetLocalWithValue(ExpectedType, valueFrom), + valuesWritten = new Compiler.Local(ctx, ctx.MapType(typeof(bool)))) { MethodInfo getEnumerator = GetEnumeratorInfo(ctx.Model, out MethodInfo moveNext, out MethodInfo current); Helpers.DebugAssert(moveNext != null); @@ -417,6 +424,7 @@ protected override void EmitWrite(ProtoBuf.Compiler.CompilerContext ctx, ProtoBu using (Compiler.Local iter = new Compiler.Local(ctx, enumeratorType)) using (Compiler.Local token = writePacked ? new Compiler.Local(ctx, ctx.MapType(typeof(SubItemToken))) : null) { + ctx.InitLocal(typeof(bool), valuesWritten); if (writePacked) { ctx.LoadValue(fieldNumber); @@ -453,6 +461,8 @@ protected override void EmitWrite(ProtoBuf.Compiler.CompilerContext ctx, ProtoBu } Tail.EmitWrite(ctx, null); + ctx.SetTo1(valuesWritten); + ctx.MarkLabel(@next); ctx.LoadAddress(iter, enumeratorType); ctx.EmitCall(moveNext, enumeratorType); @@ -465,6 +475,18 @@ protected override void EmitWrite(ProtoBuf.Compiler.CompilerContext ctx, ProtoBu ctx.LoadReaderWriter(); ctx.EmitCall(ctx.MapType(typeof(ProtoWriter)).GetMethod("EndSubItem")); } + else + { + Compiler.CodeLabel allDone = ctx.DefineLabel(); + + ctx.LoadValue(valuesWritten); + ctx.BranchIfTrue(allDone, false); + + ctx.LoadValue(fieldNumber); + ctx.LoadReaderWriter(); + ctx.EmitCall(ctx.MapType(typeof(ProtoWriter)).GetMethod(nameof(ProtoWriter.WriteEmptyList))); + ctx.MarkLabel(allDone); + } } } } @@ -494,10 +516,12 @@ public override void Write(object value, ProtoWriter dest) token = new SubItemToken(); // default } bool checkForNull = !SupportNull; + bool valuesWritten = false; foreach (object subItem in (IEnumerable)value) { if (checkForNull && subItem == null) { throw new NullReferenceException(); } Tail.Write(subItem, dest); + valuesWritten = true; } if (writePacked) { @@ -510,6 +534,10 @@ public override void Write(object value, ProtoWriter dest) ProtoWriter.EndSubItem(token, dest); } } + else if (!valuesWritten) + { + ProtoWriter.WriteEmptyList(fieldNumber, dest); + } } private bool CanUsePackedPrefix(object obj) => @@ -522,6 +550,11 @@ public override object Read(object value, ProtoReader source) int field = source.FieldNumber; object origValue = value; if (value == null) value = Activator.CreateInstance(concreteType); + if (source.IsEmptyList) + { + return value; + } + bool isList = IsList && !SuppressIList; if (packedWireType != WireType.None && source.WireType == WireType.String) { diff --git a/src/protobuf-net/Serializers/MapDecorator.cs b/src/protobuf-net/Serializers/MapDecorator.cs index 033cf26a8..2addbec58 100644 --- a/src/protobuf-net/Serializers/MapDecorator.cs +++ b/src/protobuf-net/Serializers/MapDecorator.cs @@ -78,6 +78,11 @@ public override object Read(object untyped, ProtoReader source) TDictionary typed = AppendToCollection ? ((TDictionary)untyped) : null; if (typed == null) typed = (TDictionary)Activator.CreateInstance(concreteType); + if (source.IsEmptyList) + { + return typed; + } + do { var key = DefaultKey; @@ -109,6 +114,7 @@ public override object Read(object untyped, ProtoReader source) public override void Write(object untyped, ProtoWriter dest) { + bool valuesWritten = false; foreach (var pair in (TDictionary)untyped) { ProtoWriter.WriteFieldHeader(fieldNumber, wireType, dest); @@ -116,6 +122,11 @@ public override void Write(object untyped, ProtoWriter dest) if (pair.Key != null) keyTail.Write(pair.Key, dest); if (pair.Value != null) Tail.Write(pair.Value, dest); ProtoWriter.EndSubItem(token, dest); + valuesWritten = true; + } + if (!valuesWritten) + { + ProtoWriter.WriteEmptyList(fieldNumber, dest); } } @@ -134,7 +145,10 @@ protected override void EmitWrite(CompilerContext ctx, Local valueFrom) using (Compiler.Local iter = new Compiler.Local(ctx, enumeratorType)) using (Compiler.Local token = new Compiler.Local(ctx, typeof(SubItemToken))) using (Compiler.Local kvp = new Compiler.Local(ctx, itemType)) + using (Compiler.Local valuesWritten = new Compiler.Local(ctx, ctx.MapType(typeof(bool)))) { + ctx.InitLocal(typeof(bool), valuesWritten); + ctx.LoadAddress(list, ExpectedType); ctx.EmitCall(getEnumerator, ExpectedType); ctx.StoreValue(iter); @@ -176,11 +190,24 @@ protected override void EmitWrite(CompilerContext ctx, Local valueFrom) ctx.LoadReaderWriter(); ctx.EmitCall(ctx.MapType(typeof(ProtoWriter)).GetMethod("EndSubItem")); + ctx.SetTo1(valuesWritten); + ctx.MarkLabel(@next); ctx.LoadAddress(iter, enumeratorType); ctx.EmitCall(moveNext, enumeratorType); ctx.BranchIfTrue(body, false); } + + CodeLabel allDone = ctx.DefineLabel(); + + ctx.LoadValue(valuesWritten); + ctx.BranchIfTrue(allDone, false); + + ctx.LoadValue(fieldNumber); + ctx.LoadReaderWriter(); + ctx.EmitCall(ctx.MapType(typeof(ProtoWriter)).GetMethod(nameof(ProtoWriter.WriteEmptyList))); + + ctx.MarkLabel(allDone); } } protected override void EmitRead(CompilerContext ctx, Local valueFrom) @@ -207,6 +234,11 @@ protected override void EmitRead(CompilerContext ctx, Local valueFrom) ctx.MarkLabel(notNull); } + var readComplete = ctx.DefineLabel(); + ctx.LoadReaderWriter(); + ctx.LoadValue(typeof(ProtoReader).GetProperty(nameof(ProtoReader.IsEmptyList))); + ctx.BranchIfTrue(readComplete, false); + var redoFromStart = ctx.DefineLabel(); ctx.MarkLabel(redoFromStart); @@ -287,6 +319,8 @@ protected override void EmitRead(CompilerContext ctx, Local valueFrom) ctx.EmitCall(ctx.MapType(typeof(ProtoReader)).GetMethod("TryReadFieldHeader")); ctx.BranchIfTrue(redoFromStart, false); + ctx.MarkLabel(readComplete); + if (ReturnsValue) { ctx.LoadValue(list); diff --git a/src/protobuf-net/Serializers/TypeSerializer.cs b/src/protobuf-net/Serializers/TypeSerializer.cs index dbb24246f..f41f7b94e 100644 --- a/src/protobuf-net/Serializers/TypeSerializer.cs +++ b/src/protobuf-net/Serializers/TypeSerializer.cs @@ -192,6 +192,7 @@ public object Read(object value, ProtoReader source) while ((fieldNumber = source.ReadFieldHeader()) > 0) { fieldHandled = false; + fieldNumber &= ~Serializer.EmptyListFlag; if (fieldNumber < lastFieldNumber) { lastFieldNumber = lastFieldIndex = 0; @@ -688,6 +689,10 @@ void IProtoSerializer.EmitRead(Compiler.CompilerContext ctx, Compiler.Local valu ctx.MarkLabel(@continue); ctx.EmitBasicRead("ReadFieldHeader", ctx.MapType(typeof(int))); ctx.CopyValue(); + + ctx.LoadValue(~Serializer.EmptyListFlag); + ctx.And(); + ctx.StoreValue(fieldNumber); ctx.LoadValue(0); ctx.BranchIfGreater(processField, false); diff --git a/src/protobuf-net/protobuf-net.csproj b/src/protobuf-net/protobuf-net.csproj index d95e578bd..447fecc14 100644 --- a/src/protobuf-net/protobuf-net.csproj +++ b/src/protobuf-net/protobuf-net.csproj @@ -3,7 +3,7 @@ protobuf-net protobuf-net Provides simple access to fast and efficient "Protocol Buffers" serialization from .NET applications - net35;net451;netstandard2.0;netcoreapp2.1 + netstandard2.0 true EMIT_ASSEMBLY_INFO