Fix handling of repeated wrappers

Previously we were incorrectly packing wrapper types.
This also refactors FieldCodec a bit as well, using more C# 6-ness.
pull/1145/head
Jon Skeet 9 years ago
parent f2fe50bfc5
commit f262611ff6
  1. 24
      csharp/src/Google.Protobuf.Test/WellKnownTypes/WrappersTest.cs
  2. 9
      csharp/src/Google.Protobuf/Collections/RepeatedField.cs
  3. 138
      csharp/src/Google.Protobuf/FieldCodec.cs

@ -148,6 +148,30 @@ namespace Google.Protobuf.WellKnownTypes
Assert.AreEqual("Second", message.StringField[1]); Assert.AreEqual("Second", message.StringField[1]);
} }
[Test]
public void RepeatedWrappersBinaryFormat()
{
// At one point we accidentally used a packed format for repeated wrappers, which is wrong (and weird).
// This test is just to prove that we use the right format.
var rawOutput = new MemoryStream();
var output = new CodedOutputStream(rawOutput);
// Write a value of 5
output.WriteTag(RepeatedWellKnownTypes.Int32FieldFieldNumber, WireFormat.WireType.LengthDelimited);
output.WriteLength(2);
output.WriteTag(WrappersReflection.WrapperValueFieldNumber, WireFormat.WireType.Varint);
output.WriteInt32(5);
// Write a value of 0 (empty message)
output.WriteTag(RepeatedWellKnownTypes.Int32FieldFieldNumber, WireFormat.WireType.LengthDelimited);
output.WriteLength(0);
output.Flush();
var expectedBytes = rawOutput.ToArray();
var message = new RepeatedWellKnownTypes { Int32Field = { 5, 0 } };
var actualBytes = message.ToByteArray();
Assert.AreEqual(expectedBytes, actualBytes);
}
[Test] [Test]
public void MapWrappersSerializeDeserialize() public void MapWrappersSerializeDeserialize()
{ {

@ -34,7 +34,6 @@ using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using Google.Protobuf.Compatibility;
namespace Google.Protobuf.Collections namespace Google.Protobuf.Collections
{ {
@ -96,8 +95,8 @@ namespace Google.Protobuf.Collections
// iteration. // iteration.
uint tag = input.LastTag; uint tag = input.LastTag;
var reader = codec.ValueReader; var reader = codec.ValueReader;
// Value types can be packed or not. // Non-nullable value types can be packed or not.
if (typeof(T).IsValueType() && WireFormat.GetTagWireType(tag) == WireFormat.WireType.LengthDelimited) if (FieldCodec<T>.IsPackedRepeatedField(tag))
{ {
int length = input.ReadLength(); int length = input.ReadLength();
if (length > 0) if (length > 0)
@ -134,7 +133,7 @@ namespace Google.Protobuf.Collections
return 0; return 0;
} }
uint tag = codec.Tag; uint tag = codec.Tag;
if (typeof(T).IsValueType() && WireFormat.GetTagWireType(tag) == WireFormat.WireType.LengthDelimited) if (codec.PackedRepeatedField)
{ {
int dataSize = CalculatePackedDataSize(codec); int dataSize = CalculatePackedDataSize(codec);
return CodedOutputStream.ComputeRawVarint32Size(tag) + return CodedOutputStream.ComputeRawVarint32Size(tag) +
@ -186,7 +185,7 @@ namespace Google.Protobuf.Collections
} }
var writer = codec.ValueWriter; var writer = codec.ValueWriter;
var tag = codec.Tag; var tag = codec.Tag;
if (typeof(T).IsValueType() && WireFormat.GetTagWireType(tag) == WireFormat.WireType.LengthDelimited) if (codec.PackedRepeatedField)
{ {
// Packed primitive type // Packed primitive type
uint size = (uint)CalculatePackedDataSize(codec); uint size = (uint)CalculatePackedDataSize(codec);

@ -30,6 +30,7 @@
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#endregion #endregion
using Google.Protobuf.Compatibility;
using Google.Protobuf.WellKnownTypes; using Google.Protobuf.WellKnownTypes;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -329,17 +330,24 @@ namespace Google.Protobuf
} }
/// <summary> /// <summary>
/// <para>
/// An encode/decode pair for a single field. This effectively encapsulates /// An encode/decode pair for a single field. This effectively encapsulates
/// all the information needed to read or write the field value from/to a coded /// all the information needed to read or write the field value from/to a coded
/// stream. /// stream.
/// </para>
/// <para>
/// This class is public and has to be as it is used by generated code, but its public
/// API is very limited - just what the generated code needs to call directly.
/// </para>
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This never writes default values to the stream, and is not currently designed /// This never writes default values to the stream, and does not address "packedness"
/// to play well with packed arrays. /// in repeated fields itself, other than to know whether or not the field *should* be packed.
/// </remarks> /// </remarks>
public sealed class FieldCodec<T> public sealed class FieldCodec<T>
{ {
private static readonly T DefaultDefault; private static readonly T DefaultDefault;
private static readonly bool TypeSupportsPacking = typeof(T).IsValueType() && Nullable.GetUnderlyingType(typeof(T)) == null;
static FieldCodec() static FieldCodec()
{ {
@ -354,75 +362,31 @@ namespace Google.Protobuf
// Otherwise it's the default value of the CLR type // Otherwise it's the default value of the CLR type
} }
private readonly Func<CodedInputStream, T> reader; internal static bool IsPackedRepeatedField(uint tag) =>
private readonly Action<CodedOutputStream, T> writer; TypeSupportsPacking && WireFormat.GetTagWireType(tag) == WireFormat.WireType.LengthDelimited;
private readonly Func<T, int> sizeCalculator;
private readonly uint tag;
private readonly int tagSize;
private readonly int fixedSize;
// Default value for this codec. Usually the same for every instance of the same type, but
// for string/ByteString wrapper fields the codec's default value is null, whereas for
// other string/ByteString fields it's "" or ByteString.Empty.
private readonly T defaultValue;
internal FieldCodec( internal bool PackedRepeatedField { get; }
Func<CodedInputStream, T> reader,
Action<CodedOutputStream, T> writer,
Func<T, int> sizeCalculator,
uint tag) : this(reader, writer, sizeCalculator, tag, DefaultDefault)
{
}
internal FieldCodec(
Func<CodedInputStream, T> reader,
Action<CodedOutputStream, T> writer,
Func<T, int> sizeCalculator,
uint tag,
T defaultValue)
{
this.reader = reader;
this.writer = writer;
this.sizeCalculator = sizeCalculator;
this.fixedSize = 0;
this.tag = tag;
this.defaultValue = defaultValue;
tagSize = CodedOutputStream.ComputeRawVarint32Size(tag);
}
internal FieldCodec(
Func<CodedInputStream, T> reader,
Action<CodedOutputStream, T> writer,
int fixedSize,
uint tag)
{
this.reader = reader;
this.writer = writer;
this.sizeCalculator = _ => fixedSize;
this.fixedSize = fixedSize;
this.tag = tag;
tagSize = CodedOutputStream.ComputeRawVarint32Size(tag);
}
/// <summary> /// <summary>
/// Returns the size calculator for just a value. /// Returns a delegate to write a value (unconditionally) to a coded output stream.
/// </summary> /// </summary>
internal Func<T, int> ValueSizeCalculator { get { return sizeCalculator; } } internal Action<CodedOutputStream, T> ValueWriter { get; }
/// <summary> /// <summary>
/// Returns a delegate to write a value (unconditionally) to a coded output stream. /// Returns the size calculator for just a value.
/// </summary> /// </summary>
internal Action<CodedOutputStream, T> ValueWriter { get { return writer; } } internal Func<T, int> ValueSizeCalculator { get; }
/// <summary> /// <summary>
/// Returns a delegate to read a value from a coded input stream. It is assumed that /// Returns a delegate to read a value from a coded input stream. It is assumed that
/// the stream is already positioned on the appropriate tag. /// the stream is already positioned on the appropriate tag.
/// </summary> /// </summary>
internal Func<CodedInputStream, T> ValueReader { get { return reader; } } internal Func<CodedInputStream, T> ValueReader { get; }
/// <summary> /// <summary>
/// Returns the fixed size for an entry, or 0 if sizes vary. /// Returns the fixed size for an entry, or 0 if sizes vary.
/// </summary> /// </summary>
internal int FixedSize { get { return fixedSize; } } internal int FixedSize { get; }
/// <summary> /// <summary>
/// Gets the tag of the codec. /// Gets the tag of the codec.
@ -430,15 +394,54 @@ namespace Google.Protobuf
/// <value> /// <value>
/// The tag of the codec. /// The tag of the codec.
/// </value> /// </value>
public uint Tag { get { return tag; } } internal uint Tag { get; }
/// <summary> /// <summary>
/// Gets the default value of the codec's type. /// Default value for this codec. Usually the same for every instance of the same type, but
/// for string/ByteString wrapper fields the codec's default value is null, whereas for
/// other string/ByteString fields it's "" or ByteString.Empty.
/// </summary> /// </summary>
/// <value> /// <value>
/// The default value of the codec's type. /// The default value of the codec's type.
/// </value> /// </value>
public T DefaultValue { get { return defaultValue; } } internal T DefaultValue { get; }
private readonly int tagSize;
internal FieldCodec(
Func<CodedInputStream, T> reader,
Action<CodedOutputStream, T> writer,
int fixedSize,
uint tag) : this(reader, writer, _ => fixedSize, tag)
{
FixedSize = fixedSize;
}
internal FieldCodec(
Func<CodedInputStream, T> reader,
Action<CodedOutputStream, T> writer,
Func<T, int> sizeCalculator,
uint tag) : this(reader, writer, sizeCalculator, tag, DefaultDefault)
{
}
internal FieldCodec(
Func<CodedInputStream, T> reader,
Action<CodedOutputStream, T> writer,
Func<T, int> sizeCalculator,
uint tag,
T defaultValue)
{
ValueReader = reader;
ValueWriter = writer;
ValueSizeCalculator = sizeCalculator;
FixedSize = 0;
Tag = tag;
DefaultValue = defaultValue;
tagSize = CodedOutputStream.ComputeRawVarint32Size(tag);
// Detect packed-ness once, so we can check for it within RepeatedField<T>.
PackedRepeatedField = IsPackedRepeatedField(tag);
}
/// <summary> /// <summary>
/// Write a tag and the given value, *if* the value is not the default. /// Write a tag and the given value, *if* the value is not the default.
@ -447,8 +450,8 @@ namespace Google.Protobuf
{ {
if (!IsDefault(value)) if (!IsDefault(value))
{ {
output.WriteTag(tag); output.WriteTag(Tag);
writer(output, value); ValueWriter(output, value);
} }
} }
@ -457,23 +460,14 @@ namespace Google.Protobuf
/// </summary> /// </summary>
/// <param name="input">The input stream to read from.</param> /// <param name="input">The input stream to read from.</param>
/// <returns>The value read from the stream.</returns> /// <returns>The value read from the stream.</returns>
public T Read(CodedInputStream input) public T Read(CodedInputStream input) => ValueReader(input);
{
return reader(input);
}
/// <summary> /// <summary>
/// Calculates the size required to write the given value, with a tag, /// Calculates the size required to write the given value, with a tag,
/// if the value is not the default. /// if the value is not the default.
/// </summary> /// </summary>
public int CalculateSizeWithTag(T value) public int CalculateSizeWithTag(T value) => IsDefault(value) ? 0 : ValueSizeCalculator(value) + tagSize;
{
return IsDefault(value) ? 0 : sizeCalculator(value) + tagSize;
}
private bool IsDefault(T value) private bool IsDefault(T value) => EqualityComparer<T>.Default.Equals(value, DefaultValue);
{
return EqualityComparer<T>.Default.Equals(value, defaultValue);
}
} }
} }

Loading…
Cancel
Save