From 9e4f354f14775061ed098c896170d3a2d01a3895 Mon Sep 17 00:00:00 2001 From: Jon Skeet Date: Mon, 4 Jan 2016 14:03:01 +0000 Subject: Prohibit null values in map fields On deserialization, missing values for message types are replaced with a "default" message. --- csharp/src/Google.Protobuf/Collections/MapField.cs | 53 ++++++++-------------- 1 file changed, 20 insertions(+), 33 deletions(-) (limited to 'csharp/src/Google.Protobuf/Collections') diff --git a/csharp/src/Google.Protobuf/Collections/MapField.cs b/csharp/src/Google.Protobuf/Collections/MapField.cs index c0ed28ae..04754ae3 100644 --- a/csharp/src/Google.Protobuf/Collections/MapField.cs +++ b/csharp/src/Google.Protobuf/Collections/MapField.cs @@ -53,6 +53,13 @@ namespace Google.Protobuf.Collections /// For string keys, the equality comparison is provided by . /// /// + /// Null values are not permitted in the map, either for wrapper types or regular messages. + /// If a map is deserialized from a data stream and the value is missing from an entry, a default value + /// is created instead. For primitive types, that is the regular default value (0, the empty string and so + /// on); for message types, an empty instance of the message is created, as if the map entry contained a 0-length + /// encoded value for the field. + /// + /// /// This implementation does not generally prohibit the use of key/value types which are not /// supported by Protocol Buffers (e.g. using a key type of byte) but nor does it guarantee /// that all operations will work in such cases. @@ -61,34 +68,10 @@ namespace Google.Protobuf.Collections public sealed class MapField : IDeepCloneable>, IDictionary, IEquatable>, IDictionary { // TODO: Don't create the map/list until we have an entry. (Assume many maps will be empty.) - private readonly bool allowNullValues; private readonly Dictionary>> map = new Dictionary>>(); private readonly LinkedList> list = new LinkedList>(); - /// - /// Constructs a new map field, defaulting the value nullability to only allow null values for message types - /// and non-nullable value types. - /// - public MapField() : this(typeof(IMessage).IsAssignableFrom(typeof(TValue)) || Nullable.GetUnderlyingType(typeof(TValue)) != null) - { - } - - /// - /// Constructs a new map field, overriding the choice of whether null values are permitted in the map. - /// This is used by wrapper types, where maps with string and bytes wrappers as the value types - /// support null values. - /// - /// Whether null values are permitted in the map or not. - public MapField(bool allowNullValues) - { - if (allowNullValues && typeof(TValue).IsValueType() && Nullable.GetUnderlyingType(typeof(TValue)) == null) - { - throw new ArgumentException("allowNullValues", "Non-nullable value types do not support null values"); - } - this.allowNullValues = allowNullValues; - } - /// /// Creates a deep clone of this object. /// @@ -97,13 +80,13 @@ namespace Google.Protobuf.Collections /// public MapField Clone() { - var clone = new MapField(allowNullValues); + var clone = new MapField(); // Keys are never cloneable. Values might be. if (typeof(IDeepCloneable).IsAssignableFrom(typeof(TValue))) { foreach (var pair in list) { - clone.Add(pair.Key, pair.Value == null ? pair.Value : ((IDeepCloneable)pair.Value).Clone()); + clone.Add(pair.Key, ((IDeepCloneable)pair.Value).Clone()); } } else @@ -217,7 +200,7 @@ namespace Google.Protobuf.Collections { Preconditions.CheckNotNullUnconstrained(key, "key"); // value == null check here is redundant, but avoids boxing. - if (value == null && !allowNullValues) + if (value == null) { Preconditions.CheckNotNullUnconstrained(value, "value"); } @@ -246,7 +229,7 @@ namespace Google.Protobuf.Collections public ICollection Values { get { return new MapView(this, pair => pair.Value, ContainsValue); } } /// - /// Adds the specified entries to the map. + /// Adds the specified entries to the map. The keys and values are not automatically cloned. /// /// The entries to add to the map. public void Add(IDictionary entries) @@ -346,11 +329,6 @@ namespace Google.Protobuf.Collections } } - /// - /// Returns whether or not this map allows values to be null. - /// - public bool AllowsNullValues { get { return allowNullValues; } } - /// /// Gets the number of elements contained in the map. /// @@ -632,6 +610,8 @@ namespace Google.Protobuf.Collections /// internal class MessageAdapter : IMessage { + private static readonly byte[] ZeroLengthMessageStreamData = new byte[] { 0 }; + private readonly Codec codec; internal TKey Key { get; set; } internal TValue Value { get; set; } @@ -665,6 +645,13 @@ namespace Google.Protobuf.Collections input.SkipLastField(); } } + + // Corner case: a map entry with a key but no value, where the value type is a message. + // Read it as if we'd seen an input stream with no data (i.e. create a "default" message). + if (Value == null) + { + Value = codec.valueCodec.Read(new CodedInputStream(ZeroLengthMessageStreamData)); + } } public void WriteTo(CodedOutputStream output) -- cgit v1.2.3