File: System\Net\Http\Headers\WarningHeaderValue.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.Globalization;
using System.Text;
 
namespace System.Net.Http.Headers
{
    public class WarningHeaderValue : ICloneable
    {
        private readonly int _code;
        private readonly string _agent;
        private readonly string _text;
        private readonly DateTimeOffset _date;
        private readonly bool _dateHasValue;
 
        public int Code => _code;
 
        public string Agent => _agent;
 
        public string Text => _text;
 
        public DateTimeOffset? Date => _dateHasValue ? _date : null;
 
        private WarningHeaderValue(int code, string agent, string text, DateTimeOffset? date)
        {
#if DEBUG
            // This constructor should only be used with already validated values.
            new WarningHeaderValue(code, agent, text);
#endif
 
            _code = code;
            _agent = agent;
            _text = text;
            _date = date.GetValueOrDefault();
            _dateHasValue = date.HasValue;
        }
 
        public WarningHeaderValue(int code, string agent, string text)
        {
            CheckCode(code);
            CheckAgent(agent);
            HeaderUtilities.CheckValidQuotedString(text);
 
            _code = code;
            _agent = agent;
            _text = text;
        }
 
        public WarningHeaderValue(int code, string agent, string text, DateTimeOffset date)
        {
            CheckCode(code);
            CheckAgent(agent);
            HeaderUtilities.CheckValidQuotedString(text);
 
            _code = code;
            _agent = agent;
            _text = text;
            _date = date;
            _dateHasValue = true;
        }
 
        private WarningHeaderValue(WarningHeaderValue source)
        {
            Debug.Assert(source != null);
 
            _code = source._code;
            _agent = source._agent;
            _text = source._text;
            _date = source._date;
            _dateHasValue = source._dateHasValue;
        }
 
        public override string ToString()
        {
            var sb = new ValueStringBuilder(stackalloc char[256]);
 
            // Warning codes are always 3 digits according to RFC2616
            sb.AppendSpanFormattable(_code, "000", NumberFormatInfo.InvariantInfo);
 
            sb.Append(' ');
            sb.Append(_agent);
            sb.Append(' ');
            sb.Append(_text);
 
            if (_dateHasValue)
            {
                sb.Append(" \"");
                sb.AppendSpanFormattable(_date, "r");
                sb.Append('\"');
            }
 
            return sb.ToString();
        }
 
        public override bool Equals([NotNullWhen(true)] object? obj) =>
            obj is WarningHeaderValue other &&
            _code == other._code &&
            // 'agent' is a host/token, i.e. use case-insensitive comparison. Use case-sensitive comparison for 'text'
            // since it is a quoted string.
            string.Equals(_agent, other._agent, StringComparison.OrdinalIgnoreCase) &&
            string.Equals(_text, other._text, StringComparison.Ordinal) &&
            _dateHasValue == other._dateHasValue &&
            _date == other._date;
 
        public override int GetHashCode() =>
            HashCode.Combine(
                _code,
                StringComparer.OrdinalIgnoreCase.GetHashCode(_agent),
                _text,
                _dateHasValue,
                _date);
 
        public static WarningHeaderValue Parse(string input)
        {
            int index = 0;
            return (WarningHeaderValue)GenericHeaderParser.SingleValueWarningParser.ParseValue(input, null, ref index);
        }
 
        public static bool TryParse([NotNullWhen(true)] string? input, [NotNullWhen(true)] out WarningHeaderValue? parsedValue)
        {
            int index = 0;
            parsedValue = null;
 
            if (GenericHeaderParser.SingleValueWarningParser.TryParseValue(input, null, ref index, out object? output))
            {
                parsedValue = (WarningHeaderValue)output!;
                return true;
            }
            return false;
        }
 
        internal static int GetWarningLength(string? input, int startIndex, out object? parsedValue)
        {
            Debug.Assert(startIndex >= 0);
 
            parsedValue = null;
 
            if (string.IsNullOrEmpty(input) || (startIndex >= input.Length))
            {
                return 0;
            }
 
            // Read <code> in '<code> <agent> <text> ["<date>"]'
            int current = startIndex;
 
            if (!TryReadCode(input, ref current, out int code))
            {
                return 0;
            }
 
            // Read <agent> in '<code> <agent> <text> ["<date>"]'
            if (!TryReadAgent(input, ref current, out string? agent))
            {
                return 0;
            }
 
            // Read <text> in '<code> <agent> <text> ["<date>"]'
            int textStartIndex = current;
            if (HttpRuleParser.GetQuotedStringLength(input, current, out int textLength) != HttpParseResult.Parsed)
            {
                return 0;
            }
 
            string text = input.Substring(textStartIndex, textLength);
 
            current += textLength;
 
            // Read <date> in '<code> <agent> <text> ["<date>"]'
            if (!TryReadDate(input, ref current, out DateTimeOffset? date))
            {
                return 0;
            }
 
            parsedValue = new WarningHeaderValue(code, agent, text, date);
 
            return current - startIndex;
        }
 
        private static bool TryReadAgent(string input, ref int current, [NotNullWhen(true)] out string? agent)
        {
            agent = null;
 
            int agentLength = HttpRuleParser.GetHostLength(input, current, true);
            if (agentLength == 0)
            {
                return false;
            }
 
            agent = input.Substring(current, agentLength);
            current += agentLength;
 
            int whitespaceLength = HttpRuleParser.GetWhitespaceLength(input, current);
            current += whitespaceLength;
 
            // At least one whitespace required after <agent>. Also make sure we have characters left for <text>
            if ((whitespaceLength == 0) || (current == input.Length))
            {
                return false;
            }
 
            return true;
        }
 
        private static bool TryReadCode(string input, ref int current, out int code)
        {
            code = 0;
            int codeLength = HttpRuleParser.GetNumberLength(input, current, false);
 
            // code must be a 3 digit value. We accept less digits, but we don't accept more.
            if ((codeLength == 0) || (codeLength > 3))
            {
                return false;
            }
 
            if (!HeaderUtilities.TryParseInt32(input, current, codeLength, out code))
            {
                Debug.Fail("Unable to parse value even though it was parsed as <=3 digits string. Input: '" +
                    input + "', Current: " + current + ", CodeLength: " + codeLength);
                return false;
            }
 
            current += codeLength;
 
            int whitespaceLength = HttpRuleParser.GetWhitespaceLength(input, current);
            current += whitespaceLength;
 
            // Make sure the number is followed by at least one whitespace and that we have characters left to parse.
            if ((whitespaceLength == 0) || (current == input.Length))
            {
                return false;
            }
 
            return true;
        }
 
        private static bool TryReadDate(string input, ref int current, out DateTimeOffset? date)
        {
            date = null;
 
            // Make sure we have at least one whitespace between <text> and <date> (if we have <date>)
            int whitespaceLength = HttpRuleParser.GetWhitespaceLength(input, current);
            current += whitespaceLength;
 
            // Read <date> in '<code> <agent> <text> ["<date>"]'
            if ((current < input.Length) && (input[current] == '"'))
            {
                if (whitespaceLength == 0)
                {
                    return false; // we have characters after <text> but they were not separated by a whitespace
                }
 
                current++; // skip opening '"'
 
                // Find the closing '"'
                int dateStartIndex = current;
                int quote = input.AsSpan(current).IndexOf('"');
                if (quote <= 0) // no quote was found or it was the first character (meaning an empty quoted string)
                {
                    return false;
                }
                current += quote;
 
                if (!HttpDateParser.TryParse(input.AsSpan(dateStartIndex, current - dateStartIndex), out DateTimeOffset temp))
                {
                    return false;
                }
 
                date = temp;
 
                current++; // skip closing '"'
                current += HttpRuleParser.GetWhitespaceLength(input, current);
            }
 
            return true;
        }
 
        object ICloneable.Clone()
        {
            return new WarningHeaderValue(this);
        }
 
        private static void CheckCode(int code)
        {
            ArgumentOutOfRangeException.ThrowIfNegative(code);
            ArgumentOutOfRangeException.ThrowIfGreaterThan(code, 999);
        }
 
        private static void CheckAgent(string agent)
        {
            ArgumentException.ThrowIfNullOrEmpty(agent);
 
            // 'Agent' can either be a host or a token. Since a token is a valid host, we only verify if the value
            // is a valid host.
            if (HttpRuleParser.GetHostLength(agent, 0, true) != agent.Length)
            {
                throw new FormatException(SR.Format(SR.net_http_headers_invalid_value, agent));
            }
        }
    }
}