File: FormMapping\FormDataReader.cs
Web Access
Project: src\src\aspnetcore\src\Components\Endpoints\src\Microsoft.AspNetCore.Components.Endpoints.csproj (Microsoft.AspNetCore.Components.Endpoints)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Primitives;

namespace Microsoft.AspNetCore.Components.Endpoints.FormMapping;

[DebuggerDisplay($"{{{nameof(DebuggerDisplay)}(),nq}}")]
internal struct FormDataReader : IDisposable
{
    private readonly IReadOnlyDictionary<FormKey, StringValues> _readOnlyMemoryKeys;
    private readonly Memory<char> _prefixBuffer;
    private Memory<char> _currentPrefixBuffer;
    private int _currentDepth;
    private int _errorCount;

    // As an implementation detail, reuse FormKey for the values.
    // It's just a thin wrapper over ReadOnlyMemory<char> that caches
    // the computed hash code.

    private PrefixResolver _prefixResolver;

    public FormDataReader(IReadOnlyDictionary<FormKey, StringValues> formCollection, CultureInfo culture, Memory<char> buffer)
    {
        _readOnlyMemoryKeys = formCollection;
        Culture = culture;
        _prefixBuffer = buffer;
    }

    public FormDataReader(IReadOnlyDictionary<FormKey, StringValues> formCollection, CultureInfo culture, Memory<char> buffer, IFormFileCollection formFileCollection)
        : this(formCollection, culture, buffer)
    {
        FormFileCollection = formFileCollection;
    }

    internal ReadOnlyMemory<char> CurrentPrefix => _currentPrefixBuffer;

    public IFormatProvider Culture { get; }

    public IFormFileCollection? FormFileCollection { get; internal set; }

    public int MaxRecursionDepth { get; set; } = 64;

    public int MaxCollectionSize { get; set; } = FormReader.DefaultValueCountLimit;

    public Action<string, FormattableString, string?>? ErrorHandler { get; set; }

    public Action<string, object>? AttachInstanceToErrorsHandler { get; set; }

    public int MaxErrorCount { get; set; } = 200;

    public void AddMappingError(FormattableString errorMessage, string? attemptedValue)
    {
        ArgumentNullException.ThrowIfNull(errorMessage);

        if (ErrorHandler == null)
        {
            throw new FormDataMappingException(new FormDataMappingError(_currentPrefixBuffer.ToString(), errorMessage, attemptedValue));
        }

        _errorCount++;
        if (_errorCount == MaxErrorCount)
        {
            ErrorHandler.Invoke(
                _currentPrefixBuffer.ToString(),
                FormattableStringFactory.Create($"Maximum number of errors ({MaxErrorCount}) reached. Further errors will be suppressed."),
                null);
        }

        if (_errorCount >= MaxErrorCount)
        {
            return;
        }

        ErrorHandler.Invoke(_currentPrefixBuffer.ToString(), errorMessage, attemptedValue);
    }

    public void AddMappingError(Exception exception, string? attemptedValue)
    {
        ArgumentNullException.ThrowIfNull(exception);

        // Avoid re-wrapping the exception if it is already a FormDataMappingException
        // and we don't have an ErrorHandler configured.
        if (exception is FormDataMappingException && ErrorHandler == null)
        {
            throw exception;
        }

        var errorMessage = FormattableStringFactory.Create(exception.Message);
        AddMappingError(errorMessage, attemptedValue);
    }

    public void AttachInstanceToErrors(object value)
    {
        if (AttachInstanceToErrorsHandler == null)
        {
            return;
        }

        AttachInstanceToErrorsHandler(_currentPrefixBuffer.ToString(), value);
    }

    internal FormKeyCollection GetKeys()
    {
        // Scan the input dictionary for keys matching the current prefix followed by a bracket segment.
        // This avoids building a large upfront dictionary of all prefix→key mappings.
        var prefix = _currentPrefixBuffer;
        var result = new HashSet<FormKey>();

        foreach (var kvp in _readOnlyMemoryKeys)
        {
            var key = kvp.Key.Value;

            // The key must start with the current prefix (case-insensitive).
            if (key.Length <= prefix.Length ||
                !key.Span[..prefix.Length].Equals(prefix.Span, StringComparison.OrdinalIgnoreCase))
            {
                continue;
            }

            // Immediately after the prefix, there must be a '['.
            if (key.Span[prefix.Length] != '[')
            {
                continue;
            }

            // Find the closing ']' after the '['.
            var remaining = key[prefix.Length..];
            var closeIndex = remaining.Span[1..].IndexOf(']');
            if (closeIndex == -1)
            {
                // Malformed key — no closing bracket. Skip it.
                continue;
            }

            // Extract "[value]" (closeIndex is relative to position 1, so add 2 for the full segment).
            var segment = remaining[..(closeIndex + 2)];
            result.Add(new FormKey(segment));

            // Allow one extra item through so collection/dictionary binding can still detect overflow
            // and report the existing max-size error instead of silently truncating the input.
            if (result.Count > MaxCollectionSize)
            {
                break;
            }
        }

        return result.Count > 0
            ? new FormKeyCollection(result)
            : FormKeyCollection.Empty;
    }

    // This only ever gets invoked if we have a recursive type.
    // Recursive types make an existential check for the current prefix to determine if they
    // need to continue mapping data.
    // At that point, we are placing the keys into a sorted array, and we use binary search to
    // determine if the prefix exists.
    // We only make this search on recursive paths, as opposed to in every new scope that we push.
    internal bool CurrentPrefixExists()
    {
        if (CurrentPrefix.Span.Length == 0)
        {
            // Avoid creating the prefix array at the root level.
            return _readOnlyMemoryKeys.Count > 0;
        }

        if (!_prefixResolver.HasValues)
        {
            _prefixResolver = new PrefixResolver(_readOnlyMemoryKeys.Keys, _readOnlyMemoryKeys.Count);
        }

        return _prefixResolver.HasPrefix(_currentPrefixBuffer);
    }

    internal void PopPrefix(string key)
    {
        PopPrefix(key.AsSpan());
    }

    internal void PopPrefix(ReadOnlySpan<char> key)
    {
        if (_currentDepth > MaxRecursionDepth)
        {
            return;
        }

        _currentDepth--;
        Debug.Assert(_currentDepth >= 0);
        var keyLength = key.Length;
        // If keyLength is bigger than the current scope keyLength typically means there is a
        // bug where some part of the code has not popped the scope appropriately.
        Debug.Assert(_currentPrefixBuffer.Length >= keyLength);
        if (_currentPrefixBuffer.Length == keyLength || _currentPrefixBuffer.Span[^(keyLength + 1)] != '.')
        {
            _currentPrefixBuffer = _currentPrefixBuffer[..^keyLength];
        }
        else
        {
            _currentPrefixBuffer = _currentPrefixBuffer[..^(keyLength + 1)];
        }
    }

    internal void PushPrefix(string key)
    {
        PushPrefix(key.AsSpan());
    }

    internal void PushPrefix(scoped ReadOnlySpan<char> key)
    {
        _currentDepth++;
        // We automatically append a "." before adding the suffix, except when its the first element pushed to the
        // scope, or when we are accessing a property after a collection or an indexer like items[1].
        var separator = _currentPrefixBuffer.Length > 0 && key[0] != '['
            ? ".".AsSpan()
            : "".AsSpan();
        if (_currentDepth > MaxRecursionDepth)
        {
            throw new InvalidOperationException($"The maximum recursion depth of '{MaxRecursionDepth}' was exceeded for '{_currentPrefixBuffer}{separator}{key}'.");
        }

        Debug.Assert(_prefixBuffer.Length >= (_currentPrefixBuffer.Length + separator.Length));

        separator.CopyTo(_prefixBuffer.Span[_currentPrefixBuffer.Length..]);

        var startingPoint = _currentPrefixBuffer.Length + separator.Length;
        _currentPrefixBuffer = _prefixBuffer.Slice(0, startingPoint + key.Length);
        key.CopyTo(_prefixBuffer[startingPoint..].Span);
    }

    internal readonly bool TryGetValue([NotNullWhen(true)] out string? value)
    {
        var foundSingleValue = _readOnlyMemoryKeys.TryGetValue(new FormKey(_currentPrefixBuffer), out var result) || result.Count == 1;
        if (foundSingleValue)
        {
            value = result[0];
        }
        else
        {
            value = null;
        }

        return foundSingleValue;
    }

    internal readonly bool TryGetValues(out StringValues values) =>
        _readOnlyMemoryKeys.TryGetValue(new FormKey(_currentPrefixBuffer), out values);

    internal string GetPrefix() => _currentPrefixBuffer.ToString();

    internal string GetLastPrefixSegment()
    {
        var index = _currentPrefixBuffer.Span.LastIndexOfAny(".[");
        if (index == -1)
        {
            return _currentPrefixBuffer.ToString();
        }
        if (_currentPrefixBuffer.Span[index] == '.')
        {
            return _currentPrefixBuffer.Span[(index + 1)..].ToString();
        }
        else
        {
            // Return the value without the closing bracket ]
            return _currentPrefixBuffer.Span[(index + 1)..^1].ToString();
        }
    }

    public void Dispose()
    {
        _prefixResolver.Dispose();
    }

    internal readonly struct FormKeyCollection : IEnumerable<ReadOnlyMemory<char>>
    {
        private readonly HashSet<FormKey> _values;
        internal static readonly FormKeyCollection Empty;

        public bool HasValues() => _values != null;

        public FormKeyCollection(HashSet<FormKey> values) => _values = values;

        public Enumerator GetEnumerator() => new Enumerator(_values.GetEnumerator());

        IEnumerator<ReadOnlyMemory<char>> IEnumerable<ReadOnlyMemory<char>>.GetEnumerator() => GetEnumerator();

        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

        internal struct Enumerator : IEnumerator<ReadOnlyMemory<char>>
        {
            private HashSet<FormKey>.Enumerator _enumerator;

            public Enumerator(HashSet<FormKey>.Enumerator enumerator)
            {
                _enumerator = enumerator;
            }

            public ReadOnlyMemory<char> Current => _enumerator.Current.Value;

            object IEnumerator.Current => _enumerator.Current;

            void IDisposable.Dispose() => _enumerator.Dispose();

            public bool MoveNext() => _enumerator.MoveNext();

            void IEnumerator.Reset() { }
        }
    }

    private readonly string DebuggerDisplay =>
        $"Key count = {_readOnlyMemoryKeys.Count}, Prefix = {_currentPrefixBuffer}, Error count = {_errorCount}, Current depth = {_currentDepth}";
}