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 9fa9bdc34287e..02486ab68b11b 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 @@ public EntityTagHeaderValue(string tag, bool isWeak) { } 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 @@ internal HttpContentHeaders() { } 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 @@ public void Clear() { } 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 cf6c743ce044b..0ce5ec1aa171e 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 0000000000000..a313a2306e78f --- /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 d3802a1571fce..4bd5db94fa64f 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 @@ internal static void DumpHeaders(StringBuilder sb, params HttpHeaders?[] 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 fcdee9f7c516a..6633329a68a4d 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.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; using System.Text; namespace System.Net.Http.Headers @@ -75,6 +76,9 @@ internal HttpHeaders(HttpHeaderType allowedHeaderTypes, HttpHeaderType treatAsCu 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 @@ internal bool TryGetValues(HeaderDescriptor descriptor, [NotNullWhen(true)] out { if (_headerStore != null && TryGetAndParseHeaderInfo(descriptor, out HeaderStoreItemInfo? info)) { - values = GetValuesAsStrings(descriptor, info); + values = GetStoreValuesAsStringArray(descriptor, info); return true; } @@ -246,59 +250,63 @@ internal bool Contains(HeaderDescriptor descriptor) 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 @@ private IEnumerator>> GetEnumeratorCore } 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 @@ private void AddHeaderToStore(HeaderDescriptor descriptor, object value) (_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 @@ private static bool ContainsInvalidNewLine(string value, string name) 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 @@ internal static int GetValuesAsStrings(HeaderDescriptor descriptor, object sourc } 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 @@ static int Count(object? valueStore) => 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 0000000000000..ad1a1850ea83c --- /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 8227b4bdfdad6..4ac7a34a12695 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 @@ private void SerializeHeadersToStream(Stream stream, HttpContent content, bool w } // 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 @@ protected internal override bool TryComputeLength(out long length) 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 703a340d5128e..c60370d6d8052 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 @@ private void WriteHeaderCollection(HttpRequestMessage request, HttpHeaders heade 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 970890ddcd9fb..3013d49cfbe29 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 @@ private void BufferHeaderCollection(HttpHeaders headers) 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 01640d1369290..00e3f3b6fe322 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 @@ private async ValueTask WriteHeadersAsync(HttpHeaders headers, string? cookiesFr 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 03f801e39b26a..7ddee86b7f125 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 @@ private static Memory HPackEncode(HttpHeaders headers, Encoding? valueEnco 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 58dd95c97bcfc..2a25ea7196191 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 @@ public void GetParsedValues_GetParsedValuesForKnownHeaderWithInvalidNewlineChars } [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 @@ public void GetHeaderStrings_SetValidAndInvalidHeaderValues_AllHeaderValuesRetur 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 @@ public void GetHeaderStrings_SetMultipleHeaders_AllHeaderValuesReturned() 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 @@ public void GetHeaderStrings_SetMultipleValuesOnSingleValueHeader_AllHeaderValue 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 @@ public void GetEnumerator_GetEnumeratorFromUninitializedHeaderStore_ReturnsEmpty { MockHeaders headers = new MockHeaders(); - var enumerator = headers.GetEnumerator(); + IEnumerator>> enumerator = headers.GetEnumerator(); Assert.False(enumerator.MoveNext()); } @@ -1577,7 +1660,7 @@ public void GetEnumerator_FirstHeaderWithOneValueSecondHeaderWithTwoValues_Enume // 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 @@ public void GetEnumerator_FirstCustomHeaderWithEmptyValueSecondKnownHeaderWithEm 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 @@ public void GetEnumerator_UseExplicitInterfaceImplementation_EnumeratorReturnsNo System.Collections.IEnumerable headersAsIEnumerable = headers; - var enumerator = headersAsIEnumerable.GetEnumerator(); + IEnumerator enumerator = headersAsIEnumerable.GetEnumerator(); KeyValuePair> currentValue; @@ -2048,6 +2131,64 @@ public void AddHeaders_SourceHasInvalidHeaderValues_InvalidHeadersRemovedFromSou 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 b5df4722a1620..4ce88a86089b8 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 @@ public void AcceptCharset_AddMultipleValuesAndGetValueString_AllValuesAddedUsing 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 @@ public void UserAgent_AddMultipleValuesAndGetValueString_AllValuesAddedUsingTheC 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 3b78905590051..186f1cbd88808 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" /> + + CreateSecKeyAndSecWebSocketAccept() private static void ValidateHeader(HttpHeaders headers, string name, string expectedValue) { - if (!headers.TryGetValues(name, out IEnumerable? 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.