File: EntityTagHeaderValue.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 Microsoft.Extensions.Primitives;
 
namespace Microsoft.Net.Http.Headers;
 
/// <summary>
/// Represents an entity-tag (<c>etag</c>) header value.
/// </summary>
public class EntityTagHeaderValue
{
    // Note that the ETag header does not allow a * but we're not that strict: We allow both '*' and ETag values in a single value.
    // We can't guarantee that a single parsed value will be used directly in an ETag header.
    private static readonly HttpHeaderParser<EntityTagHeaderValue> SingleValueParser
        = new GenericHeaderParser<EntityTagHeaderValue>(false, GetEntityTagLength);
    // Note that if multiple ETag values are allowed (e.g. 'If-Match', 'If-None-Match'), according to the RFC
    // the value must either be '*' or a list of ETag values. It's not allowed to have both '*' and a list of
    // ETag values. We're not that strict: We allow both '*' and ETag values in a list. If the server sends such
    // an invalid list, we want to be able to represent it using the corresponding header property.
    private static readonly HttpHeaderParser<EntityTagHeaderValue> MultipleValueParser
        = new GenericHeaderParser<EntityTagHeaderValue>(true, GetEntityTagLength);
 
    private StringSegment _tag;
    private bool _isWeak;
 
    private EntityTagHeaderValue()
    {
        // Used by the parser to create a new instance of this type.
    }
 
    /// <summary>
    /// Initializes a new instance of the <see cref="EntityTagHeaderValue"/>.
    /// </summary>
    /// <param name="tag">A <see cref="StringSegment"/> that contains an <see cref="EntityTagHeaderValue"/>.</param>
    public EntityTagHeaderValue(StringSegment tag)
        : this(tag, isWeak: false)
    {
    }
 
    /// <summary>
    /// Initializes a new instance of the <see cref="EntityTagHeaderValue"/>.
    /// </summary>
    /// <param name="tag">A <see cref="StringSegment"/> that contains an <see cref="EntityTagHeaderValue"/>.</param>
    /// <param name="isWeak">A value that indicates if this entity-tag header is a weak validator.</param>
    public EntityTagHeaderValue(StringSegment tag, bool isWeak)
    {
        if (StringSegment.IsNullOrEmpty(tag))
        {
            throw new ArgumentException("An empty string is not allowed.", nameof(tag));
        }
 
        if (!isWeak && StringSegment.Equals(tag, "*", StringComparison.Ordinal))
        {
            // * is valid, but W/* isn't.
            _tag = tag;
        }
        else if ((HttpRuleParser.GetQuotedStringLength(tag, 0, out var length) != HttpParseResult.Parsed) ||
            (length != tag.Length))
        {
            // Note that we don't allow 'W/' prefixes for weak ETags in the 'tag' parameter. If the user wants to
            // add a weak ETag, they can set 'isWeak' to true.
            throw new FormatException("Invalid ETag name");
        }
 
        _tag = tag;
        _isWeak = isWeak;
    }
 
    /// <summary>
    /// Gets the "any" etag.
    /// </summary>
    public static EntityTagHeaderValue Any { get; } = new EntityTagHeaderValue("*", isWeak: false);
 
    /// <summary>
    /// Gets the quoted tag.
    /// </summary>
    public StringSegment Tag => _tag;
 
    /// <summary>
    /// Gets a value that determines if the entity-tag header is a weak validator.
    /// </summary>
    public bool IsWeak => _isWeak;
 
    /// <inheritdoc />
    public override string ToString()
    {
        if (_isWeak)
        {
            return "W/" + _tag.ToString();
        }
        return _tag.ToString();
    }
 
    /// <summary>
    /// Check against another <see cref="EntityTagHeaderValue"/> for equality.
    /// This equality check should not be used to determine if two values match under the RFC specifications (<see href="https://tools.ietf.org/html/rfc7232#section-2.3.2"/>).
    /// </summary>
    /// <param name="obj">The other value to check against for equality.</param>
    /// <returns>
    /// <c>true</c> if the strength and tag of the two values match,
    /// <c>false</c> if the other value is null, is not an <see cref="EntityTagHeaderValue"/>, or if there is a mismatch of strength or tag between the two values.
    /// </returns>
    public override bool Equals(object? obj)
    {
        // Since the tag is a quoted-string we treat it case-sensitive.
        return obj is EntityTagHeaderValue other && _isWeak == other._isWeak && StringSegment.Equals(_tag, other._tag, StringComparison.Ordinal);
    }
 
    /// <inheritdoc />
    public override int GetHashCode()
    {
        // Since the tag is a quoted-string we treat it case-sensitive.
        return _tag.GetHashCode() ^ _isWeak.GetHashCode();
    }
 
    /// <summary>
    /// Compares against another <see cref="EntityTagHeaderValue"/> to see if they match under the RFC specifications (<see href="https://tools.ietf.org/html/rfc7232#section-2.3.2"/>).
    /// </summary>
    /// <param name="other">The other <see cref="EntityTagHeaderValue"/> to compare against.</param>
    /// <param name="useStrongComparison"><c>true</c> to use a strong comparison, <c>false</c> to use a weak comparison</param>
    /// <returns>
    /// <c>true</c> if the <see cref="EntityTagHeaderValue"/> match for the given comparison type,
    /// <c>false</c> if the other value is null or the comparison failed.
    /// </returns>
    public bool Compare(EntityTagHeaderValue? other, bool useStrongComparison)
    {
        if (other == null)
        {
            return false;
        }
 
        if (useStrongComparison)
        {
            return !IsWeak && !other.IsWeak && StringSegment.Equals(Tag, other.Tag, StringComparison.Ordinal);
        }
        else
        {
            return StringSegment.Equals(Tag, other.Tag, StringComparison.Ordinal);
        }
    }
 
    /// <summary>
    /// Parses <paramref name="input"/> as a <see cref="EntityTagHeaderValue"/> value.
    /// </summary>
    /// <param name="input">The values to parse.</param>
    /// <returns>The parsed values.</returns>
    public static EntityTagHeaderValue Parse(StringSegment input)
    {
        var index = 0;
        return SingleValueParser.ParseValue(input, ref index)!;
    }
 
    /// <summary>
    /// Attempts to parse the specified <paramref name="input"/> as a <see cref="EntityTagHeaderValue"/>.
    /// </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="EntityTagHeaderValue"/>, otherwise <see langword="false"/>.</returns>
    public static bool TryParse(StringSegment input, [NotNullWhen(true)] out EntityTagHeaderValue? parsedValue)
    {
        var index = 0;
        return SingleValueParser.TryParseValue(input, ref index, out parsedValue);
    }
 
    /// <summary>
    /// Parses a sequence of inputs as a sequence of <see cref="EntityTagHeaderValue"/> values.
    /// </summary>
    /// <param name="inputs">The values to parse.</param>
    /// <returns>The parsed values.</returns>
    public static IList<EntityTagHeaderValue> ParseList(IList<string>? inputs)
    {
        return MultipleValueParser.ParseValues(inputs);
    }
 
    /// <summary>
    /// Parses a sequence of inputs as a sequence of <see cref="EntityTagHeaderValue"/> values using string parsing rules.
    /// </summary>
    /// <param name="inputs">The values to parse.</param>
    /// <returns>The parsed values.</returns>
    public static IList<EntityTagHeaderValue> ParseStrictList(IList<string>? inputs)
    {
        return MultipleValueParser.ParseStrictValues(inputs);
    }
 
    /// <summary>
    /// Attempts to parse the sequence of values as a sequence of <see cref="EntityTagHeaderValue"/>.
    /// </summary>
    /// <param name="inputs">The values to parse.</param>
    /// <param name="parsedValues">The parsed values.</param>
    /// <returns><see langword="true"/> if all inputs are valid <see cref="EntityTagHeaderValue"/>, otherwise <see langword="false"/>.</returns>
    public static bool TryParseList(IList<string>? inputs, [NotNullWhen(true)] out IList<EntityTagHeaderValue>? parsedValues)
    {
        return MultipleValueParser.TryParseValues(inputs, out parsedValues);
    }
 
    /// <summary>
    /// Attempts to parse the sequence of values as a sequence of <see cref="EntityTagHeaderValue"/> using string parsing rules.
    /// </summary>
    /// <param name="inputs">The values to parse.</param>
    /// <param name="parsedValues">The parsed values.</param>
    /// <returns><see langword="true"/> if all inputs are valid <see cref="EntityTagHeaderValue"/>, otherwise <see langword="false"/>.</returns>
    public static bool TryParseStrictList(IList<string>? inputs, [NotNullWhen(true)] out IList<EntityTagHeaderValue>? parsedValues)
    {
        return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues);
    }
 
    internal static int GetEntityTagLength(StringSegment input, int startIndex, out EntityTagHeaderValue? parsedValue)
    {
        Contract.Requires(startIndex >= 0);
 
        parsedValue = null;
 
        if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length))
        {
            return 0;
        }
 
        // Caller must remove leading whitespaces. If not, we'll return 0.
        var isWeak = false;
        var current = startIndex;
 
        var firstChar = input[startIndex];
        if (firstChar == '*')
        {
            // We have '*' value, indicating "any" ETag.
            parsedValue = Any;
            current++;
        }
        else
        {
            // The RFC defines 'W/' as prefix, but we'll be flexible and also accept lower-case 'w'.
            if ((firstChar == 'W') || (firstChar == 'w'))
            {
                current++;
                // We need at least 3 more chars: the '/' character followed by two quotes.
                if ((current + 2 >= input.Length) || (input[current] != '/'))
                {
                    return 0;
                }
                isWeak = true;
                current++; // we have a weak-entity tag.
                current = current + HttpRuleParser.GetWhitespaceLength(input, current);
            }
 
            var tagStartIndex = current;
            if (HttpRuleParser.GetQuotedStringLength(input, current, out var tagLength) != HttpParseResult.Parsed)
            {
                return 0;
            }
 
            parsedValue = new EntityTagHeaderValue();
            if (tagLength == input.Length)
            {
                // Most of the time we'll have strong ETags without leading/trailing whitespaces.
                Contract.Assert(startIndex == 0);
                Contract.Assert(!isWeak);
                parsedValue._tag = input;
                parsedValue._isWeak = false;
            }
            else
            {
                parsedValue._tag = input.Subsegment(tagStartIndex, tagLength);
                parsedValue._isWeak = isWeak;
            }
 
            current = current + tagLength;
        }
        current = current + HttpRuleParser.GetWhitespaceLength(input, current);
 
        return current - startIndex;
    }
}