• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 #region Copyright notice and license
2 // Protocol Buffers - Google's data interchange format
3 // Copyright 2015 Google Inc.  All rights reserved.
4 //
5 // Use of this source code is governed by a BSD-style
6 // license that can be found in the LICENSE file or at
7 // https://developers.google.com/open-source/licenses/bsd
8 #endregion
9 
10 using System;
11 using System.Collections;
12 using System.Collections.Generic;
13 using System.Diagnostics;
14 using System.IO;
15 using System.Linq;
16 using System.Security;
17 
18 namespace Google.Protobuf.Collections
19 {
20     /// <summary>
21     /// The contents of a repeated field: essentially, a collection with some extra
22     /// restrictions (no null values) and capabilities (deep cloning).
23     /// </summary>
24     /// <remarks>
25     /// This implementation does not generally prohibit the use of types which are not
26     /// supported by Protocol Buffers but nor does it guarantee that all operations will work in such cases.
27     /// </remarks>
28     /// <typeparam name="T">The element type of the repeated field.</typeparam>
29     [DebuggerDisplay("Count = {Count}")]
30     [DebuggerTypeProxy(typeof(RepeatedField<>.RepeatedFieldDebugView))]
31     public sealed class RepeatedField<T> : IList<T>, IList, IDeepCloneable<RepeatedField<T>>, IEquatable<RepeatedField<T>>, IReadOnlyList<T>
32     {
33         private static readonly EqualityComparer<T> EqualityComparer = ProtobufEqualityComparers.GetEqualityComparer<T>();
34         private static readonly T[] EmptyArray = new T[0];
35         private const int MinArraySize = 8;
36 
37         private T[] array = EmptyArray;
38         private int count = 0;
39 
40         /// <summary>
41         /// Creates a deep clone of this repeated field.
42         /// </summary>
43         /// <remarks>
44         /// If the field type is
45         /// a message type, each element is also cloned; otherwise, it is
46         /// assumed that the field type is primitive (including string and
47         /// bytes, both of which are immutable) and so a simple copy is
48         /// equivalent to a deep clone.
49         /// </remarks>
50         /// <returns>A deep clone of this repeated field.</returns>
Clone()51         public RepeatedField<T> Clone()
52         {
53             RepeatedField<T> clone = new RepeatedField<T>();
54             if (array != EmptyArray)
55             {
56                 clone.array = (T[])array.Clone();
57                 if (clone.array is IDeepCloneable<T>[] cloneableArray)
58                 {
59                     for (int i = 0; i < count; i++)
60                     {
61                         clone.array[i] = cloneableArray[i].Clone();
62                     }
63                 }
64             }
65             clone.count = count;
66             return clone;
67         }
68 
69         /// <summary>
70         /// Adds the entries from the given input stream, decoding them with the specified codec.
71         /// </summary>
72         /// <param name="input">The input stream to read from.</param>
73         /// <param name="codec">The codec to use in order to read each entry.</param>
AddEntriesFrom(CodedInputStream input, FieldCodec<T> codec)74         public void AddEntriesFrom(CodedInputStream input, FieldCodec<T> codec)
75         {
76             ParseContext.Initialize(input, out ParseContext ctx);
77             try
78             {
79                 AddEntriesFrom(ref ctx, codec);
80             }
81             finally
82             {
83                 ctx.CopyStateTo(input);
84             }
85         }
86 
87         /// <summary>
88         /// Adds the entries from the given parse context, decoding them with the specified codec.
89         /// </summary>
90         /// <param name="ctx">The input to read from.</param>
91         /// <param name="codec">The codec to use in order to read each entry.</param>
92         [SecuritySafeCritical]
AddEntriesFrom(ref ParseContext ctx, FieldCodec<T> codec)93         public void AddEntriesFrom(ref ParseContext ctx, FieldCodec<T> codec)
94         {
95             // TODO: Inline some of the Add code, so we can avoid checking the size on every
96             // iteration.
97             uint tag = ctx.state.lastTag;
98             var reader = codec.ValueReader;
99             // Non-nullable value types can be packed or not.
100             if (FieldCodec<T>.IsPackedRepeatedField(tag))
101             {
102                 int length = ctx.ReadLength();
103                 if (length > 0)
104                 {
105                     int oldLimit = SegmentedBufferHelper.PushLimit(ref ctx.state, length);
106 
107                     // If the content is fixed size then we can calculate the length
108                     // of the repeated field and pre-initialize the underlying collection.
109                     //
110                     // Check that the supplied length doesn't exceed the underlying buffer.
111                     // That prevents a malicious length from initializing a very large collection.
112                     if (codec.FixedSize > 0 && length % codec.FixedSize == 0 && ParsingPrimitives.IsDataAvailable(ref ctx.state, length))
113                     {
114                         EnsureSize(count + (length / codec.FixedSize));
115 
116                         while (!SegmentedBufferHelper.IsReachedLimit(ref ctx.state))
117                         {
118                             // Only FieldCodecs with a fixed size can reach here, and they are all known
119                             // types that don't allow the user to specify a custom reader action.
120                             // reader action will never return null.
121                             array[count++] = reader(ref ctx);
122                         }
123                     }
124                     else
125                     {
126                         // Content is variable size so add until we reach the limit.
127                         while (!SegmentedBufferHelper.IsReachedLimit(ref ctx.state))
128                         {
129                             Add(reader(ref ctx));
130                         }
131                     }
132                     SegmentedBufferHelper.PopLimit(ref ctx.state, oldLimit);
133                 }
134                 // Empty packed field. Odd, but valid - just ignore.
135             }
136             else
137             {
138                 // Not packed... (possibly not packable)
139                 do
140                 {
141                     Add(reader(ref ctx));
142                 } while (ParsingPrimitives.MaybeConsumeTag(ref ctx.buffer, ref ctx.state, tag));
143             }
144         }
145 
146         /// <summary>
147         /// Calculates the size of this collection based on the given codec.
148         /// </summary>
149         /// <param name="codec">The codec to use when encoding each field.</param>
150         /// <returns>The number of bytes that would be written to an output by one of the <c>WriteTo</c> methods,
151         /// using the same codec.</returns>
CalculateSize(FieldCodec<T> codec)152         public int CalculateSize(FieldCodec<T> codec)
153         {
154             if (count == 0)
155             {
156                 return 0;
157             }
158             uint tag = codec.Tag;
159             if (codec.PackedRepeatedField)
160             {
161                 int dataSize = CalculatePackedDataSize(codec);
162                 return CodedOutputStream.ComputeRawVarint32Size(tag) +
163                     CodedOutputStream.ComputeLengthSize(dataSize) +
164                     dataSize;
165             }
166             else
167             {
168                 var sizeCalculator = codec.ValueSizeCalculator;
169                 int size = count * CodedOutputStream.ComputeRawVarint32Size(tag);
170                 if (codec.EndTag != 0)
171                 {
172                     size += count * CodedOutputStream.ComputeRawVarint32Size(codec.EndTag);
173                 }
174                 for (int i = 0; i < count; i++)
175                 {
176                     size += sizeCalculator(array[i]);
177                 }
178                 return size;
179             }
180         }
181 
CalculatePackedDataSize(FieldCodec<T> codec)182         private int CalculatePackedDataSize(FieldCodec<T> codec)
183         {
184             int fixedSize = codec.FixedSize;
185             if (fixedSize == 0)
186             {
187                 var calculator = codec.ValueSizeCalculator;
188                 int tmp = 0;
189                 for (int i = 0; i < count; i++)
190                 {
191                     tmp += calculator(array[i]);
192                 }
193                 return tmp;
194             }
195             else
196             {
197                 return fixedSize * Count;
198             }
199         }
200 
201         /// <summary>
202         /// Writes the contents of this collection to the given <see cref="CodedOutputStream"/>,
203         /// encoding each value using the specified codec.
204         /// </summary>
205         /// <param name="output">The output stream to write to.</param>
206         /// <param name="codec">The codec to use when encoding each value.</param>
WriteTo(CodedOutputStream output, FieldCodec<T> codec)207         public void WriteTo(CodedOutputStream output, FieldCodec<T> codec)
208         {
209             WriteContext.Initialize(output, out WriteContext ctx);
210             try
211             {
212                 WriteTo(ref ctx, codec);
213             }
214             finally
215             {
216                 ctx.CopyStateTo(output);
217             }
218         }
219 
220         /// <summary>
221         /// Writes the contents of this collection to the given write context,
222         /// encoding each value using the specified codec.
223         /// </summary>
224         /// <param name="ctx">The write context to write to.</param>
225         /// <param name="codec">The codec to use when encoding each value.</param>
226         [SecuritySafeCritical]
WriteTo(ref WriteContext ctx, FieldCodec<T> codec)227         public void WriteTo(ref WriteContext ctx, FieldCodec<T> codec)
228         {
229             if (count == 0)
230             {
231                 return;
232             }
233             var writer = codec.ValueWriter;
234             var tag = codec.Tag;
235             if (codec.PackedRepeatedField)
236             {
237                 // Packed primitive type
238                 int size = CalculatePackedDataSize(codec);
239                 ctx.WriteTag(tag);
240                 ctx.WriteLength(size);
241                 for (int i = 0; i < count; i++)
242                 {
243                     writer(ref ctx, array[i]);
244                 }
245             }
246             else
247             {
248                 // Not packed: a simple tag/value pair for each value.
249                 // Can't use codec.WriteTagAndValue, as that omits default values.
250                 for (int i = 0; i < count; i++)
251                 {
252                     ctx.WriteTag(tag);
253                     writer(ref ctx, array[i]);
254                     if (codec.EndTag != 0)
255                     {
256                         ctx.WriteTag(codec.EndTag);
257                     }
258                 }
259             }
260         }
261 
262         /// <summary>
263         /// Gets and sets the capacity of the RepeatedField's internal array.
264         /// When set, the internal array is reallocated to the given capacity.
265         /// <exception cref="ArgumentOutOfRangeException">The new value is less than <see cref="Count"/>.</exception>
266         /// </summary>
267         public int Capacity
268         {
269             get { return array.Length; }
270             set
271             {
272                 if (value < count)
273                 {
274                     throw new ArgumentOutOfRangeException("Capacity", value,
275                         $"Cannot set Capacity to a value smaller than the current item count, {count}");
276                 }
277 
278                 if (value >= 0 && value != array.Length)
279                 {
280                     SetSize(value);
281                 }
282             }
283         }
284 
285         // May increase the size of the internal array, but will never shrink it.
EnsureSize(int size)286         private void EnsureSize(int size)
287         {
288             if (array.Length < size)
289             {
290                 size = Math.Max(size, MinArraySize);
291                 int newSize = Math.Max(array.Length * 2, size);
292                 SetSize(newSize);
293             }
294         }
295 
296         // Sets the internal array to an exact size.
SetSize(int size)297         private void SetSize(int size)
298         {
299             if (size != array.Length)
300             {
301                 var tmp = new T[size];
302                 Array.Copy(array, 0, tmp, 0, count);
303                 array = tmp;
304             }
305         }
306 
307         /// <summary>
308         /// Adds the specified item to the collection.
309         /// </summary>
310         /// <param name="item">The item to add.</param>
Add(T item)311         public void Add(T item)
312         {
313             ProtoPreconditions.CheckNotNullUnconstrained(item, nameof(item));
314             EnsureSize(count + 1);
315             array[count++] = item;
316         }
317 
318         /// <summary>
319         /// Removes all items from the collection.
320         /// </summary>
Clear()321         public void Clear()
322         {
323             // Clear the content of the array (so that any objects it referred to can be garbage collected)
324             // but keep the capacity the same. This allows large repeated fields to be reused without
325             // array reallocation.
326             Array.Clear(array, 0, count);
327             count = 0;
328         }
329 
330         /// <summary>
331         /// Determines whether this collection contains the given item.
332         /// </summary>
333         /// <param name="item">The item to find.</param>
334         /// <returns><c>true</c> if this collection contains the given item; <c>false</c> otherwise.</returns>
335         public bool Contains(T item) => IndexOf(item) != -1;
336 
337         /// <summary>
338         /// Copies this collection to the given array.
339         /// </summary>
340         /// <param name="array">The array to copy to.</param>
341         /// <param name="arrayIndex">The first index of the array to copy to.</param>
CopyTo(T[] array, int arrayIndex)342         public void CopyTo(T[] array, int arrayIndex)
343         {
344             Array.Copy(this.array, 0, array, arrayIndex, count);
345         }
346 
347         /// <summary>
348         /// Removes the specified item from the collection
349         /// </summary>
350         /// <param name="item">The item to remove.</param>
351         /// <returns><c>true</c> if the item was found and removed; <c>false</c> otherwise.</returns>
Remove(T item)352         public bool Remove(T item)
353         {
354             int index = IndexOf(item);
355             if (index == -1)
356             {
357                 return false;
358             }
359             Array.Copy(array, index + 1, array, index, count - index - 1);
360             count--;
361             array[count] = default;
362             return true;
363         }
364 
365         /// <summary>
366         /// Gets the number of elements contained in the collection.
367         /// </summary>
368         public int Count => count;
369 
370         /// <summary>
371         /// Gets a value indicating whether the collection is read-only.
372         /// </summary>
373         public bool IsReadOnly => false;
374 
375         /// <summary>
376         /// Adds all of the specified values into this collection.
377         /// </summary>
378         /// <param name="values">The values to add to this collection.</param>
AddRange(IEnumerable<T> values)379         public void AddRange(IEnumerable<T> values)
380         {
381             ProtoPreconditions.CheckNotNull(values, nameof(values));
382 
383             // Optimization 1: If the collection we're adding is already a RepeatedField<T>,
384             // we know the values are valid.
385             if (values is RepeatedField<T> otherRepeatedField)
386             {
387                 EnsureSize(count + otherRepeatedField.count);
388                 Array.Copy(otherRepeatedField.array, 0, array, count, otherRepeatedField.count);
389                 count += otherRepeatedField.count;
390                 return;
391             }
392 
393             // Optimization 2: The collection is an ICollection, so we can expand
394             // just once and ask the collection to copy itself into the array.
395             if (values is ICollection collection)
396             {
397                 var extraCount = collection.Count;
398                 // For reference types and nullable value types, we need to check that there are no nulls
399                 // present. (This isn't a thread-safe approach, but we don't advertise this is thread-safe.)
400                 // We expect the JITter to optimize this test to true/false, so it's effectively conditional
401                 // specialization.
402                 if (default(T) == null)
403                 {
404                     // TODO: Measure whether iterating once to check and then letting the collection copy
405                     // itself is faster or slower than iterating and adding as we go. For large
406                     // collections this will not be great in terms of cache usage... but the optimized
407                     // copy may be significantly faster than doing it one at a time.
408                     foreach (var item in collection)
409                     {
410                         if (item == null)
411                         {
412                             throw new ArgumentException("Sequence contained null element", nameof(values));
413                         }
414                     }
415                 }
416                 EnsureSize(count + extraCount);
417                 collection.CopyTo(array, count);
418                 count += extraCount;
419                 return;
420             }
421 
422             // We *could* check for ICollection<T> as well, but very very few collections implement
423             // ICollection<T> but not ICollection. (HashSet<T> does, for one...)
424 
425             // Fall back to a slower path of adding items one at a time.
426             foreach (T item in values)
427             {
428                 Add(item);
429             }
430         }
431 
432         /// <summary>
433         /// Adds all of the specified values into this collection. This method is present to
434         /// allow repeated fields to be constructed from queries within collection initializers.
435         /// Within non-collection-initializer code, consider using the equivalent <see cref="AddRange"/>
436         /// method instead for clarity.
437         /// </summary>
438         /// <param name="values">The values to add to this collection.</param>
Add(IEnumerable<T> values)439         public void Add(IEnumerable<T> values)
440         {
441             AddRange(values);
442         }
443 
444         /// <summary>
445         /// Returns an enumerator that iterates through the collection.
446         /// </summary>
447         /// <returns>
448         /// An enumerator that can be used to iterate through the collection.
449         /// </returns>
GetEnumerator()450         public IEnumerator<T> GetEnumerator()
451         {
452             for (int i = 0; i < count; i++)
453             {
454                 yield return array[i];
455             }
456         }
457 
458         /// <summary>
459         /// Determines whether the specified <see cref="System.Object" />, is equal to this instance.
460         /// </summary>
461         /// <param name="obj">The <see cref="System.Object" /> to compare with this instance.</param>
462         /// <returns>
463         ///   <c>true</c> if the specified <see cref="System.Object" /> is equal to this instance; otherwise, <c>false</c>.
464         /// </returns>
465         public override bool Equals(object obj) => Equals(obj as RepeatedField<T>);
466 
467         /// <summary>
468         /// Returns an enumerator that iterates through a collection.
469         /// </summary>
470         /// <returns>
471         /// An <see cref="T:System.Collections.IEnumerator" /> object that can be used to iterate through the collection.
472         /// </returns>
IEnumerable.GetEnumerator()473         IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
474 
475         /// <summary>
476         /// Returns a hash code for this instance.
477         /// </summary>
478         /// <returns>
479         /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
480         /// </returns>
GetHashCode()481         public override int GetHashCode()
482         {
483             int hash = 0;
484             for (int i = 0; i < count; i++)
485             {
486                 hash = hash * 31 + array[i].GetHashCode();
487             }
488             return hash;
489         }
490 
491         /// <summary>
492         /// Compares this repeated field with another for equality.
493         /// </summary>
494         /// <param name="other">The repeated field to compare this with.</param>
495         /// <returns><c>true</c> if <paramref name="other"/> refers to an equal repeated field; <c>false</c> otherwise.</returns>
Equals(RepeatedField<T> other)496         public bool Equals(RepeatedField<T> other)
497         {
498             if (other is null)
499             {
500                 return false;
501             }
502             if (ReferenceEquals(other, this))
503             {
504                 return true;
505             }
506             if (other.Count != this.Count)
507             {
508                 return false;
509             }
510             EqualityComparer<T> comparer = EqualityComparer;
511             for (int i = 0; i < count; i++)
512             {
513                 if (!comparer.Equals(array[i], other.array[i]))
514                 {
515                     return false;
516                 }
517             }
518             return true;
519         }
520 
521         /// <summary>
522         /// Returns the index of the given item within the collection, or -1 if the item is not
523         /// present.
524         /// </summary>
525         /// <param name="item">The item to find in the collection.</param>
526         /// <returns>The zero-based index of the item, or -1 if it is not found.</returns>
IndexOf(T item)527         public int IndexOf(T item)
528         {
529             ProtoPreconditions.CheckNotNullUnconstrained(item, nameof(item));
530             EqualityComparer<T> comparer = EqualityComparer;
531             for (int i = 0; i < count; i++)
532             {
533                 if (comparer.Equals(array[i], item))
534                 {
535                     return i;
536                 }
537             }
538             return -1;
539         }
540 
541         /// <summary>
542         /// Inserts the given item at the specified index.
543         /// </summary>
544         /// <param name="index">The index at which to insert the item.</param>
545         /// <param name="item">The item to insert.</param>
Insert(int index, T item)546         public void Insert(int index, T item)
547         {
548             ProtoPreconditions.CheckNotNullUnconstrained(item, nameof(item));
549             if (index < 0 || index > count)
550             {
551                 throw new ArgumentOutOfRangeException(nameof(index));
552             }
553             EnsureSize(count + 1);
554             Array.Copy(array, index, array, index + 1, count - index);
555             array[index] = item;
556             count++;
557         }
558 
559         /// <summary>
560         /// Removes the item at the given index.
561         /// </summary>
562         /// <param name="index">The zero-based index of the item to remove.</param>
RemoveAt(int index)563         public void RemoveAt(int index)
564         {
565             if (index < 0 || index >= count)
566             {
567                 throw new ArgumentOutOfRangeException(nameof(index));
568             }
569             Array.Copy(array, index + 1, array, index, count - index - 1);
570             count--;
571             array[count] = default;
572         }
573 
574         /// <summary>
575         /// Returns a string representation of this repeated field, in the same
576         /// way as it would be represented by the default JSON formatter.
577         /// </summary>
ToString()578         public override string ToString()
579         {
580             var writer = new StringWriter();
581             JsonFormatter.Default.WriteList(writer, this);
582             return writer.ToString();
583         }
584 
585         /// <summary>
586         /// Gets or sets the item at the specified index.
587         /// </summary>
588         /// <value>
589         /// The element at the specified index.
590         /// </value>
591         /// <param name="index">The zero-based index of the element to get or set.</param>
592         /// <returns>The item at the specified index.</returns>
593         public T this[int index]
594         {
595             get
596             {
597                 if (index < 0 || index >= count)
598                 {
599                     throw new ArgumentOutOfRangeException(nameof(index));
600                 }
601                 return array[index];
602             }
603             set
604             {
605                 if (index < 0 || index >= count)
606                 {
607                     throw new ArgumentOutOfRangeException(nameof(index));
608                 }
609                 ProtoPreconditions.CheckNotNullUnconstrained(value, nameof(value));
610                 array[index] = value;
611             }
612         }
613 
614         #region Explicit interface implementation for IList and ICollection.
615         bool IList.IsFixedSize => false;
616 
ICollection.CopyTo(Array array, int index)617         void ICollection.CopyTo(Array array, int index) => Array.Copy(this.array, 0, array, index, count);
618 
619         bool ICollection.IsSynchronized => false;
620 
621         object ICollection.SyncRoot => this;
622 
623         object IList.this[int index]
624         {
625             get => this[index];
626             set => this[index] = (T)value;
627         }
628 
IList.Add(object value)629         int IList.Add(object value)
630         {
631             Add((T) value);
632             return count - 1;
633         }
634 
635         bool IList.Contains(object value) => (value is T t && Contains(t));
636 
637         int IList.IndexOf(object value) => (value is T t) ? IndexOf(t) : -1;
638 
IList.Insert(int index, object value)639         void IList.Insert(int index, object value) => Insert(index, (T) value);
640 
IList.Remove(object value)641         void IList.Remove(object value)
642         {
643             if (value is T t)
644             {
645                 Remove(t);
646             }
647         }
648         #endregion
649 
650         private sealed class RepeatedFieldDebugView
651         {
652             private readonly RepeatedField<T> list;
653 
RepeatedFieldDebugView(RepeatedField<T> list)654             public RepeatedFieldDebugView(RepeatedField<T> list)
655             {
656                 this.list = list;
657             }
658 
659             [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
660             public T[] Items => list.ToArray();
661         }
662     }
663 }
664