|
// 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;
using System.Diagnostics.CodeAnalysis;
using System.Text;
namespace System.Net.Http.Headers
{
public class ContentRangeHeaderValue : ICloneable
{
private string _unit = null!;
private long _from;
private long _to;
private long _length;
public string Unit
{
get { return _unit; }
set
{
HeaderUtilities.CheckValidToken(value);
_unit = value;
}
}
public long? From => HasRange ? _from : null;
public long? To => HasRange ? _to : null;
public long? Length => HasLength ? _length : null;
public bool HasLength => _length >= 0; // e.g. "Content-Range: bytes 12-34/*"
public bool HasRange => _from >= 0; // e.g. "Content-Range: bytes */1234"
public ContentRangeHeaderValue(long from, long to, long length)
{
// Scenario: "Content-Range: bytes 12-34/5678"
ArgumentOutOfRangeException.ThrowIfNegative(length);
ArgumentOutOfRangeException.ThrowIfNegative(to);
ArgumentOutOfRangeException.ThrowIfGreaterThan(to, length);
ArgumentOutOfRangeException.ThrowIfNegative(from);
ArgumentOutOfRangeException.ThrowIfGreaterThan(from, to);
_from = from;
_to = to;
_length = length;
_unit = HeaderUtilities.BytesUnit;
}
public ContentRangeHeaderValue(long length)
{
// Scenario: "Content-Range: bytes */1234"
ArgumentOutOfRangeException.ThrowIfNegative(length);
_length = length;
_unit = HeaderUtilities.BytesUnit;
_from = -1;
}
public ContentRangeHeaderValue(long from, long to)
{
// Scenario: "Content-Range: bytes 12-34/*"
ArgumentOutOfRangeException.ThrowIfNegative(to);
ArgumentOutOfRangeException.ThrowIfNegative(from);
ArgumentOutOfRangeException.ThrowIfGreaterThan(from, to);
_from = from;
_to = to;
_unit = HeaderUtilities.BytesUnit;
_length = -1;
}
private ContentRangeHeaderValue()
{
_from = -1;
_length = -1;
}
private ContentRangeHeaderValue(ContentRangeHeaderValue source)
{
Debug.Assert(source != null);
_from = source._from;
_to = source._to;
_length = source._length;
_unit = source._unit;
}
public override bool Equals([NotNullWhen(true)] object? obj) =>
obj is ContentRangeHeaderValue other &&
_from == other._from &&
_to == other._to &&
_length == other._length &&
string.Equals(_unit, other._unit, StringComparison.OrdinalIgnoreCase);
public override int GetHashCode() =>
HashCode.Combine(
StringComparer.OrdinalIgnoreCase.GetHashCode(_unit),
_from,
_to,
_length);
public override string ToString()
{
var sb = new ValueStringBuilder(stackalloc char[256]);
sb.Append(_unit);
sb.Append(' ');
if (HasRange)
{
sb.AppendSpanFormattable(_from);
sb.Append('-');
sb.AppendSpanFormattable(_to);
}
else
{
sb.Append('*');
}
sb.Append('/');
if (HasLength)
{
sb.AppendSpanFormattable(_length);
}
else
{
sb.Append('*');
}
return sb.ToString();
}
public static ContentRangeHeaderValue Parse(string input)
{
int index = 0;
return (ContentRangeHeaderValue)GenericHeaderParser.ContentRangeParser.ParseValue(input, null, ref index);
}
public static bool TryParse([NotNullWhen(true)] string? input, [NotNullWhen(true)] out ContentRangeHeaderValue? parsedValue)
{
int index = 0;
parsedValue = null;
if (GenericHeaderParser.ContentRangeParser.TryParseValue(input, null, ref index, out object? output))
{
parsedValue = (ContentRangeHeaderValue)output!;
return true;
}
return false;
}
internal static int GetContentRangeLength(string? input, int startIndex, out object? parsedValue)
{
Debug.Assert(startIndex >= 0);
parsedValue = null;
if (string.IsNullOrEmpty(input) || (startIndex >= input.Length))
{
return 0;
}
// Parse the unit string: <unit> in '<unit> <from>-<to>/<length>'
int unitLength = HttpRuleParser.GetTokenLength(input, startIndex);
if (unitLength == 0)
{
return 0;
}
string unit = input.Substring(startIndex, unitLength);
int current = startIndex + unitLength;
int separatorLength = HttpRuleParser.GetWhitespaceLength(input, current);
if (separatorLength == 0)
{
return 0;
}
current += separatorLength;
if (current == input.Length)
{
return 0;
}
// Read range values <from> and <to> in '<unit> <from>-<to>/<length>'
int fromStartIndex = current;
int fromLength;
int toStartIndex;
int toLength;
if (!TryGetRangeLength(input, ref current, out fromLength, out toStartIndex, out toLength))
{
return 0;
}
// After the range is read we expect the length separator '/'
if ((current == input.Length) || (input[current] != '/'))
{
return 0;
}
current++; // Skip '/' separator
current += HttpRuleParser.GetWhitespaceLength(input, current);
if (current == input.Length)
{
return 0;
}
// We may not have a length (e.g. 'bytes 1-2/*'). But if we do, parse the length now.
int lengthStartIndex = current;
int lengthLength;
if (!TryGetLengthLength(input, ref current, out lengthLength))
{
return 0;
}
if (!TryCreateContentRange(input, unit, fromStartIndex, fromLength, toStartIndex, toLength,
lengthStartIndex, lengthLength, out parsedValue))
{
return 0;
}
return current - startIndex;
}
private static bool TryGetLengthLength(string input, ref int current, out int lengthLength)
{
lengthLength = 0;
if (input[current] == '*')
{
current++;
}
else
{
// Parse length value: <length> in '<unit> <from>-<to>/<length>'
lengthLength = HttpRuleParser.GetNumberLength(input, current, false);
if ((lengthLength == 0) || (lengthLength > HttpRuleParser.MaxInt64Digits))
{
return false;
}
current += lengthLength;
}
current += HttpRuleParser.GetWhitespaceLength(input, current);
return true;
}
private static bool TryGetRangeLength(string input, ref int current, out int fromLength, out int toStartIndex,
out int toLength)
{
fromLength = 0;
toStartIndex = 0;
toLength = 0;
// Check if we have a value like 'bytes */133'. If yes, skip the range part and continue parsing the
// length separator '/'.
if (input[current] == '*')
{
current++;
}
else
{
// Parse first range value: <from> in '<unit> <from>-<to>/<length>'
fromLength = HttpRuleParser.GetNumberLength(input, current, false);
if ((fromLength == 0) || (fromLength > HttpRuleParser.MaxInt64Digits))
{
return false;
}
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 false;
}
current++; // skip the '-' character
current += HttpRuleParser.GetWhitespaceLength(input, current);
if (current == input.Length)
{
return false;
}
// Parse second range value: <to> in '<unit> <from>-<to>/<length>'
toStartIndex = current;
toLength = HttpRuleParser.GetNumberLength(input, current, false);
if ((toLength == 0) || (toLength > HttpRuleParser.MaxInt64Digits))
{
return false;
}
current += toLength;
}
current += HttpRuleParser.GetWhitespaceLength(input, current);
return true;
}
private static bool TryCreateContentRange(string input, string unit, int fromStartIndex, int fromLength,
int toStartIndex, int toLength, int lengthStartIndex, int lengthLength, [NotNullWhen(true)] out object? parsedValue)
{
parsedValue = null;
long from = 0;
if ((fromLength > 0) && !HeaderUtilities.TryParseInt64(input, fromStartIndex, fromLength, out from))
{
return false;
}
long to = 0;
if ((toLength > 0) && !HeaderUtilities.TryParseInt64(input, toStartIndex, toLength, out to))
{
return false;
}
// 'from' must not be greater than 'to'
if ((fromLength > 0) && (toLength > 0) && (from > to))
{
return false;
}
long length = 0;
if ((lengthLength > 0) && !HeaderUtilities.TryParseInt64(input, lengthStartIndex, lengthLength, out length))
{
return false;
}
// 'from' and 'to' must be less than 'length'
if ((toLength > 0) && (lengthLength > 0) && (to >= length))
{
return false;
}
ContentRangeHeaderValue result = new ContentRangeHeaderValue();
result._unit = unit;
if (fromLength > 0)
{
result._from = from;
result._to = to;
}
if (lengthLength > 0)
{
result._length = length;
}
parsedValue = result;
return true;
}
object ICloneable.Clone()
{
return new ContentRangeHeaderValue(this);
}
}
}
|