File: RangeItemHeaderValue.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.Contracts;
using System.Globalization;
using Microsoft.Extensions.Primitives;
 
namespace Microsoft.Net.Http.Headers;
 
/// <summary>
/// Represents a byte range in a Range header value.
/// <para>
/// The <see cref="RangeItemHeaderValue"/> class provides support for a byte range in a <c>Range</c> as defined
/// in <see href="https://tools.ietf.org/html/rfc2616">RFC 2616</see>.
/// </para>
/// </summary>
public class RangeItemHeaderValue
{
    private readonly long? _from;
    private readonly long? _to;
 
    /// <summary>
    /// Initializes a new instance of the <see cref="RangeItemHeaderValue"/> class.
    /// </summary>
    /// <param name="from">The position at which to start sending data.</param>
    /// <param name="to">The position at which to stop sending data.</param>
    public RangeItemHeaderValue(long? from, long? to)
    {
        if (!from.HasValue && !to.HasValue)
        {
            throw new ArgumentException("Invalid header range.");
        }
        if (from.HasValue && (from.GetValueOrDefault() < 0))
        {
            throw new ArgumentOutOfRangeException(nameof(from));
        }
        if (to.HasValue && (to.GetValueOrDefault() < 0))
        {
            throw new ArgumentOutOfRangeException(nameof(to));
        }
        if (from.HasValue && to.HasValue && (from.GetValueOrDefault() > to.GetValueOrDefault()))
        {
            throw new ArgumentOutOfRangeException(nameof(from));
        }
 
        _from = from;
        _to = to;
    }
 
    /// <summary>
    /// Gets the position at which to start sending data.
    /// </summary>
    public long? From
    {
        get { return _from; }
    }
 
    /// <summary>
    /// Gets the position at which to stop sending data.
    /// </summary>
    public long? To
    {
        get { return _to; }
    }
 
    /// <inheritdoc />
    public override string ToString()
    {
        if (!_from.HasValue)
        {
            return "-" + _to.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo);
        }
        else if (!_to.HasValue)
        {
            return _from.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo) + "-";
        }
        return _from.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo) + "-" +
            _to.GetValueOrDefault().ToString(NumberFormatInfo.InvariantInfo);
    }
 
    /// <inheritdoc />
    public override bool Equals(object? obj)
    {
        return obj is RangeItemHeaderValue other && ((_from == other._from) && (_to == other._to));
    }
 
    /// <inheritdoc />
    public override int GetHashCode()
    {
        if (!_from.HasValue)
        {
            return _to.GetValueOrDefault().GetHashCode();
        }
        else if (!_to.HasValue)
        {
            return _from.GetValueOrDefault().GetHashCode();
        }
        return _from.GetValueOrDefault().GetHashCode() ^ _to.GetValueOrDefault().GetHashCode();
    }
 
    // Returns the length of a range list. E.g. "1-2, 3-4, 5-6" adds 3 ranges to 'rangeCollection'. Note that empty
    // list segments are allowed, e.g. ",1-2, , 3-4,,".
    internal static int GetRangeItemListLength(
        StringSegment input,
        int startIndex,
        ICollection<RangeItemHeaderValue> rangeCollection)
    {
        Contract.Requires(startIndex >= 0);
        Contract.Ensures((Contract.Result<int>() == 0) || (rangeCollection.Count > 0),
            "If we can parse the string, then we expect to have at least one range item.");
 
        if ((StringSegment.IsNullOrEmpty(input)) || (startIndex >= input.Length))
        {
            return 0;
        }
 
        // Empty segments are allowed, so skip all delimiter-only segments (e.g. ", ,").
        var current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(input, startIndex, true, separatorFound: out _);
        // It's OK if we didn't find leading separator characters. Ignore 'separatorFound'.
 
        if (current == input.Length)
        {
            return 0;
        }
 
        while (true)
        {
            var rangeLength = GetRangeItemLength(input, current, out var range);
 
            if (rangeLength == 0)
            {
                return 0;
            }
 
            rangeCollection!.Add(range!);
 
            current = current + rangeLength;
            current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(input, current, true, out var separatorFound);
 
            // If the string is not consumed, we must have a delimiter, otherwise the string is not a valid
            // range list.
            if ((current < input.Length) && !separatorFound)
            {
                return 0;
            }
 
            if (current == input.Length)
            {
                return current - startIndex;
            }
        }
    }
 
    internal static int GetRangeItemLength(StringSegment input, int startIndex, out RangeItemHeaderValue? parsedValue)
    {
        Contract.Requires(startIndex >= 0);
 
        // This parser parses number ranges: e.g. '1-2', '1-', '-2'.
 
        parsedValue = null;
 
        if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length))
        {
            return 0;
        }
 
        // Caller must remove leading whitespaces. If not, we'll return 0.
        var current = startIndex;
 
        // Try parse the first value of a value pair.
        var fromStartIndex = current;
        var fromLength = HttpRuleParser.GetNumberLength(input, current, false);
 
        if (fromLength > HttpRuleParser.MaxInt64Digits)
        {
            return 0;
        }
 
        current = current + fromLength;
        current = current + HttpRuleParser.GetWhitespaceLength(input, current);
 
        // After the first value, the '-' character must follow.
        if ((current == input.Length) || (input[current] != '-'))
        {
            // We need a '-' character otherwise this can't be a valid range.
            return 0;
        }
 
        current++; // skip the '-' character
        current = current + HttpRuleParser.GetWhitespaceLength(input, current);
 
        var toStartIndex = current;
        var toLength = 0;
 
        // If we didn't reach the end of the string, try parse the second value of the range.
        if (current < input.Length)
        {
            toLength = HttpRuleParser.GetNumberLength(input, current, false);
 
            if (toLength > HttpRuleParser.MaxInt64Digits)
            {
                return 0;
            }
 
            current = current + toLength;
            current = current + HttpRuleParser.GetWhitespaceLength(input, current);
        }
 
        if ((fromLength == 0) && (toLength == 0))
        {
            return 0; // At least one value must be provided in order to be a valid range.
        }
 
        // Try convert first value to int64
        long from = 0;
        if ((fromLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Subsegment(fromStartIndex, fromLength), out from))
        {
            return 0;
        }
 
        // Try convert second value to int64
        long to = 0;
        if ((toLength > 0) && !HeaderUtilities.TryParseNonNegativeInt64(input.Subsegment(toStartIndex, toLength), out to))
        {
            return 0;
        }
 
        // 'from' must not be greater than 'to'
        if ((fromLength > 0) && (toLength > 0) && (from > to))
        {
            return 0;
        }
 
        parsedValue = new RangeItemHeaderValue((fromLength == 0 ? (long?)null : (long?)from),
            (toLength == 0 ? (long?)null : (long?)to));
        return current - startIndex;
    }
}