File: System\Net\Http\Headers\NameValueHeaderValue.cs
Web Access
Project: src\src\libraries\System.Net.Http\src\System.Net.Http.csproj (System.Net.Http)
// 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;
using System.Diagnostics.CodeAnalysis;
using System.Text;
 
namespace System.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.
    public class NameValueHeaderValue : ICloneable
    {
        private static readonly Func<NameValueHeaderValue> s_defaultNameValueCreator = CreateNameValue;
 
        private string _name = null!; // Name always set after default constructor used
        private string? _value;
 
        public string Name
        {
            get { return _name; }
        }
 
        public string? Value
        {
            get { return _value; }
            set
            {
                CheckValueFormat(value);
                _value = value;
            }
        }
 
        internal NameValueHeaderValue()
        {
        }
 
        public NameValueHeaderValue(string name)
            : this(name, null)
        {
        }
 
        public NameValueHeaderValue(string name, string? value)
        {
            CheckNameValueFormat(name, value);
 
            _name = name;
            _value = value;
        }
 
        protected internal NameValueHeaderValue(NameValueHeaderValue source)
        {
            Debug.Assert(source != null);
 
            _name = source._name;
            _value = source._value;
        }
 
        public override int GetHashCode()
        {
            Debug.Assert(_name != null);
 
            int nameHashCode = StringComparer.OrdinalIgnoreCase.GetHashCode(_name);
 
            if (!string.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 ^ StringComparer.OrdinalIgnoreCase.GetHashCode(_value);
            }
 
            return nameHashCode;
        }
 
        public override bool Equals([NotNullWhen(true)] object? obj)
        {
            NameValueHeaderValue? other = obj as NameValueHeaderValue;
 
            if (other == null)
            {
                return false;
            }
 
            if (!string.Equals(_name, 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 (string.IsNullOrEmpty(_value))
            {
                return string.IsNullOrEmpty(other._value);
            }
 
            if (_value[0] == '"')
            {
                // We have a quoted string, so we need to do case-sensitive comparison.
                return string.Equals(_value, other._value, StringComparison.Ordinal);
            }
            else
            {
                return string.Equals(_value, other._value, StringComparison.OrdinalIgnoreCase);
            }
        }
 
        public static NameValueHeaderValue Parse(string input)
        {
            int index = 0;
            return (NameValueHeaderValue)GenericHeaderParser.SingleValueNameValueParser.ParseValue(
                input, null, ref index);
        }
 
        public static bool TryParse([NotNullWhen(true)] string? input, [NotNullWhen(true)] out NameValueHeaderValue? parsedValue)
        {
            int index = 0;
            parsedValue = null;
 
            if (GenericHeaderParser.SingleValueNameValueParser.TryParseValue(input, null, ref index, out object? output))
            {
                parsedValue = (NameValueHeaderValue)output!;
                return true;
            }
            return false;
        }
 
        public override string ToString()
        {
            if (!string.IsNullOrEmpty(_value))
            {
                return _name + "=" + _value;
            }
            return _name;
        }
 
        private void AddToStringBuilder(StringBuilder sb)
        {
            if (GetType() != typeof(NameValueHeaderValue))
            {
                // If this is a derived instance, we need to give its
                // ToString a chance.
                sb.Append(ToString());
            }
            else
            {
                // Otherwise, we can use the base behavior and avoid
                // the string concatenation.
                sb.Append(_name);
                if (!string.IsNullOrEmpty(_value))
                {
                    sb.Append('=');
                    sb.Append(_value);
                }
            }
        }
 
        internal static void ToString(UnvalidatedObjectCollection<NameValueHeaderValue>? values, char separator, bool leadingSeparator,
            StringBuilder destination)
        {
            Debug.Assert(destination != null);
 
            if ((values == null) || (values.Count == 0))
            {
                return;
            }
 
            foreach (var value in values)
            {
                if (leadingSeparator || (destination.Length > 0))
                {
                    destination.Append(separator);
                    destination.Append(' ');
                }
                value.AddToStringBuilder(destination);
            }
        }
 
        internal static int GetHashCode(UnvalidatedObjectCollection<NameValueHeaderValue>? values)
        {
            if ((values == null) || (values.Count == 0))
            {
                return 0;
            }
 
            int result = 0;
            foreach (var value in values)
            {
                result ^= value.GetHashCode();
            }
            return result;
        }
 
        internal static int GetNameValueLength(string input, int startIndex, out NameValueHeaderValue? parsedValue)
        {
            return GetNameValueLength(input, startIndex, s_defaultNameValueCreator, out parsedValue);
        }
 
        internal static int GetNameValueLength(string input, int startIndex,
            Func<NameValueHeaderValue> nameValueCreator, out NameValueHeaderValue? parsedValue)
        {
            Debug.Assert(input != null);
            Debug.Assert(startIndex >= 0);
            Debug.Assert(nameValueCreator != null);
 
            parsedValue = null;
 
            if (string.IsNullOrEmpty(input) || (startIndex >= input.Length))
            {
                return 0;
            }
 
            // Parse the name, i.e. <name> in name/value string "<name>=<value>". Caller must remove
            // leading whitespace.
            int nameLength = HttpRuleParser.GetTokenLength(input, startIndex);
 
            if (nameLength == 0)
            {
                return 0;
            }
 
            string name = input.Substring(startIndex, nameLength);
            int current = startIndex + nameLength;
            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 = nameValueCreator();
                parsedValue._name = name;
                current += HttpRuleParser.GetWhitespaceLength(input, current); // skip whitespace
                return current - startIndex;
            }
 
            current++; // skip delimiter.
            current += HttpRuleParser.GetWhitespaceLength(input, current);
 
            // Parse the value, i.e. <value> in name/value string "<name>=<value>"
            int valueLength = GetValueLength(input, current);
 
            if (valueLength == 0)
            {
                return 0; // We have an invalid value.
            }
 
            // Use parameterless ctor to avoid double-parsing of name and value, i.e. skip public ctor validation.
            parsedValue = nameValueCreator();
            parsedValue._name = name;
            parsedValue._value = input.Substring(current, valueLength);
            current += valueLength;
            current += HttpRuleParser.GetWhitespaceLength(input, current); // skip whitespace
            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(string? input, int startIndex, char delimiter,
            UnvalidatedObjectCollection<NameValueHeaderValue> nameValueCollection)
        {
            Debug.Assert(nameValueCollection != null);
            Debug.Assert(startIndex >= 0);
 
            if ((string.IsNullOrEmpty(input)) || (startIndex >= input.Length))
            {
                return 0;
            }
 
            int current = startIndex + HttpRuleParser.GetWhitespaceLength(input, startIndex);
            while (true)
            {
                NameValueHeaderValue? parameter;
                int nameValueLength = NameValueHeaderValue.GetNameValueLength(input, current,
                    s_defaultNameValueCreator, out parameter);
 
                if (nameValueLength == 0)
                {
                    return 0;
                }
 
                nameValueCollection.Add(parameter!);
                current += nameValueLength;
                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 whitespace and try to parse again.
                current++; // skip delimiter.
                current += HttpRuleParser.GetWhitespaceLength(input, current);
            }
        }
 
        internal static NameValueHeaderValue? Find(UnvalidatedObjectCollection<NameValueHeaderValue>? values, string name)
        {
            Debug.Assert((name != null) && (name.Length > 0));
 
            if ((values == null) || (values.Count == 0))
            {
                return null;
            }
 
            foreach (var value in values)
            {
                if (string.Equals(value.Name, name, StringComparison.OrdinalIgnoreCase))
                {
                    return value;
                }
            }
            return null;
        }
 
        internal static int GetValueLength(string input, int startIndex)
        {
            Debug.Assert(input != null);
 
            if (startIndex >= input.Length)
            {
                return 0;
            }
 
            int 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(string name, string? value)
        {
            HeaderUtilities.CheckValidToken(name);
            CheckValueFormat(value);
        }
 
        private static void CheckValueFormat(string? value)
        {
            // Either value is null/empty or a valid token/quoted string https://tools.ietf.org/html/rfc7230#section-3.2.6
            if (string.IsNullOrEmpty(value))
            {
                return;
            }
 
            // Trailing/leading space are not allowed
            if (value.StartsWith(' ') || value.StartsWith('\t') || value.EndsWith(' ') || value.EndsWith('\t'))
            {
                ThrowFormatException(value);
            }
 
            if (value[0] == '"')
            {
                HttpParseResult parseResult = HttpRuleParser.GetQuotedStringLength(value, 0, out int valueLength);
                if (parseResult != HttpParseResult.Parsed || valueLength != value.Length)
                {
                    ThrowFormatException(value);
                }
            }
            else if (HttpRuleParser.ContainsNewLine(value))
            {
                ThrowFormatException(value);
            }
 
            static void ThrowFormatException(string value) =>
                throw new FormatException(SR.Format(System.Globalization.CultureInfo.InvariantCulture, SR.net_http_headers_invalid_value, value));
        }
 
        private static NameValueHeaderValue CreateNameValue()
        {
            return new NameValueHeaderValue();
        }
 
        // Implement ICloneable explicitly to allow derived types to "override" the implementation.
        object ICloneable.Clone()
        {
            return new NameValueHeaderValue(this);
        }
    }
}