| | | 1 | | // Licensed to the .NET Foundation under one or more agreements. |
| | | 2 | | // The .NET Foundation licenses this file to you under the MIT license. |
| | | 3 | | |
| | | 4 | | using System.Collections.Generic; |
| | | 5 | | using System.Diagnostics; |
| | | 6 | | using System.Diagnostics.CodeAnalysis; |
| | | 7 | | using System.Globalization; |
| | | 8 | | using System.Runtime.CompilerServices; |
| | | 9 | | using System.Text; |
| | | 10 | | using System.Threading; |
| | | 11 | | |
| | | 12 | | namespace System.Net.Http.Headers |
| | | 13 | | { |
| | | 14 | | public class CacheControlHeaderValue : ICloneable |
| | | 15 | | { |
| | | 16 | | private const string maxAgeString = "max-age"; |
| | | 17 | | private const string maxStaleString = "max-stale"; |
| | | 18 | | private const string minFreshString = "min-fresh"; |
| | | 19 | | private const string mustRevalidateString = "must-revalidate"; |
| | | 20 | | private const string noCacheString = "no-cache"; |
| | | 21 | | private const string noStoreString = "no-store"; |
| | | 22 | | private const string noTransformString = "no-transform"; |
| | | 23 | | private const string onlyIfCachedString = "only-if-cached"; |
| | | 24 | | private const string privateString = "private"; |
| | | 25 | | private const string proxyRevalidateString = "proxy-revalidate"; |
| | | 26 | | private const string publicString = "public"; |
| | | 27 | | private const string sharedMaxAgeString = "s-maxage"; |
| | | 28 | | |
| | 1 | 29 | | private static readonly GenericHeaderParser s_nameValueListParser = GenericHeaderParser.MultipleValueNameValuePa |
| | | 30 | | |
| | | 31 | | [Flags] |
| | | 32 | | private enum Flags : int |
| | | 33 | | { |
| | | 34 | | None = 0, |
| | | 35 | | MaxAgeHasValue = 1 << 0, |
| | | 36 | | SharedMaxAgeHasValue = 1 << 1, |
| | | 37 | | MaxStaleLimitHasValue = 1 << 2, |
| | | 38 | | MinFreshHasValue = 1 << 3, |
| | | 39 | | NoCache = 1 << 4, |
| | | 40 | | NoStore = 1 << 5, |
| | | 41 | | MaxStale = 1 << 6, |
| | | 42 | | NoTransform = 1 << 7, |
| | | 43 | | OnlyIfCached = 1 << 8, |
| | | 44 | | Public = 1 << 9, |
| | | 45 | | Private = 1 << 10, |
| | | 46 | | MustRevalidate = 1 << 11, |
| | | 47 | | ProxyRevalidate = 1 << 12, |
| | | 48 | | } |
| | | 49 | | |
| | | 50 | | private Flags _flags; |
| | | 51 | | private TokenObjectCollection? _noCacheHeaders; |
| | | 52 | | private TimeSpan _maxAge; |
| | | 53 | | private TimeSpan _sharedMaxAge; |
| | | 54 | | private TimeSpan _maxStaleLimit; |
| | | 55 | | private TimeSpan _minFresh; |
| | | 56 | | private TokenObjectCollection? _privateHeaders; |
| | | 57 | | private UnvalidatedObjectCollection<NameValueHeaderValue>? _extensions; |
| | | 58 | | |
| | | 59 | | private void SetTimeSpan(ref TimeSpan fieldRef, Flags flag, TimeSpan? value) |
| | 0 | 60 | | { |
| | 0 | 61 | | fieldRef = value.GetValueOrDefault(); |
| | 0 | 62 | | SetFlag(flag, value.HasValue); |
| | 0 | 63 | | } |
| | | 64 | | |
| | | 65 | | private void SetFlag(Flags flag, bool value) |
| | 0 | 66 | | { |
| | 0 | 67 | | Debug.Assert(sizeof(Flags) == sizeof(int)); |
| | | 68 | | |
| | | 69 | | // This type is not thread-safe, but we do a minimal amount of synchronization to ensure |
| | | 70 | | // that concurrent modifications of different properties don't interfere with each other. |
| | 0 | 71 | | if (value) |
| | 0 | 72 | | { |
| | 0 | 73 | | Interlocked.Or(ref _flags, flag); |
| | 0 | 74 | | } |
| | | 75 | | else |
| | 0 | 76 | | { |
| | 0 | 77 | | Interlocked.And(ref _flags, ~flag); |
| | 0 | 78 | | } |
| | 0 | 79 | | } |
| | | 80 | | |
| | | 81 | | public bool NoCache |
| | | 82 | | { |
| | 4998 | 83 | | get => (_flags & Flags.NoCache) != 0; |
| | 0 | 84 | | set => SetFlag(Flags.NoCache, value); |
| | | 85 | | } |
| | | 86 | | |
| | 0 | 87 | | public ICollection<string> NoCacheHeaders => _noCacheHeaders ??= new TokenObjectCollection(); |
| | | 88 | | |
| | | 89 | | public bool NoStore |
| | | 90 | | { |
| | 4998 | 91 | | get => (_flags & Flags.NoStore) != 0; |
| | 0 | 92 | | set => SetFlag(Flags.NoStore, value); |
| | | 93 | | } |
| | | 94 | | |
| | | 95 | | public TimeSpan? MaxAge |
| | | 96 | | { |
| | 0 | 97 | | get => (_flags & Flags.MaxAgeHasValue) == 0 ? null : _maxAge; |
| | 0 | 98 | | set => SetTimeSpan(ref _maxAge, Flags.MaxAgeHasValue, value); |
| | | 99 | | } |
| | | 100 | | |
| | | 101 | | public TimeSpan? SharedMaxAge |
| | | 102 | | { |
| | 0 | 103 | | get => (_flags & Flags.SharedMaxAgeHasValue) == 0 ? null : _sharedMaxAge; |
| | 0 | 104 | | set => SetTimeSpan(ref _sharedMaxAge, Flags.SharedMaxAgeHasValue, value); |
| | | 105 | | } |
| | | 106 | | |
| | | 107 | | public bool MaxStale |
| | | 108 | | { |
| | 4998 | 109 | | get => (_flags & Flags.MaxStale) != 0; |
| | 0 | 110 | | set => SetFlag(Flags.MaxStale, value); |
| | | 111 | | } |
| | | 112 | | |
| | | 113 | | public TimeSpan? MaxStaleLimit |
| | | 114 | | { |
| | 0 | 115 | | get => (_flags & Flags.MaxStaleLimitHasValue) == 0 ? null : _maxStaleLimit; |
| | 0 | 116 | | set => SetTimeSpan(ref _maxStaleLimit, Flags.MaxStaleLimitHasValue, value); |
| | | 117 | | } |
| | | 118 | | |
| | | 119 | | public TimeSpan? MinFresh |
| | | 120 | | { |
| | 0 | 121 | | get => (_flags & Flags.MinFreshHasValue) == 0 ? null : _minFresh; |
| | 0 | 122 | | set => SetTimeSpan(ref _minFresh, Flags.MinFreshHasValue, value); |
| | | 123 | | } |
| | | 124 | | |
| | | 125 | | public bool NoTransform |
| | | 126 | | { |
| | 4998 | 127 | | get => (_flags & Flags.NoTransform) != 0; |
| | 0 | 128 | | set => SetFlag(Flags.NoTransform, value); |
| | | 129 | | } |
| | | 130 | | |
| | | 131 | | public bool OnlyIfCached |
| | | 132 | | { |
| | 4998 | 133 | | get => (_flags & Flags.OnlyIfCached) != 0; |
| | 0 | 134 | | set => SetFlag(Flags.OnlyIfCached, value); |
| | | 135 | | } |
| | | 136 | | |
| | | 137 | | public bool Public |
| | | 138 | | { |
| | 4998 | 139 | | get => (_flags & Flags.Public) != 0; |
| | 0 | 140 | | set => SetFlag(Flags.Public, value); |
| | | 141 | | } |
| | | 142 | | |
| | | 143 | | public bool Private |
| | | 144 | | { |
| | 4998 | 145 | | get => (_flags & Flags.Private) != 0; |
| | 0 | 146 | | set => SetFlag(Flags.Private, value); |
| | | 147 | | } |
| | | 148 | | |
| | 0 | 149 | | public ICollection<string> PrivateHeaders => _privateHeaders ??= new TokenObjectCollection(); |
| | | 150 | | |
| | | 151 | | public bool MustRevalidate |
| | | 152 | | { |
| | 4998 | 153 | | get => (_flags & Flags.MustRevalidate) != 0; |
| | 0 | 154 | | set => SetFlag(Flags.MustRevalidate, value); |
| | | 155 | | } |
| | | 156 | | |
| | | 157 | | public bool ProxyRevalidate |
| | | 158 | | { |
| | 4998 | 159 | | get => (_flags & Flags.ProxyRevalidate) != 0; |
| | 0 | 160 | | set => SetFlag(Flags.ProxyRevalidate, value); |
| | | 161 | | } |
| | | 162 | | |
| | 232494 | 163 | | public ICollection<NameValueHeaderValue> Extensions => _extensions ??= new UnvalidatedObjectCollection<NameValue |
| | | 164 | | |
| | 3332 | 165 | | public CacheControlHeaderValue() |
| | 3332 | 166 | | { |
| | 3332 | 167 | | } |
| | | 168 | | |
| | 0 | 169 | | private CacheControlHeaderValue(CacheControlHeaderValue source) |
| | 0 | 170 | | { |
| | 0 | 171 | | Debug.Assert(source != null); |
| | | 172 | | |
| | 0 | 173 | | _flags = source._flags; |
| | 0 | 174 | | _maxAge = source._maxAge; |
| | 0 | 175 | | _sharedMaxAge = source._sharedMaxAge; |
| | 0 | 176 | | _maxStaleLimit = source._maxStaleLimit; |
| | 0 | 177 | | _minFresh = source._minFresh; |
| | | 178 | | |
| | 0 | 179 | | if (source._noCacheHeaders != null) |
| | 0 | 180 | | { |
| | 0 | 181 | | foreach (string noCacheHeader in source._noCacheHeaders) |
| | 0 | 182 | | { |
| | 0 | 183 | | NoCacheHeaders.Add(noCacheHeader); |
| | 0 | 184 | | } |
| | 0 | 185 | | } |
| | | 186 | | |
| | 0 | 187 | | if (source._privateHeaders != null) |
| | 0 | 188 | | { |
| | 0 | 189 | | foreach (string privateHeader in source._privateHeaders) |
| | 0 | 190 | | { |
| | 0 | 191 | | PrivateHeaders.Add(privateHeader); |
| | 0 | 192 | | } |
| | 0 | 193 | | } |
| | | 194 | | |
| | 0 | 195 | | _extensions = source._extensions.Clone(); |
| | 0 | 196 | | } |
| | | 197 | | |
| | | 198 | | public override string ToString() |
| | 4998 | 199 | | { |
| | 4998 | 200 | | StringBuilder sb = StringBuilderCache.Acquire(); |
| | | 201 | | |
| | 4998 | 202 | | AppendValueIfRequired(sb, NoStore, noStoreString); |
| | 4998 | 203 | | AppendValueIfRequired(sb, NoTransform, noTransformString); |
| | 4998 | 204 | | AppendValueIfRequired(sb, OnlyIfCached, onlyIfCachedString); |
| | 4998 | 205 | | AppendValueIfRequired(sb, Public, publicString); |
| | 4998 | 206 | | AppendValueIfRequired(sb, MustRevalidate, mustRevalidateString); |
| | 4998 | 207 | | AppendValueIfRequired(sb, ProxyRevalidate, proxyRevalidateString); |
| | | 208 | | |
| | 4998 | 209 | | if (NoCache) |
| | 0 | 210 | | { |
| | 0 | 211 | | AppendValueWithSeparatorIfRequired(sb, noCacheString); |
| | 0 | 212 | | if ((_noCacheHeaders != null) && (_noCacheHeaders.Count > 0)) |
| | 0 | 213 | | { |
| | 0 | 214 | | sb.Append("=\""); |
| | 0 | 215 | | AppendValues(sb, _noCacheHeaders); |
| | 0 | 216 | | sb.Append('\"'); |
| | 0 | 217 | | } |
| | 0 | 218 | | } |
| | | 219 | | |
| | 4998 | 220 | | if ((_flags & Flags.MaxAgeHasValue) != 0) |
| | 0 | 221 | | { |
| | 0 | 222 | | AppendValueWithSeparatorIfRequired(sb, maxAgeString); |
| | 0 | 223 | | sb.Append('='); |
| | 0 | 224 | | int maxAge = (int)_maxAge.TotalSeconds; |
| | 0 | 225 | | if (maxAge >= 0) |
| | 0 | 226 | | { |
| | 0 | 227 | | sb.Append(maxAge); |
| | 0 | 228 | | } |
| | | 229 | | else |
| | 0 | 230 | | { |
| | | 231 | | // In the corner case where the value is negative, ensure it uses |
| | | 232 | | // the invariant's negative sign rather than the current culture's. |
| | 0 | 233 | | sb.Append(NumberFormatInfo.InvariantInfo, $"{maxAge}"); |
| | 0 | 234 | | } |
| | 0 | 235 | | } |
| | | 236 | | |
| | 4998 | 237 | | if ((_flags & Flags.SharedMaxAgeHasValue) != 0) |
| | 0 | 238 | | { |
| | 0 | 239 | | AppendValueWithSeparatorIfRequired(sb, sharedMaxAgeString); |
| | 0 | 240 | | sb.Append('='); |
| | 0 | 241 | | int sharedMaxAge = (int)_sharedMaxAge.TotalSeconds; |
| | 0 | 242 | | if (sharedMaxAge >= 0) |
| | 0 | 243 | | { |
| | 0 | 244 | | sb.Append(sharedMaxAge); |
| | 0 | 245 | | } |
| | | 246 | | else |
| | 0 | 247 | | { |
| | | 248 | | // In the corner case where the value is negative, ensure it uses |
| | | 249 | | // the invariant's negative sign rather than the current culture's. |
| | 0 | 250 | | sb.Append(NumberFormatInfo.InvariantInfo, $"{sharedMaxAge}"); |
| | 0 | 251 | | } |
| | 0 | 252 | | } |
| | | 253 | | |
| | 4998 | 254 | | if (MaxStale) |
| | 0 | 255 | | { |
| | 0 | 256 | | AppendValueWithSeparatorIfRequired(sb, maxStaleString); |
| | 0 | 257 | | if ((_flags & Flags.MaxStaleLimitHasValue) != 0) |
| | 0 | 258 | | { |
| | 0 | 259 | | sb.Append('='); |
| | 0 | 260 | | int maxStaleLimit = (int)_maxStaleLimit.TotalSeconds; |
| | 0 | 261 | | if (maxStaleLimit >= 0) |
| | 0 | 262 | | { |
| | 0 | 263 | | sb.Append(maxStaleLimit); |
| | 0 | 264 | | } |
| | | 265 | | else |
| | 0 | 266 | | { |
| | | 267 | | // In the corner case where the value is negative, ensure it uses |
| | | 268 | | // the invariant's negative sign rather than the current culture's. |
| | 0 | 269 | | sb.Append(NumberFormatInfo.InvariantInfo, $"{maxStaleLimit}"); |
| | 0 | 270 | | } |
| | 0 | 271 | | } |
| | 0 | 272 | | } |
| | | 273 | | |
| | 4998 | 274 | | if ((_flags & Flags.MinFreshHasValue) != 0) |
| | 0 | 275 | | { |
| | 0 | 276 | | AppendValueWithSeparatorIfRequired(sb, minFreshString); |
| | 0 | 277 | | sb.Append('='); |
| | 0 | 278 | | int minFresh = (int)_minFresh.TotalSeconds; |
| | 0 | 279 | | if (minFresh >= 0) |
| | 0 | 280 | | { |
| | 0 | 281 | | sb.Append(minFresh); |
| | 0 | 282 | | } |
| | | 283 | | else |
| | 0 | 284 | | { |
| | | 285 | | // In the corner case where the value is negative, ensure it uses |
| | | 286 | | // the invariant's negative sign rather than the current culture's. |
| | 0 | 287 | | sb.Append(NumberFormatInfo.InvariantInfo, $"{minFresh}"); |
| | 0 | 288 | | } |
| | 0 | 289 | | } |
| | | 290 | | |
| | 4998 | 291 | | if (Private) |
| | 0 | 292 | | { |
| | 0 | 293 | | AppendValueWithSeparatorIfRequired(sb, privateString); |
| | 0 | 294 | | if ((_privateHeaders != null) && (_privateHeaders.Count > 0)) |
| | 0 | 295 | | { |
| | 0 | 296 | | sb.Append("=\""); |
| | 0 | 297 | | AppendValues(sb, _privateHeaders); |
| | 0 | 298 | | sb.Append('\"'); |
| | 0 | 299 | | } |
| | 0 | 300 | | } |
| | | 301 | | |
| | 4998 | 302 | | NameValueHeaderValue.ToString(_extensions, ',', false, sb); |
| | | 303 | | |
| | 4998 | 304 | | return StringBuilderCache.GetStringAndRelease(sb); |
| | 4998 | 305 | | } |
| | | 306 | | |
| | | 307 | | public override bool Equals([NotNullWhen(true)] object? obj) => |
| | 0 | 308 | | obj is CacheControlHeaderValue other && |
| | 0 | 309 | | _flags == other._flags && |
| | 0 | 310 | | _maxAge == other._maxAge && |
| | 0 | 311 | | _sharedMaxAge == other._sharedMaxAge && |
| | 0 | 312 | | _maxStaleLimit == other._maxStaleLimit && |
| | 0 | 313 | | _minFresh == other._minFresh && |
| | 0 | 314 | | HeaderUtilities.AreEqualCollections(_noCacheHeaders, other._noCacheHeaders, StringComparer.OrdinalIgnoreCase |
| | 0 | 315 | | HeaderUtilities.AreEqualCollections(_privateHeaders, other._privateHeaders, StringComparer.OrdinalIgnoreCase |
| | 0 | 316 | | HeaderUtilities.AreEqualCollections(_extensions, other._extensions); |
| | | 317 | | |
| | | 318 | | public override int GetHashCode() => |
| | 0 | 319 | | HashCode.Combine( |
| | 0 | 320 | | _flags, |
| | 0 | 321 | | _maxAge, |
| | 0 | 322 | | _sharedMaxAge, |
| | 0 | 323 | | _maxStaleLimit, |
| | 0 | 324 | | _minFresh, |
| | 0 | 325 | | (_noCacheHeaders is null ? 0 : _noCacheHeaders.GetHashCode(StringComparer.OrdinalIgnoreCase)), |
| | 0 | 326 | | (_privateHeaders is null ? 0 : _privateHeaders.GetHashCode(StringComparer.OrdinalIgnoreCase)), |
| | 0 | 327 | | NameValueHeaderValue.GetHashCode(_extensions)); |
| | | 328 | | |
| | | 329 | | public static CacheControlHeaderValue Parse(string? input) |
| | 0 | 330 | | { |
| | 0 | 331 | | int index = 0; |
| | 0 | 332 | | return (CacheControlHeaderValue)CacheControlHeaderParser.Parser.ParseValue(input, null, ref index) ?? new Ca |
| | 0 | 333 | | } |
| | | 334 | | |
| | | 335 | | public static bool TryParse(string? input, [NotNullWhen(true)] out CacheControlHeaderValue? parsedValue) |
| | 0 | 336 | | { |
| | 0 | 337 | | int index = 0; |
| | 0 | 338 | | parsedValue = null; |
| | | 339 | | |
| | 0 | 340 | | if (CacheControlHeaderParser.Parser.TryParseValue(input, null, ref index, out object? output)) |
| | 0 | 341 | | { |
| | 0 | 342 | | parsedValue = (CacheControlHeaderValue?)output ?? new CacheControlHeaderValue(); |
| | 0 | 343 | | return true; |
| | | 344 | | } |
| | 0 | 345 | | return false; |
| | 0 | 346 | | } |
| | | 347 | | |
| | | 348 | | internal static int GetCacheControlLength(string? input, int startIndex, CacheControlHeaderValue? storeValue, |
| | | 349 | | out CacheControlHeaderValue? parsedValue) |
| | 5134 | 350 | | { |
| | 5134 | 351 | | Debug.Assert(startIndex >= 0); |
| | | 352 | | |
| | 5134 | 353 | | parsedValue = null; |
| | | 354 | | |
| | 5134 | 355 | | if (string.IsNullOrEmpty(input) || (startIndex >= input.Length)) |
| | 0 | 356 | | { |
| | 0 | 357 | | return 0; |
| | | 358 | | } |
| | | 359 | | |
| | | 360 | | // Cache-Control header consists of a list of name/value pairs, where the value is optional. So use an |
| | | 361 | | // instance of NameValueHeaderParser to parse the string. |
| | 5134 | 362 | | int current = startIndex; |
| | 5134 | 363 | | List<NameValueHeaderValue> nameValueList = new List<NameValueHeaderValue>(); |
| | 237756 | 364 | | while (current < input.Length) |
| | 232758 | 365 | | { |
| | 232758 | 366 | | if (!s_nameValueListParser.TryParseValue(input, null, ref current, out object? nameValue)) |
| | 136 | 367 | | { |
| | 136 | 368 | | return 0; |
| | | 369 | | } |
| | | 370 | | |
| | 232622 | 371 | | Debug.Assert(nameValue is not null); |
| | 232622 | 372 | | nameValueList.Add((NameValueHeaderValue)nameValue); |
| | 232622 | 373 | | } |
| | | 374 | | |
| | | 375 | | // If we get here, we were able to successfully parse the string as list of name/value pairs. Now analyze |
| | | 376 | | // the name/value pairs. |
| | | 377 | | |
| | | 378 | | // Cache-Control is a header supporting lists of values. However, expose the header as an instance of |
| | | 379 | | // CacheControlHeaderValue. So if we already have an instance of CacheControlHeaderValue, add the values |
| | | 380 | | // from this string to the existing instances. |
| | 4998 | 381 | | CacheControlHeaderValue? result = storeValue ?? new CacheControlHeaderValue(); |
| | | 382 | | |
| | 4998 | 383 | | if (!TrySetCacheControlValues(result, nameValueList)) |
| | 0 | 384 | | { |
| | 0 | 385 | | return 0; |
| | | 386 | | } |
| | | 387 | | |
| | | 388 | | // If we had an existing store value and we just updated that instance, return 'null' to indicate that |
| | | 389 | | // we don't have a new instance of CacheControlHeaderValue, but just updated an existing one. This is the |
| | | 390 | | // case if we have multiple 'Cache-Control' headers set in a request/response message. |
| | 4998 | 391 | | if (storeValue == null) |
| | 3332 | 392 | | { |
| | 3332 | 393 | | parsedValue = result; |
| | 3332 | 394 | | } |
| | | 395 | | |
| | | 396 | | // If we get here we successfully parsed the whole string. |
| | 4998 | 397 | | return input.Length - startIndex; |
| | 5134 | 398 | | } |
| | | 399 | | |
| | | 400 | | private static bool TrySetCacheControlValues(CacheControlHeaderValue cc, List<NameValueHeaderValue> nameValueLis |
| | 4998 | 401 | | { |
| | 479982 | 402 | | foreach (NameValueHeaderValue nameValue in nameValueList) |
| | 232494 | 403 | | { |
| | 232494 | 404 | | string name = nameValue.Name.ToLowerInvariant(); |
| | 232494 | 405 | | string? value = nameValue.Value; |
| | | 406 | | |
| | 232494 | 407 | | Flags flagsToSet = Flags.None; |
| | 232494 | 408 | | bool success = value is null; |
| | | 409 | | |
| | 232494 | 410 | | switch (name) |
| | | 411 | | { |
| | | 412 | | case noCacheString: |
| | 0 | 413 | | flagsToSet = Flags.NoCache; |
| | 0 | 414 | | success = TrySetOptionalTokenList(nameValue, ref cc._noCacheHeaders); |
| | 0 | 415 | | break; |
| | | 416 | | |
| | | 417 | | case noStoreString: |
| | 0 | 418 | | flagsToSet = Flags.NoStore; |
| | 0 | 419 | | break; |
| | | 420 | | |
| | | 421 | | case maxAgeString: |
| | 0 | 422 | | flagsToSet = Flags.MaxAgeHasValue; |
| | 0 | 423 | | success = TrySetTimeSpan(value, ref cc._maxAge); |
| | 0 | 424 | | break; |
| | | 425 | | |
| | | 426 | | case maxStaleString: |
| | 0 | 427 | | flagsToSet = Flags.MaxStale; |
| | 0 | 428 | | if (TrySetTimeSpan(value, ref cc._maxStaleLimit)) |
| | 0 | 429 | | { |
| | 0 | 430 | | success = true; |
| | 0 | 431 | | flagsToSet = Flags.MaxStale | Flags.MaxStaleLimitHasValue; |
| | 0 | 432 | | } |
| | 0 | 433 | | break; |
| | | 434 | | |
| | | 435 | | case minFreshString: |
| | 0 | 436 | | flagsToSet = Flags.MinFreshHasValue; |
| | 0 | 437 | | success = TrySetTimeSpan(value, ref cc._minFresh); |
| | 0 | 438 | | break; |
| | | 439 | | |
| | | 440 | | case noTransformString: |
| | 0 | 441 | | flagsToSet = Flags.NoTransform; |
| | 0 | 442 | | break; |
| | | 443 | | |
| | | 444 | | case onlyIfCachedString: |
| | 0 | 445 | | flagsToSet = Flags.OnlyIfCached; |
| | 0 | 446 | | break; |
| | | 447 | | |
| | | 448 | | case publicString: |
| | 0 | 449 | | flagsToSet = Flags.Public; |
| | 0 | 450 | | break; |
| | | 451 | | |
| | | 452 | | case privateString: |
| | 0 | 453 | | flagsToSet = Flags.Private; |
| | 0 | 454 | | success = TrySetOptionalTokenList(nameValue, ref cc._privateHeaders); |
| | 0 | 455 | | break; |
| | | 456 | | |
| | | 457 | | case mustRevalidateString: |
| | 0 | 458 | | flagsToSet = Flags.MustRevalidate; |
| | 0 | 459 | | break; |
| | | 460 | | |
| | | 461 | | case proxyRevalidateString: |
| | 0 | 462 | | flagsToSet = Flags.ProxyRevalidate; |
| | 0 | 463 | | break; |
| | | 464 | | |
| | | 465 | | case sharedMaxAgeString: |
| | 0 | 466 | | flagsToSet = Flags.SharedMaxAgeHasValue; |
| | 0 | 467 | | success = TrySetTimeSpan(value, ref cc._sharedMaxAge); |
| | 0 | 468 | | break; |
| | | 469 | | |
| | | 470 | | default: |
| | 232494 | 471 | | success = true; |
| | 232494 | 472 | | cc.Extensions.Add(nameValue); |
| | 232494 | 473 | | break; |
| | | 474 | | } |
| | | 475 | | |
| | 232494 | 476 | | if (success) |
| | 232494 | 477 | | { |
| | 232494 | 478 | | cc._flags |= flagsToSet; |
| | 232494 | 479 | | } |
| | | 480 | | else |
| | 0 | 481 | | { |
| | 0 | 482 | | return false; |
| | | 483 | | } |
| | 232494 | 484 | | } |
| | | 485 | | |
| | 4998 | 486 | | return true; |
| | 4998 | 487 | | } |
| | | 488 | | |
| | | 489 | | private static bool TrySetOptionalTokenList(NameValueHeaderValue nameValue, ref TokenObjectCollection? destinati |
| | 0 | 490 | | { |
| | 0 | 491 | | Debug.Assert(nameValue != null); |
| | | 492 | | |
| | 0 | 493 | | if (nameValue.Value == null) |
| | 0 | 494 | | { |
| | 0 | 495 | | return true; |
| | | 496 | | } |
| | | 497 | | |
| | | 498 | | // We need the string to be at least 3 chars long: 2x quotes and at least 1 character. Also make sure we |
| | | 499 | | // have a quoted string. Note that NameValueHeaderValue will never have leading/trailing whitespace. |
| | 0 | 500 | | string valueString = nameValue.Value; |
| | 0 | 501 | | if ((valueString.Length < 3) || !valueString.StartsWith('\"') || !valueString.EndsWith('\"')) |
| | 0 | 502 | | { |
| | 0 | 503 | | return false; |
| | | 504 | | } |
| | | 505 | | |
| | | 506 | | // We have a quoted string. Now verify that the string contains a list of valid tokens separated by ','. |
| | 0 | 507 | | int current = 1; // skip the initial '"' character. |
| | 0 | 508 | | int maxLength = valueString.Length - 1; // -1 because we don't want to parse the final '"'. |
| | 0 | 509 | | int originalValueCount = destination == null ? 0 : destination.Count; |
| | 0 | 510 | | while (current < maxLength) |
| | 0 | 511 | | { |
| | 0 | 512 | | current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(valueString, current, true, |
| | 0 | 513 | | out _); |
| | | 514 | | |
| | 0 | 515 | | if (current == maxLength) |
| | 0 | 516 | | { |
| | 0 | 517 | | break; |
| | | 518 | | } |
| | | 519 | | |
| | 0 | 520 | | int tokenLength = HttpRuleParser.GetTokenLength(valueString, current); |
| | | 521 | | |
| | 0 | 522 | | if (tokenLength == 0) |
| | 0 | 523 | | { |
| | | 524 | | // We already skipped whitespace and separators. If we don't have a token it must be an invalid |
| | | 525 | | // character. |
| | 0 | 526 | | return false; |
| | | 527 | | } |
| | | 528 | | |
| | 0 | 529 | | destination ??= new TokenObjectCollection(); |
| | 0 | 530 | | destination.Add(valueString.Substring(current, tokenLength)); |
| | | 531 | | |
| | 0 | 532 | | current += tokenLength; |
| | 0 | 533 | | } |
| | | 534 | | |
| | | 535 | | // After parsing a valid token list, we expect to have at least one value |
| | 0 | 536 | | if ((destination != null) && (destination.Count > originalValueCount)) |
| | 0 | 537 | | { |
| | 0 | 538 | | return true; |
| | | 539 | | } |
| | | 540 | | |
| | 0 | 541 | | return false; |
| | 0 | 542 | | } |
| | | 543 | | |
| | | 544 | | private static bool TrySetTimeSpan(string? value, ref TimeSpan timeSpan) |
| | 0 | 545 | | { |
| | 0 | 546 | | if (value is null || !HeaderUtilities.TryParseInt32(value, out int seconds)) |
| | 0 | 547 | | { |
| | 0 | 548 | | return false; |
| | | 549 | | } |
| | | 550 | | |
| | 0 | 551 | | timeSpan = new TimeSpan(0, 0, seconds); |
| | 0 | 552 | | return true; |
| | 0 | 553 | | } |
| | | 554 | | |
| | | 555 | | private static void AppendValueIfRequired(StringBuilder sb, bool appendValue, string value) |
| | 29988 | 556 | | { |
| | 29988 | 557 | | if (appendValue) |
| | 0 | 558 | | { |
| | 0 | 559 | | AppendValueWithSeparatorIfRequired(sb, value); |
| | 0 | 560 | | } |
| | 29988 | 561 | | } |
| | | 562 | | |
| | | 563 | | private static void AppendValueWithSeparatorIfRequired(StringBuilder sb, string value) |
| | 0 | 564 | | { |
| | 0 | 565 | | if (sb.Length > 0) |
| | 0 | 566 | | { |
| | 0 | 567 | | sb.Append(", "); |
| | 0 | 568 | | } |
| | 0 | 569 | | sb.Append(value); |
| | 0 | 570 | | } |
| | | 571 | | |
| | | 572 | | private static void AppendValues(StringBuilder sb, TokenObjectCollection values) |
| | 0 | 573 | | { |
| | 0 | 574 | | bool first = true; |
| | 0 | 575 | | foreach (string value in values) |
| | 0 | 576 | | { |
| | 0 | 577 | | if (first) |
| | 0 | 578 | | { |
| | 0 | 579 | | first = false; |
| | 0 | 580 | | } |
| | | 581 | | else |
| | 0 | 582 | | { |
| | 0 | 583 | | sb.Append(", "); |
| | 0 | 584 | | } |
| | | 585 | | |
| | 0 | 586 | | sb.Append(value); |
| | 0 | 587 | | } |
| | 0 | 588 | | } |
| | | 589 | | |
| | | 590 | | object ICloneable.Clone() |
| | 0 | 591 | | { |
| | 0 | 592 | | return new CacheControlHeaderValue(this); |
| | 0 | 593 | | } |
| | | 594 | | |
| | | 595 | | private sealed class TokenObjectCollection : ObjectCollection<string> |
| | | 596 | | { |
| | 0 | 597 | | public override void Validate(string item) => HeaderUtilities.CheckValidToken(item); |
| | | 598 | | |
| | | 599 | | public int GetHashCode(StringComparer comparer) |
| | 0 | 600 | | { |
| | 0 | 601 | | int hashcode = 0; |
| | | 602 | | |
| | 0 | 603 | | foreach (string value in this) |
| | 0 | 604 | | { |
| | 0 | 605 | | hashcode ^= comparer.GetHashCode(value); |
| | 0 | 606 | | } |
| | | 607 | | |
| | 0 | 608 | | return hashcode; |
| | 0 | 609 | | } |
| | | 610 | | } |
| | | 611 | | } |
| | | 612 | | } |