using System.Globalization;
namespace Sunfish.Web;
/// <summary>Un rango de bytes pedido con "Range: bytes=..." (solo se admite uno).</summary>
public readonly record struct ByteRange(long Start, long End)
{
public long Length => End - Start + 1;
public enum ParseResult { None, Valid, Unsatisfiable }
/// <summary>
/// Interpreta la cabecera Range para un fichero de <paramref name="size"/> bytes.
/// None: sin cabecera, mal formada o con varios rangos (se responde el fichero entero).
/// </summary>
public static ParseResult TryParse(string? header, long size, out ByteRange range)
{
range = default;
if (string.IsNullOrWhiteSpace(header))
return ParseResult.None;
header = header.Trim();
if (!header.StartsWith("bytes=", StringComparison.OrdinalIgnoreCase))
return ParseResult.None;
string spec = header[6..].Trim();
if (spec.Contains(','))
return ParseResult.None;
int dash = spec.IndexOf('-');
if (dash < 0)
return ParseResult.None;
string first = spec[..dash].Trim();
string last = spec[(dash + 1)..].Trim();
if (first.Length == 0)
{
// "-N": los ultimos N bytes
if (!long.TryParse(last, NumberStyles.None, CultureInfo.InvariantCulture, out long suffix))
return ParseResult.None;
if (suffix == 0 || size == 0)
return ParseResult.Unsatisfiable;
long start = Math.Max(0, size - suffix);
range = new ByteRange(start, size - 1);
return ParseResult.Valid;
}
if (!long.TryParse(first, NumberStyles.None, CultureInfo.InvariantCulture, out long from))
return ParseResult.None;
long to = size - 1;
if (last.Length > 0)
{
if (!long.TryParse(last, NumberStyles.None, CultureInfo.InvariantCulture, out to) || to < from)
return ParseResult.None;
to = Math.Min(to, size - 1);
}
if (from >= size)
return ParseResult.Unsatisfiable;
range = new ByteRange(from, to);
return ParseResult.Valid;
}
}