#region Copyright notice and license // Protocol Buffers - Google's data interchange format // Copyright 2015 Google Inc. All rights reserved. // // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd #endregion using Google.Protobuf.Compatibility; using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Security; namespace Google.Protobuf.Collections { /// /// Representation of a map field in a Protocol Buffer message. /// /// Key type in the map. Must be a type supported by Protocol Buffer map keys. /// Value type in the map. Must be a type supported by Protocol Buffers. /// /// /// 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. /// /// /// The order in which entries are returned when iterating over this object is undefined, and may change /// in future versions. /// /// [DebuggerDisplay("Count = {Count}")] [DebuggerTypeProxy(typeof(MapField<,>.MapFieldDebugView))] public sealed class MapField : IDeepCloneable>, IDictionary, IEquatable>, IDictionary, IReadOnlyDictionary { private static readonly EqualityComparer ValueEqualityComparer = ProtobufEqualityComparers.GetEqualityComparer(); private static readonly EqualityComparer KeyEqualityComparer = ProtobufEqualityComparers.GetEqualityComparer(); // TODO: Don't create the map/list until we have an entry. (Assume many maps will be empty.) private readonly Dictionary>> map = new(KeyEqualityComparer); private readonly LinkedList> list = new(); /// /// Creates a deep clone of this object. /// /// /// A deep clone of this object. /// public MapField Clone() { 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, ((IDeepCloneable)pair.Value).Clone()); } } else { // Nothing is cloneable, so we don't need to worry. clone.Add(this); } return clone; } /// /// Adds the specified key/value pair to the map. /// /// /// This operation fails if the key already exists in the map. To replace an existing entry, use the indexer. /// /// The key to add /// The value to add. /// The given key already exists in map. public void Add(TKey key, TValue value) { // Validation of arguments happens in ContainsKey and the indexer if (ContainsKey(key)) { throw new ArgumentException("Key already exists in map", nameof(key)); } this[key] = value; } /// /// Determines whether the specified key is present in the map. /// /// The key to check. /// true if the map contains the given key; false otherwise. public bool ContainsKey(TKey key) { ProtoPreconditions.CheckNotNullUnconstrained(key, nameof(key)); return map.ContainsKey(key); } private bool ContainsValue(TValue value) => list.Any(pair => ValueEqualityComparer.Equals(pair.Value, value)); /// /// Removes the entry identified by the given key from the map. /// /// The key indicating the entry to remove from the map. /// true if the map contained the given key before the entry was removed; false otherwise. public bool Remove(TKey key) { ProtoPreconditions.CheckNotNullUnconstrained(key, nameof(key)); if (map.TryGetValue(key, out LinkedListNode> node)) { map.Remove(key); node.List.Remove(node); return true; } else { return false; } } /// /// Gets the value associated with the specified key. /// /// The key whose value to get. /// When this method returns, the value associated with the specified key, if the key is found; /// otherwise, the default value for the type of the parameter. /// This parameter is passed uninitialized. /// true if the map contains an element with the specified key; otherwise, false. public bool TryGetValue(TKey key, out TValue value) { if (map.TryGetValue(key, out LinkedListNode> node)) { value = node.Value.Value; return true; } else { value = default; return false; } } /// /// Gets or sets the value associated with the specified key. /// /// The key of the value to get or set. /// The property is retrieved and key does not exist in the collection. /// The value associated with the specified key. If the specified key is not found, /// a get operation throws a , and a set operation creates a new element with the specified key. public TValue this[TKey key] { get { ProtoPreconditions.CheckNotNullUnconstrained(key, nameof(key)); if (TryGetValue(key, out TValue value)) { return value; } throw new KeyNotFoundException(); } set { ProtoPreconditions.CheckNotNullUnconstrained(key, nameof(key)); // value == null check here is redundant, but avoids boxing. if (value == null) { ProtoPreconditions.CheckNotNullUnconstrained(value, nameof(value)); } var pair = new KeyValuePair(key, value); if (map.TryGetValue(key, out LinkedListNode> node)) { node.Value = pair; } else { node = list.AddLast(pair); map[key] = node; } } } /// /// Gets a collection containing the keys in the map. /// public ICollection Keys => new MapView(this, pair => pair.Key, ContainsKey); /// /// Gets a collection containing the values in the map. /// public ICollection Values => new MapView(this, pair => pair.Value, ContainsValue); /// /// 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) { ProtoPreconditions.CheckNotNull(entries, nameof(entries)); foreach (var pair in entries) { Add(pair.Key, pair.Value); } } /// /// Adds the specified entries to the map, replacing any existing entries with the same keys. /// The keys and values are not automatically cloned. /// /// This method primarily exists to be called from MergeFrom methods in generated classes for messages. /// The entries to add to the map. public void MergeFrom(IDictionary entries) { ProtoPreconditions.CheckNotNull(entries, nameof(entries)); foreach (var pair in entries) { this[pair.Key] = pair.Value; } } /// /// Returns an enumerator that iterates through the collection. /// /// /// An enumerator that can be used to iterate through the collection. /// public IEnumerator> GetEnumerator() => list.GetEnumerator(); /// /// Returns an enumerator that iterates through a collection. /// /// /// An object that can be used to iterate through the collection. /// IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /// /// Adds the specified item to the map. /// /// The item to add to the map. void ICollection>.Add(KeyValuePair item) => Add(item.Key, item.Value); /// /// Removes all items from the map. /// public void Clear() { list.Clear(); map.Clear(); } /// /// Determines whether map contains an entry equivalent to the given key/value pair. /// /// The key/value pair to find. /// bool ICollection>.Contains(KeyValuePair item) => TryGetValue(item.Key, out TValue value) && ValueEqualityComparer.Equals(item.Value, value); /// /// Copies the key/value pairs in this map to an array. /// /// The array to copy the entries into. /// The index of the array at which to start copying values. void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) => list.CopyTo(array, arrayIndex); /// /// Removes the specified key/value pair from the map. /// /// Both the key and the value must be found for the entry to be removed. /// The key/value pair to remove. /// true if the key/value pair was found and removed; false otherwise. bool ICollection>.Remove(KeyValuePair item) { if (item.Key == null) { throw new ArgumentException("Key is null", nameof(item)); } if (map.TryGetValue(item.Key, out LinkedListNode> node) && EqualityComparer.Default.Equals(item.Value, node.Value.Value)) { map.Remove(item.Key); node.List.Remove(node); return true; } else { return false; } } /// /// Gets the number of elements contained in the map. /// public int Count => list.Count; /// /// Gets a value indicating whether the map is read-only. /// public bool IsReadOnly => false; /// /// Determines whether the specified , is equal to this instance. /// /// The to compare with this instance. /// /// true if the specified is equal to this instance; otherwise, false. /// public override bool Equals(object other) => Equals(other as MapField); /// /// Returns a hash code for this instance. /// /// /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. /// public override int GetHashCode() { var keyComparer = KeyEqualityComparer; var valueComparer = ValueEqualityComparer; int hash = 0; foreach (var pair in list) { hash ^= keyComparer.GetHashCode(pair.Key) * 31 + valueComparer.GetHashCode(pair.Value); } return hash; } /// /// Compares this map with another for equality. /// /// /// The order of the key/value pairs in the maps is not deemed significant in this comparison. /// /// The map to compare this with. /// true if refers to an equal map; false otherwise. public bool Equals(MapField other) { if (other == null) { return false; } if (other == this) { return true; } if (other.Count != this.Count) { return false; } var valueComparer = ValueEqualityComparer; foreach (var pair in this) { if (!other.TryGetValue(pair.Key, out TValue value)) { return false; } if (!valueComparer.Equals(value, pair.Value)) { return false; } } return true; } /// /// Adds entries to the map from the given stream. /// /// /// It is assumed that the stream is initially positioned after the tag specified by the codec. /// This method will continue reading entries from the stream until the end is reached, or /// a different tag is encountered. /// /// Stream to read from /// Codec describing how the key/value pairs are encoded public void AddEntriesFrom(CodedInputStream input, Codec codec) { ParseContext.Initialize(input, out ParseContext ctx); try { AddEntriesFrom(ref ctx, codec); } finally { ctx.CopyStateTo(input); } } /// /// Adds entries to the map from the given parse context. /// /// /// It is assumed that the input is initially positioned after the tag specified by the codec. /// This method will continue reading entries from the input until the end is reached, or /// a different tag is encountered. /// /// Input to read from /// Codec describing how the key/value pairs are encoded [SecuritySafeCritical] public void AddEntriesFrom(ref ParseContext ctx, Codec codec) { do { KeyValuePair entry = ParsingPrimitivesMessages.ReadMapEntry(ref ctx, codec); this[entry.Key] = entry.Value; } while (ParsingPrimitives.MaybeConsumeTag(ref ctx.buffer, ref ctx.state, codec.MapTag)); } /// /// Writes the contents of this map to the given coded output stream, using the specified codec /// to encode each entry. /// /// The output stream to write to. /// The codec to use for each entry. public void WriteTo(CodedOutputStream output, Codec codec) { WriteContext.Initialize(output, out WriteContext ctx); try { IEnumerable> listToWrite = list; if (output.Deterministic) { listToWrite = GetSortedListCopy(list); } WriteTo(ref ctx, codec, listToWrite); } finally { ctx.CopyStateTo(output); } } internal IEnumerable> GetSortedListCopy(IEnumerable> listToSort) { // We can't sort the list in place, as that would invalidate the linked list. // Instead, we create a new list, sort that, and then write it out. var listToWrite = new List>(listToSort); listToWrite.Sort((pair1, pair2) => { if (typeof(TKey) == typeof(string)) { // Use Ordinal, otherwise Comparer.Default uses StringComparer.CurrentCulture return StringComparer.Ordinal.Compare(pair1.Key.ToString(), pair2.Key.ToString()); } return Comparer.Default.Compare(pair1.Key, pair2.Key); }); return listToWrite; } /// /// Writes the contents of this map to the given write context, using the specified codec /// to encode each entry. /// /// The write context to write to. /// The codec to use for each entry. [SecuritySafeCritical] public void WriteTo(ref WriteContext ctx, Codec codec) { IEnumerable> listToWrite = list; if (ctx.state.CodedOutputStream?.Deterministic ?? false) { listToWrite = GetSortedListCopy(list); } WriteTo(ref ctx, codec, listToWrite); } [SecuritySafeCritical] private void WriteTo(ref WriteContext ctx, Codec codec, IEnumerable> listKvp) { foreach (var entry in listKvp) { ctx.WriteTag(codec.MapTag); WritingPrimitives.WriteLength(ref ctx.buffer, ref ctx.state, CalculateEntrySize(codec, entry)); codec.KeyCodec.WriteTagAndValue(ref ctx, entry.Key); codec.ValueCodec.WriteTagAndValue(ref ctx, entry.Value); } } /// /// Calculates the size of this map based on the given entry codec. /// /// The codec to use to encode each entry. /// public int CalculateSize(Codec codec) { if (Count == 0) { return 0; } int size = 0; foreach (var entry in list) { int entrySize = CalculateEntrySize(codec, entry); size += CodedOutputStream.ComputeRawVarint32Size(codec.MapTag); size += CodedOutputStream.ComputeLengthSize(entrySize) + entrySize; } return size; } private static int CalculateEntrySize(Codec codec, KeyValuePair entry) { return codec.KeyCodec.CalculateSizeWithTag(entry.Key) + codec.ValueCodec.CalculateSizeWithTag(entry.Value); } /// /// Returns a string representation of this repeated field, in the same /// way as it would be represented by the default JSON formatter. /// public override string ToString() { var writer = new StringWriter(); JsonFormatter.Default.WriteDictionary(writer, this); return writer.ToString(); } #region IDictionary explicit interface implementation void IDictionary.Add(object key, object value) => Add((TKey)key, (TValue)value); bool IDictionary.Contains(object key) => key is TKey k && ContainsKey(k); IDictionaryEnumerator IDictionary.GetEnumerator() => new DictionaryEnumerator(GetEnumerator()); void IDictionary.Remove(object key) { ProtoPreconditions.CheckNotNull(key, nameof(key)); if (key is TKey k) { Remove(k); } } void ICollection.CopyTo(Array array, int index) { // This is ugly and slow as heck, but with any luck it will never be used anyway. ICollection temp = this.Select(pair => new DictionaryEntry(pair.Key, pair.Value)).ToList(); temp.CopyTo(array, index); } bool IDictionary.IsFixedSize => false; ICollection IDictionary.Keys => (ICollection)Keys; ICollection IDictionary.Values => (ICollection)Values; bool ICollection.IsSynchronized => false; object ICollection.SyncRoot => this; object IDictionary.this[object key] { get { ProtoPreconditions.CheckNotNull(key, nameof(key)); if (key is TKey k) { TryGetValue(k, out TValue value); return value; } return null; } set { this[(TKey)key] = (TValue)value; } } #endregion #region IReadOnlyDictionary explicit interface implementation IEnumerable IReadOnlyDictionary.Keys => Keys; IEnumerable IReadOnlyDictionary.Values => Values; #endregion private class DictionaryEnumerator : IDictionaryEnumerator { private readonly IEnumerator> enumerator; internal DictionaryEnumerator(IEnumerator> enumerator) { this.enumerator = enumerator; } public bool MoveNext() => enumerator.MoveNext(); public void Reset() => enumerator.Reset(); public object Current => Entry; public DictionaryEntry Entry => new DictionaryEntry(Key, Value); public object Key => enumerator.Current.Key; public object Value => enumerator.Current.Value; } /// /// A codec for a specific map field. This contains all the information required to encode and /// decode the nested messages. /// public sealed class Codec { private readonly FieldCodec keyCodec; private readonly FieldCodec valueCodec; private readonly uint mapTag; /// /// Creates a new entry codec based on a separate key codec and value codec, /// and the tag to use for each map entry. /// /// The key codec. /// The value codec. /// The map tag to use to introduce each map entry. public Codec(FieldCodec keyCodec, FieldCodec valueCodec, uint mapTag) { this.keyCodec = keyCodec; this.valueCodec = valueCodec; this.mapTag = mapTag; } /// /// The key codec. /// internal FieldCodec KeyCodec => keyCodec; /// /// The value codec. /// internal FieldCodec ValueCodec => valueCodec; /// /// The tag used in the enclosing message to indicate map entries. /// internal uint MapTag => mapTag; } private class MapView : ICollection, ICollection { private readonly MapField parent; private readonly Func, T> projection; private readonly Func containsCheck; internal MapView( MapField parent, Func, T> projection, Func containsCheck) { this.parent = parent; this.projection = projection; this.containsCheck = containsCheck; } public int Count => parent.Count; public bool IsReadOnly => true; public bool IsSynchronized => false; public object SyncRoot => parent; public void Add(T item) => throw new NotSupportedException(); public void Clear() => throw new NotSupportedException(); public bool Contains(T item) => containsCheck(item); public void CopyTo(T[] array, int arrayIndex) { if (arrayIndex < 0) { throw new ArgumentOutOfRangeException(nameof(arrayIndex)); } if (arrayIndex + Count > array.Length) { throw new ArgumentException("Not enough space in the array", nameof(array)); } foreach (var item in this) { array[arrayIndex++] = item; } } public IEnumerator GetEnumerator() { return parent.list.Select(projection).GetEnumerator(); } public bool Remove(T item) => throw new NotSupportedException(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public void CopyTo(Array array, int index) { if (index < 0) { throw new ArgumentOutOfRangeException(nameof(index)); } if (index + Count > array.Length) { throw new ArgumentException("Not enough space in the array", nameof(array)); } foreach (var item in this) { array.SetValue(item, index++); } } } private sealed class MapFieldDebugView { private readonly MapField map; public MapFieldDebugView(MapField map) { this.map = map; } [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] public KeyValuePair[] Items => map.list.ToArray(); } } }