File: System\Text\Json\Document\JsonDocument.PropertyNameSet.cs
Web Access
Project: src\src\libraries\System.Text.Json\src\System.Text.Json.csproj (System.Text.Json)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
 
namespace System.Text.Json
{
    public sealed partial class JsonDocument
    {
        // Ensure this stays on the stack by making it a ref struct.
        private ref struct PropertyNameSet : IDisposable
        {
            // Data structure to track property names in an object while deserializing
            // into a JsonDocument and validate that there are no duplicates. A small
            // array is used when the number of properties is small and no properties
            // are escaped. Otherwise a hash set is used.
 
            private HashSet<ReadOnlyMemory<byte>>? _hashSet;
 
            private const int ArraySetThreshold = 16;
            private int _arraySetCount;
            private bool _useArraySet = true;
 
#if NET
            [InlineArray(ArraySetThreshold)]
            private struct InlineRangeArray16
            {
                private (int Start, int Length) _element0;
            }
 
            private InlineRangeArray16 _arraySet;
#else
            private readonly (int Start, int Length)[] _arraySet = new (int Start, int Length)[ArraySetThreshold];
#endif
 
            public PropertyNameSet()
            {
            }
 
            internal void SetCapacity(int capacity)
            {
                if (capacity <= ArraySetThreshold)
                {
                    _useArraySet = true;
                }
                else
                {
                    _useArraySet = false;
                    if (_hashSet is null)
                    {
                        _hashSet = new HashSet<ReadOnlyMemory<byte>>(
#if NET
                            capacity,
#endif
                            PropertyNameComparer.Instance);
                    }
                    else
                    {
#if NET
                        _hashSet.EnsureCapacity(capacity);
#endif
                    }
                }
            }
 
            internal void AddPropertyName(JsonProperty property, JsonDocument document)
            {
                DbRow dbRow = document._parsedData.Get(property.Value.MetadataDbIndex - DbRow.Size);
                Debug.Assert(dbRow.TokenType is JsonTokenType.PropertyName);
 
                ReadOnlyMemory<byte> utf8Json = document._utf8Json;
                ReadOnlyMemory<byte> propertyName = utf8Json.Slice(dbRow.Location, dbRow.SizeOrLength);
 
                if (dbRow.HasComplexChildren)
                {
                    SwitchToHashSet(utf8Json);
                    propertyName = JsonReaderHelper.GetUnescaped(propertyName.Span);
                }
 
                if (_useArraySet)
                {
                    for (int i = 0; i < _arraySetCount; i++)
                    {
                        (int Start, int Length) range = _arraySet[i];
                        ReadOnlySpan<byte> previousPropertyName = utf8Json.Span.Slice(range.Start, range.Length);
 
                        if (previousPropertyName.SequenceEqual(propertyName.Span))
                        {
                            ThrowHelper.ThrowJsonException_DuplicatePropertyNotAllowed(propertyName.Span);
                        }
                    }
 
                    _arraySet[_arraySetCount] = (dbRow.Location, dbRow.SizeOrLength);
                    _arraySetCount++;
                }
                else
                {
                    Debug.Assert(_hashSet is not null);
 
                    if (!_hashSet.Add(propertyName))
                    {
                        ThrowHelper.ThrowJsonException_DuplicatePropertyNotAllowed(propertyName.Span);
                    }
                }
            }
 
            private void SwitchToHashSet(ReadOnlyMemory<byte> utf8Json)
            {
                if (_useArraySet)
                {
                    _hashSet ??= new HashSet<ReadOnlyMemory<byte>>(
#if NET
                        ArraySetThreshold,
#endif
                        PropertyNameComparer.Instance);
 
                    for (int i = 0; i < _arraySetCount; i++)
                    {
                        (int Start, int Length) range = _arraySet[i];
                        ReadOnlyMemory<byte> propertyName = utf8Json.Slice(range.Start, range.Length);
                        bool success = _hashSet.Add(propertyName);
                        Debug.Assert(success, $"Property name {propertyName} should not already exist in the set.");
                    }
 
                    _useArraySet = false;
                    _arraySetCount = 0;
                }
            }
 
            internal void Reset()
            {
                _hashSet?.Clear();
                _arraySetCount = 0;
            }
 
            public readonly void Dispose()
            {
            }
 
            private sealed class PropertyNameComparer : IEqualityComparer<ReadOnlyMemory<byte>>
            {
                internal static readonly PropertyNameComparer Instance = new();
 
                public bool Equals(ReadOnlyMemory<byte> left, ReadOnlyMemory<byte> right) =>
                    left.Length == right.Length && left.Span.SequenceEqual(right.Span);
 
                public int GetHashCode(ReadOnlyMemory<byte> name)
                {
                    // Marvin is the currently used hash algorithm for string comparisons.
                    // The seed is unique to this process so an item's hash code can't easily be
                    // discovered by an adversary trying to perform a denial of service attack.
                    return Marvin.ComputeHash32(name.Span, Marvin.DefaultSeed);
                }
            }
        }
    }
}