From 59b44a4c245885ee910e3db14ec5041db80ae44d Mon Sep 17 00:00:00 2001 From: Chris R Date: Tue, 25 Aug 2015 16:32:24 -0700 Subject: [PATCH] Move *CommaSeperatedValues APIs from IHeaderDictionary to extension. --- .../IHeaderDictionary.cs | 29 - .../HeaderDictionaryExtensions.cs | 52 ++ .../ParsingHelpers.cs | 618 ++++++++++++++++++ src/Microsoft.AspNet.Http/HeaderDictionary.cs | 42 -- src/Microsoft.AspNet.Http/ParsingHelpers.cs | 79 --- 5 files changed, 670 insertions(+), 150 deletions(-) create mode 100644 src/Microsoft.AspNet.Http.Extensions/HeaderDictionaryExtensions.cs create mode 100644 src/Microsoft.AspNet.Http.Extensions/ParsingHelpers.cs diff --git a/src/Microsoft.AspNet.Http.Abstractions/IHeaderDictionary.cs b/src/Microsoft.AspNet.Http.Abstractions/IHeaderDictionary.cs index d2192475fa..36137ed45c 100644 --- a/src/Microsoft.AspNet.Http.Abstractions/IHeaderDictionary.cs +++ b/src/Microsoft.AspNet.Http.Abstractions/IHeaderDictionary.cs @@ -30,34 +30,5 @@ namespace Microsoft.AspNet.Http /// Gets a collection containing the keys. /// new ICollection Keys { get; } - - /// - /// Get the associated values from the collection separated into individual values. - /// Quoted values will not be split, and the quotes will be removed. - /// - /// The header name. - /// the associated values from the collection separated into individual values, or null if the key is not present. - StringValues GetCommaSeparatedValues(string key); - - /// - /// Add a new value. Appends to the header list if already present - /// - /// The header name. - /// The header value. - void Append(string key, StringValues value); - - /// - /// Quotes any values containing comas, and then coma joins all of the values with any existing values. - /// - /// The header name. - /// The header values. - void AppendCommaSeparatedValues(string key, params string[] values); - - /// - /// Quotes any values containing comas, and then coma joins all of the values. - /// - /// The header name. - /// The header values. - void SetCommaSeparatedValues(string key, params string[] values); } } diff --git a/src/Microsoft.AspNet.Http.Extensions/HeaderDictionaryExtensions.cs b/src/Microsoft.AspNet.Http.Extensions/HeaderDictionaryExtensions.cs new file mode 100644 index 0000000000..c986a530c0 --- /dev/null +++ b/src/Microsoft.AspNet.Http.Extensions/HeaderDictionaryExtensions.cs @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Http.Internal; +using Microsoft.Framework.Primitives; + +namespace Microsoft.AspNet.Http +{ + public static class HeaderDictionaryExtensions + { + /// + /// Add new values. Each item remains a separate array entry. + /// + /// The header name. + /// The header value. + public static void Append(this IHeaderDictionary headers, string key, StringValues value) + { + ParsingHelpers.AppendHeaderUnmodified(headers, key, value); + } + + /// + /// Quotes any values containing comas, and then coma joins all of the values with any existing values. + /// + /// The header name. + /// The header values. + public static void AppendCommaSeparatedValues(this IHeaderDictionary headers, string key, params string[] values) + { + ParsingHelpers.AppendHeaderJoined(headers, key, values); + } + + /// + /// Get the associated values from the collection separated into individual values. + /// Quoted values will not be split, and the quotes will be removed. + /// + /// The header name. + /// the associated values from the collection separated into individual values, or StringValues.Empty if the key is not present. + public static string[] GetCommaSeparatedValues(this IHeaderDictionary headers, string key) + { + return ParsingHelpers.GetHeaderSplit(headers, key); + } + + /// + /// Quotes any values containing comas, and then coma joins all of the values. + /// + /// The header name. + /// The header values. + public static void SetCommaSeparatedValues(this IHeaderDictionary headers, string key, params string[] values) + { + ParsingHelpers.SetHeaderJoined(headers, key, values); + } + } +} diff --git a/src/Microsoft.AspNet.Http.Extensions/ParsingHelpers.cs b/src/Microsoft.AspNet.Http.Extensions/ParsingHelpers.cs new file mode 100644 index 0000000000..fcce4dd404 --- /dev/null +++ b/src/Microsoft.AspNet.Http.Extensions/ParsingHelpers.cs @@ -0,0 +1,618 @@ +// Copyright (c) .NET Foundation. 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; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Framework.Internal; +using Microsoft.Framework.Primitives; + +namespace Microsoft.AspNet.Http.Internal +{ + internal struct HeaderSegment : IEquatable + { + private readonly StringSegment _formatting; + private readonly StringSegment _data; + + // + // Initializes a new instance of the class. + // + public HeaderSegment(StringSegment formatting, StringSegment data) + { + _formatting = formatting; + _data = data; + } + + public StringSegment Formatting + { + get { return _formatting; } + } + + public StringSegment Data + { + get { return _data; } + } + + #region Equality members + + public bool Equals(HeaderSegment other) + { + return _formatting.Equals(other._formatting) && _data.Equals(other._data); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + return obj is HeaderSegment && Equals((HeaderSegment)obj); + } + + public override int GetHashCode() + { + unchecked + { + return (_formatting.GetHashCode() * 397) ^ _data.GetHashCode(); + } + } + + public static bool operator ==(HeaderSegment left, HeaderSegment right) + { + return left.Equals(right); + } + + public static bool operator !=(HeaderSegment left, HeaderSegment right) + { + return !left.Equals(right); + } + + #endregion + } + + [System.CodeDom.Compiler.GeneratedCode("App_Packages", "")] + internal struct HeaderSegmentCollection : IEnumerable, IEquatable + { + private readonly StringValues _headers; + + public HeaderSegmentCollection(StringValues headers) + { + _headers = headers; + } + + #region Equality members + + public bool Equals(HeaderSegmentCollection other) + { + return Equals(_headers, other._headers); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + return obj is HeaderSegmentCollection && Equals((HeaderSegmentCollection)obj); + } + + public override int GetHashCode() + { + return (!StringValues.IsNullOrEmpty(_headers) ? _headers.GetHashCode() : 0); + } + + public static bool operator ==(HeaderSegmentCollection left, HeaderSegmentCollection right) + { + return left.Equals(right); + } + + public static bool operator !=(HeaderSegmentCollection left, HeaderSegmentCollection right) + { + return !left.Equals(right); + } + + #endregion + + public Enumerator GetEnumerator() + { + return new Enumerator(_headers); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + internal struct Enumerator : IEnumerator + { + private readonly StringValues _headers; + private int _index; + + private string _header; + private int _headerLength; + private int _offset; + + private int _leadingStart; + private int _leadingEnd; + private int _valueStart; + private int _valueEnd; + private int _trailingStart; + + private Mode _mode; + + public Enumerator(StringValues headers) + { + _headers = headers; + _header = string.Empty; + _headerLength = -1; + _index = -1; + _offset = -1; + _leadingStart = -1; + _leadingEnd = -1; + _valueStart = -1; + _valueEnd = -1; + _trailingStart = -1; + _mode = Mode.Leading; + } + + private enum Mode + { + Leading, + Value, + ValueQuoted, + Trailing, + Produce, + } + + private enum Attr + { + Value, + Quote, + Delimiter, + Whitespace + } + + public HeaderSegment Current + { + get + { + return new HeaderSegment( + new StringSegment(_header, _leadingStart, _leadingEnd - _leadingStart), + new StringSegment(_header, _valueStart, _valueEnd - _valueStart)); + } + } + + object IEnumerator.Current + { + get { return Current; } + } + + public void Dispose() + { + } + + public bool MoveNext() + { + while (true) + { + if (_mode == Mode.Produce) + { + _leadingStart = _trailingStart; + _leadingEnd = -1; + _valueStart = -1; + _valueEnd = -1; + _trailingStart = -1; + + if (_offset == _headerLength && + _leadingStart != -1 && + _leadingStart != _offset) + { + // Also produce trailing whitespace + _leadingEnd = _offset; + return true; + } + _mode = Mode.Leading; + } + + // if end of a string + if (_offset == _headerLength) + { + ++_index; + _offset = -1; + _leadingStart = 0; + _leadingEnd = -1; + _valueStart = -1; + _valueEnd = -1; + _trailingStart = -1; + + // if that was the last string + if (_index == _headers.Count) + { + // no more move nexts + return false; + } + + // grab the next string + _header = _headers[_index] ?? string.Empty; + _headerLength = _header.Length; + } + while (true) + { + ++_offset; + char ch = _offset == _headerLength ? (char)0 : _header[_offset]; + // todo - array of attrs + Attr attr = char.IsWhiteSpace(ch) ? Attr.Whitespace : ch == '\"' ? Attr.Quote : (ch == ',' || ch == (char)0) ? Attr.Delimiter : Attr.Value; + + switch (_mode) + { + case Mode.Leading: + switch (attr) + { + case Attr.Delimiter: + _leadingEnd = _offset; + _mode = Mode.Produce; + break; + case Attr.Quote: + _leadingEnd = _offset; + _valueStart = _offset; + _mode = Mode.ValueQuoted; + break; + case Attr.Value: + _leadingEnd = _offset; + _valueStart = _offset; + _mode = Mode.Value; + break; + case Attr.Whitespace: + // more + break; + } + break; + case Mode.Value: + switch (attr) + { + case Attr.Quote: + _mode = Mode.ValueQuoted; + break; + case Attr.Delimiter: + _valueEnd = _offset; + _trailingStart = _offset; + _mode = Mode.Produce; + break; + case Attr.Value: + // more + break; + case Attr.Whitespace: + _valueEnd = _offset; + _trailingStart = _offset; + _mode = Mode.Trailing; + break; + } + break; + case Mode.ValueQuoted: + switch (attr) + { + case Attr.Quote: + _mode = Mode.Value; + break; + case Attr.Delimiter: + if (ch == (char)0) + { + _valueEnd = _offset; + _trailingStart = _offset; + _mode = Mode.Produce; + } + break; + case Attr.Value: + case Attr.Whitespace: + // more + break; + } + break; + case Mode.Trailing: + switch (attr) + { + case Attr.Delimiter: + _mode = Mode.Produce; + break; + case Attr.Quote: + // back into value + _trailingStart = -1; + _valueEnd = -1; + _mode = Mode.ValueQuoted; + break; + case Attr.Value: + // back into value + _trailingStart = -1; + _valueEnd = -1; + _mode = Mode.Value; + break; + case Attr.Whitespace: + // more + break; + } + break; + } + if (_mode == Mode.Produce) + { + return true; + } + } + } + } + + public void Reset() + { + _index = 0; + _offset = 0; + _leadingStart = 0; + _leadingEnd = 0; + _valueStart = 0; + _valueEnd = 0; + } + } + } + + [System.CodeDom.Compiler.GeneratedCode("App_Packages", "")] + internal struct StringSegment : IEquatable + { + private readonly string _buffer; + private readonly int _offset; + private readonly int _count; + + // + // Initializes a new instance of the class. + // + public StringSegment(string buffer, int offset, int count) + { + _buffer = buffer; + _offset = offset; + _count = count; + } + + public string Buffer + { + get { return _buffer; } + } + + public int Offset + { + get { return _offset; } + } + + public int Count + { + get { return _count; } + } + + public string Value + { + get { return _offset == -1 ? null : _buffer.Substring(_offset, _count); } + } + + public bool HasValue + { + get { return _offset != -1 && _count != 0 && _buffer != null; } + } + + #region Equality members + + public bool Equals(StringSegment other) + { + return string.Equals(_buffer, other._buffer) && _offset == other._offset && _count == other._count; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + return obj is StringSegment && Equals((StringSegment)obj); + } + + public override int GetHashCode() + { + unchecked + { + int hashCode = (_buffer != null ? _buffer.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ _offset; + hashCode = (hashCode * 397) ^ _count; + return hashCode; + } + } + + public static bool operator ==(StringSegment left, StringSegment right) + { + return left.Equals(right); + } + + public static bool operator !=(StringSegment left, StringSegment right) + { + return !left.Equals(right); + } + + #endregion + + public bool StartsWith([NotNull] string text, StringComparison comparisonType) + { + int textLength = text.Length; + if (!HasValue || _count < textLength) + { + return false; + } + + return string.Compare(_buffer, _offset, text, 0, textLength, comparisonType) == 0; + } + + public bool EndsWith([NotNull] string text, StringComparison comparisonType) + { + int textLength = text.Length; + if (!HasValue || _count < textLength) + { + return false; + } + + return string.Compare(_buffer, _offset + _count - textLength, text, 0, textLength, comparisonType) == 0; + } + + public bool Equals([NotNull] string text, StringComparison comparisonType) + { + int textLength = text.Length; + if (!HasValue || _count != textLength) + { + return false; + } + + return string.Compare(_buffer, _offset, text, 0, textLength, comparisonType) == 0; + } + + public string Substring(int offset, int length) + { + return _buffer.Substring(_offset + offset, length); + } + + public StringSegment Subsegment(int offset, int length) + { + return new StringSegment(_buffer, _offset + offset, length); + } + + public override string ToString() + { + return Value ?? string.Empty; + } + } + + internal static class ParsingHelpers + { + public static StringValues GetHeader(IDictionary headers, string key) + { + StringValues value; + return headers.TryGetValue(key, out value) ? value : StringValues.Empty; + } + + public static StringValues GetHeaderSplit(IDictionary headers, string key) + { + var values = GetHeaderUnmodified(headers, key); + return new StringValues(GetHeaderSplitImplementation(values).ToArray()); + } + + private static IEnumerable GetHeaderSplitImplementation(StringValues values) + { + foreach (var segment in new HeaderSegmentCollection(values)) + { + if (segment.Data.HasValue) + { + yield return DeQuote(segment.Data.Value); + } + } + } + + public static StringValues GetHeaderUnmodified([NotNull] IDictionary headers, string key) + { + StringValues values; + return headers.TryGetValue(key, out values) ? values : StringValues.Empty; + } + + public static void SetHeaderJoined([NotNull] IDictionary headers, [NotNull] string key, StringValues value) + { + if (string.IsNullOrWhiteSpace(key)) + { + throw new ArgumentNullException(nameof(key)); + } + if (StringValues.IsNullOrEmpty(value)) + { + headers.Remove(key); + } + else + { + headers[key] = string.Join(",", value.Select(QuoteIfNeeded)); + } + } + + // Quote items that contain comas and are not already quoted. + private static string QuoteIfNeeded(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + // Ignore + } + else if (value.Contains(',')) + { + if (value[0] != '"' || value[value.Length - 1] != '"') + { + value = '"' + value + '"'; + } + } + + return value; + } + + private static string DeQuote(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + // Ignore + } + else if (value.Length > 1 && value[0] == '"' && value[value.Length - 1] == '"') + { + value = value.Substring(1, value.Length - 2); + } + + return value; + } + + public static void SetHeaderUnmodified([NotNull] IDictionary headers, [NotNull] string key, StringValues? values) + { + if (string.IsNullOrWhiteSpace(key)) + { + throw new ArgumentNullException(nameof(key)); + } + if (!values.HasValue || StringValues.IsNullOrEmpty(values.Value)) + { + headers.Remove(key); + } + else + { + headers[key] = values.Value; + } + } + + public static void AppendHeaderJoined([NotNull] IDictionary headers, [NotNull] string key, params string[] values) + { + if (values == null || values.Length == 0) + { + return; + } + + string existing = GetHeader(headers, key); + if (existing == null) + { + SetHeaderJoined(headers, key, values); + } + else + { + headers[key] = existing + "," + string.Join(",", values.Select(value => QuoteIfNeeded(value))); + } + } + + public static void AppendHeaderUnmodified([NotNull] IDictionary headers, [NotNull] string key, StringValues values) + { + if (values.Count == 0) + { + return; + } + + var existing = GetHeaderUnmodified(headers, key); + SetHeaderUnmodified(headers, key, StringValues.Concat(existing, values)); + } + } +} diff --git a/src/Microsoft.AspNet.Http/HeaderDictionary.cs b/src/Microsoft.AspNet.Http/HeaderDictionary.cs index d7cb70c481..05d5d7fdf4 100644 --- a/src/Microsoft.AspNet.Http/HeaderDictionary.cs +++ b/src/Microsoft.AspNet.Http/HeaderDictionary.cs @@ -104,48 +104,6 @@ namespace Microsoft.AspNet.Http.Internal return Store.GetEnumerator(); } - - /// - /// Get the associated values from the collection separated into individual values. - /// Quoted values will not be split, and the quotes will be removed. - /// - /// The header name. - /// the associated values from the collection separated into individual values, or StringValues.Empty if the key is not present. - public StringValues GetCommaSeparatedValues(string key) - { - return ParsingHelpers.GetHeaderSplit(Store, key); - } - - /// - /// Add new values. Each item remains a separate array entry. - /// - /// The header name. - /// The header value. - public void Append(string key, StringValues value) - { - ParsingHelpers.AppendHeaderUnmodified(Store, key, value); - } - - /// - /// Quotes any values containing comas, and then coma joins all of the values with any existing values. - /// - /// The header name. - /// The header values. - public void AppendCommaSeparatedValues(string key, params string[] values) - { - ParsingHelpers.AppendHeaderJoined(Store, key, values); - } - - /// - /// Quotes any values containing comas, and then coma joins all of the values. - /// - /// The header name. - /// The header values. - public void SetCommaSeparatedValues(string key, params string[] values) - { - ParsingHelpers.SetHeaderJoined(Store, key, values); - } - /// /// Adds the given header and values to the collection. /// diff --git a/src/Microsoft.AspNet.Http/ParsingHelpers.cs b/src/Microsoft.AspNet.Http/ParsingHelpers.cs index bc373b4094..54455b90b3 100644 --- a/src/Microsoft.AspNet.Http/ParsingHelpers.cs +++ b/src/Microsoft.AspNet.Http/ParsingHelpers.cs @@ -540,40 +540,6 @@ namespace Microsoft.AspNet.Http.Internal } } - public static void SetHeaderJoined([NotNull] IDictionary headers, [NotNull] string key, StringValues value) - { - if (string.IsNullOrWhiteSpace(key)) - { - throw new ArgumentNullException(nameof(key)); - } - if (StringValues.IsNullOrEmpty(value)) - { - headers.Remove(key); - } - else - { - headers[key] = string.Join(",", value.Select(QuoteIfNeeded)); - } - } - - // Quote items that contain comas and are not already quoted. - private static string QuoteIfNeeded(string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - // Ignore - } - else if (value.Contains(',')) - { - if (value[0] != '"' || value[value.Length - 1] != '"') - { - value = '"' + value + '"'; - } - } - - return value; - } - private static string DeQuote(string value) { if (string.IsNullOrWhiteSpace(value)) @@ -588,51 +554,6 @@ namespace Microsoft.AspNet.Http.Internal return value; } - public static void SetHeaderUnmodified([NotNull] IDictionary headers, [NotNull] string key, StringValues? values) - { - if (string.IsNullOrWhiteSpace(key)) - { - throw new ArgumentNullException(nameof(key)); - } - if (!values.HasValue || StringValues.IsNullOrEmpty(values.Value)) - { - headers.Remove(key); - } - else - { - headers[key] = values.Value; - } - } - - public static void AppendHeaderJoined([NotNull] IDictionary headers, [NotNull] string key, params string[] values) - { - if (values == null || values.Length == 0) - { - return; - } - - string existing = GetHeader(headers, key); - if (existing == null) - { - SetHeaderJoined(headers, key, values); - } - else - { - headers[key] = existing + "," + string.Join(",", values.Select(value => QuoteIfNeeded(value))); - } - } - - public static void AppendHeaderUnmodified([NotNull] IDictionary headers, [NotNull] string key, StringValues values) - { - if (values.Count == 0) - { - return; - } - - var existing = GetHeaderUnmodified(headers, key); - SetHeaderUnmodified(headers, key, StringValues.Concat(existing, values)); - } - public static long? GetContentLength([NotNull] IHeaderDictionary headers) { const NumberStyles styles = NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite;