File: NameValueHeaderValue.cs
Web Access
Project: src\src\Http\Headers\src\Microsoft.Net.Http.Headers.csproj (Microsoft.Net.Http.Headers)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.Text;
using Microsoft.Extensions.Primitives;
 
namespace Microsoft.Net.Http.Headers;
 
// According to the RFC, in places where a "parameter" is required, the value is mandatory
// (e.g. Media-Type, Accept). However, we don't introduce a dedicated type for it. So NameValueHeaderValue supports
// name-only values in addition to name/value pairs.
/// <summary>
/// Represents a name/value pair used in various headers as defined in RFC 2616.
/// </summary>
public class NameValueHeaderValue
{
    private static readonly HttpHeaderParser<NameValueHeaderValue> SingleValueParser
        = new GenericHeaderParser<NameValueHeaderValue>(false, GetNameValueLength);
    internal static readonly HttpHeaderParser<NameValueHeaderValue> MultipleValueParser
        = new GenericHeaderParser<NameValueHeaderValue>(true, GetNameValueLength);
 
    private StringSegment _name;
    private StringSegment _value;
    private bool _isReadOnly;
 
    private NameValueHeaderValue()
    {
        // Used by the parser to create a new instance of this type.
    }
 
    /// <summary>
    /// Initializes a new instance of <see cref="NameValueHeaderValue"/>.
    /// </summary>
    /// <param name="name">The header name.</param>
    public NameValueHeaderValue(StringSegment name)
        : this(name, null)
    {
    }
 
    /// <summary>
    /// Initializes a new instance of <see cref="NameValueHeaderValue"/>.
    /// </summary>
    /// <param name="name">The header name.</param>
    /// <param name="value">The header value.</param>
    public NameValueHeaderValue(StringSegment name, StringSegment value)
    {
        CheckNameValueFormat(name, value);
 
        _name = name;
        _value = value;
    }
 
    /// <summary>
    /// Gets the header name.
    /// </summary>
    public StringSegment Name
    {
        get { return _name; }
    }
 
    /// <summary>
    /// Gets or sets the header value.
    /// </summary>
    public StringSegment Value
    {
        get { return _value; }
        set
        {
            HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
            CheckValueFormat(value);
            _value = value;
        }
    }
 
    /// <summary>
    /// Gets a value that determines if this header is read only.
    /// </summary>
    public bool IsReadOnly { get { return _isReadOnly; } }
 
    /// <summary>
    /// Provides a copy of this object without the cost of re-validating the values.
    /// </summary>
    /// <returns>A copy.</returns>
    public NameValueHeaderValue Copy()
    {
        return new NameValueHeaderValue()
        {
            _name = _name,
            _value = _value
        };
    }
 
    /// <summary>
    /// Provides a copy of this instance while making it immutable.
    /// </summary>
    /// <returns>The readonly <see cref="NameValueHeaderValue"/>.</returns>
    public NameValueHeaderValue CopyAsReadOnly()
    {
        if (IsReadOnly)
        {
            return this;
        }
 
        return new NameValueHeaderValue()
        {
            _name = _name,
            _value = _value,
            _isReadOnly = true
        };
    }
 
    /// <inheritdoc/>
    public override int GetHashCode()
    {
        Contract.Assert(_name != null);
 
        var nameHashCode = StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_name);
 
        if (!StringSegment.IsNullOrEmpty(_value))
        {
            // If we have a quoted-string, then just use the hash code. If we have a token, convert to lowercase
            // and retrieve the hash code.
            if (_value[0] == '"')
            {
                return nameHashCode ^ _value.GetHashCode();
            }
 
            return nameHashCode ^ StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_value);
        }
 
        return nameHashCode;
    }
 
    /// <inheritdoc/>
    public override bool Equals(object? obj)
    {
        var other = obj as NameValueHeaderValue;
 
        if (other == null)
        {
            return false;
        }
 
        if (!_name.Equals(other._name, StringComparison.OrdinalIgnoreCase))
        {
            return false;
        }
 
        // RFC2616: 14.20: unquoted tokens should use case-INsensitive comparison; quoted-strings should use
        // case-sensitive comparison. The RFC doesn't mention how to compare quoted-strings outside the "Expect"
        // header. We treat all quoted-strings the same: case-sensitive comparison.
 
        if (StringSegment.IsNullOrEmpty(_value))
        {
            return StringSegment.IsNullOrEmpty(other._value);
        }
 
        if (_value[0] == '"')
        {
            // We have a quoted string, so we need to do case-sensitive comparison.
            return (_value.Equals(other._value, StringComparison.Ordinal));
        }
        else
        {
            return (_value.Equals(other._value, StringComparison.OrdinalIgnoreCase));
        }
    }
 
    /// <summary>
    /// If the value is a quoted-string as defined by <see href="https://tools.ietf.org/html/rfc7230#section-3.2.6">the RFC specification</see>,
    /// removes quotes and unescapes backslashes and quotes.
    /// </summary>
    /// <returns>An unescaped version of <see cref="Value"/>.</returns>
    public StringSegment GetUnescapedValue()
    {
        if (!HeaderUtilities.IsQuoted(_value))
        {
            return _value;
        }
        return HeaderUtilities.UnescapeAsQuotedString(_value);
    }
 
    /// <summary>
    /// Sets <see cref="Value"/> after it has been quoted as defined by <see href="https://tools.ietf.org/html/rfc7230#section-3.2.6">the RFC specification</see>.
    /// </summary>
    /// <param name="value"></param>
    public void SetAndEscapeValue(StringSegment value)
    {
        HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
        if (StringSegment.IsNullOrEmpty(value) || (GetValueLength(value, 0) == value.Length))
        {
            _value = value;
        }
        else
        {
            Value = HeaderUtilities.EscapeAsQuotedString(value);
        }
    }
 
    /// <summary>
    /// Parses <paramref name="input"/> as a <see cref="NameValueHeaderValue"/> value.
    /// </summary>
    /// <param name="input">The values to parse.</param>
    /// <returns>The parsed values.</returns>
    public static NameValueHeaderValue Parse(StringSegment input)
    {
        var index = 0;
        return SingleValueParser.ParseValue(input, ref index)!;
    }
 
    /// <summary>
    /// Attempts to parse the specified <paramref name="input"/> as a <see cref="NameValueHeaderValue"/>.
    /// </summary>
    /// <param name="input">The value to parse.</param>
    /// <param name="parsedValue">The parsed value.</param>
    /// <returns><see langword="true"/> if input is a valid <see cref="NameValueHeaderValue"/>, otherwise <see langword="false"/>.</returns>
    public static bool TryParse(StringSegment input, [NotNullWhen(true)] out NameValueHeaderValue? parsedValue)
    {
        var index = 0;
        return SingleValueParser.TryParseValue(input, ref index, out parsedValue!);
    }
 
    /// <summary>
    /// Parses a sequence of inputs as a sequence of <see cref="NameValueHeaderValue"/> values.
    /// </summary>
    /// <param name="input">The values to parse.</param>
    /// <returns>The parsed values.</returns>
    public static IList<NameValueHeaderValue> ParseList(IList<string>? input)
    {
        return MultipleValueParser.ParseValues(input);
    }
 
    /// <summary>
    /// Parses a sequence of inputs as a sequence of <see cref="NameValueHeaderValue"/> values using string parsing rules.
    /// </summary>
    /// <param name="input">The values to parse.</param>
    /// <returns>The parsed values.</returns>
    public static IList<NameValueHeaderValue> ParseStrictList(IList<string>? input)
    {
        return MultipleValueParser.ParseStrictValues(input);
    }
 
    /// <summary>
    /// Attempts to parse the sequence of values as a sequence of <see cref="NameValueHeaderValue"/>.
    /// </summary>
    /// <param name="input">The values to parse.</param>
    /// <param name="parsedValues">The parsed values.</param>
    /// <returns><see langword="true"/> if all inputs are valid <see cref="NameValueHeaderValue"/>, otherwise <see langword="false"/>.</returns>
    public static bool TryParseList(IList<string>? input, [NotNullWhen(true)] out IList<NameValueHeaderValue>? parsedValues)
    {
        return MultipleValueParser.TryParseValues(input, out parsedValues);
    }
 
    /// <summary>
    /// Attempts to parse the sequence of values as a sequence of <see cref="NameValueHeaderValue"/> using string parsing rules.
    /// </summary>
    /// <param name="input">The values to parse.</param>
    /// <param name="parsedValues">The parsed values.</param>
    /// <returns><see langword="true"/> if all inputs are valid <see cref="StringWithQualityHeaderValue"/>, otherwise <see langword="false"/>.</returns>
    public static bool TryParseStrictList(IList<string>? input, [NotNullWhen(true)] out IList<NameValueHeaderValue>? parsedValues)
    {
        return MultipleValueParser.TryParseStrictValues(input, out parsedValues);
    }
 
    /// <inheritdoc />
    public override string ToString()
    {
        if (!StringSegment.IsNullOrEmpty(_value))
        {
            return _name + "=" + _value;
        }
        return _name.ToString();
    }
 
    internal static void ToString(
        IList<NameValueHeaderValue>? values,
        char separator,
        bool leadingSeparator,
        StringBuilder destination)
    {
        Contract.Assert(destination != null);
 
        if ((values == null) || (values.Count == 0))
        {
            return;
        }
 
        for (var i = 0; i < values.Count; i++)
        {
            if (leadingSeparator || (destination.Length > 0))
            {
                destination.Append(separator);
                destination.Append(' ');
            }
            destination.Append(values[i].Name.AsSpan());
            if (!StringSegment.IsNullOrEmpty(values[i].Value))
            {
                destination.Append('=');
                destination.Append(values[i].Value.AsSpan());
            }
        }
    }
 
    internal static string? ToString(IList<NameValueHeaderValue>? values, char separator, bool leadingSeparator)
    {
        if ((values == null) || (values.Count == 0))
        {
            return null;
        }
 
        var sb = new StringBuilder();
 
        ToString(values, separator, leadingSeparator, sb);
 
        return sb.ToString();
    }
 
    internal static int GetHashCode(IList<NameValueHeaderValue>? values)
    {
        if ((values == null) || (values.Count == 0))
        {
            return 0;
        }
 
        var result = 0;
        for (var i = 0; i < values.Count; i++)
        {
            result = result ^ values[i].GetHashCode();
        }
        return result;
    }
 
    private static int GetNameValueLength(StringSegment input, int startIndex, out NameValueHeaderValue? parsedValue)
    {
        Contract.Requires(startIndex >= 0);
 
        parsedValue = null;
 
        if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length))
        {
            return 0;
        }
 
        // Parse the name, i.e. <name> in name/value string "<name>=<value>". Caller must remove
        // leading whitespaces.
        var nameLength = HttpRuleParser.GetTokenLength(input, startIndex);
 
        if (nameLength == 0)
        {
            return 0;
        }
 
        var name = input.Subsegment(startIndex, nameLength);
        var current = startIndex + nameLength;
        current = current + HttpRuleParser.GetWhitespaceLength(input, current);
 
        // Parse the separator between name and value
        if ((current == input.Length) || (input[current] != '='))
        {
            // We only have a name and that's OK. Return.
            parsedValue = new NameValueHeaderValue();
            parsedValue._name = name;
            current = current + HttpRuleParser.GetWhitespaceLength(input, current); // skip whitespaces
            return current - startIndex;
        }
 
        current++; // skip delimiter.
        current = current + HttpRuleParser.GetWhitespaceLength(input, current);
 
        // Parse the value, i.e. <value> in name/value string "<name>=<value>"
        int valueLength = GetValueLength(input, current);
 
        // Value after the '=' may be empty
        // Use parameterless ctor to avoid double-parsing of name and value, i.e. skip public ctor validation.
        parsedValue = new NameValueHeaderValue();
        parsedValue._name = name;
        parsedValue._value = input.Subsegment(current, valueLength);
        current = current + valueLength;
        current = current + HttpRuleParser.GetWhitespaceLength(input, current); // skip whitespaces
        return current - startIndex;
    }
 
    // Returns the length of a name/value list, separated by 'delimiter'. E.g. "a=b, c=d, e=f" adds 3
    // name/value pairs to 'nameValueCollection' if 'delimiter' equals ','.
    internal static int GetNameValueListLength(
        StringSegment input,
        int startIndex,
        char delimiter,
        IList<NameValueHeaderValue> nameValueCollection)
    {
        Contract.Requires(startIndex >= 0);
 
        if ((StringSegment.IsNullOrEmpty(input)) || (startIndex >= input.Length))
        {
            return 0;
        }
 
        var current = startIndex + HttpRuleParser.GetWhitespaceLength(input, startIndex);
        while (true)
        {
            var nameValueLength = GetNameValueLength(input, current, out var parameter);
 
            if (nameValueLength == 0)
            {
                // There may be a trailing ';'
                return current - startIndex;
            }
 
            nameValueCollection!.Add(parameter!);
            current = current + nameValueLength;
            current = current + HttpRuleParser.GetWhitespaceLength(input, current);
 
            if ((current == input.Length) || (input[current] != delimiter))
            {
                // We're done and we have at least one valid name/value pair.
                return current - startIndex;
            }
 
            // input[current] is 'delimiter'. Skip the delimiter and whitespaces and try to parse again.
            current++; // skip delimiter.
            current = current + HttpRuleParser.GetWhitespaceLength(input, current);
        }
    }
 
    /// <summary>
    /// Finds a <see cref="NameValueHeaderValue"/> with the specified <paramref name="name"/>.
    /// </summary>
    /// <param name="values">The collection to search.</param>
    /// <param name="name">The name to find.</param>
    /// <returns>The <see cref="NameValueHeaderValue" /> if found, otherwise <see langword="null" />.</returns>
    public static NameValueHeaderValue? Find(IList<NameValueHeaderValue>? values, StringSegment name)
    {
        Contract.Requires(name.Length > 0);
 
        if ((values == null) || (values.Count == 0))
        {
            return null;
        }
 
        for (var i = 0; i < values.Count; i++)
        {
            var value = values[i];
            if (value.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
            {
                return value;
            }
        }
        return null;
    }
 
    internal static int GetValueLength(StringSegment input, int startIndex)
    {
        if (startIndex >= input.Length)
        {
            return 0;
        }
 
        var valueLength = HttpRuleParser.GetTokenLength(input, startIndex);
 
        if (valueLength == 0)
        {
            // A value can either be a token or a quoted string. Check if it is a quoted string.
            if (HttpRuleParser.GetQuotedStringLength(input, startIndex, out valueLength) != HttpParseResult.Parsed)
            {
                // We have an invalid value. Reset the name and return.
                return 0;
            }
        }
        return valueLength;
    }
 
    private static void CheckNameValueFormat(StringSegment name, StringSegment value)
    {
        HeaderUtilities.CheckValidToken(name, nameof(name));
        CheckValueFormat(value);
    }
 
    private static void CheckValueFormat(StringSegment value)
    {
        // Either value is null/empty or a valid token/quoted string
        if (!(StringSegment.IsNullOrEmpty(value) || (GetValueLength(value, 0) == value.Length)))
        {
            throw new FormatException(string.Format(CultureInfo.InvariantCulture, "The header value is invalid: '{0}'", value));
        }
    }
}