|
// 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;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
namespace System.Net.Http.Headers
{
/// <summary>
/// Key/value pairs of headers. The value is either a raw <see cref="string"/> or a <see cref="HttpHeaders.HeaderStoreItemInfo"/>.
/// We're using a custom type instead of <see cref="KeyValuePair{TKey, TValue}"/> because we need ref access to fields.
/// </summary>
internal struct HeaderEntry
{
public HeaderDescriptor Key;
public object Value;
public HeaderEntry(HeaderDescriptor key, object value)
{
Key = key;
Value = value;
}
}
public abstract class HttpHeaders : IEnumerable<KeyValuePair<string, IEnumerable<string>>>
{
// This type is used to store a collection of headers in 'headerStore':
// - A header can have multiple values.
// - A header can have an associated parser which is able to parse the raw string value into a strongly typed object.
// - If a header has an associated parser and the provided raw value can't be parsed, the value is considered
// invalid. Invalid values are stored if added using TryAddWithoutValidation(). If the value was added using Add(),
// Add() will throw FormatException.
// - Since parsing header values is expensive and users usually only care about a few headers, header values are
// lazily initialized.
//
// Given the properties above, a header value can have three states:
// - 'raw': The header value was added using TryAddWithoutValidation() and it wasn't parsed yet.
// - 'parsed': The header value was successfully parsed. It was either added using Add() where the value was parsed
// immediately, or if added using TryAddWithoutValidation() a user already accessed a property/method triggering the
// value to be parsed.
// - 'invalid': The header value was parsed, but parsing failed because the value is invalid. Storing invalid values
// allows users to still retrieve the value (by calling GetValues()), but it will not be exposed as strongly typed
// object. E.g. the client receives a response with the following header: 'Via: 1.1 proxy, invalid'
// - HttpHeaders.GetValues() will return "1.1 proxy", "invalid"
// - HttpResponseHeaders.Via collection will only contain one ViaHeaderValue object with value "1.1 proxy"
/// <summary>Either a <see cref="HeaderEntry"/> array or a Dictionary<<see cref="HeaderDescriptor"/>, <see cref="object"/>> </summary>
private object? _headerStore;
private int _count;
private readonly HttpHeaderType _allowedHeaderTypes;
private readonly HttpHeaderType _treatAsCustomHeaderTypes;
protected HttpHeaders()
: this(HttpHeaderType.All, HttpHeaderType.None)
{
}
internal HttpHeaders(HttpHeaderType allowedHeaderTypes, HttpHeaderType treatAsCustomHeaderTypes)
{
// Should be no overlap
Debug.Assert((allowedHeaderTypes & treatAsCustomHeaderTypes) == 0);
_allowedHeaderTypes = allowedHeaderTypes & ~HttpHeaderType.NonTrailing;
_treatAsCustomHeaderTypes = treatAsCustomHeaderTypes & ~HttpHeaderType.NonTrailing;
}
/// <summary>Gets a view of the contents of this headers collection that does not parse nor validate the data upon access.</summary>
public HttpHeadersNonValidated NonValidated => new HttpHeadersNonValidated(this);
public void Add(string name, string? value) => Add(GetHeaderDescriptor(name), value);
internal void Add(HeaderDescriptor descriptor, string? value)
{
// We don't use GetOrCreateHeaderInfo() here, since this would create a new header in the store. If parsing
// the value then throws, we would have to remove the header from the store again. So just get a
// HeaderStoreItemInfo object and try to parse the value. If it works, we'll add the header.
PrepareHeaderInfoForAdd(descriptor, out HeaderStoreItemInfo info, out bool addToStore);
ParseAndAddValue(descriptor, info, value);
// If we get here, then the value could be parsed correctly. If we created a new HeaderStoreItemInfo, add
// it to the store if we added at least one value.
if (addToStore && (info.ParsedAndInvalidValues != null))
{
info.AssertContainsNoInvalidValues();
Debug.Assert(!Contains(descriptor));
AddEntryToStore(new HeaderEntry(descriptor, info));
}
}
public void Add(string name, IEnumerable<string?> values) => Add(GetHeaderDescriptor(name), values);
internal void Add(HeaderDescriptor descriptor, IEnumerable<string?> values)
{
ArgumentNullException.ThrowIfNull(values);
PrepareHeaderInfoForAdd(descriptor, out HeaderStoreItemInfo info, out bool addToStore);
try
{
// Note that if the first couple of values are valid followed by an invalid value, the valid values
// will be added to the store before the exception for the invalid value is thrown.
foreach (string? value in values)
{
ParseAndAddValue(descriptor, info, value);
}
}
finally
{
// Even if one of the values was invalid, make sure we add the header for the valid ones. We need to be
// consistent here: If values get added to an _existing_ header, then all values until the invalid one
// get added. Same here: If multiple values get added to a _new_ header, make sure the header gets added
// with the valid values.
// However, if all values for a _new_ header were invalid, then don't add the header.
if (addToStore && (info.ParsedAndInvalidValues != null))
{
info.AssertContainsNoInvalidValues();
Debug.Assert(!Contains(descriptor));
AddEntryToStore(new HeaderEntry(descriptor, info));
}
}
}
public bool TryAddWithoutValidation(string name, string? value) =>
TryGetHeaderDescriptor(name, out HeaderDescriptor descriptor) &&
TryAddWithoutValidation(descriptor, value);
internal bool TryAddWithoutValidation(HeaderDescriptor descriptor, string? value)
{
// Normalize null values to be empty values, which are allowed. If the user adds multiple
// null/empty values, all of them are added to the collection. This will result in delimiter-only
// values, e.g. adding two null-strings (or empty, or whitespace-only) results in "My-Header: ,".
value ??= string.Empty;
ref object? storeValueRef = ref GetValueRefOrAddDefault(descriptor);
object? currentValue = storeValueRef;
if (currentValue is null)
{
storeValueRef = value;
}
else
{
if (currentValue is not HeaderStoreItemInfo info)
{
// The header store contained a single raw string value, so promote it
// to being a HeaderStoreItemInfo and add to it.
Debug.Assert(currentValue is string);
storeValueRef = info = new HeaderStoreItemInfo() { RawValue = currentValue };
}
AddRawValue(info, value);
}
return true;
}
public bool TryAddWithoutValidation(string name, IEnumerable<string?> values) =>
TryGetHeaderDescriptor(name, out HeaderDescriptor descriptor) &&
TryAddWithoutValidation(descriptor, values);
internal bool TryAddWithoutValidation(HeaderDescriptor descriptor, IEnumerable<string?> values)
{
ArgumentNullException.ThrowIfNull(values);
if (values is IList<string?> valuesList)
{
int count = valuesList.Count;
if (count > 0)
{
// The store value is either a string (a single unparsed value) or a HeaderStoreItemInfo.
// The RawValue on HeaderStoreItemInfo can likewise be either a single string or a List<string>.
ref object? storeValueRef = ref GetValueRefOrAddDefault(descriptor);
object? storeValue = storeValueRef;
// If the storeValue was already set or we're adding more than 1 value,
// we'll have to store the values in a List<string> on HeaderStoreItemInfo.
if (storeValue is not null || count > 1)
{
if (storeValue is not HeaderStoreItemInfo info)
{
storeValueRef = info = new HeaderStoreItemInfo { RawValue = storeValue };
}
object? rawValue = info.RawValue;
if (rawValue is not List<string> rawValues)
{
info.RawValue = rawValues = new List<string>();
if (rawValue != null)
{
rawValues.EnsureCapacity(count + 1);
rawValues.Add((string)rawValue);
}
}
rawValues.EnsureCapacity(rawValues.Count + count);
for (int i = 0; i < count; i++)
{
rawValues.Add(valuesList[i] ?? string.Empty);
}
}
else
{
// We're adding a single value to a new header entry. We can store the unparsed value as-is.
storeValueRef = valuesList[0] ?? string.Empty;
}
}
}
else
{
foreach (string? value in values)
{
TryAddWithoutValidation(descriptor, value ?? string.Empty);
}
}
return true;
}
public IEnumerable<string> GetValues(string name) => GetValues(GetHeaderDescriptor(name));
internal IEnumerable<string> GetValues(HeaderDescriptor descriptor)
{
if (TryGetValues(descriptor, out IEnumerable<string>? values))
{
return values;
}
throw new InvalidOperationException(SR.net_http_headers_not_found);
}
public bool TryGetValues(string name, [NotNullWhen(true)] out IEnumerable<string>? values)
{
if (TryGetHeaderDescriptor(name, out HeaderDescriptor descriptor))
{
return TryGetValues(descriptor, out values);
}
values = null;
return false;
}
internal bool TryGetValues(HeaderDescriptor descriptor, [NotNullWhen(true)] out IEnumerable<string>? values)
{
if (TryGetAndParseHeaderInfo(descriptor, out HeaderStoreItemInfo? info))
{
values = GetStoreValuesAsStringArray(descriptor, info);
return true;
}
values = null;
return false;
}
public bool Contains(string name) => Contains(GetHeaderDescriptor(name));
public override string ToString()
{
// Return all headers as string similar to:
// HeaderName1: Value1, Value2
// HeaderName2: Value1
// ...
var vsb = new ValueStringBuilder(stackalloc char[512]);
foreach (HeaderEntry entry in GetEntries())
{
vsb.Append(entry.Key.Name);
vsb.Append(": ");
GetStoreValuesAsStringOrStringArray(entry.Key, entry.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 = entry.Key.Separator;
Debug.Assert(multiValue is not null && multiValue.Length > 0);
vsb.Append(multiValue[0]);
for (int i = 1; i < multiValue.Length; i++)
{
vsb.Append(separator);
vsb.Append(multiValue[i]);
}
}
vsb.Append(Environment.NewLine);
}
return vsb.ToString();
}
internal string GetHeaderString(HeaderDescriptor descriptor)
{
if (TryGetHeaderValue(descriptor, out object? info))
{
GetStoreValuesAsStringOrStringArray(descriptor, info, out string? singleValue, out string[]? multiValue);
Debug.Assert(singleValue is not null ^ multiValue is not null);
if (singleValue is not null)
{
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).
return string.Join(descriptor.Separator, multiValue!);
}
return string.Empty;
}
#region IEnumerable<KeyValuePair<string, IEnumerable<string>>> Members
public IEnumerator<KeyValuePair<string, IEnumerable<string>>> GetEnumerator() => _count == 0 ?
((IEnumerable<KeyValuePair<string, IEnumerable<string>>>)Array.Empty<KeyValuePair<string, IEnumerable<string>>>()).GetEnumerator() :
GetEnumeratorCore();
private IEnumerator<KeyValuePair<string, IEnumerable<string>>> GetEnumeratorCore()
{
HeaderEntry[]? entries = GetEntriesArray();
Debug.Assert(_count != 0 && entries is not null, "Caller should have validated the collection is not empty");
for (int i = 0; i < _count; i++)
{
HeaderEntry entry = entries[i];
if (entry.Value is not HeaderStoreItemInfo info)
{
// To retain consistent semantics, we need to upgrade a raw string to a HeaderStoreItemInfo
// during enumeration so that we can parse the raw value in order to a) return
// the correct set of parsed values, and b) update the instance for subsequent enumerations
// to reflect that parsing.
#nullable disable // https://github.com/dotnet/roslyn/issues/73928
ref object storeValueRef = ref EntriesAreLiveView
? ref entries[i].Value
: ref CollectionsMarshal.GetValueRefOrNullRef((Dictionary<HeaderDescriptor, object>)_headerStore, entry.Key);
info = ReplaceWithHeaderStoreItemInfo(ref storeValueRef, entry.Value);
#nullable restore
}
// Make sure we parse all raw values before returning the result. Note that this has to be
// done before we calculate the array length (next line): A raw value may contain a list of
// values.
ParseRawHeaderValues(entry.Key, info);
string[] values = GetStoreValuesAsStringArray(entry.Key, info);
yield return new KeyValuePair<string, IEnumerable<string>>(entry.Key.Name, values);
}
}
#endregion
#region IEnumerable Members
Collections.IEnumerator Collections.IEnumerable.GetEnumerator() => GetEnumerator();
#endregion
internal void AddParsedValue(HeaderDescriptor descriptor, object value)
{
Debug.Assert(value != null);
Debug.Assert(descriptor.Parser != null, "Can't add parsed value if there is no parser available.");
HeaderStoreItemInfo info = GetOrCreateHeaderInfo(descriptor);
// If the current header has only one value, we can't add another value. The strongly typed property
// must not call AddParsedValue(), but SetParsedValue(). E.g. for headers like 'Date', 'Host'.
Debug.Assert(descriptor.Parser.SupportsMultipleValues, $"Header '{descriptor.Name}' doesn't support multiple values");
AddParsedValue(info, value);
}
internal void SetParsedValue(HeaderDescriptor descriptor, object value)
{
Debug.Assert(value != null);
Debug.Assert(descriptor.Parser != null, "Can't add parsed value if there is no parser available.");
// This method will first clear all values. This is used e.g. when setting the 'Date' or 'Host' header.
// i.e. headers not supporting collections.
HeaderStoreItemInfo info = GetOrCreateHeaderInfo(descriptor);
info.ParsedAndInvalidValues = null;
info.RawValue = null;
AddParsedValue(info, value);
}
internal void SetOrRemoveParsedValue(HeaderDescriptor descriptor, object? value)
{
if (value == null)
{
Remove(descriptor);
}
else
{
SetParsedValue(descriptor, value);
}
}
public bool Remove(string name) => Remove(GetHeaderDescriptor(name));
internal bool RemoveParsedValue(HeaderDescriptor descriptor, object value)
{
Debug.Assert(value != null);
// If we have a value for this header, then verify if we have a single value. If so, compare that
// value with 'item'. If we have a list of values, then remove 'item' from the list.
if (TryGetAndParseHeaderInfo(descriptor, out HeaderStoreItemInfo? info))
{
Debug.Assert(descriptor.Parser != null, "Can't add parsed value if there is no parser available.");
Debug.Assert(descriptor.Parser.SupportsMultipleValues,
"This method should not be used for single-value headers. Use Remove(string) instead.");
// If there is no entry, just return.
var parsedValue = info.ParsedAndInvalidValues;
if (parsedValue == null)
{
return false;
}
bool result = false;
IEqualityComparer? comparer = descriptor.Parser.Comparer;
List<object>? parsedValues = parsedValue as List<object>;
if (parsedValues == null)
{
if (parsedValue is not InvalidValue)
{
Debug.Assert(parsedValue.GetType() == value.GetType(),
"Stored value does not have the same type as 'value'.");
if (AreEqual(value, parsedValue, comparer))
{
info.ParsedAndInvalidValues = null;
result = true;
}
}
}
else
{
foreach (object item in parsedValues)
{
if (item is not InvalidValue)
{
Debug.Assert(item.GetType() == value.GetType(),
"One of the stored values does not have the same type as 'value'.");
if (AreEqual(value, item, comparer))
{
// Remove 'item' rather than 'value', since the 'comparer' may consider two values
// equal even though the default obj.Equals() may not (e.g. if 'comparer' does
// case-insensitive comparison for strings, but string.Equals() is case-sensitive).
result = parsedValues.Remove(item);
break;
}
}
}
// If we removed the last item in a list, remove the list.
if (parsedValues.Count == 0)
{
info.AssertContainsNoInvalidValues();
info.ParsedAndInvalidValues = null;
}
}
// If there is no value for the header left, remove the header.
if (info.IsEmpty)
{
bool headerRemoved = Remove(descriptor);
Debug.Assert(headerRemoved, $"Existing header '{descriptor.Name}' couldn't be removed.");
}
return result;
}
return false;
}
internal bool ContainsParsedValue(HeaderDescriptor descriptor, object value)
{
Debug.Assert(value != null);
// If we have a value for this header, then verify if we have a single value. If so, compare that
// value with 'item'. If we have a list of values, then compare each item in the list with 'item'.
if (TryGetAndParseHeaderInfo(descriptor, out HeaderStoreItemInfo? info))
{
Debug.Assert(descriptor.Parser != null, "Can't add parsed value if there is no parser available.");
Debug.Assert(descriptor.Parser.SupportsMultipleValues,
"This method should not be used for single-value headers. Use equality comparer instead.");
// If there is no entry, just return.
var parsedValue = info.ParsedAndInvalidValues;
if (parsedValue == null)
{
return false;
}
List<object>? parsedValues = parsedValue as List<object>;
IEqualityComparer? comparer = descriptor.Parser.Comparer;
if (parsedValues == null)
{
if (parsedValue is not InvalidValue)
{
Debug.Assert(parsedValue.GetType() == value.GetType(),
"Stored value does not have the same type as 'value'.");
return AreEqual(value, parsedValue, comparer);
}
}
else
{
foreach (object item in parsedValues)
{
if (item is not InvalidValue)
{
Debug.Assert(item.GetType() == value.GetType(),
"One of the stored values does not have the same type as 'value'.");
if (AreEqual(value, item, comparer))
{
return true;
}
}
}
return false;
}
}
return false;
}
internal virtual void AddHeaders(HttpHeaders sourceHeaders)
{
Debug.Assert(sourceHeaders != null);
Debug.Assert(GetType() == sourceHeaders.GetType(), "Can only copy headers from an instance of the same type.");
// Only add header values if they're not already set on the message. Note that we don't merge
// collections: If both the default headers and the message have set some values for a certain
// header, then we don't try to merge the values.
if (_count == 0 && sourceHeaders._headerStore is HeaderEntry[] sourceEntries)
{
// If the target collection is empty, we don't have to search for existing values
_count = sourceHeaders._count;
if (_headerStore is not HeaderEntry[] entries || entries.Length < _count)
{
entries = new HeaderEntry[sourceEntries.Length];
_headerStore = entries;
}
for (int i = 0; i < _count && i < sourceEntries.Length; i++)
{
HeaderEntry entry = sourceEntries[i];
if (entry.Value is HeaderStoreItemInfo info)
{
entry.Value = CloneHeaderInfo(entry.Key, info);
}
entries[i] = entry;
}
}
else
{
foreach (HeaderEntry entry in sourceHeaders.GetEntries())
{
ref object? storeValueRef = ref GetValueRefOrAddDefault(entry.Key);
if (storeValueRef is null)
{
object sourceValue = entry.Value;
if (sourceValue is HeaderStoreItemInfo info)
{
storeValueRef = CloneHeaderInfo(entry.Key, info);
}
else
{
Debug.Assert(sourceValue is string);
storeValueRef = sourceValue;
}
}
}
}
}
private static HeaderStoreItemInfo CloneHeaderInfo(HeaderDescriptor descriptor, HeaderStoreItemInfo sourceInfo)
{
lock (sourceInfo)
{
var destinationInfo = new HeaderStoreItemInfo
{
// Always copy raw values
RawValue = CloneStringHeaderInfoValues(sourceInfo.RawValue)
};
if (descriptor.Parser == null)
{
sourceInfo.AssertContainsNoInvalidValues();
destinationInfo.ParsedAndInvalidValues = CloneStringHeaderInfoValues(sourceInfo.ParsedAndInvalidValues);
}
else
{
// We have a parser, so we also have to clone invalid values and parsed values.
if (sourceInfo.ParsedAndInvalidValues != null)
{
List<object>? sourceValues = sourceInfo.ParsedAndInvalidValues as List<object>;
if (sourceValues == null)
{
CloneAndAddValue(destinationInfo, sourceInfo.ParsedAndInvalidValues);
}
else
{
foreach (object item in sourceValues)
{
CloneAndAddValue(destinationInfo, item);
}
}
}
}
return destinationInfo;
}
}
private static void CloneAndAddValue(HeaderStoreItemInfo destinationInfo, object source)
{
// We only have one value. Clone it and assign it to the store.
if (source is ICloneable cloneableValue)
{
Debug.Assert(source is not InvalidValue);
AddParsedValue(destinationInfo, cloneableValue.Clone());
}
else
{
// If it doesn't implement ICloneable, it's a value type or an immutable type like String/Uri.
AddParsedValue(destinationInfo, source);
}
}
[return: NotNullIfNotNull(nameof(source))]
private static object? CloneStringHeaderInfoValues(object? source)
{
if (source == null)
{
return null;
}
List<object>? sourceValues = source as List<object>;
if (sourceValues == null)
{
// If we just have one value, return the reference to the string (strings are immutable so it's OK
// to use the reference).
return source;
}
else
{
// If we have a list of strings, create a new list and copy all strings to the new list.
return new List<object>(sourceValues);
}
}
private HeaderStoreItemInfo GetOrCreateHeaderInfo(HeaderDescriptor descriptor)
{
if (TryGetAndParseHeaderInfo(descriptor, out HeaderStoreItemInfo? info))
{
return info;
}
else
{
return CreateAndAddHeaderToStore(descriptor);
}
}
private HeaderStoreItemInfo CreateAndAddHeaderToStore(HeaderDescriptor descriptor)
{
Debug.Assert(!Contains(descriptor));
// If we don't have the header in the store yet, add it now.
HeaderStoreItemInfo result = new HeaderStoreItemInfo();
// If the descriptor header type is in _treatAsCustomHeaderTypes, it must be converted to a custom header before calling this method
Debug.Assert((descriptor.HeaderType & _treatAsCustomHeaderTypes) == 0);
AddEntryToStore(new HeaderEntry(descriptor, result));
return result;
}
internal bool TryGetHeaderValue(HeaderDescriptor descriptor, [NotNullWhen(true)] out object? value)
{
ref object storeValueRef = ref GetValueRefOrNullRef(descriptor);
if (Unsafe.IsNullRef(ref storeValueRef))
{
value = null;
return false;
}
else
{
value = storeValueRef;
return true;
}
}
private bool TryGetAndParseHeaderInfo(HeaderDescriptor key, [NotNullWhen(true)] out HeaderStoreItemInfo? info)
{
ref object storeValueRef = ref GetValueRefOrNullRef(key);
if (!Unsafe.IsNullRef(ref storeValueRef))
{
object value = storeValueRef;
info = value is HeaderStoreItemInfo hsi
? hsi
: ReplaceWithHeaderStoreItemInfo(ref storeValueRef, value);
ParseRawHeaderValues(key, info);
return true;
}
info = null;
return false;
}
/// <summary>
/// Replaces <paramref name="storeValueRef"/> with a new <see cref="HeaderStoreItemInfo"/>,
/// or returns the existing <see cref="HeaderStoreItemInfo"/> if a different thread beat us to it.
/// </summary>
/// <remarks>
/// This helper should be used any time we're upgrading a storage slot from an unparsed string to a HeaderStoreItemInfo *while reading*.
/// Concurrent writes to the header collection are UB, so we don't need to worry about race conditions when doing the replacement there.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static HeaderStoreItemInfo ReplaceWithHeaderStoreItemInfo(ref object storeValueRef, object value)
{
Debug.Assert(value is string);
var info = new HeaderStoreItemInfo() { RawValue = value };
object previousValue = Interlocked.CompareExchange(ref storeValueRef, info, value);
if (ReferenceEquals(previousValue, value))
{
return info;
}
// Rare race condition: Another thread replaced the value with a HeaderStoreItemInfo.
return (HeaderStoreItemInfo)previousValue;
}
private static void ParseRawHeaderValues(HeaderDescriptor descriptor, HeaderStoreItemInfo info)
{
// Unlike TryGetHeaderInfo() this method tries to parse all non-validated header values (if any)
// before returning to the caller.
lock (info)
{
Debug.Assert(!info.IsEmpty);
if (info.RawValue != null)
{
if (info.RawValue is List<string> rawValues)
{
foreach (string rawValue in rawValues)
{
ParseSingleRawHeaderValue(info, descriptor, rawValue);
}
}
else
{
string? rawValue = info.RawValue as string;
Debug.Assert(rawValue is not null);
ParseSingleRawHeaderValue(info, descriptor, rawValue);
}
// At this point all values are either in info.ParsedValue, info.InvalidValue. Reset RawValue.
Debug.Assert(info.ParsedAndInvalidValues is not null);
info.RawValue = null;
}
}
}
private static void ParseSingleRawHeaderValue(HeaderStoreItemInfo info, HeaderDescriptor descriptor, string rawValue)
{
Debug.Assert(Monitor.IsEntered(info));
if (descriptor.Parser == null)
{
if (HttpRuleParser.ContainsNewLine(rawValue))
{
if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(null, SR.Format(SR.net_http_log_headers_no_newlines, descriptor.Name, rawValue));
AddInvalidValue(info, rawValue);
}
else
{
AddParsedValue(info, rawValue);
}
}
else
{
if (!TryParseAndAddRawHeaderValue(descriptor, info, rawValue, true))
{
if (NetEventSource.Log.IsEnabled()) NetEventSource.Log.HeadersInvalidValue(descriptor.Name, rawValue);
}
}
}
// See Add(name, string)
internal bool TryParseAndAddValue(HeaderDescriptor descriptor, string? value)
{
// We don't use GetOrCreateHeaderInfo() here, since this would create a new header in the store. If parsing
// the value then throws, we would have to remove the header from the store again. So just get a
// HeaderStoreItemInfo object and try to parse the value. If it works, we'll add the header.
HeaderStoreItemInfo info;
bool addToStore;
PrepareHeaderInfoForAdd(descriptor, out info, out addToStore);
bool result = TryParseAndAddRawHeaderValue(descriptor, info, value, false);
if (result && addToStore && (info.ParsedAndInvalidValues != null))
{
info.AssertContainsNoInvalidValues();
// If we get here, then the value could be parsed correctly. If we created a new HeaderStoreItemInfo, add
// it to the store if we added at least one value.
Debug.Assert(!Contains(descriptor));
AddEntryToStore(new HeaderEntry(descriptor, info));
}
return result;
}
// See ParseAndAddValue
private static bool TryParseAndAddRawHeaderValue(HeaderDescriptor descriptor, HeaderStoreItemInfo info, string? value, bool addWhenInvalid)
{
Debug.Assert(info != null);
Debug.Assert(descriptor.Parser != null);
// Values are added as 'invalid' if we either can't parse the value OR if we already have a value
// and the current header doesn't support multiple values: e.g. trying to add a date/time value
// to the 'Date' header if we already have a date/time value will result in the second value being
// added to the 'invalid' header values.
if (!info.CanAddParsedValue(descriptor.Parser))
{
if (addWhenInvalid)
{
AddInvalidValue(info, value ?? string.Empty);
}
return false;
}
int index = 0;
if (descriptor.Parser.TryParseValue(value, info.ParsedAndInvalidValues, ref index, out object? parsedValue))
{
// The raw string only represented one value (which was successfully parsed). Add the value and return.
if ((value == null) || (index == value.Length))
{
if (parsedValue != null)
{
AddParsedValue(info, parsedValue);
}
else if (addWhenInvalid && info.ParsedAndInvalidValues is null)
{
AddInvalidValue(info, value ?? string.Empty);
}
return true;
}
Debug.Assert(index < value.Length, "Parser must return an index value within the string length.");
// If we successfully parsed a value, but there are more left to read, store the results in a temp
// list. Only when all values are parsed successfully write the list to the store.
List<object> parsedValues = new List<object>();
if (parsedValue != null)
{
parsedValues.Add(parsedValue);
}
while (index < value.Length)
{
if (descriptor.Parser.TryParseValue(value, info.ParsedAndInvalidValues, ref index, out parsedValue))
{
if (parsedValue != null)
{
parsedValues.Add(parsedValue);
}
}
else
{
if (addWhenInvalid)
{
AddInvalidValue(info, value);
}
return false;
}
}
// All values were parsed correctly. Copy results to the store.
foreach (object item in parsedValues)
{
AddParsedValue(info, item);
}
if (parsedValues.Count == 0 && addWhenInvalid && info.ParsedAndInvalidValues is null)
{
AddInvalidValue(info, value);
}
return true;
}
Debug.Assert(value != null);
if (addWhenInvalid)
{
AddInvalidValue(info, value ?? string.Empty);
}
return false;
}
private static void AddParsedValue(HeaderStoreItemInfo info, object value)
{
Debug.Assert(!(value is List<object>),
"Header value types must not derive from List<object> since this type is used internally to store " +
"lists of values. So we would not be able to distinguish between a single value and a list of values.");
AddValueToStoreValue<object>(value, ref info.ParsedAndInvalidValues);
}
private static void AddInvalidValue(HeaderStoreItemInfo info, string value)
{
AddValueToStoreValue<object>(new InvalidValue(value), ref info.ParsedAndInvalidValues);
}
private static void AddRawValue(HeaderStoreItemInfo info, string value)
{
AddValueToStoreValue<string>(value, ref info.RawValue);
}
private static void AddValueToStoreValue<T>(T value, ref object? currentStoreValue) where T : class
{
// If there is no value set yet, then add current item as value (we don't create a list
// if not required). If 'info.Value' is already assigned then make sure 'info.Value' is a
// List<T> and append 'item' to the list.
if (currentStoreValue == null)
{
currentStoreValue = value;
}
else
{
List<T>? storeValues = currentStoreValue as List<T>;
if (storeValues == null)
{
storeValues = new List<T>(2);
Debug.Assert(currentStoreValue is T);
storeValues.Add((T)currentStoreValue);
currentStoreValue = storeValues;
}
Debug.Assert(value is T);
storeValues.Add((T)value);
}
}
internal object? GetSingleParsedValue(HeaderDescriptor descriptor)
{
if (!TryGetAndParseHeaderInfo(descriptor, out HeaderStoreItemInfo? info))
{
return null;
}
return info.GetSingleParsedValue();
}
internal object? GetParsedAndInvalidValues(HeaderDescriptor descriptor)
{
if (!TryGetAndParseHeaderInfo(descriptor, out HeaderStoreItemInfo? info))
{
return null;
}
return info.ParsedAndInvalidValues;
}
internal virtual bool IsAllowedHeaderName(HeaderDescriptor descriptor) => true;
private void PrepareHeaderInfoForAdd(HeaderDescriptor descriptor, out HeaderStoreItemInfo info, out bool addToStore)
{
if (!IsAllowedHeaderName(descriptor))
{
throw new InvalidOperationException(SR.Format(SR.net_http_headers_not_allowed_header_name, descriptor.Name));
}
addToStore = false;
if (!TryGetAndParseHeaderInfo(descriptor, out info!))
{
info = new HeaderStoreItemInfo();
addToStore = true;
}
}
private static void ParseAndAddValue(HeaderDescriptor descriptor, HeaderStoreItemInfo info, string? value)
{
Debug.Assert(info != null);
if (descriptor.Parser == null)
{
// If we don't have a parser for the header, we consider the value valid if it doesn't contains
// newline characters. We add the values as "parsed value". Note that we allow empty values.
CheckContainsNewLine(value);
AddParsedValue(info, value ?? string.Empty);
return;
}
// If the header only supports 1 value, we can add the current value only if there is no
// value already set.
if (!info.CanAddParsedValue(descriptor.Parser))
{
throw new FormatException(SR.Format(System.Globalization.CultureInfo.InvariantCulture, SR.net_http_headers_single_value_header, descriptor.Name));
}
int index = 0;
object parsedValue = descriptor.Parser.ParseValue(value, info.ParsedAndInvalidValues, ref index);
// The raw string only represented one value (which was successfully parsed). Add the value and return.
// If value is null we still have to first call ParseValue() to allow the parser to decide whether null is
// a valid value. If it is (i.e. no exception thrown), we set the parsed value (if any) and return.
if ((value == null) || (index == value.Length))
{
// If the returned value is null, then it means the header accepts empty values. i.e. we don't throw
// but we don't add 'null' to the store either.
if (parsedValue != null)
{
AddParsedValue(info, parsedValue);
}
return;
}
Debug.Assert(index < value.Length, "Parser must return an index value within the string length.");
// If we successfully parsed a value, but there are more left to read, store the results in a temp
// list. Only when all values are parsed successfully write the list to the store.
List<object> parsedValues = new List<object>();
if (parsedValue != null)
{
parsedValues.Add(parsedValue);
}
while (index < value.Length)
{
parsedValue = descriptor.Parser.ParseValue(value, info.ParsedAndInvalidValues, ref index);
if (parsedValue != null)
{
parsedValues.Add(parsedValue);
}
}
// All values were parsed correctly. Copy results to the store.
foreach (object item in parsedValues)
{
AddParsedValue(info, item);
}
}
private HeaderDescriptor GetHeaderDescriptor(string name)
{
ArgumentException.ThrowIfNullOrEmpty(name);
if (!HeaderDescriptor.TryGet(name, out HeaderDescriptor descriptor))
{
throw new FormatException(SR.Format(SR.net_http_headers_invalid_header_name, name));
}
if ((descriptor.HeaderType & _allowedHeaderTypes) != 0)
{
return descriptor;
}
else if ((descriptor.HeaderType & _treatAsCustomHeaderTypes) != 0)
{
return descriptor.AsCustomHeader();
}
throw new InvalidOperationException(SR.Format(SR.net_http_headers_not_allowed_header_name, name));
}
internal bool TryGetHeaderDescriptor(string name, out HeaderDescriptor descriptor)
{
if (string.IsNullOrEmpty(name))
{
descriptor = default;
return false;
}
if (HeaderDescriptor.TryGet(name, out descriptor))
{
HttpHeaderType headerType = descriptor.HeaderType;
if ((headerType & _allowedHeaderTypes) != 0)
{
return true;
}
if ((headerType & _treatAsCustomHeaderTypes) != 0)
{
descriptor = descriptor.AsCustomHeader();
return true;
}
}
return false;
}
internal static void CheckContainsNewLine(string? value)
{
if (value == null)
{
return;
}
if (HttpRuleParser.ContainsNewLine(value))
{
throw new FormatException(SR.net_http_headers_no_newlines);
}
}
internal static string[] GetStoreValuesAsStringArray(HeaderDescriptor descriptor, HeaderStoreItemInfo info)
{
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(sourceValues is string);
singleValue = (string)sourceValues;
multiValue = null;
return;
}
lock (info)
{
int length = GetValueCount(info);
scoped Span<string?> values;
singleValue = null;
if (length == 1)
{
multiValue = null;
values = new Span<string?>(ref singleValue);
}
else
{
Debug.Assert(length > 1, "The header should have been removed when it became empty");
values = (multiValue = new string[length])!;
}
int currentIndex = 0;
ReadStoreValues<object?>(values, info.ParsedAndInvalidValues, descriptor.Parser, ref currentIndex);
ReadStoreValues<string?>(values, info.RawValue, null, ref currentIndex);
Debug.Assert(currentIndex == length);
}
}
internal static int GetStoreValuesIntoStringArray(HeaderDescriptor descriptor, object sourceValues, [NotNull] ref string[]? values)
{
values ??= Array.Empty<string>();
HeaderStoreItemInfo? info = sourceValues as HeaderStoreItemInfo;
if (info is null)
{
Debug.Assert(sourceValues is string);
if (values.Length == 0)
{
values = new string[1];
}
values[0] = (string)sourceValues;
return 1;
}
lock (info)
{
int length = GetValueCount(info);
Debug.Assert(length > 0);
if (values.Length < length)
{
values = new string[length];
}
int currentIndex = 0;
ReadStoreValues<object?>(values!, info.ParsedAndInvalidValues, descriptor.Parser, ref currentIndex);
ReadStoreValues<string?>(values!, info.RawValue, null, ref currentIndex);
Debug.Assert(currentIndex == length);
return length;
}
}
private static int GetValueCount(HeaderStoreItemInfo info)
{
Debug.Assert(info != null);
Debug.Assert(Monitor.IsEntered(info));
return Count<object>(info.ParsedAndInvalidValues) + Count<string>(info.RawValue);
static int Count<T>(object? valueStore) =>
valueStore is null ? 0 :
valueStore is List<T> list ? list.Count :
1;
}
private static void ReadStoreValues<T>(Span<string?> values, object? storeValue, HttpHeaderParser? parser, ref int currentIndex)
{
if (storeValue != null)
{
List<T>? storeValues = storeValue as List<T>;
if (storeValues == null)
{
values[currentIndex] = parser == null || storeValue is InvalidValue ? storeValue.ToString() : parser.ToString(storeValue);
currentIndex++;
}
else
{
foreach (object? item in storeValues)
{
Debug.Assert(item != null);
values[currentIndex] = parser == null || item is InvalidValue ? item.ToString() : parser.ToString(item);
currentIndex++;
}
}
}
}
private static bool AreEqual(object value, object? storeValue, IEqualityComparer? comparer)
{
Debug.Assert(value != null);
if (comparer != null)
{
return comparer.Equals(value, storeValue);
}
// We don't have a comparer, so use the Equals() method.
return value.Equals(storeValue);
}
internal sealed class InvalidValue
{
private readonly string _value;
public InvalidValue(string value)
{
Debug.Assert(value is not null);
_value = value;
}
public override string ToString() => _value;
}
internal sealed class HeaderStoreItemInfo
{
internal HeaderStoreItemInfo() { }
internal object? RawValue;
internal object? ParsedAndInvalidValues;
public bool CanAddParsedValue(HttpHeaderParser parser)
{
Debug.Assert(parser != null, "There should be no reason to call CanAddValue if there is no parser for the current header.");
// If the header only supports one value, and we have already a value set, then we can't add
// another value. E.g. the 'Date' header only supports one value. We can't add multiple timestamps
// to 'Date'.
// So if this is a known header, ask the parser if it supports multiple values and check whether
// we already have a (valid or invalid) value.
// Note that we ignore the rawValue by purpose: E.g. we are parsing 2 raw values for a header only
// supporting 1 value. When the first value gets parsed, CanAddValue returns true and we add the
// parsed value to ParsedValue. When the second value is parsed, CanAddValue returns false, because
// we have already a parsed value.
return parser.SupportsMultipleValues || ParsedAndInvalidValues is null;
}
[Conditional("DEBUG")]
public void AssertContainsNoInvalidValues()
{
if (ParsedAndInvalidValues is not null)
{
if (ParsedAndInvalidValues is List<object> list)
{
foreach (object item in list)
{
Debug.Assert(item is not InvalidValue);
}
}
else
{
Debug.Assert(ParsedAndInvalidValues is not InvalidValue);
}
}
}
public object? GetSingleParsedValue()
{
if (ParsedAndInvalidValues is not null)
{
if (ParsedAndInvalidValues is List<object> list)
{
AssertContainsSingleParsedValue(list);
foreach (object item in list)
{
if (item is not InvalidValue)
{
return item;
}
}
}
else
{
if (ParsedAndInvalidValues is not InvalidValue)
{
return ParsedAndInvalidValues;
}
}
}
return null;
}
[Conditional("DEBUG")]
private static void AssertContainsSingleParsedValue(List<object> list)
{
int count = 0;
foreach (object item in list)
{
if (item is not InvalidValue)
{
count++;
}
}
Debug.Assert(count == 1, "Only a single parsed value should be stored for this parser");
}
public bool IsEmpty => RawValue == null && ParsedAndInvalidValues == null;
}
#region Low-level implementation details that work with _headerStore directly
private const int InitialCapacity = 4;
internal const int ArrayThreshold = 64; // Above this threshold, header ordering will not be preserved
internal HeaderEntry[]? GetEntriesArray()
{
object? store = _headerStore;
if (store is null)
{
return null;
}
else if (store is HeaderEntry[] entries)
{
return entries;
}
else
{
return GetEntriesFromDictionary();
}
HeaderEntry[] GetEntriesFromDictionary()
{
var dictionary = (Dictionary<HeaderDescriptor, object>)_headerStore!;
var entries = new HeaderEntry[dictionary.Count];
int i = 0;
foreach (KeyValuePair<HeaderDescriptor, object> entry in dictionary)
{
entries[i++] = new HeaderEntry
{
Key = entry.Key,
Value = entry.Value
};
}
return entries;
}
}
internal ReadOnlySpan<HeaderEntry> GetEntries()
{
return new ReadOnlySpan<HeaderEntry>(GetEntriesArray(), 0, _count);
}
internal int Count => _count;
private bool EntriesAreLiveView => _headerStore is HeaderEntry[];
private ref object GetValueRefOrNullRef(HeaderDescriptor key)
{
ref object valueRef = ref Unsafe.NullRef<object>();
object? store = _headerStore;
if (store is HeaderEntry[] entries)
{
for (int i = 0; i < _count && i < entries.Length; i++)
{
if (key.Equals(entries[i].Key))
{
valueRef = ref entries[i].Value;
break;
}
}
}
else if (store is not null)
{
valueRef = ref CollectionsMarshal.GetValueRefOrNullRef(Unsafe.As<Dictionary<HeaderDescriptor, object>>(store), key);
}
return ref valueRef;
}
private ref object? GetValueRefOrAddDefault(HeaderDescriptor key)
{
object? store = _headerStore;
if (store is HeaderEntry[] entries)
{
for (int i = 0; i < _count && i < entries.Length; i++)
{
if (key.Equals(entries[i].Key))
{
return ref entries[i].Value!;
}
}
int count = _count;
_count++;
if ((uint)count < (uint)entries.Length)
{
entries[count].Key = key;
return ref entries[count].Value!;
}
return ref GrowEntriesAndAddDefault(key);
}
else if (store is null)
{
_count++;
entries = new HeaderEntry[InitialCapacity];
_headerStore = entries;
ref HeaderEntry firstEntry = ref MemoryMarshal.GetArrayDataReference(entries);
firstEntry.Key = key;
return ref firstEntry.Value!;
}
else
{
return ref DictionaryGetValueRefOrAddDefault(key);
}
ref object? GrowEntriesAndAddDefault(HeaderDescriptor key)
{
var entries = (HeaderEntry[])_headerStore!;
if (entries.Length == ArrayThreshold)
{
return ref ConvertToDictionaryAndAddDefault(key);
}
else
{
Array.Resize(ref entries, entries.Length << 1);
_headerStore = entries;
ref HeaderEntry firstNewEntry = ref entries[entries.Length >> 1];
firstNewEntry.Key = key;
return ref firstNewEntry.Value!;
}
}
ref object? ConvertToDictionaryAndAddDefault(HeaderDescriptor key)
{
var entries = (HeaderEntry[])_headerStore!;
var dictionary = new Dictionary<HeaderDescriptor, object>(ArrayThreshold);
_headerStore = dictionary;
foreach (HeaderEntry entry in entries)
{
dictionary.Add(entry.Key, entry.Value);
}
Debug.Assert(dictionary.Count == _count - 1);
return ref CollectionsMarshal.GetValueRefOrAddDefault(dictionary, key, out _);
}
ref object? DictionaryGetValueRefOrAddDefault(HeaderDescriptor key)
{
var dictionary = (Dictionary<HeaderDescriptor, object>)_headerStore!;
ref object? value = ref CollectionsMarshal.GetValueRefOrAddDefault(dictionary, key, out _);
if (value is null)
{
_count++;
}
return ref value;
}
}
private void AddEntryToStore(HeaderEntry entry)
{
Debug.Assert(!Contains(entry.Key));
if (_headerStore is HeaderEntry[] entries)
{
int count = _count;
if ((uint)count < (uint)entries.Length)
{
entries[count] = entry;
_count++;
return;
}
}
GetValueRefOrAddDefault(entry.Key) = entry.Value;
}
internal bool Contains(HeaderDescriptor key)
{
return !Unsafe.IsNullRef(ref GetValueRefOrNullRef(key));
}
public void Clear()
{
if (_headerStore is HeaderEntry[] entries)
{
Array.Clear(entries, 0, _count);
}
else
{
_headerStore = null;
}
_count = 0;
}
internal bool Remove(HeaderDescriptor key)
{
bool removed = false;
object? store = _headerStore;
if (store is HeaderEntry[] entries)
{
for (int i = 0; i < _count && i < entries.Length; i++)
{
if (key.Equals(entries[i].Key))
{
while (i + 1 < _count && (uint)(i + 1) < (uint)entries.Length)
{
entries[i] = entries[i + 1];
i++;
}
entries[i] = default;
removed = true;
break;
}
}
}
else if (store is not null)
{
removed = Unsafe.As<Dictionary<HeaderDescriptor, object>>(store).Remove(key);
}
if (removed)
{
_count--;
}
return removed;
}
#endregion // _headerStore implementation
}
}
|