File: System\Net\Http\Headers\CacheControlHeaderValue.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.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
 
namespace System.Net.Http.Headers
{
    public class CacheControlHeaderValue : ICloneable
    {
        private const string maxAgeString = "max-age";
        private const string maxStaleString = "max-stale";
        private const string minFreshString = "min-fresh";
        private const string mustRevalidateString = "must-revalidate";
        private const string noCacheString = "no-cache";
        private const string noStoreString = "no-store";
        private const string noTransformString = "no-transform";
        private const string onlyIfCachedString = "only-if-cached";
        private const string privateString = "private";
        private const string proxyRevalidateString = "proxy-revalidate";
        private const string publicString = "public";
        private const string sharedMaxAgeString = "s-maxage";
 
        private static readonly GenericHeaderParser s_nameValueListParser = GenericHeaderParser.MultipleValueNameValueParser;
 
        [Flags]
        private enum Flags : int
        {
            None = 0,
            MaxAgeHasValue = 1 << 0,
            SharedMaxAgeHasValue = 1 << 1,
            MaxStaleLimitHasValue = 1 << 2,
            MinFreshHasValue = 1 << 3,
            NoCache = 1 << 4,
            NoStore = 1 << 5,
            MaxStale = 1 << 6,
            NoTransform = 1 << 7,
            OnlyIfCached = 1 << 8,
            Public = 1 << 9,
            Private = 1 << 10,
            MustRevalidate = 1 << 11,
            ProxyRevalidate = 1 << 12,
        }
 
        private Flags _flags;
        private TokenObjectCollection? _noCacheHeaders;
        private TimeSpan _maxAge;
        private TimeSpan _sharedMaxAge;
        private TimeSpan _maxStaleLimit;
        private TimeSpan _minFresh;
        private TokenObjectCollection? _privateHeaders;
        private UnvalidatedObjectCollection<NameValueHeaderValue>? _extensions;
 
        private void SetTimeSpan(ref TimeSpan fieldRef, Flags flag, TimeSpan? value)
        {
            fieldRef = value.GetValueOrDefault();
            SetFlag(flag, value.HasValue);
        }
 
        private void SetFlag(Flags flag, bool value)
        {
            Debug.Assert(sizeof(Flags) == sizeof(int));
 
            // This type is not thread-safe, but we do a minimal amount of synchronization to ensure
            // that concurrent modifications of different properties don't interfere with each other.
            if (value)
            {
                Interlocked.Or(ref Unsafe.As<Flags, int>(ref _flags), (int)flag);
            }
            else
            {
                Interlocked.And(ref Unsafe.As<Flags, int>(ref _flags), (int)~flag);
            }
        }
 
        public bool NoCache
        {
            get => (_flags & Flags.NoCache) != 0;
            set => SetFlag(Flags.NoCache, value);
        }
 
        public ICollection<string> NoCacheHeaders => _noCacheHeaders ??= new TokenObjectCollection();
 
        public bool NoStore
        {
            get => (_flags & Flags.NoStore) != 0;
            set => SetFlag(Flags.NoStore, value);
        }
 
        public TimeSpan? MaxAge
        {
            get => (_flags & Flags.MaxAgeHasValue) == 0 ? null : _maxAge;
            set => SetTimeSpan(ref _maxAge, Flags.MaxAgeHasValue, value);
        }
 
        public TimeSpan? SharedMaxAge
        {
            get => (_flags & Flags.SharedMaxAgeHasValue) == 0 ? null : _sharedMaxAge;
            set => SetTimeSpan(ref _sharedMaxAge, Flags.SharedMaxAgeHasValue, value);
        }
 
        public bool MaxStale
        {
            get => (_flags & Flags.MaxStale) != 0;
            set => SetFlag(Flags.MaxStale, value);
        }
 
        public TimeSpan? MaxStaleLimit
        {
            get => (_flags & Flags.MaxStaleLimitHasValue) == 0 ? null : _maxStaleLimit;
            set => SetTimeSpan(ref _maxStaleLimit, Flags.MaxStaleLimitHasValue, value);
        }
 
        public TimeSpan? MinFresh
        {
            get => (_flags & Flags.MinFreshHasValue) == 0 ? null : _minFresh;
            set => SetTimeSpan(ref _minFresh, Flags.MinFreshHasValue, value);
        }
 
        public bool NoTransform
        {
            get => (_flags & Flags.NoTransform) != 0;
            set => SetFlag(Flags.NoTransform, value);
        }
 
        public bool OnlyIfCached
        {
            get => (_flags & Flags.OnlyIfCached) != 0;
            set => SetFlag(Flags.OnlyIfCached, value);
        }
 
        public bool Public
        {
            get => (_flags & Flags.Public) != 0;
            set => SetFlag(Flags.Public, value);
        }
 
        public bool Private
        {
            get => (_flags & Flags.Private) != 0;
            set => SetFlag(Flags.Private, value);
        }
 
        public ICollection<string> PrivateHeaders => _privateHeaders ??= new TokenObjectCollection();
 
        public bool MustRevalidate
        {
            get => (_flags & Flags.MustRevalidate) != 0;
            set => SetFlag(Flags.MustRevalidate, value);
        }
 
        public bool ProxyRevalidate
        {
            get => (_flags & Flags.ProxyRevalidate) != 0;
            set => SetFlag(Flags.ProxyRevalidate, value);
        }
 
        public ICollection<NameValueHeaderValue> Extensions => _extensions ??= new UnvalidatedObjectCollection<NameValueHeaderValue>();
 
        public CacheControlHeaderValue()
        {
        }
 
        private CacheControlHeaderValue(CacheControlHeaderValue source)
        {
            Debug.Assert(source != null);
 
            _flags = source._flags;
            _maxAge = source._maxAge;
            _sharedMaxAge = source._sharedMaxAge;
            _maxStaleLimit = source._maxStaleLimit;
            _minFresh = source._minFresh;
 
            if (source._noCacheHeaders != null)
            {
                foreach (string noCacheHeader in source._noCacheHeaders)
                {
                    NoCacheHeaders.Add(noCacheHeader);
                }
            }
 
            if (source._privateHeaders != null)
            {
                foreach (string privateHeader in source._privateHeaders)
                {
                    PrivateHeaders.Add(privateHeader);
                }
            }
 
            _extensions = source._extensions.Clone();
        }
 
        public override string ToString()
        {
            StringBuilder sb = StringBuilderCache.Acquire();
 
            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 ((_flags & Flags.MaxAgeHasValue) != 0)
            {
                AppendValueWithSeparatorIfRequired(sb, maxAgeString);
                sb.Append('=');
                int maxAge = (int)_maxAge.TotalSeconds;
                if (maxAge >= 0)
                {
                    sb.Append(maxAge);
                }
                else
                {
                    // In the corner case where the value is negative, ensure it uses
                    // the invariant's negative sign rather than the current culture's.
                    sb.Append(NumberFormatInfo.InvariantInfo, $"{maxAge}");
                }
            }
 
            if ((_flags & Flags.SharedMaxAgeHasValue) != 0)
            {
                AppendValueWithSeparatorIfRequired(sb, sharedMaxAgeString);
                sb.Append('=');
                int sharedMaxAge = (int)_sharedMaxAge.TotalSeconds;
                if (sharedMaxAge >= 0)
                {
                    sb.Append(sharedMaxAge);
                }
                else
                {
                    // In the corner case where the value is negative, ensure it uses
                    // the invariant's negative sign rather than the current culture's.
                    sb.Append(NumberFormatInfo.InvariantInfo, $"{sharedMaxAge}");
                }
            }
 
            if (MaxStale)
            {
                AppendValueWithSeparatorIfRequired(sb, maxStaleString);
                if ((_flags & Flags.MaxStaleLimitHasValue) != 0)
                {
                    sb.Append('=');
                    int maxStaleLimit = (int)_maxStaleLimit.TotalSeconds;
                    if (maxStaleLimit >= 0)
                    {
                        sb.Append(maxStaleLimit);
                    }
                    else
                    {
                        // In the corner case where the value is negative, ensure it uses
                        // the invariant's negative sign rather than the current culture's.
                        sb.Append(NumberFormatInfo.InvariantInfo, $"{maxStaleLimit}");
                    }
                }
            }
 
            if ((_flags & Flags.MinFreshHasValue) != 0)
            {
                AppendValueWithSeparatorIfRequired(sb, minFreshString);
                sb.Append('=');
                int minFresh = (int)_minFresh.TotalSeconds;
                if (minFresh >= 0)
                {
                    sb.Append(minFresh);
                }
                else
                {
                    // In the corner case where the value is negative, ensure it uses
                    // the invariant's negative sign rather than the current culture's.
                    sb.Append(NumberFormatInfo.InvariantInfo, $"{minFresh}");
                }
            }
 
            if (Private)
            {
                AppendValueWithSeparatorIfRequired(sb, privateString);
                if ((_privateHeaders != null) && (_privateHeaders.Count > 0))
                {
                    sb.Append("=\"");
                    AppendValues(sb, _privateHeaders);
                    sb.Append('\"');
                }
            }
 
            NameValueHeaderValue.ToString(_extensions, ',', false, sb);
 
            return StringBuilderCache.GetStringAndRelease(sb);
        }
 
        public override bool Equals([NotNullWhen(true)] object? obj) =>
            obj is CacheControlHeaderValue other &&
            _flags == other._flags &&
            _maxAge == other._maxAge &&
            _sharedMaxAge == other._sharedMaxAge &&
            _maxStaleLimit == other._maxStaleLimit &&
            _minFresh == other._minFresh &&
            HeaderUtilities.AreEqualCollections(_noCacheHeaders, other._noCacheHeaders, StringComparer.OrdinalIgnoreCase) &&
            HeaderUtilities.AreEqualCollections(_privateHeaders, other._privateHeaders, StringComparer.OrdinalIgnoreCase) &&
            HeaderUtilities.AreEqualCollections(_extensions, other._extensions);
 
        public override int GetHashCode() =>
            HashCode.Combine(
                _flags,
                _maxAge,
                _sharedMaxAge,
                _maxStaleLimit,
                _minFresh,
                (_noCacheHeaders is null ? 0 : _noCacheHeaders.GetHashCode(StringComparer.OrdinalIgnoreCase)),
                (_privateHeaders is null ? 0 : _privateHeaders.GetHashCode(StringComparer.OrdinalIgnoreCase)),
                NameValueHeaderValue.GetHashCode(_extensions));
 
        public static CacheControlHeaderValue Parse(string? input)
        {
            int index = 0;
            return (CacheControlHeaderValue)CacheControlHeaderParser.Parser.ParseValue(input, null, ref index) ?? new CacheControlHeaderValue();
        }
 
        public static bool TryParse(string? input, [NotNullWhen(true)] out CacheControlHeaderValue? parsedValue)
        {
            int index = 0;
            parsedValue = null;
 
            if (CacheControlHeaderParser.Parser.TryParseValue(input, null, ref index, out object? output))
            {
                parsedValue = (CacheControlHeaderValue?)output ?? new CacheControlHeaderValue();
                return true;
            }
            return false;
        }
 
        internal static int GetCacheControlLength(string? input, int startIndex, CacheControlHeaderValue? storeValue,
            out CacheControlHeaderValue? parsedValue)
        {
            Debug.Assert(startIndex >= 0);
 
            parsedValue = null;
 
            if (string.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.
            int current = startIndex;
            List<NameValueHeaderValue> nameValueList = new List<NameValueHeaderValue>();
            while (current < input.Length)
            {
                if (!s_nameValueListParser.TryParseValue(input, null, ref current, out object? nameValue))
                {
                    return 0;
                }
 
                Debug.Assert(nameValue is not null);
                nameValueList.Add((NameValueHeaderValue)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. So if we already have an instance of CacheControlHeaderValue, add the values
            // from this string to the existing instances.
            CacheControlHeaderValue? result = storeValue ?? new CacheControlHeaderValue();
 
            if (!TrySetCacheControlValues(result, nameValueList))
            {
                return 0;
            }
 
            // If we had an existing store value and we just updated that instance, return 'null' to indicate that
            // we don't have a new instance of CacheControlHeaderValue, but just updated an existing one. This is the
            // case if we have multiple 'Cache-Control' headers set in a request/response message.
            if (storeValue == null)
            {
                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)
        {
            foreach (NameValueHeaderValue nameValue in nameValueList)
            {
                string name = nameValue.Name.ToLowerInvariant();
                string? value = nameValue.Value;
 
                Flags flagsToSet = Flags.None;
                bool success = value is null;
 
                switch (name)
                {
                    case noCacheString:
                        flagsToSet = Flags.NoCache;
                        success = TrySetOptionalTokenList(nameValue, ref cc._noCacheHeaders);
                        break;
 
                    case noStoreString:
                        flagsToSet = Flags.NoStore;
                        break;
 
                    case maxAgeString:
                        flagsToSet = Flags.MaxAgeHasValue;
                        success = TrySetTimeSpan(value, ref cc._maxAge);
                        break;
 
                    case maxStaleString:
                        flagsToSet = Flags.MaxStale;
                        if (TrySetTimeSpan(value, ref cc._maxStaleLimit))
                        {
                            success = true;
                            flagsToSet = Flags.MaxStale | Flags.MaxStaleLimitHasValue;
                        }
                        break;
 
                    case minFreshString:
                        flagsToSet = Flags.MinFreshHasValue;
                        success = TrySetTimeSpan(value, ref cc._minFresh);
                        break;
 
                    case noTransformString:
                        flagsToSet = Flags.NoTransform;
                        break;
 
                    case onlyIfCachedString:
                        flagsToSet = Flags.OnlyIfCached;
                        break;
 
                    case publicString:
                        flagsToSet = Flags.Public;
                        break;
 
                    case privateString:
                        flagsToSet = Flags.Private;
                        success = TrySetOptionalTokenList(nameValue, ref cc._privateHeaders);
                        break;
 
                    case mustRevalidateString:
                        flagsToSet = Flags.MustRevalidate;
                        break;
 
                    case proxyRevalidateString:
                        flagsToSet = Flags.ProxyRevalidate;
                        break;
 
                    case sharedMaxAgeString:
                        flagsToSet = Flags.SharedMaxAgeHasValue;
                        success = TrySetTimeSpan(value, ref cc._sharedMaxAge);
                        break;
 
                    default:
                        success = true;
                        cc.Extensions.Add(nameValue);
                        break;
                }
 
                if (success)
                {
                    cc._flags |= flagsToSet;
                }
                else
                {
                    return false;
                }
            }
 
            return true;
        }
 
        private static bool TrySetOptionalTokenList(NameValueHeaderValue nameValue, ref TokenObjectCollection? destination)
        {
            Debug.Assert(nameValue != null);
 
            if (nameValue.Value == null)
            {
                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 whitespace.
            string valueString = nameValue.Value;
            if ((valueString.Length < 3) || !valueString.StartsWith('\"') || !valueString.EndsWith('\"'))
            {
                return false;
            }
 
            // We have a quoted string. Now verify that the string contains a list of valid tokens separated by ','.
            int current = 1; // skip the initial '"' character.
            int maxLength = valueString.Length - 1; // -1 because we don't want to parse the final '"'.
            int originalValueCount = destination == null ? 0 : destination.Count;
            while (current < maxLength)
            {
                current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(valueString, current, true,
                    out _);
 
                if (current == maxLength)
                {
                    break;
                }
 
                int tokenLength = HttpRuleParser.GetTokenLength(valueString, current);
 
                if (tokenLength == 0)
                {
                    // We already skipped whitespace and separators. If we don't have a token it must be an invalid
                    // character.
                    return false;
                }
 
                destination ??= new TokenObjectCollection();
                destination.Add(valueString.Substring(current, tokenLength));
 
                current += tokenLength;
            }
 
            // After parsing a valid token list, we expect to have at least one value
            if ((destination != null) && (destination.Count > originalValueCount))
            {
                return true;
            }
 
            return false;
        }
 
        private static bool TrySetTimeSpan(string? value, ref TimeSpan timeSpan)
        {
            if (value is null || !HeaderUtilities.TryParseInt32(value, out int 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, TokenObjectCollection values)
        {
            bool first = true;
            foreach (string value in values)
            {
                if (first)
                {
                    first = false;
                }
                else
                {
                    sb.Append(", ");
                }
 
                sb.Append(value);
            }
        }
 
        object ICloneable.Clone()
        {
            return new CacheControlHeaderValue(this);
        }
 
        private sealed class TokenObjectCollection : ObjectCollection<string>
        {
            public override void Validate(string item) => HeaderUtilities.CheckValidToken(item);
 
            public int GetHashCode(StringComparer comparer)
            {
                int hashcode = 0;
 
                foreach (string value in this)
                {
                    hashcode ^= comparer.GetHashCode(value);
                }
 
                return hashcode;
            }
        }
    }
}