| File: FormReader.cs | Web Access |
| Project: src\aspnetcore\src\Http\WebUtilities\src\Microsoft.AspNetCore.WebUtilities.csproj (Microsoft.AspNetCore.WebUtilities) |
// 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.Diagnostics.CodeAnalysis; using System.Text; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.WebUtilities; /// <summary> /// Used to read an 'application/x-www-form-urlencoded' form. /// </summary> public class FormReader : IDisposable { /// <summary> /// Gets the default value for <see cref="ValueCountLimit"/>. /// Defaults to 1024. /// </summary> public const int DefaultValueCountLimit = 1024; /// <summary> /// Gets the default value for <see cref="KeyLengthLimit"/>. /// Defaults to 2,048 bytes, which is approximately 2KB. /// </summary> public const int DefaultKeyLengthLimit = 1024 * 2; /// <summary> /// Gets the default value for <see cref="ValueLengthLimit" />. /// Defaults to 4,194,304 bytes, which is approximately 4MB. /// </summary> public const int DefaultValueLengthLimit = 1024 * 1024 * 4; private const int _rentedCharPoolLength = 8192; private readonly TextReader _reader; private readonly char[] _buffer; private readonly ArrayPool<char> _charPool; private readonly StringBuilder _builder = new StringBuilder(); private int _bufferOffset; private int _bufferCount; private string? _currentKey; private string? _currentValue; private bool _endOfStream; private bool _disposed; /// <summary> /// Initializes a new instance of <see cref="FormReader"/>. /// </summary> /// <param name="data">The data to read.</param> public FormReader(string data) : this(data, ArrayPool<char>.Shared) { } /// <summary> /// Initializes a new instance of <see cref="FormReader"/>. /// </summary> /// <param name="data">The data to read.</param> /// <param name="charPool">The <see cref="ArrayPool{T}"/> to use.</param> public FormReader(string data, ArrayPool<char> charPool) { ArgumentNullException.ThrowIfNull(data); _buffer = charPool.Rent(_rentedCharPoolLength); _charPool = charPool; _reader = new StringReader(data); } /// <summary> /// Initializes a new instance of <see cref="FormReader"/>. /// </summary> /// <param name="stream">The <see cref="Stream"/> to read. Assumes a utf-8 encoded stream.</param> public FormReader(Stream stream) : this(stream, Encoding.UTF8, ArrayPool<char>.Shared) { } /// <summary> /// Initializes a new instance of <see cref="FormReader"/>. /// </summary> /// <param name="stream">The <see cref="Stream"/> to read.</param> /// <param name="encoding">The character encoding to use.</param> public FormReader(Stream stream, Encoding encoding) : this(stream, encoding, ArrayPool<char>.Shared) { } /// <summary> /// Initializes a new instance of <see cref="FormReader"/>. /// </summary> /// <param name="stream">The <see cref="Stream"/> to read.</param> /// <param name="encoding">The character encoding to use.</param> /// <param name="charPool">The <see cref="ArrayPool{T}"/> to use.</param> public FormReader(Stream stream, Encoding encoding, ArrayPool<char> charPool) { ArgumentNullException.ThrowIfNull(stream); ArgumentNullException.ThrowIfNull(encoding); _buffer = charPool.Rent(_rentedCharPoolLength); _charPool = charPool; _reader = new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks: true, bufferSize: 1024 * 2, leaveOpen: true); } /// <summary> /// The limit on the number of form values to allow in ReadForm or ReadFormAsync. /// </summary> public int ValueCountLimit { get; set; } = DefaultValueCountLimit; /// <summary> /// The limit on the length of form keys. /// </summary> public int KeyLengthLimit { get; set; } = DefaultKeyLengthLimit; /// <summary> /// The limit on the length of form values. /// </summary> public int ValueLengthLimit { get; set; } = DefaultValueLengthLimit; // Format: key1=value1&key2=value2 /// <summary> /// Reads the next key value pair from the form. /// For unbuffered data use the async overload instead. /// </summary> /// <returns>The next key value pair, or null when the end of the form is reached.</returns> public KeyValuePair<string, string>? ReadNextPair() { ReadNextPairImpl(); if (ReadSucceeded()) { return new KeyValuePair<string, string>(_currentKey, _currentValue); } return null; } private void ReadNextPairImpl() { StartReadNextPair(); while (!_endOfStream) { // Empty if (_bufferCount == 0) { Buffer(); } if (TryReadNextPair()) { break; } } } // Format: key1=value1&key2=value2 /// <summary> /// Asynchronously reads the next key value pair from the form. /// </summary> /// <param name="cancellationToken"></param> /// <returns>The next key value pair, or null when the end of the form is reached.</returns> public async Task<KeyValuePair<string, string>?> ReadNextPairAsync(CancellationToken cancellationToken = new CancellationToken()) { await ReadNextPairAsyncImpl(cancellationToken); if (ReadSucceeded()) { return new KeyValuePair<string, string>(_currentKey, _currentValue); } return null; } private async Task ReadNextPairAsyncImpl(CancellationToken cancellationToken = new CancellationToken()) { StartReadNextPair(); while (!_endOfStream) { // Empty if (_bufferCount == 0) { await BufferAsync(cancellationToken); } if (TryReadNextPair()) { break; } } } private void StartReadNextPair() { _currentKey = null; _currentValue = null; } private bool TryReadNextPair() { if (_currentKey == null) { if (!TryReadWord('=', KeyLengthLimit, out _currentKey)) { return false; } if (_bufferCount == 0) { return false; } } if (_currentValue == null) { if (!TryReadWord('&', ValueLengthLimit, out _currentValue)) { return false; } } return true; } private bool TryReadWord(char separator, int limit, [NotNullWhen(true)] out string? value) { do { if (ReadChar(separator, limit, out value)) { return true; } } while (_bufferCount > 0); return false; } private bool ReadChar(char separator, int limit, [NotNullWhen(true)] out string? word) { // End if (_bufferCount == 0) { word = BuildWord(); return true; } var c = _buffer[_bufferOffset++]; _bufferCount--; if (c == separator) { word = BuildWord(); return true; } if (_builder.Length >= limit) { throw new InvalidDataException($"Form key or value length limit {limit} exceeded."); } _builder.Append(c); word = null; return false; } // '+' un-escapes to ' ', %HH un-escapes as ASCII (or utf-8?) private string BuildWord() { _builder.Replace('+', ' '); var result = _builder.ToString(); _builder.Clear(); return Uri.UnescapeDataString(result); // TODO: Replace this, it's not completely accurate. } private void Buffer() { _bufferOffset = 0; _bufferCount = _reader.Read(_buffer, 0, _buffer.Length); _endOfStream = _bufferCount == 0; } private async Task BufferAsync(CancellationToken cancellationToken) { // TODO: StreamReader doesn't support cancellation? cancellationToken.ThrowIfCancellationRequested(); _bufferOffset = 0; _bufferCount = await _reader.ReadAsync(_buffer, 0, _buffer.Length); _endOfStream = _bufferCount == 0; } /// <summary> /// Parses text from an HTTP form body. /// </summary> /// <returns>The collection containing the parsed HTTP form body.</returns> public Dictionary<string, StringValues> ReadForm() { var accumulator = new KeyValueAccumulator(); while (!_endOfStream) { ReadNextPairImpl(); Append(ref accumulator); } return accumulator.GetResults(); } /// <summary> /// Parses an HTTP form body. /// </summary> /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param> /// <returns>The collection containing the parsed HTTP form body.</returns> public async Task<Dictionary<string, StringValues>> ReadFormAsync(CancellationToken cancellationToken = new CancellationToken()) { var accumulator = new KeyValueAccumulator(); while (!_endOfStream) { await ReadNextPairAsyncImpl(cancellationToken); Append(ref accumulator); } return accumulator.GetResults(); } [MemberNotNullWhen(true, nameof(_currentKey), nameof(_currentValue))] private bool ReadSucceeded() { return _currentKey != null && _currentValue != null; } private void Append(ref KeyValueAccumulator accumulator) { if (ReadSucceeded()) { accumulator.Append(_currentKey, _currentValue); if (accumulator.ValueCount > ValueCountLimit) { throw new InvalidDataException($"Form value count limit {ValueCountLimit} exceeded."); } } } /// <inheritdoc /> public void Dispose() { if (!_disposed) { _disposed = true; _charPool.Return(_buffer); } } }