// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Microsoft.AspNet.WebUtilities { /// /// Used to read an 'application/x-www-form-urlencoded' form. /// public class FormReader { private readonly TextReader _reader; private readonly char[] _buffer = new char[1024]; private readonly StringBuilder _builder = new StringBuilder(); private int _bufferOffset; private int _bufferCount; public FormReader([NotNull] string data) { _reader = new StringReader(data); } public FormReader([NotNull] Stream stream, [NotNull] Encoding encoding) { _reader = new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks: true, bufferSize: 1024 * 2, leaveOpen: true); } // Format: key1=value1&key2=value2 /// /// Reads the next key value pair from the form. /// For unbuffered data use the async overload instead. /// /// The next key value pair, or null when the end of the form is reached. public KeyValuePair? ReadNextPair() { var key = ReadWord('='); if (string.IsNullOrEmpty(key) && _bufferCount == 0) { return null; } var value = ReadWord('&'); return new KeyValuePair(key, value); } // Format: key1=value1&key2=value2 /// /// Asynchronously reads the next key value pair from the form. /// /// /// The next key value pair, or null when the end of the form is reached. public async Task?> ReadNextPairAsync(CancellationToken cancellationToken) { var key = await ReadWordAsync('=', cancellationToken); if (string.IsNullOrEmpty(key) && _bufferCount == 0) { return null; } var value = await ReadWordAsync('&', cancellationToken); return new KeyValuePair(key, value); } private string ReadWord(char seperator) { // TODO: Configurable value size limit while (true) { // Empty if (_bufferCount == 0) { Buffer(); } // End if (_bufferCount == 0) { return BuildWord(); } var c = _buffer[_bufferOffset++]; _bufferCount--; if (c == seperator) { return BuildWord(); } _builder.Append(c); } } private async Task ReadWordAsync(char seperator, CancellationToken cancellationToken) { // TODO: Configurable value size limit while (true) { // Empty if (_bufferCount == 0) { await BufferAsync(cancellationToken); } // End if (_bufferCount == 0) { return BuildWord(); } var c = _buffer[_bufferOffset++]; _bufferCount--; if (c == seperator) { return BuildWord(); } _builder.Append(c); } } // '+' 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); } private async Task BufferAsync(CancellationToken cancellationToken) { // TODO: StreamReader doesn't support cancellation? cancellationToken.ThrowIfCancellationRequested(); _bufferOffset = 0; _bufferCount = await _reader.ReadAsync(_buffer, 0, _buffer.Length); } /// /// Parses text from an HTTP form body. /// /// The HTTP form body to parse. /// The collection containing the parsed HTTP form body. public static IDictionary ReadForm(string text) { var reader = new FormReader(text); var accumulator = new KeyValueAccumulator(StringComparer.OrdinalIgnoreCase); var pair = reader.ReadNextPair(); while (pair.HasValue) { accumulator.Append(pair.Value.Key, pair.Value.Value); pair = reader.ReadNextPair(); } return accumulator.GetResults(); } /// /// Parses an HTTP form body. /// /// The HTTP form body to parse. /// The collection containing the parsed HTTP form body. public static Task> ReadFormAsync(Stream stream, CancellationToken cancellationToken = new CancellationToken()) { return ReadFormAsync(stream, Encoding.UTF8, cancellationToken); } /// /// Parses an HTTP form body. /// /// The HTTP form body to parse. /// The collection containing the parsed HTTP form body. public static async Task> ReadFormAsync(Stream stream, Encoding encoding, CancellationToken cancellationToken = new CancellationToken()) { var reader = new FormReader(stream, encoding); var accumulator = new KeyValueAccumulator(StringComparer.OrdinalIgnoreCase); var pair = await reader.ReadNextPairAsync(cancellationToken); while (pair.HasValue) { accumulator.Append(pair.Value.Key, pair.Value.Value); pair = await reader.ReadNextPairAsync(cancellationToken); } return accumulator.GetResults(); } } }