File: System\Net\Http\Headers\RangeItemHeaderValue.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;
 
namespace System.Net.Http.Headers
{
    public class RangeItemHeaderValue : ICloneable
    {
        // Set to -1 if not set.
        private readonly long _from;
        private readonly long _to;
 
        public long? From => _from >= 0 ? _from : null;
 
        public long? To => _to >= 0 ? _to : null;
 
        public RangeItemHeaderValue(long? from, long? to)
        {
            if (!from.HasValue && !to.HasValue)
            {
                throw new ArgumentException(SR.net_http_headers_invalid_range);
            }
            if (from.HasValue)
            {
                ArgumentOutOfRangeException.ThrowIfNegative(from.GetValueOrDefault(), nameof(from));
            }
            if (to.HasValue)
            {
                ArgumentOutOfRangeException.ThrowIfNegative(to.GetValueOrDefault(), nameof(to));
            }
            if (from.HasValue && to.HasValue)
            {
                ArgumentOutOfRangeException.ThrowIfGreaterThan(from.GetValueOrDefault(), to.GetValueOrDefault(), nameof(from));
            }
 
            _from = from ?? -1;
            _to = to ?? -1;
        }
 
        internal RangeItemHeaderValue(RangeItemHeaderValue source)
        {
            Debug.Assert(source != null);
 
            _from = source._from;
            _to = source._to;
        }
 
        public override string ToString()
        {
            Span<char> stackBuffer = stackalloc char[128];
 
            if (_from < 0)
            {
                return string.Create(CultureInfo.InvariantCulture, stackBuffer, $"-{_to}");
            }
 
            if (_to < 0)
            {
                return string.Create(CultureInfo.InvariantCulture, stackBuffer, $"{_from}-"); ;
            }
 
            return string.Create(CultureInfo.InvariantCulture, stackBuffer, $"{_from}-{_to}");
        }
 
        public override bool Equals([NotNullWhen(true)] object? obj) =>
            obj is RangeItemHeaderValue other &&
            _from == other._from &&
            _to == other._to;
 
        public override int GetHashCode() =>
            HashCode.Combine(_from, _to);
 
        // 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(string? input, int startIndex,
            ICollection<RangeItemHeaderValue> rangeCollection)
        {
            Debug.Assert(rangeCollection != null);
            Debug.Assert(startIndex >= 0);
 
            if ((string.IsNullOrEmpty(input)) || (startIndex >= input.Length))
            {
                return 0;
            }
 
            // Empty segments are allowed, so skip all delimiter-only segments (e.g. ", ,").
            int current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(input, startIndex, true, out _);
            // It's OK if we didn't find leading separator characters. Ignore 'separatorFound'.
 
            if (current == input.Length)
            {
                return 0;
            }
 
            while (true)
            {
                int rangeLength = GetRangeItemLength(input, current, out RangeItemHeaderValue? range);
 
                if (rangeLength == 0)
                {
                    return 0;
                }
 
                rangeCollection.Add(range!);
 
                current += rangeLength;
                current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(input, current, true, out bool 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(string? input, int startIndex, out RangeItemHeaderValue? parsedValue)
        {
            Debug.Assert(startIndex >= 0);
 
            // This parser parses number ranges: e.g. '1-2', '1-', '-2'.
 
            parsedValue = null;
 
            if (string.IsNullOrEmpty(input) || (startIndex >= input.Length))
            {
                return 0;
            }
 
            // Caller must remove leading whitespace. If not, we'll return 0.
            int current = startIndex;
 
            // Try parse the first value of a value pair.
            int fromStartIndex = current;
            int fromLength = HttpRuleParser.GetNumberLength(input, current, false);
 
            if (fromLength > HttpRuleParser.MaxInt64Digits)
            {
                return 0;
            }
 
            current += fromLength;
            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 += HttpRuleParser.GetWhitespaceLength(input, current);
 
            int toStartIndex = current;
            int 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 += toLength;
                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.TryParseInt64(input, fromStartIndex, fromLength, out from))
            {
                return 0;
            }
 
            // Try convert second value to int64
            long to = 0;
            if ((toLength > 0) && !HeaderUtilities.TryParseInt64(input, 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 ? null : from), (toLength == 0 ? null : to));
            return current - startIndex;
        }
 
        object ICloneable.Clone()
        {
            return new RangeItemHeaderValue(this);
        }
    }
}