File: CacheControlHeaderValue.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;
 
/// <summary>
/// Represents the <c>Cache-Control</c> HTTP header.
/// </summary>
public class CacheControlHeaderValue
{
    /// <summary>
    /// A constant for the <c>public</c> cache-control directive.
    /// </summary>
    public static readonly string PublicString = "public";
 
    /// <summary>
    /// A constant for the <c>private</c> cache-control directive.
    /// </summary>
    public static readonly string PrivateString = "private";
 
    /// <summary>
    /// A constant for the <c>max-age</c> cache-control directive.
    /// </summary>
    public static readonly string MaxAgeString = "max-age";
 
    /// <summary>
    /// A constant for the <c>s-maxage</c> cache-control directive.
    /// </summary>
    public static readonly string SharedMaxAgeString = "s-maxage";
 
    /// <summary>
    /// A constant for the <c>no-cache</c> cache-control directive.
    /// </summary>
    public static readonly string NoCacheString = "no-cache";
 
    /// <summary>
    /// A constant for the <c>no-store</c> cache-control directive.
    /// </summary>
    public static readonly string NoStoreString = "no-store";
 
    /// <summary>
    /// A constant for the <c>max-stale</c> cache-control directive.
    /// </summary>
    public static readonly string MaxStaleString = "max-stale";
 
    /// <summary>
    /// A constant for the <c>min-fresh</c> cache-control directive.
    /// </summary>
    public static readonly string MinFreshString = "min-fresh";
 
    /// <summary>
    /// A constant for the <c>no-transform</c> cache-control directive.
    /// </summary>
    public static readonly string NoTransformString = "no-transform";
 
    /// <summary>
    /// A constant for the <c>only-if-cached</c> cache-control directive.
    /// </summary>
    public static readonly string OnlyIfCachedString = "only-if-cached";
 
    /// <summary>
    /// A constant for the <c>must-revalidate</c> cache-control directive.
    /// </summary>
    public static readonly string MustRevalidateString = "must-revalidate";
 
    /// <summary>
    /// A constant for the <c>proxy-revalidate</c> cache-control directive.
    /// </summary>
    public static readonly string ProxyRevalidateString = "proxy-revalidate";
 
    // The Cache-Control header is special: It is a header supporting a list of values, but we represent the list
    // as _one_ instance of CacheControlHeaderValue. I.e we set 'SupportsMultipleValues' to 'true' since it is
    // OK to have multiple Cache-Control headers in a request/response message. However, after parsing all
    // Cache-Control headers, only one instance of CacheControlHeaderValue is created (if all headers contain valid
    // values, otherwise we may have multiple strings containing the invalid values).
    private static readonly HttpHeaderParser<CacheControlHeaderValue> Parser
        = new GenericHeaderParser<CacheControlHeaderValue>(true, GetCacheControlLength);
 
    private static readonly Action<StringSegment> CheckIsValidTokenAction = CheckIsValidToken;
 
    private bool _noCache;
    private ICollection<StringSegment>? _noCacheHeaders;
    private bool _noStore;
    private TimeSpan? _maxAge;
    private TimeSpan? _sharedMaxAge;
    private bool _maxStale;
    private TimeSpan? _maxStaleLimit;
    private TimeSpan? _minFresh;
    private bool _noTransform;
    private bool _onlyIfCached;
    private bool _public;
    private bool _private;
    private ICollection<StringSegment>? _privateHeaders;
    private bool _mustRevalidate;
    private bool _proxyRevalidate;
    private IList<NameValueHeaderValue>? _extensions;
 
    /// <summary>
    /// Initializes a new instance of <see cref="CacheControlHeaderValue"/>.
    /// </summary>
    public CacheControlHeaderValue()
    {
        // This type is unique in that there is no single required parameter.
    }
 
    /// <summary>
    /// Gets or sets a value for the <c>no-cache</c> directive.
    /// <para>
    /// Configuring no-cache indicates that the client must re-validate cached responses with the original server
    /// before using it.
    /// </para>
    /// </summary>
    /// <remarks>See <see href="https://tools.ietf.org/html/rfc7234#section-5.2.1.4"/>.</remarks>
    public bool NoCache
    {
        get { return _noCache; }
        set { _noCache = value; }
    }
 
    /// <summary>
    /// Gets a collection of field names in the "no-cache" directive in a cache-control header field on an HTTP response.
    /// </summary>
    public ICollection<StringSegment> NoCacheHeaders
    {
        get
        {
            if (_noCacheHeaders == null)
            {
                _noCacheHeaders = new ObjectCollection<StringSegment>(CheckIsValidTokenAction);
            }
            return _noCacheHeaders;
        }
    }
 
    /// <summary>
    /// Gets or sets a value for the <c>no-store</c> directive.
    /// <para>
    /// Configuring no-store indicates that the response may not be stored in any cache.
    /// </para>
    /// </summary>
    /// <remarks>See <see href="https://tools.ietf.org/html/rfc7234#section-5.2.1.5"/>.</remarks>
    public bool NoStore
    {
        get { return _noStore; }
        set { _noStore = value; }
    }
 
    /// <summary>
    /// Gets or sets a value for the <c>max-age</c> directive.
    /// <para>
    /// max-age specifies the maximum amount of time the response is considered fresh.
    /// </para>
    /// </summary>
    /// <remarks>See <see href="https://tools.ietf.org/html/rfc7234#section-5.2.1.1"/>.</remarks>
    public TimeSpan? MaxAge
    {
        get { return _maxAge; }
        set { _maxAge = value; }
    }
 
    /// <summary>
    /// Gets or sets a value for the <c>s-maxage</c> directive.
    /// <para>
    /// Overrides <see cref="MaxAge">max-age</see>, but only for shared caches (such as proxies).
    /// </para>
    /// </summary>
    /// <remarks>See <see href="https://tools.ietf.org/html/rfc7234#section-5.2.2.9"/>.</remarks>
    public TimeSpan? SharedMaxAge
    {
        get { return _sharedMaxAge; }
        set { _sharedMaxAge = value; }
    }
 
    /// <summary>
    /// Gets or sets a value that determines if the <c>max-stale</c> is included.
    /// <para>
    /// <c>max-stale</c> that the client will accept stale responses. The maximum tolerance for staleness
    /// is specified by <see cref="MaxStaleLimit"/>.
    /// </para>
    /// </summary>
    /// <remarks>See <see href="https://tools.ietf.org/html/rfc7234#section-5.2.1.2"/>.</remarks>
    public bool MaxStale
    {
        get { return _maxStale; }
        set { _maxStale = value; }
    }
 
    /// <summary>
    /// Gets or sets a value for the <c>max-stale</c> directive.
    /// <para>
    /// Indicates the maximum duration an HTTP client is willing to accept a response that has exceeded its expiration time.
    /// </para>
    /// </summary>
    /// <remarks>See <see href="https://tools.ietf.org/html/rfc7234#section-5.2.1.2"/>.</remarks>
    public TimeSpan? MaxStaleLimit
    {
        get { return _maxStaleLimit; }
        set { _maxStaleLimit = value; }
    }
 
    /// <summary>
    /// Gets or sets a value for the <c>min-fresh</c> directive.
    /// <para>
    /// Indicates the freshness lifetime that an HTTP client is willing to accept a response.
    /// </para>
    /// </summary>
    /// <remarks>See <see href="https://tools.ietf.org/html/rfc7234#section-5.2.1.3"/>.</remarks>
    public TimeSpan? MinFresh
    {
        get { return _minFresh; }
        set { _minFresh = value; }
    }
 
    /// <summary>
    /// Gets or sets a value for the <c>no-transform</c> request directive.
    /// <para>
    /// Forbids intermediate caches or proxies from editing the response payload.
    /// </para>
    /// </summary>
    /// <remarks>See <see href="https://tools.ietf.org/html/rfc7234#section-5.2.1.6"/>.</remarks>
    public bool NoTransform
    {
        get { return _noTransform; }
        set { _noTransform = value; }
    }
 
    /// <summary>
    /// Gets or sets a value for the <c>only-if-cached</c> request directive.
    /// <para>
    /// Indicates that the client only wishes to obtain a stored response
    /// </para>
    /// </summary>
    /// <remarks>See <see href="https://tools.ietf.org/html/rfc7234#section-5.2.1.7"/>.</remarks>
    public bool OnlyIfCached
    {
        get { return _onlyIfCached; }
        set { _onlyIfCached = value; }
    }
 
    /// <summary>
    /// Gets or sets a value that determines if the <c>public</c> response directive is included.
    /// <para>
    /// Indicates that the response may be stored by any cache.
    /// </para>
    /// </summary>
    /// <remarks>See <see href="https://tools.ietf.org/html/rfc7234#section-5.2.2.5"/>.</remarks>
    public bool Public
    {
        get { return _public; }
        set { _public = value; }
    }
 
    /// <summary>
    /// Gets or sets a value that determines if the <c>private</c> response directive is included.
    /// <para>
    /// Indicates that the response may not be stored by a shared cache.
    /// </para>
    /// </summary>
    /// <remarks>See <see href="https://tools.ietf.org/html/rfc7234#section-5.2.2.6"/>.</remarks>
    public bool Private
    {
        get { return _private; }
        set { _private = value; }
    }
 
    /// <summary>
    /// Gets a collection of field names in the "private" directive in a cache-control header field on an HTTP response.
    /// </summary>
    public ICollection<StringSegment> PrivateHeaders
    {
        get
        {
            if (_privateHeaders == null)
            {
                _privateHeaders = new ObjectCollection<StringSegment>(CheckIsValidTokenAction);
            }
            return _privateHeaders;
        }
    }
 
    /// <summary>
    /// Gets or sets a value that determines if the <c>must-revalidate</c> response directive is included.
    /// <para>
    /// Indicates that caches must revalidate the use of stale caches with the origin server before their use.
    /// </para>
    /// </summary>
    /// <remarks>See <see href="https://tools.ietf.org/html/rfc7234#section-5.2.2.1"/>.</remarks>
    public bool MustRevalidate
    {
        get { return _mustRevalidate; }
        set { _mustRevalidate = value; }
    }
 
    /// <summary>
    /// Gets or sets a value that determines if the <c>proxy-validate</c> response directive is included.
    /// <para>
    /// Indicates that shared caches must revalidate the use of stale caches with the origin server before their use.
    /// </para>
    /// </summary>
    /// <remarks>See <see href="https://tools.ietf.org/html/rfc7234#section-5.2.2.1"/>.</remarks>
    public bool ProxyRevalidate
    {
        get { return _proxyRevalidate; }
        set { _proxyRevalidate = value; }
    }
 
    /// <summary>
    /// Gets cache-extension tokens, each with an optional assigned value.
    /// </summary>
    public IList<NameValueHeaderValue> Extensions
    {
        get
        {
            if (_extensions == null)
            {
                _extensions = new ObjectCollection<NameValueHeaderValue>();
            }
            return _extensions;
        }
    }
 
    /// <inheritdoc />
    public override string ToString()
    {
        var sb = new StringBuilder();
 
        AppendValueIfRequired(sb, _noStore, NoStoreString);
        AppendValueIfRequired(sb, _noTransform, NoTransformString);
        AppendValueIfRequired(sb, _onlyIfCached, OnlyIfCachedString);
        AppendValueIfRequired(sb, _public, PublicString);
        AppendValueIfRequired(sb, _mustRevalidate, MustRevalidateString);
        AppendValueIfRequired(sb, _proxyRevalidate, ProxyRevalidateString);
 
        if (_noCache)
        {
            AppendValueWithSeparatorIfRequired(sb, NoCacheString);
            if ((_noCacheHeaders != null) && (_noCacheHeaders.Count > 0))
            {
                sb.Append("=\"");
                AppendValues(sb, _noCacheHeaders);
                sb.Append('\"');
            }
        }
 
        if (_maxAge.HasValue)
        {
            AppendValueWithSeparatorIfRequired(sb, MaxAgeString);
            sb.Append('=');
            sb.Append(((int)_maxAge.GetValueOrDefault().TotalSeconds).ToString(NumberFormatInfo.InvariantInfo));
        }
 
        if (_sharedMaxAge.HasValue)
        {
            AppendValueWithSeparatorIfRequired(sb, SharedMaxAgeString);
            sb.Append('=');
            sb.Append(((int)_sharedMaxAge.GetValueOrDefault().TotalSeconds).ToString(NumberFormatInfo.InvariantInfo));
        }
 
        if (_maxStale)
        {
            AppendValueWithSeparatorIfRequired(sb, MaxStaleString);
            if (_maxStaleLimit.HasValue)
            {
                sb.Append('=');
                sb.Append(((int)_maxStaleLimit.GetValueOrDefault().TotalSeconds).ToString(NumberFormatInfo.InvariantInfo));
            }
        }
 
        if (_minFresh.HasValue)
        {
            AppendValueWithSeparatorIfRequired(sb, MinFreshString);
            sb.Append('=');
            sb.Append(((int)_minFresh.GetValueOrDefault().TotalSeconds).ToString(NumberFormatInfo.InvariantInfo));
        }
 
        if (_private)
        {
            AppendValueWithSeparatorIfRequired(sb, PrivateString);
            if ((_privateHeaders != null) && (_privateHeaders.Count > 0))
            {
                sb.Append("=\"");
                AppendValues(sb, _privateHeaders);
                sb.Append('\"');
            }
        }
 
        NameValueHeaderValue.ToString(_extensions, ',', false, sb);
 
        return sb.ToString();
    }
 
    /// <inheritdoc />
    public override bool Equals(object? obj)
    {
        var other = obj as CacheControlHeaderValue;
 
        if (other == null)
        {
            return false;
        }
 
        if ((_noCache != other._noCache) || (_noStore != other._noStore) || (_maxAge != other._maxAge) ||
            (_sharedMaxAge != other._sharedMaxAge) || (_maxStale != other._maxStale) ||
            (_maxStaleLimit != other._maxStaleLimit) || (_minFresh != other._minFresh) ||
            (_noTransform != other._noTransform) || (_onlyIfCached != other._onlyIfCached) ||
            (_public != other._public) || (_private != other._private) ||
            (_mustRevalidate != other._mustRevalidate) || (_proxyRevalidate != other._proxyRevalidate))
        {
            return false;
        }
 
        if (!HeaderUtilities.AreEqualCollections(_noCacheHeaders, other._noCacheHeaders,
            StringSegmentComparer.OrdinalIgnoreCase))
        {
            return false;
        }
 
        if (!HeaderUtilities.AreEqualCollections(_privateHeaders, other._privateHeaders,
            StringSegmentComparer.OrdinalIgnoreCase))
        {
            return false;
        }
 
        if (!HeaderUtilities.AreEqualCollections(_extensions, other._extensions))
        {
            return false;
        }
 
        return true;
    }
 
    /// <inheritdoc />
    public override int GetHashCode()
    {
        // Use a different bit for bool fields: bool.GetHashCode() will return 0 (false) or 1 (true). So we would
        // end up having the same hash code for e.g. two instances where one has only noCache set and the other
        // only noStore.
        int result = _noCache.GetHashCode() ^ (_noStore.GetHashCode() << 1) ^ (_maxStale.GetHashCode() << 2) ^
            (_noTransform.GetHashCode() << 3) ^ (_onlyIfCached.GetHashCode() << 4) ^
            (_public.GetHashCode() << 5) ^ (_private.GetHashCode() << 6) ^
            (_mustRevalidate.GetHashCode() << 7) ^ (_proxyRevalidate.GetHashCode() << 8);
 
        // XOR the hashcode of timespan values with different numbers to make sure two instances with the same
        // timespan set on different fields result in different hashcodes.
        result = result ^ (_maxAge.HasValue ? _maxAge.GetValueOrDefault().GetHashCode() ^ 1 : 0) ^
            (_sharedMaxAge.HasValue ? _sharedMaxAge.GetValueOrDefault().GetHashCode() ^ 2 : 0) ^
            (_maxStaleLimit.HasValue ? _maxStaleLimit.GetValueOrDefault().GetHashCode() ^ 4 : 0) ^
            (_minFresh.HasValue ? _minFresh.GetValueOrDefault().GetHashCode() ^ 8 : 0);
 
        if ((_noCacheHeaders != null) && (_noCacheHeaders.Count > 0))
        {
            foreach (var noCacheHeader in _noCacheHeaders)
            {
                result = result ^ StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(noCacheHeader);
            }
        }
 
        if ((_privateHeaders != null) && (_privateHeaders.Count > 0))
        {
            foreach (var privateHeader in _privateHeaders)
            {
                result = result ^ StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(privateHeader);
            }
        }
 
        if ((_extensions != null) && (_extensions.Count > 0))
        {
            foreach (var extension in _extensions)
            {
                result = result ^ extension.GetHashCode();
            }
        }
 
        return result;
    }
 
    /// <summary>
    /// Parses <paramref name="input"/> as a <see cref="CacheControlHeaderValue"/> value.
    /// </summary>
    /// <param name="input">The values to parse.</param>
    /// <returns>The parsed values.</returns>
    public static CacheControlHeaderValue Parse(StringSegment input)
    {
        var index = 0;
        // Cache-Control is unusual because there are no required values so the parser will succeed for an empty string, but still return null.
        var result = Parser.ParseValue(input, ref index);
        if (result == null)
        {
            throw new FormatException("No cache directives found.");
        }
        return result;
    }
 
    /// <summary>
    /// Attempts to parse the specified <paramref name="input"/> as a <see cref="CacheControlHeaderValue"/>.
    /// </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="CacheControlHeaderValue"/>, otherwise <see langword="false"/>.</returns>
    public static bool TryParse(StringSegment input, [NotNullWhen(true)] out CacheControlHeaderValue? parsedValue)
    {
        var index = 0;
        // Cache-Control is unusual because there are no required values so the parser will succeed for an empty string, but still return null.
        if (Parser.TryParseValue(input, ref index, out parsedValue) && parsedValue != null)
        {
            return true;
        }
        parsedValue = null;
        return false;
    }
 
    private static int GetCacheControlLength(StringSegment input, int startIndex, out CacheControlHeaderValue? parsedValue)
    {
        Contract.Requires(startIndex >= 0);
 
        parsedValue = null;
 
        if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length))
        {
            return 0;
        }
 
        // Cache-Control header consists of a list of name/value pairs, where the value is optional. So use an
        // instance of NameValueHeaderParser to parse the string.
        var current = startIndex;
        var nameValueList = new List<NameValueHeaderValue>();
        while (current < input.Length)
        {
            if (!NameValueHeaderValue.MultipleValueParser.TryParseValue(input, ref current, out var nameValue))
            {
                return 0;
            }
 
            if (nameValue != null)
            {
                nameValueList.Add(nameValue);
            }
        }
 
        // If we get here, we were able to successfully parse the string as list of name/value pairs. Now analyze
        // the name/value pairs.
 
        // Cache-Control is a header supporting lists of values. However, expose the header as an instance of
        // CacheControlHeaderValue.
        var result = new CacheControlHeaderValue();
 
        if (!TrySetCacheControlValues(result, nameValueList))
        {
            return 0;
        }
 
        parsedValue = result;
 
        // If we get here we successfully parsed the whole string.
        return input.Length - startIndex;
    }
 
    private static bool TrySetCacheControlValues(
        CacheControlHeaderValue cc,
        List<NameValueHeaderValue> nameValueList)
    {
        for (var i = 0; i < nameValueList.Count; i++)
        {
            var nameValue = nameValueList[i];
            var name = nameValue.Name;
            var success = true;
 
            switch (name.Length)
            {
                case 6:
                    if (StringSegment.Equals(PublicString, name, StringComparison.OrdinalIgnoreCase))
                    {
                        success = TrySetTokenOnlyValue(nameValue, ref cc._public);
                    }
                    else
                    {
                        goto default;
                    }
                    break;
 
                case 7:
                    if (StringSegment.Equals(MaxAgeString, name, StringComparison.OrdinalIgnoreCase))
                    {
                        success = TrySetTimeSpan(nameValue, ref cc._maxAge);
                    }
                    else if (StringSegment.Equals(PrivateString, name, StringComparison.OrdinalIgnoreCase))
                    {
                        success = TrySetOptionalTokenList(nameValue, ref cc._private, ref cc._privateHeaders);
                    }
                    else
                    {
                        goto default;
                    }
                    break;
 
                case 8:
                    if (StringSegment.Equals(NoCacheString, name, StringComparison.OrdinalIgnoreCase))
                    {
                        success = TrySetOptionalTokenList(nameValue, ref cc._noCache, ref cc._noCacheHeaders);
                    }
                    else if (StringSegment.Equals(NoStoreString, name, StringComparison.OrdinalIgnoreCase))
                    {
                        success = TrySetTokenOnlyValue(nameValue, ref cc._noStore);
                    }
                    else if (StringSegment.Equals(SharedMaxAgeString, name, StringComparison.OrdinalIgnoreCase))
                    {
                        success = TrySetTimeSpan(nameValue, ref cc._sharedMaxAge);
                    }
                    else
                    {
                        goto default;
                    }
                    break;
 
                case 9:
                    if (StringSegment.Equals(MaxStaleString, name, StringComparison.OrdinalIgnoreCase))
                    {
                        success = ((nameValue.Value == null) || TrySetTimeSpan(nameValue, ref cc._maxStaleLimit));
                        if (success)
                        {
                            cc._maxStale = true;
                        }
                    }
                    else if (StringSegment.Equals(MinFreshString, name, StringComparison.OrdinalIgnoreCase))
                    {
                        success = TrySetTimeSpan(nameValue, ref cc._minFresh);
                    }
                    else
                    {
                        goto default;
                    }
                    break;
 
                case 12:
                    if (StringSegment.Equals(NoTransformString, name, StringComparison.OrdinalIgnoreCase))
                    {
                        success = TrySetTokenOnlyValue(nameValue, ref cc._noTransform);
                    }
                    else
                    {
                        goto default;
                    }
                    break;
 
                case 14:
                    if (StringSegment.Equals(OnlyIfCachedString, name, StringComparison.OrdinalIgnoreCase))
                    {
                        success = TrySetTokenOnlyValue(nameValue, ref cc._onlyIfCached);
                    }
                    else
                    {
                        goto default;
                    }
                    break;
 
                case 15:
                    if (StringSegment.Equals(MustRevalidateString, name, StringComparison.OrdinalIgnoreCase))
                    {
                        success = TrySetTokenOnlyValue(nameValue, ref cc._mustRevalidate);
                    }
                    else
                    {
                        goto default;
                    }
                    break;
 
                case 16:
                    if (StringSegment.Equals(ProxyRevalidateString, name, StringComparison.OrdinalIgnoreCase))
                    {
                        success = TrySetTokenOnlyValue(nameValue, ref cc._proxyRevalidate);
                    }
                    else
                    {
                        goto default;
                    }
                    break;
 
                default:
                    cc.Extensions.Add(nameValue); // success is always true
                    break;
            }
 
            if (!success)
            {
                return false;
            }
        }
 
        return true;
    }
 
    private static bool TrySetTokenOnlyValue(NameValueHeaderValue nameValue, ref bool boolField)
    {
        if (nameValue.Value != null)
        {
            return false;
        }
 
        boolField = true;
        return true;
    }
 
    private static bool TrySetOptionalTokenList(
        NameValueHeaderValue nameValue,
        ref bool boolField,
        ref ICollection<StringSegment>? destination)
    {
        if (nameValue.Value == null)
        {
            boolField = true;
            return true;
        }
 
        // We need the string to be at least 3 chars long: 2x quotes and at least 1 character. Also make sure we
        // have a quoted string. Note that NameValueHeaderValue will never have leading/trailing whitespaces.
        var valueString = nameValue.Value;
        if ((valueString.Length < 3) || (valueString[0] != '\"') || (valueString[valueString.Length - 1] != '\"'))
        {
            return false;
        }
 
        // We have a quoted string. Now verify that the string contains a list of valid tokens separated by ','.
        var current = 1; // skip the initial '"' character.
        var maxLength = valueString.Length - 1; // -1 because we don't want to parse the final '"'.
        var originalValueCount = destination == null ? 0 : destination.Count;
        while (current < maxLength)
        {
            current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(valueString, current, true,
                out var separatorFound);
 
            if (current == maxLength)
            {
                break;
            }
 
            var tokenLength = HttpRuleParser.GetTokenLength(valueString, current);
 
            if (tokenLength == 0)
            {
                // We already skipped whitespaces and separators. If we don't have a token it must be an invalid
                // character.
                return false;
            }
 
            if (destination == null)
            {
                destination = new ObjectCollection<StringSegment>(CheckIsValidTokenAction);
            }
 
            destination.Add(valueString.Subsegment(current, tokenLength));
 
            current = current + tokenLength;
        }
 
        // After parsing a valid token list, we expect to have at least one value
        if ((destination != null) && (destination.Count > originalValueCount))
        {
            boolField = true;
            return true;
        }
 
        return false;
    }
 
    private static bool TrySetTimeSpan(NameValueHeaderValue nameValue, ref TimeSpan? timeSpan)
    {
        if (nameValue.Value == null)
        {
            return false;
        }
 
        int seconds;
        if (!HeaderUtilities.TryParseNonNegativeInt32(nameValue.Value, out seconds))
        {
            return false;
        }
 
        timeSpan = new TimeSpan(0, 0, seconds);
 
        return true;
    }
 
    private static void AppendValueIfRequired(StringBuilder sb, bool appendValue, string value)
    {
        if (appendValue)
        {
            AppendValueWithSeparatorIfRequired(sb, value);
        }
    }
 
    private static void AppendValueWithSeparatorIfRequired(StringBuilder sb, string value)
    {
        if (sb.Length > 0)
        {
            sb.Append(", ");
        }
        sb.Append(value);
    }
 
    private static void AppendValues(StringBuilder sb, IEnumerable<StringSegment> values)
    {
        var first = true;
        foreach (var value in values)
        {
            if (first)
            {
                first = false;
            }
            else
            {
                sb.Append(", ");
            }
 
            sb.Append(value.AsSpan());
        }
    }
 
    private static void CheckIsValidToken(StringSegment item)
    {
        HeaderUtilities.CheckValidToken(item, nameof(item));
    }
}