From 86903ddabfcf229711310a28853f3508ec03cf67 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Sun, 6 Jun 2021 17:23:38 -0400 Subject: [PATCH] Add HttpHeaders.NonValidated (#53555) This adds an HttpHeaders.NonValidated property, which returns a type that provides a non-validating / non-parsing / non-allocating view of headers in the collection. Querying the resulting collection does not force parsing or validation on the contents of the headers, handing back exactly the raw data that it contains; if a header doesn't contain a raw value but instead contains an already parsed value, a string representation of that header value(s) is returned. When using the strongly-typed members, querying and enumeration is allocation-free, unless strings need to be created to represent already parsed values. --- .../System.Net.Http/ref/System.Net.Http.cs | 48 ++++++ .../System.Net.Http/src/System.Net.Http.csproj | 2 + .../System/Net/Http/Headers/HeaderStringValues.cs | 130 ++++++++++++++ .../src/System/Net/Http/Headers/HeaderUtilities.cs | 2 +- .../src/System/Net/Http/Headers/HttpHeaders.cs | 189 +++++++++------------ .../Net/Http/Headers/HttpHeadersNonValidated.cs | 172 +++++++++++++++++++ .../src/System/Net/Http/MultipartContent.cs | 5 +- .../Net/Http/SocketsHttpHandler/Http2Connection.cs | 2 +- .../Http/SocketsHttpHandler/Http3RequestStream.cs | 2 +- .../Net/Http/SocketsHttpHandler/HttpConnection.cs | 2 +- .../tests/UnitTests/HPack/HPackRoundtripTests.cs | 2 +- .../tests/UnitTests/Headers/HttpHeadersTest.cs | 173 +++++++++++++++++-- .../UnitTests/Headers/HttpRequestHeadersTest.cs | 8 +- .../UnitTests/System.Net.Http.Unit.Tests.csproj | 4 + .../Net/WebSockets/WebSocketHandle.Managed.cs | 23 ++- 15 files changed, 616 insertions(+), 148 deletions(-) create mode 100644 src/libraries/System.Net.Http/src/System/Net/Http/Headers/HeaderStringValues.cs create mode 100644 src/libraries/System.Net.Http/src/System/Net/Http/Headers/HttpHeadersNonValidated.cs diff --git a/src/libraries/System.Net.Http/ref/System.Net.Http.cs b/src/libraries/System.Net.Http/ref/System.Net.Http.cs index 9fa9bdc..02486ab 100644 --- a/src/libraries/System.Net.Http/ref/System.Net.Http.cs +++ b/src/libraries/System.Net.Http/ref/System.Net.Http.cs @@ -4,6 +4,7 @@ // Changes to this file must follow the https://aka.ms/api-review process. // ------------------------------------------------------------------------------ +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; namespace System.Net.Http @@ -520,6 +521,26 @@ namespace System.Net.Http.Headers public override string ToString() { throw null; } public static bool TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? input, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Net.Http.Headers.EntityTagHeaderValue? parsedValue) { throw null; } } + public readonly partial struct HeaderStringValues : System.Collections.Generic.IEnumerable, System.Collections.Generic.IReadOnlyCollection, System.Collections.IEnumerable + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public int Count { get { throw null; } } + public System.Net.Http.Headers.HeaderStringValues.Enumerator GetEnumerator() { throw null; } + System.Collections.Generic.IEnumerator System.Collections.Generic.IEnumerable.GetEnumerator() { throw null; } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + public override string ToString() { throw null; } + public partial struct Enumerator : System.Collections.Generic.IEnumerator, System.Collections.IEnumerator, System.IDisposable + { + private object _dummy; + private int _dummyPrimitive; + public string Current { get { throw null; } } + object System.Collections.IEnumerator.Current { get { throw null; } } + public void Dispose() { } + public bool MoveNext() { throw null; } + void System.Collections.IEnumerator.Reset() { } + } + } public sealed partial class HttpContentHeaders : System.Net.Http.Headers.HttpHeaders { internal HttpContentHeaders() { } @@ -538,6 +559,7 @@ namespace System.Net.Http.Headers public abstract partial class HttpHeaders : System.Collections.Generic.IEnumerable>>, System.Collections.IEnumerable { protected HttpHeaders() { } + public System.Net.Http.Headers.HttpHeadersNonValidated NonValidated { get { throw null; } } public void Add(string name, System.Collections.Generic.IEnumerable values) { } public void Add(string name, string? value) { } public void Clear() { } @@ -551,6 +573,32 @@ namespace System.Net.Http.Headers public bool TryAddWithoutValidation(string name, string? value) { throw null; } public bool TryGetValues(string name, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Collections.Generic.IEnumerable? values) { throw null; } } + public readonly partial struct HttpHeadersNonValidated : System.Collections.Generic.IEnumerable>, System.Collections.Generic.IReadOnlyCollection>, System.Collections.Generic.IReadOnlyDictionary, System.Collections.IEnumerable + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public int Count { get { throw null; } } + public System.Net.Http.Headers.HeaderStringValues this[string headerName] { get { throw null; } } + System.Collections.Generic.IEnumerable System.Collections.Generic.IReadOnlyDictionary.Keys { get { throw null; } } + System.Collections.Generic.IEnumerable System.Collections.Generic.IReadOnlyDictionary.Values { get { throw null; } } + public bool Contains(string headerName) { throw null; } + bool System.Collections.Generic.IReadOnlyDictionary.ContainsKey(string key) { throw null; } + public System.Net.Http.Headers.HttpHeadersNonValidated.Enumerator GetEnumerator() { throw null; } + System.Collections.Generic.IEnumerator> System.Collections.Generic.IEnumerable>.GetEnumerator() { throw null; } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + public bool TryGetValues(string headerName, out System.Net.Http.Headers.HeaderStringValues values) { throw null; } + bool System.Collections.Generic.IReadOnlyDictionary.TryGetValue(string key, out System.Net.Http.Headers.HeaderStringValues value) { throw null; } + public partial struct Enumerator : System.Collections.Generic.IEnumerator>, System.Collections.IEnumerator, System.IDisposable + { + private object _dummy; + private int _dummyPrimitive; + public System.Collections.Generic.KeyValuePair Current { get { throw null; } } + object System.Collections.IEnumerator.Current { get { throw null; } } + public void Dispose() { } + public bool MoveNext() { throw null; } + void System.Collections.IEnumerator.Reset() { } + } + } public sealed partial class HttpHeaderValueCollection : System.Collections.Generic.ICollection, System.Collections.Generic.IEnumerable, System.Collections.IEnumerable where T : class { internal HttpHeaderValueCollection() { } diff --git a/src/libraries/System.Net.Http/src/System.Net.Http.csproj b/src/libraries/System.Net.Http/src/System.Net.Http.csproj index cf6c743..0ce5ec1 100644 --- a/src/libraries/System.Net.Http/src/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/src/System.Net.Http.csproj @@ -76,11 +76,13 @@ + + diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HeaderStringValues.cs b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HeaderStringValues.cs new file mode 100644 index 0000000..a313a23 --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HeaderStringValues.cs @@ -0,0 +1,130 @@ +// 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.Collections.Generic; + +namespace System.Net.Http.Headers +{ + /// Provides a collection of header string values. + public readonly struct HeaderStringValues : IReadOnlyCollection + { + /// The associated header. This is used only for producing a string from when it's an array. + private readonly HeaderDescriptor _header; + /// A string or string array (or null if the instance is default). + private readonly object _value; + + /// Initializes the instance. + /// The header descriptor associated with the header value. + /// The header value. + internal HeaderStringValues(HeaderDescriptor descriptor, string value) + { + _header = descriptor; + _value = value; + } + + /// Initializes the instance. + /// The header descriptor associated with the header values. + /// The header values. + internal HeaderStringValues(HeaderDescriptor descriptor, string[] values) + { + _header = descriptor; + _value = values; + } + + /// Gets the number of header values in the collection. + public int Count => _value switch + { + string => 1, + string[] values => values.Length, + _ => 0 + }; + + /// Gets a string containing all the headers in the collection. + /// + public override string ToString() => _value switch + { + string value => value, + string[] values => string.Join(_header.Parser is HttpHeaderParser parser && parser.SupportsMultipleValues ? parser.Separator : HttpHeaderParser.DefaultSeparator, values), + _ => string.Empty, + }; + + /// Gets an enumerator for all of the strings in the collection. + /// + public Enumerator GetEnumerator() => new Enumerator(_value); + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// Enumerates the elements of a . + public struct Enumerator : IEnumerator + { + /// If this wraps a string[], that array. Otherwise, null. + private readonly string[]? _values; + /// The current string header value. If this wraps a single string, that string. + private string? _current; + /// Current state of the iteration. + private int _index; + + /// Initializes the enumerator with a string or string[]. + /// The string or string[] value, or null if this collection is empty. + internal Enumerator(object value) + { + if (value is string s) + { + _values = null; + _current = s; + } + else + { + _values = value as string[]; + _current = null; + } + + _index = 0; + } + + /// + public bool MoveNext() + { + int index = _index; + if (index < 0) + { + return false; + } + + string[]? values = _values; + if (values != null) + { + if ((uint)index < (uint)values.Length) + { + _index = index + 1; + _current = values[index]; + return true; + } + + _index = -1; + return false; + } + + _index = -1; + return _current != null; + } + + /// + public string Current => _current!; + + /// + object IEnumerator.Current => Current; + + /// + public void Dispose() { } + + /// + void IEnumerator.Reset() => throw new NotSupportedException(); + } + } +} diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HeaderUtilities.cs b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HeaderUtilities.cs index d3802a1..4bd5db9 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HeaderUtilities.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HeaderUtilities.cs @@ -357,7 +357,7 @@ namespace System.Net.Http.Headers { if (headers[i] is HttpHeaders hh) { - foreach (KeyValuePair header in hh.EnumerateWithoutValidation()) + foreach (KeyValuePair header in hh.NonValidated) { foreach (string headerValue in header.Value) { diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HttpHeaders.cs b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HttpHeaders.cs index fcdee9f..6633329 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HttpHeaders.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HttpHeaders.cs @@ -5,6 +5,7 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; using System.Text; namespace System.Net.Http.Headers @@ -75,6 +76,9 @@ namespace System.Net.Http.Headers internal Dictionary? HeaderStore => _headerStore; + /// Gets a view of the contents of this headers collection that does not parse nor validate the data upon access. + public HttpHeadersNonValidated NonValidated => new HttpHeadersNonValidated(this); + public void Add(string name, string? value) => Add(GetHeaderDescriptor(name), value); internal void Add(HeaderDescriptor descriptor, string? value) @@ -226,7 +230,7 @@ namespace System.Net.Http.Headers { if (_headerStore != null && TryGetAndParseHeaderInfo(descriptor, out HeaderStoreItemInfo? info)) { - values = GetValuesAsStrings(descriptor, info); + values = GetStoreValuesAsStringArray(descriptor, info); return true; } @@ -246,59 +250,63 @@ namespace System.Net.Http.Headers public override string ToString() { - if (_headerStore == null || _headerStore.Count == 0) - { - return string.Empty; - } - // Return all headers as string similar to: // HeaderName1: Value1, Value2 // HeaderName2: Value1 // ... - var sb = new StringBuilder(); - foreach (KeyValuePair header in GetHeaderStrings()) - { - sb.Append(header.Key).Append(": ").AppendLine(header.Value); - } - return sb.ToString(); - } + var vsb = new ValueStringBuilder(stackalloc char[512]); - internal IEnumerable> GetHeaderStrings() - { - if (_headerStore == null) + if (_headerStore is Dictionary headerStore) { - yield break; - } + foreach (KeyValuePair header in headerStore) + { + vsb.Append(header.Key.Name); + vsb.Append(": "); - foreach (KeyValuePair header in _headerStore) - { - string stringValue = GetHeaderString(header.Key, header.Value); + GetStoreValuesAsStringOrStringArray(header.Key, header.Value, out string? singleValue, out string[]? multiValue); + Debug.Assert(singleValue is not null ^ multiValue is not null); + + if (singleValue is not null) + { + vsb.Append(singleValue); + } + else + { + // Note that if we get multiple values for a header that doesn't support multiple values, we'll + // just separate the values using a comma (default separator). + string? separator = header.Key.Parser is HttpHeaderParser parser && parser.SupportsMultipleValues ? parser.Separator : HttpHeaderParser.DefaultSeparator; - yield return new KeyValuePair(header.Key.Name, stringValue); + for (int i = 0; i < multiValue!.Length; i++) + { + if (i != 0) vsb.Append(separator); + vsb.Append(multiValue[i]); + } + } + + vsb.Append(Environment.NewLine); + } } + + return vsb.ToString(); } - internal string GetHeaderString(HeaderDescriptor descriptor, object? exclude = null) + internal string GetHeaderString(HeaderDescriptor descriptor) { if (TryGetHeaderValue(descriptor, out object? info)) { - string[] values = GetValuesAsStrings(descriptor, info, exclude); + GetStoreValuesAsStringOrStringArray(descriptor, info, out string? singleValue, out string[]? multiValue); + Debug.Assert(singleValue is not null ^ multiValue is not null); - if (values.Length == 1) + if (singleValue is not null) { - return values[0]; + return singleValue; } // Note that if we get multiple values for a header that doesn't support multiple values, we'll // just separate the values using a comma (default separator). - string? separator = HttpHeaderParser.DefaultSeparator; - if (descriptor.Parser != null && descriptor.Parser.SupportsMultipleValues) - { - separator = descriptor.Parser.Separator; - } - - return string.Join(separator, values); + string? separator = descriptor.Parser != null && descriptor.Parser.SupportsMultipleValues ? descriptor.Parser.Separator : HttpHeaderParser.DefaultSeparator; + return string.Join(separator, multiValue!); } return string.Empty; @@ -343,29 +351,12 @@ namespace System.Net.Http.Headers } else { - string[] values = GetValuesAsStrings(descriptor, info); + string[] values = GetStoreValuesAsStringArray(descriptor, info); yield return new KeyValuePair>(descriptor.Name, values); } } } - internal IEnumerable> EnumerateWithoutValidation() - { - if (_headerStore == null) - { - yield break; - } - - foreach (KeyValuePair header in _headerStore) - { - string[] values = TryGetHeaderValue(header.Key, out object? info) ? - GetValuesAsStrings(header.Key, info) : - Array.Empty(); - - yield return new KeyValuePair(header.Key.Name, values); - } - } - #endregion #region IEnumerable Members @@ -713,7 +704,7 @@ namespace System.Net.Http.Headers (_headerStore ??= new Dictionary()).Add(descriptor, value); } - private bool TryGetHeaderValue(HeaderDescriptor descriptor, [NotNullWhen(true)] out object? value) + internal bool TryGetHeaderValue(HeaderDescriptor descriptor, [NotNullWhen(true)] out object? value) { if (_headerStore == null) { @@ -1162,46 +1153,46 @@ namespace System.Net.Http.Headers return false; } - private static string[] GetValuesAsStrings(HeaderDescriptor descriptor, object value, object? exclude = null) + internal static string[] GetStoreValuesAsStringArray(HeaderDescriptor descriptor, HeaderStoreItemInfo info) { - HeaderStoreItemInfo? info = value as HeaderStoreItemInfo; + GetStoreValuesAsStringOrStringArray(descriptor, info, out string? singleValue, out string[]? multiValue); + Debug.Assert(singleValue is not null ^ multiValue is not null); + return multiValue ?? new[] { singleValue! }; + } + + internal static void GetStoreValuesAsStringOrStringArray(HeaderDescriptor descriptor, object sourceValues, out string? singleValue, out string[]? multiValue) + { + HeaderStoreItemInfo? info = sourceValues as HeaderStoreItemInfo; if (info is null) { - Debug.Assert(value is string); - return new string[1] { (string)value }; + Debug.Assert(sourceValues is string); + singleValue = (string)sourceValues; + multiValue = null; + return; } int length = GetValueCount(info); - string[] values; - if (length > 0) + Span values; + singleValue = null; + if (length == 1) { - values = new string[length]; - int currentIndex = 0; - - ReadStoreValues(values, info.RawValue, null, null, ref currentIndex); - ReadStoreValues(values, info.ParsedValue, descriptor.Parser, exclude, ref currentIndex); - - // Set parser parameter to 'null' for invalid values: The invalid values is always a string so we - // don't need the parser to "serialize" the value to a string. - ReadStoreValues(values, info.InvalidValue, null, null, ref currentIndex); - - // The values array may not be full because some values were excluded - if (currentIndex < length) - { - values = values.AsSpan(0, currentIndex).ToArray(); - } + multiValue = null; + values = MemoryMarshal.CreateSpan(ref singleValue, 1); } else { - values = Array.Empty(); + values = multiValue = length != 0 ? new string[length] : Array.Empty(); } - Debug.Assert(values != null); - return values; + int currentIndex = 0; + ReadStoreValues(values, info.RawValue, null, ref currentIndex); + ReadStoreValues(values, info.ParsedValue, descriptor.Parser, ref currentIndex); + ReadStoreValues(values, info.InvalidValue, null, ref currentIndex); + Debug.Assert(currentIndex == length); } - internal static int GetValuesAsStrings(HeaderDescriptor descriptor, object sourceValues, [NotNull] ref string[]? values) + internal static int GetStoreValuesIntoStringArray(HeaderDescriptor descriptor, object sourceValues, [NotNull] ref string[]? values) { values ??= Array.Empty(); @@ -1229,9 +1220,9 @@ namespace System.Net.Http.Headers } int currentIndex = 0; - ReadStoreValues(values, info.RawValue, null, null, ref currentIndex); - ReadStoreValues(values, info.ParsedValue, descriptor.Parser, null, ref currentIndex); - ReadStoreValues(values, info.InvalidValue, null, null, ref currentIndex); + ReadStoreValues(values, info.RawValue, null, ref currentIndex); + ReadStoreValues(values, info.ParsedValue, descriptor.Parser, ref currentIndex); + ReadStoreValues(values, info.InvalidValue, null, ref currentIndex); Debug.Assert(currentIndex == length); } @@ -1253,55 +1244,29 @@ namespace System.Net.Http.Headers 1; } - private static void ReadStoreValues(string?[] values, object? storeValue, HttpHeaderParser? parser, - T exclude, ref int currentIndex) + private static void ReadStoreValues(Span values, object? storeValue, HttpHeaderParser? parser, ref int currentIndex) { - Debug.Assert(values != null); - if (storeValue != null) { List? storeValues = storeValue as List; if (storeValues == null) { - if (ShouldAdd(storeValue, parser, exclude)) - { - values[currentIndex] = parser == null ? storeValue.ToString() : parser.ToString(storeValue); - currentIndex++; - } + values[currentIndex] = parser == null ? storeValue.ToString() : parser.ToString(storeValue); + currentIndex++; } else { foreach (object? item in storeValues) { - if (ShouldAdd(item, parser, exclude)) - { - Debug.Assert(item != null); - values[currentIndex] = parser == null ? item.ToString() : parser.ToString(item); - currentIndex++; - } + Debug.Assert(item != null); + values[currentIndex] = parser == null ? item.ToString() : parser.ToString(item); + currentIndex++; } } } } - private static bool ShouldAdd(object? storeValue, HttpHeaderParser? parser, T exclude) - { - bool add = true; - if (parser != null && exclude != null) - { - if (parser.Comparer != null) - { - add = !parser.Comparer.Equals(exclude, storeValue); - } - else - { - add = !exclude.Equals(storeValue); - } - } - return add; - } - private bool AreEqual(object value, object? storeValue, IEqualityComparer? comparer) { Debug.Assert(value != null); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HttpHeadersNonValidated.cs b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HttpHeadersNonValidated.cs new file mode 100644 index 0000000..ad1a185 --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/Headers/HttpHeadersNonValidated.cs @@ -0,0 +1,172 @@ +// 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.Collections.Generic; +using System.Diagnostics; + +namespace System.Net.Http.Headers +{ + /// Provides a view on top of a collection that avoids forcing validation or parsing on its contents. + /// + /// The view surfaces data as it's stored in the headers collection. Any header values that have not yet been parsed / validated won't be + /// as part of any accesses from this view, e.g. a raw header value of "one, two" that hasn't yet been parsed due to other operations + /// on the will be surfaced as a single header value rather than two. For any header values that have already + /// been parsed and validated, that value will be converted to a string to be returned from operations on this view. + /// + public readonly struct HttpHeadersNonValidated : IReadOnlyDictionary + { + /// The wrapped headers collection. + private readonly HttpHeaders? _headers; + + /// Initializes the view. + /// The wrapped headers collection. + internal HttpHeadersNonValidated(HttpHeaders headers) => _headers = headers; + + /// Gets the number of headers stored in the collection. + /// Multiple header values associated with the same header name are considered to be one header as far as this count is concerned. + public int Count => _headers?.HeaderStore?.Count ?? 0; + + /// Gets whether the collection contains the specified header. + /// The name of the header. + /// true if the collection contains the header; otherwise, false. + public bool Contains(string headerName) => + _headers is HttpHeaders headers && + HeaderDescriptor.TryGet(headerName, out HeaderDescriptor descriptor) && + headers.TryGetHeaderValue(descriptor, out _); + + /// Gets the values for the specified header name. + /// The name of the header. + /// The values for the specified header. + /// The header was not contained in the collection. + public HeaderStringValues this[string headerName] + { + get + { + if (TryGetValues(headerName, out HeaderStringValues values)) + { + return values; + } + + throw new KeyNotFoundException(SR.net_http_headers_not_found); + } + } + + /// + bool IReadOnlyDictionary.ContainsKey(string key) => Contains(key); + + /// Attempts to retrieve the values associated with the specified header name. + /// The name of the header. + /// The retrieved header values. + /// true if the collection contains the specified header; otherwise, false. + public bool TryGetValues(string headerName, out HeaderStringValues values) + { + if (_headers is HttpHeaders headers && + HeaderDescriptor.TryGet(headerName, out HeaderDescriptor descriptor) && + headers.TryGetHeaderValue(descriptor, out object? info)) + { + HttpHeaders.GetStoreValuesAsStringOrStringArray(descriptor, info, out string? singleValue, out string[]? multiValue); + Debug.Assert(singleValue is not null ^ multiValue is not null); + values = singleValue is not null ? + new HeaderStringValues(descriptor, singleValue) : + new HeaderStringValues(descriptor, multiValue!); + return true; + } + + values = default; + return false; + } + + /// + bool IReadOnlyDictionary.TryGetValue(string key, out HeaderStringValues value) => TryGetValues(key, out value); + + /// Gets an enumerator that iterates through the . + /// An enumerator that iterates through the . + public Enumerator GetEnumerator() => + _headers is HttpHeaders headers && headers.HeaderStore is Dictionary store ? + new Enumerator(store.GetEnumerator()) : + default; + + /// + IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + IEnumerable IReadOnlyDictionary.Keys + { + get + { + foreach (KeyValuePair header in this) + { + yield return header.Key; + } + } + } + + /// + IEnumerable IReadOnlyDictionary.Values + { + get + { + foreach (KeyValuePair header in this) + { + yield return header.Value; + } + } + } + + /// Enumerates the elements of a . + public struct Enumerator : IEnumerator> + { + /// The wrapped enumerator for the underlying headers dictionary. + private Dictionary.Enumerator _headerStoreEnumerator; + /// The current value. + private KeyValuePair _current; + /// true if the enumerator was constructed via the ctor; otherwise, false./ + private bool _valid; + + /// Initializes the enumerator. + /// The underlying dictionary enumerator. + internal Enumerator(Dictionary.Enumerator headerStoreEnumerator) + { + _headerStoreEnumerator = headerStoreEnumerator; + _current = default; + _valid = true; + } + + /// + public bool MoveNext() + { + if (_valid && _headerStoreEnumerator.MoveNext()) + { + KeyValuePair current = _headerStoreEnumerator.Current; + + HttpHeaders.GetStoreValuesAsStringOrStringArray(current.Key, current.Value, out string? singleValue, out string[]? multiValue); + Debug.Assert(singleValue is not null ^ multiValue is not null); + + _current = new KeyValuePair( + current.Key.Name, + singleValue is not null ? new HeaderStringValues(current.Key, singleValue) : new HeaderStringValues(current.Key, multiValue!)); + return true; + } + + _current = default; + return false; + } + + /// + public KeyValuePair Current => _current; + + /// + object IEnumerator.Current => _current; + + /// + public void Dispose() { } + + /// + void IEnumerator.Reset() => throw new NotSupportedException(); + } + } +} diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/MultipartContent.cs b/src/libraries/System.Net.Http/src/System/Net/Http/MultipartContent.cs index 8227b4b..4ac7a34 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/MultipartContent.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/MultipartContent.cs @@ -4,7 +4,6 @@ using System.Buffers; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Net.Http.Headers; using System.Text; @@ -333,7 +332,7 @@ namespace System.Net.Http } // Add headers. - foreach (KeyValuePair> headerPair in content.Headers) + foreach (KeyValuePair headerPair in content.Headers.NonValidated) { Encoding headerValueEncoding = HeaderEncodingSelector?.Invoke(headerPair.Key, content) ?? HttpRuleParser.DefaultHttpEncoding; @@ -388,7 +387,7 @@ namespace System.Net.Http foreach (HttpContent content in _nestedContent) { // Headers. - foreach (KeyValuePair> headerPair in content.Headers) + foreach (KeyValuePair headerPair in content.Headers.NonValidated) { currentLength += headerPair.Key.Length + ColonSpaceLength; diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs index 703a340..c60370d 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs @@ -1136,7 +1136,7 @@ namespace System.Net.Http ref string[]? tmpHeaderValuesArray = ref t_headerValues; foreach (KeyValuePair header in headers.HeaderStore) { - int headerValuesCount = HttpHeaders.GetValuesAsStrings(header.Key, header.Value, ref tmpHeaderValuesArray); + int headerValuesCount = HttpHeaders.GetStoreValuesIntoStringArray(header.Key, header.Value, ref tmpHeaderValuesArray); Debug.Assert(headerValuesCount > 0, "No values for header??"); ReadOnlySpan headerValues = tmpHeaderValuesArray.AsSpan(0, headerValuesCount); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs index 970890d..3013d49 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http3RequestStream.cs @@ -587,7 +587,7 @@ namespace System.Net.Http foreach (KeyValuePair header in headers.HeaderStore) { - int headerValuesCount = HttpHeaders.GetValuesAsStrings(header.Key, header.Value, ref _headerValues); + int headerValuesCount = HttpHeaders.GetStoreValuesIntoStringArray(header.Key, header.Value, ref _headerValues); Debug.Assert(headerValuesCount > 0, "No values for header??"); ReadOnlySpan headerValues = _headerValues.AsSpan(0, headerValuesCount); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs index 01640d1..00e3f3b 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs @@ -263,7 +263,7 @@ namespace System.Net.Http await WriteTwoBytesAsync((byte)':', (byte)' ', async).ConfigureAwait(false); } - int headerValuesCount = HttpHeaders.GetValuesAsStrings(header.Key, header.Value, ref _headerValues); + int headerValuesCount = HttpHeaders.GetStoreValuesIntoStringArray(header.Key, header.Value, ref _headerValues); Debug.Assert(headerValuesCount > 0, "No values for header??"); if (headerValuesCount > 0) { diff --git a/src/libraries/System.Net.Http/tests/UnitTests/HPack/HPackRoundtripTests.cs b/src/libraries/System.Net.Http/tests/UnitTests/HPack/HPackRoundtripTests.cs index 03f801e..7ddee86 100644 --- a/src/libraries/System.Net.Http/tests/UnitTests/HPack/HPackRoundtripTests.cs +++ b/src/libraries/System.Net.Http/tests/UnitTests/HPack/HPackRoundtripTests.cs @@ -62,7 +62,7 @@ namespace System.Net.Http.Unit.Tests.HPack foreach (KeyValuePair header in headers.HeaderStore) { - int headerValuesCount = HttpHeaders.GetValuesAsStrings(header.Key, header.Value, ref headerValues); + int headerValuesCount = HttpHeaders.GetStoreValuesIntoStringArray(header.Key, header.Value, ref headerValues); Assert.InRange(headerValuesCount, 0, int.MaxValue); ReadOnlySpan headerValuesSpan = headerValues.AsSpan(0, headerValuesCount); diff --git a/src/libraries/System.Net.Http/tests/UnitTests/Headers/HttpHeadersTest.cs b/src/libraries/System.Net.Http/tests/UnitTests/Headers/HttpHeadersTest.cs index 58dd95c..2a25ea7 100644 --- a/src/libraries/System.Net.Http/tests/UnitTests/Headers/HttpHeadersTest.cs +++ b/src/libraries/System.Net.Http/tests/UnitTests/Headers/HttpHeadersTest.cs @@ -1,12 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Collections; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Net.Http.Headers; -using System.Text; +using System.Tests; using Xunit; @@ -1421,7 +1421,17 @@ namespace System.Net.Http.Tests } [Fact] - public void GetHeaderStrings_SetValidAndInvalidHeaderValues_AllHeaderValuesReturned() + public void NonValidated_Default_Empty() + { + HttpHeadersNonValidated v = default; + Assert.Equal(0, v.Count); + Assert.Empty(v); + Assert.False(v.TryGetValues("Host", out HeaderStringValues values)); + Assert.Empty(values); + } + + [Fact] + public void NonValidated_SetValidAndInvalidHeaderValues_AllHeaderValuesReturned() { MockHeaderParser parser = new MockHeaderParser("---"); MockHeaders headers = new MockHeaders(parser); @@ -1431,16 +1441,25 @@ namespace System.Net.Http.Tests headers.TryAddWithoutValidation(headers.Descriptor, "value2,value3"); headers.TryAddWithoutValidation(headers.Descriptor, invalidHeaderValue); - foreach (var header in headers.GetHeaderStrings()) + string expectedValue = "value2,value3---" + invalidHeaderValue + "---" + parsedPrefix + "1"; + + Assert.Equal(1, headers.NonValidated.Count); + + int iterations = 0; + foreach (KeyValuePair header in headers.NonValidated) { - Assert.Equal(headers.Descriptor.Name, header.Key); // Note that raw values don't get parsed but just added to the result. - Assert.Equal("value2,value3---" + invalidHeaderValue + "---" + parsedPrefix + "1", header.Value); + iterations++; + Assert.Equal(headers.Descriptor.Name, header.Key); + Assert.Equal(3, header.Value.Count); + Assert.Equal(expectedValue, header.Value.ToString()); } + + Assert.Equal(1, iterations); } [Fact] - public void GetHeaderStrings_SetMultipleHeaders_AllHeaderValuesReturned() + public void NonValidated_SetMultipleHeaders_AllHeaderValuesReturned() { MockHeaderParser parser = new MockHeaderParser(true); MockHeaders headers = new MockHeaders(parser); @@ -1456,17 +1475,17 @@ namespace System.Net.Http.Tests string[] expectedHeaderValues = { parsedPrefix + "1", "value2", "", "value41, value42" }; int i = 0; - foreach (var header in headers.GetHeaderStrings()) + foreach (KeyValuePair header in headers.NonValidated) { Assert.NotEqual(expectedHeaderNames.Length, i); Assert.Equal(expectedHeaderNames[i], header.Key); - Assert.Equal(expectedHeaderValues[i], header.Value); + Assert.Equal(expectedHeaderValues[i], header.Value.ToString()); i++; } } [Fact] - public void GetHeaderStrings_SetMultipleValuesOnSingleValueHeader_AllHeaderValuesReturned() + public void NonValidated_SetMultipleValuesOnSingleValueHeader_AllHeaderValuesReturned() { MockHeaderParser parser = new MockHeaderParser(false); MockHeaders headers = new MockHeaders(parser); @@ -1474,11 +1493,75 @@ namespace System.Net.Http.Tests headers.TryAddWithoutValidation(headers.Descriptor, "value1"); headers.TryAddWithoutValidation(headers.Descriptor, rawPrefix); - foreach (var header in headers.GetHeaderStrings()) + foreach (KeyValuePair header in headers.NonValidated) { Assert.Equal(headers.Descriptor.Name, header.Key); // Note that the added rawPrefix did not get parsed - Assert.Equal("value1, " + rawPrefix, header.Value); + Assert.Equal("value1, " + rawPrefix, header.Value.ToString()); + } + } + + [ActiveIssue("https://github.com/dotnet/runtime/issues/53647", TestPlatforms.Browser)] + [Fact] + public void NonValidated_ValidAndInvalidValues_DictionaryMembersWork() + { + var headers = new HttpResponseHeaders(); + IReadOnlyDictionary nonValidated = headers.NonValidated; + + Assert.True(headers.TryAddWithoutValidation("Location", "http:/invalidLocation")); + Assert.True(headers.TryAddWithoutValidation("Location", "http:/anotherLocation")); + Assert.True(headers.TryAddWithoutValidation("Date", "not a date")); + + Assert.Equal(2, nonValidated.Count); + + Assert.True(nonValidated.ContainsKey("Location")); + Assert.True(nonValidated.ContainsKey("Date")); + + Assert.False(nonValidated.ContainsKey("Age")); + Assert.False(nonValidated.TryGetValue("Age", out _)); + Assert.Throws(() => nonValidated["Age"]); + + Assert.True(nonValidated.TryGetValue("Location", out HeaderStringValues locations)); + Assert.Equal(2, locations.Count); + Assert.Equal(new[] { "http:/invalidLocation", "http:/anotherLocation" }, locations.ToArray()); + Assert.Equal("http:/invalidLocation, http:/anotherLocation", locations.ToString()); + + Assert.True(nonValidated.TryGetValue("Date", out HeaderStringValues dates)); + Assert.Equal(1, dates.Count); + Assert.Equal(new[] { "not a date" }, dates.ToArray()); + Assert.Equal("not a date", dates.ToString()); + + dates = nonValidated["Date"]; + Assert.Equal(1, dates.Count); + Assert.Equal(new[] { "not a date" }, dates.ToArray()); + Assert.Equal("not a date", dates.ToString()); + + Assert.Equal(new HashSet { "Location", "Date" }, nonValidated.Keys.ToHashSet()); + } + + [ActiveIssue("https://github.com/dotnet/runtime/issues/53647", TestPlatforms.Browser)] + [Fact] + public void NonValidated_ValidInvalidAndRaw_AllReturned() + { + var headers = new HttpResponseHeaders(); + IReadOnlyDictionary nonValidated = headers.NonValidated; + + // Parsed value + headers.Date = new DateTimeOffset(1, 2, 3, 4, 5, 6, TimeSpan.Zero); + + // Invalid value + headers.TryAddWithoutValidation("Date", "not a date"); + foreach (KeyValuePair> _ in headers) { } + + // Raw value + headers.TryAddWithoutValidation("Date", "another not a date"); + + // All three show up + Assert.Equal(1, nonValidated.Count); + Assert.Equal(3, nonValidated["Date"].Count); + using (new ThreadCultureChange(new CultureInfo("en-US"))) + { + Assert.Equal(new HashSet { "not a date", "another not a date", "Sat, 03 Feb 0001 04:05:06 GMT" }, nonValidated["Date"].ToHashSet()); } } @@ -1562,7 +1645,7 @@ namespace System.Net.Http.Tests { MockHeaders headers = new MockHeaders(); - var enumerator = headers.GetEnumerator(); + IEnumerator>> enumerator = headers.GetEnumerator(); Assert.False(enumerator.MoveNext()); } @@ -1577,7 +1660,7 @@ namespace System.Net.Http.Tests // The value added with TryAddWithoutValidation() wasn't parsed yet. Assert.Equal(1, headers.Parser.TryParseValueCallCount); - var enumerator = headers.GetEnumerator(); + IEnumerator>> enumerator = headers.GetEnumerator(); // Getting the enumerator doesn't trigger parsing. Assert.Equal(1, headers.Parser.TryParseValueCallCount); @@ -1610,7 +1693,7 @@ namespace System.Net.Http.Tests headers.Add(customHeaderName, string.Empty); headers.Add(headers.Descriptor, string.Empty); - var enumerator = headers.GetEnumerator(); + IEnumerator>> enumerator = headers.GetEnumerator(); Assert.True(enumerator.MoveNext()); Assert.Equal(customHeaderName, enumerator.Current.Key); @@ -1631,7 +1714,7 @@ namespace System.Net.Http.Tests System.Collections.IEnumerable headersAsIEnumerable = headers; - var enumerator = headersAsIEnumerable.GetEnumerator(); + IEnumerator enumerator = headersAsIEnumerable.GetEnumerator(); KeyValuePair> currentValue; @@ -2048,6 +2131,64 @@ namespace System.Net.Http.Tests Assert.False(destination.Contains("custom"), "destination contains 'custom' header."); } + [Fact] + public void HeaderStringValues_Default_Empty() + { + HeaderStringValues v = default; + Assert.Equal(0, v.Count); + Assert.Empty(v); + Assert.Equal(string.Empty, v.ToString()); + } + + [Fact] + public void HeaderStringValues_Constructed_ProducesExpectedResults() + { + // 0 strings + foreach (HeaderStringValues hsv in new[] { new HeaderStringValues(KnownHeaders.Accept.Descriptor, Array.Empty()) }) + { + Assert.Equal(0, hsv.Count); + + HeaderStringValues.Enumerator e = hsv.GetEnumerator(); + + Assert.False(e.MoveNext()); + + Assert.Equal(string.Empty, hsv.ToString()); + } + + // 1 string + foreach (HeaderStringValues hsv in new[] { new HeaderStringValues(KnownHeaders.Accept.Descriptor, "hello"), new HeaderStringValues(KnownHeaders.Accept.Descriptor, new[] { "hello" }) }) + { + Assert.Equal(1, hsv.Count); + + HeaderStringValues.Enumerator e = hsv.GetEnumerator(); + + Assert.True(e.MoveNext()); + Assert.Equal("hello", e.Current); + + Assert.False(e.MoveNext()); + + Assert.Equal("hello", hsv.ToString()); + } + + // 2 strings + foreach (HeaderStringValues hsv in new[] { new HeaderStringValues(KnownHeaders.Accept.Descriptor, new[] { "hello", "world" }) }) + { + Assert.Equal(2, hsv.Count); + + HeaderStringValues.Enumerator e = hsv.GetEnumerator(); + + Assert.True(e.MoveNext()); + Assert.Equal("hello", e.Current); + + Assert.True(e.MoveNext()); + Assert.Equal("world", e.Current); + + Assert.False(e.MoveNext()); + + Assert.Equal("hello, world", hsv.ToString()); + } + } + public static IEnumerable GetInvalidHeaderNames() { yield return new object[] { "invalid header" }; diff --git a/src/libraries/System.Net.Http/tests/UnitTests/Headers/HttpRequestHeadersTest.cs b/src/libraries/System.Net.Http/tests/UnitTests/Headers/HttpRequestHeadersTest.cs index b5df472..4ce88a8 100644 --- a/src/libraries/System.Net.Http/tests/UnitTests/Headers/HttpRequestHeadersTest.cs +++ b/src/libraries/System.Net.Http/tests/UnitTests/Headers/HttpRequestHeadersTest.cs @@ -151,10 +151,10 @@ namespace System.Net.Http.Tests headers.Add("Accept-Charset", "utf-8"); headers.AcceptCharset.Add(new StringWithQualityHeaderValue("iso-8859-5", 0.5)); - foreach (var header in headers.GetHeaderStrings()) + foreach (var header in headers.NonValidated) { Assert.Equal("Accept-Charset", header.Key); - Assert.Equal("utf-8, iso-8859-5; q=0.5, invalid value", header.Value); + Assert.Equal("utf-8, iso-8859-5; q=0.5, invalid value", header.Value.ToString()); } } @@ -656,10 +656,10 @@ namespace System.Net.Http.Tests headers.Add("User-Agent", "custom2/1.1"); headers.UserAgent.Add(new ProductInfoHeaderValue("(comment)")); - foreach (var header in headers.GetHeaderStrings()) + foreach (var header in headers.NonValidated) { Assert.Equal("User-Agent", header.Key); - Assert.Equal("custom2/1.1 (comment) custom\u4F1A", header.Value); + Assert.Equal("custom2/1.1 (comment) custom\u4F1A", header.Value.ToString()); } } diff --git a/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj b/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj index 3b78905..186f1cb 100644 --- a/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj +++ b/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj @@ -106,6 +106,8 @@ Link="ProductionCode\System\Net\Http\Headers\GenericHeaderParser.cs" /> + + ? values)) + if (headers.NonValidated.TryGetValues(name, out HeaderStringValues hsv)) { - throw new WebSocketException(WebSocketError.Faulted, SR.Format(SR.net_WebSockets_MissingResponseHeader, name)); - } + if (hsv.Count == 1) + { + foreach (string value in hsv) + { + if (string.Equals(value, expectedValue, StringComparison.OrdinalIgnoreCase)) + { + return; + } + break; + } + } - Debug.Assert(values is string[]); - string[] array = (string[])values; - if (array.Length != 1 || !string.Equals(array[0], expectedValue, StringComparison.OrdinalIgnoreCase)) - { - throw new WebSocketException(WebSocketError.HeaderError, SR.Format(SR.net_WebSockets_InvalidResponseHeader, name, string.Join(", ", array))); + throw new WebSocketException(WebSocketError.HeaderError, SR.Format(SR.net_WebSockets_InvalidResponseHeader, name, hsv)); } + + throw new WebSocketException(WebSocketError.Faulted, SR.Format(SR.net_WebSockets_MissingResponseHeader, name)); } /// Used as a sentinel to indicate that ClientWebSocket should use the system's default proxy. -- 2.7.4