File: System\Net\Http\Headers\ViaHeaderValue.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
{
    public class ViaHeaderValue : ICloneable
    {
        private readonly string? _protocolName;
        private readonly string _protocolVersion;
        private readonly string _receivedBy;
        private readonly string? _comment;
 
        public string? ProtocolName => _protocolName;
 
        public string ProtocolVersion => _protocolVersion;
 
        public string ReceivedBy => _receivedBy;
 
        public string? Comment => _comment;
 
        private ViaHeaderValue(string protocolVersion, string receivedBy, string? protocolName, string? comment, bool _)
        {
#if DEBUG
            // This constructor should only be used with already validated values.
            new ViaHeaderValue(protocolVersion, receivedBy, protocolName, comment);
#endif
 
            _protocolVersion = protocolVersion;
            _receivedBy = receivedBy;
            _protocolName = protocolName;
            _comment = comment;
        }
 
        public ViaHeaderValue(string protocolVersion, string receivedBy)
            : this(protocolVersion, receivedBy, null, null)
        {
        }
 
        public ViaHeaderValue(string protocolVersion, string receivedBy, string? protocolName)
            : this(protocolVersion, receivedBy, protocolName, null)
        {
        }
 
        public ViaHeaderValue(string protocolVersion, string receivedBy, string? protocolName, string? comment)
        {
            HeaderUtilities.CheckValidToken(protocolVersion);
            CheckReceivedBy(receivedBy);
 
            if (!string.IsNullOrEmpty(protocolName))
            {
                HeaderUtilities.CheckValidToken(protocolName);
                _protocolName = protocolName;
            }
 
            if (!string.IsNullOrEmpty(comment))
            {
                HeaderUtilities.CheckValidComment(comment);
                _comment = comment;
            }
 
            _protocolVersion = protocolVersion;
            _receivedBy = receivedBy;
        }
 
        private ViaHeaderValue(ViaHeaderValue source)
        {
            Debug.Assert(source != null);
 
            _protocolName = source._protocolName;
            _protocolVersion = source._protocolVersion;
            _receivedBy = source._receivedBy;
            _comment = source._comment;
        }
 
        public override string ToString()
        {
            var sb = new ValueStringBuilder(stackalloc char[256]);
 
            if (!string.IsNullOrEmpty(_protocolName))
            {
                sb.Append(_protocolName);
                sb.Append('/');
            }
 
            sb.Append(_protocolVersion);
            sb.Append(' ');
            sb.Append(_receivedBy);
 
            if (!string.IsNullOrEmpty(_comment))
            {
                sb.Append(' ');
                sb.Append(_comment);
            }
 
            return sb.ToString();
        }
 
        public override bool Equals([NotNullWhen(true)] object? obj) =>
            obj is ViaHeaderValue other &&
            // Note that for token and host case-insensitive comparison is used. Comments are compared using case-
            // sensitive comparison.
            string.Equals(_protocolVersion, other._protocolVersion, StringComparison.OrdinalIgnoreCase) &&
            string.Equals(_receivedBy, other._receivedBy, StringComparison.OrdinalIgnoreCase) &&
            string.Equals(_protocolName, other._protocolName, StringComparison.OrdinalIgnoreCase) &&
            string.Equals(_comment, other._comment, StringComparison.Ordinal);
 
        public override int GetHashCode() =>
            HashCode.Combine(
                StringComparer.OrdinalIgnoreCase.GetHashCode(_protocolVersion),
                StringComparer.OrdinalIgnoreCase.GetHashCode(_receivedBy),
                _protocolName is null ? 0 : StringComparer.OrdinalIgnoreCase.GetHashCode(_protocolName),
                _comment);
 
        public static ViaHeaderValue Parse(string input)
        {
            int index = 0;
            return (ViaHeaderValue)GenericHeaderParser.SingleValueViaParser.ParseValue(input, null, ref index);
        }
 
        public static bool TryParse([NotNullWhen(true)] string? input, [NotNullWhen(true)] out ViaHeaderValue? parsedValue)
        {
            int index = 0;
            parsedValue = null;
 
            if (GenericHeaderParser.SingleValueViaParser.TryParseValue(input, null, ref index, out object? output))
            {
                parsedValue = (ViaHeaderValue)output!;
                return true;
            }
            return false;
        }
 
        internal static int GetViaLength(string? input, int startIndex, out object? parsedValue)
        {
            Debug.Assert(startIndex >= 0);
 
            parsedValue = null;
 
            if (string.IsNullOrEmpty(input) || (startIndex >= input.Length))
            {
                return 0;
            }
 
            // Read <protocolName> and <protocolVersion> in '[<protocolName>/]<protocolVersion> <receivedBy> [<comment>]'
            int current = GetProtocolEndIndex(input, startIndex, out string? protocolName, out string? protocolVersion);
 
            // If we reached the end of the string after reading protocolName/Version we return (we expect at least
            // <receivedBy> to follow). If reading protocolName/Version read 0 bytes, we return.
            if ((current == 0) || (current == input.Length))
            {
                return 0;
            }
            Debug.Assert(protocolVersion != null);
 
            // Read <receivedBy> in '[<protocolName>/]<protocolVersion> <receivedBy> [<comment>]'
            int receivedByLength = HttpRuleParser.GetHostLength(input, current, true);
            if (receivedByLength == 0)
            {
                return 0;
            }
 
            string receivedBy = input.Substring(current, receivedByLength);
            current += receivedByLength;
 
            current += HttpRuleParser.GetWhitespaceLength(input, current);
 
            string? comment = null;
            if ((current < input.Length) && (input[current] == '('))
            {
                // We have a <comment> in '[<protocolName>/]<protocolVersion> <receivedBy> [<comment>]'
                if (HttpRuleParser.GetCommentLength(input, current, out int commentLength) != HttpParseResult.Parsed)
                {
                    return 0; // We found a '(' character but it wasn't a valid comment. Abort.
                }
 
                comment = input.Substring(current, commentLength);
 
                current += commentLength;
                current += HttpRuleParser.GetWhitespaceLength(input, current);
            }
 
            parsedValue = new ViaHeaderValue(protocolVersion, receivedBy, protocolName, comment, false);
            return current - startIndex;
        }
 
        private static int GetProtocolEndIndex(string input, int startIndex, out string? protocolName,
            out string? protocolVersion)
        {
            // We have a string of the form '[<protocolName>/]<protocolVersion> <receivedBy> [<comment>]'. The first
            // token may either be the protocol name or protocol version. We'll only find out after reading the token
            // and by looking at the following character: If it is a '/' we just parsed the protocol name, otherwise
            // the protocol version.
            protocolName = null;
            protocolVersion = null;
 
            int current = startIndex;
            int protocolVersionOrNameLength = HttpRuleParser.GetTokenLength(input, current);
 
            if (protocolVersionOrNameLength == 0)
            {
                return 0;
            }
 
            current = startIndex + protocolVersionOrNameLength;
            int whitespaceLength = HttpRuleParser.GetWhitespaceLength(input, current);
            current += whitespaceLength;
 
            if (current == input.Length)
            {
                return 0;
            }
 
            if (input[current] == '/')
            {
                // We parsed the protocol name
                protocolName = input.Substring(startIndex, protocolVersionOrNameLength);
 
                current++; // skip the '/' delimiter
                current += HttpRuleParser.GetWhitespaceLength(input, current);
 
                protocolVersionOrNameLength = HttpRuleParser.GetTokenLength(input, current);
 
                if (protocolVersionOrNameLength == 0)
                {
                    return 0; // We have a string "<token>/" followed by non-token chars. This is invalid.
                }
 
                protocolVersion = input.Substring(current, protocolVersionOrNameLength);
 
                current += protocolVersionOrNameLength;
                whitespaceLength = HttpRuleParser.GetWhitespaceLength(input, current);
                current += whitespaceLength;
            }
            else
            {
                protocolVersion = input.Substring(startIndex, protocolVersionOrNameLength);
            }
 
            if (whitespaceLength == 0)
            {
                return 0; // We were able to parse [<protocolName>/]<protocolVersion> but it wasn't followed by a WS
            }
 
            return current;
        }
 
        object ICloneable.Clone()
        {
            return new ViaHeaderValue(this);
        }
 
        private static void CheckReceivedBy(string receivedBy)
        {
            ArgumentException.ThrowIfNullOrEmpty(receivedBy);
 
            // 'receivedBy' 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(receivedBy, 0, true) != receivedBy.Length)
            {
                throw new FormatException(SR.Format(System.Globalization.CultureInfo.InvariantCulture, SR.net_http_headers_invalid_value, receivedBy));
            }
        }
    }
}