|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Buffers;
using System.Buffers.Binary;
using System.Diagnostics;
namespace System.Net.Http
{
/// <summary>
/// Variable length integer encoding and decoding methods. Based on https://tools.ietf.org/html/draft-ietf-quic-transport-24#section-16.
/// A variable-length integer can use 1, 2, 4, or 8 bytes.
/// </summary>
internal static class VariableLengthIntegerHelper
{
public const int MaximumEncodedLength = 8;
// The high 4 bits indicate the length of the integer.
// 00 = length 1
// 01 = length 2
// 10 = length 4
// 11 = length 8
private const byte LengthMask = 0xC0;
private const byte InitialOneByteLengthMask = 0x00;
private const byte InitialTwoByteLengthMask = 0x40;
private const byte InitialFourByteLengthMask = 0x80;
private const byte InitialEightByteLengthMask = 0xC0;
// Bits to subtract to remove the length.
private const uint TwoByteLengthMask = 0x4000;
private const uint FourByteLengthMask = 0x80000000;
private const ulong EightByteLengthMask = 0xC000000000000000;
// public for internal use in aspnetcore
public const uint OneByteLimit = (1U << 6) - 1;
public const uint TwoByteLimit = (1U << 14) - 1;
public const uint FourByteLimit = (1U << 30) - 1;
public const long EightByteLimit = (1L << 62) - 1;
public static bool TryRead(ReadOnlySpan<byte> buffer, out long value, out int bytesRead)
{
if (buffer.Length != 0)
{
byte firstByte = buffer[0];
switch (firstByte & LengthMask)
{
case InitialOneByteLengthMask:
value = firstByte;
bytesRead = 1;
return true;
case InitialTwoByteLengthMask:
if (BinaryPrimitives.TryReadUInt16BigEndian(buffer, out ushort serializedShort))
{
value = serializedShort - TwoByteLengthMask;
bytesRead = 2;
return true;
}
break;
case InitialFourByteLengthMask:
if (BinaryPrimitives.TryReadUInt32BigEndian(buffer, out uint serializedInt))
{
value = serializedInt - FourByteLengthMask;
bytesRead = 4;
return true;
}
break;
default: // InitialEightByteLengthMask
Debug.Assert((firstByte & LengthMask) == InitialEightByteLengthMask);
if (BinaryPrimitives.TryReadUInt64BigEndian(buffer, out ulong serializedLong))
{
value = (long)(serializedLong - EightByteLengthMask);
Debug.Assert(value >= 0 && value <= EightByteLimit, "Serialized values are within [0, 2^62).");
bytesRead = 8;
return true;
}
break;
}
}
value = 0;
bytesRead = 0;
return false;
}
public static bool TryRead(ref SequenceReader<byte> reader, out long value)
{
// Hot path: we probably have the entire integer in one unbroken span.
if (TryRead(reader.UnreadSpan, out value, out int bytesRead))
{
reader.Advance(bytesRead);
return true;
}
// Cold path: copy to a temporary buffer before calling span-based read.
return TryReadSlow(ref reader, out value);
static bool TryReadSlow(ref SequenceReader<byte> reader, out long value)
{
ReadOnlySpan<byte> span = reader.CurrentSpan;
if (reader.TryPeek(out byte firstByte))
{
int length =
(firstByte & LengthMask) switch
{
InitialOneByteLengthMask => 1,
InitialTwoByteLengthMask => 2,
InitialFourByteLengthMask => 4,
_ => 8 // LengthEightByte
};
Span<byte> temp = (stackalloc byte[8])[..length];
if (reader.TryCopyTo(temp))
{
bool result = TryRead(temp, out value, out int bytesRead);
Debug.Assert(result);
Debug.Assert(bytesRead == length);
reader.Advance(bytesRead);
return true;
}
}
value = 0;
return false;
}
}
public static long GetInteger(in ReadOnlySequence<byte> buffer, out SequencePosition consumed, out SequencePosition examined)
{
var reader = new SequenceReader<byte>(buffer);
if (TryRead(ref reader, out long value))
{
consumed = examined = buffer.GetPosition(reader.Consumed);
return value;
}
else
{
consumed = default;
examined = buffer.End;
return -1;
}
}
public static bool TryWrite(Span<byte> buffer, long longToEncode, out int bytesWritten)
{
Debug.Assert(longToEncode >= 0);
Debug.Assert(longToEncode <= EightByteLimit);
if (longToEncode <= OneByteLimit)
{
if (buffer.Length != 0)
{
buffer[0] = (byte)longToEncode;
bytesWritten = 1;
return true;
}
}
else if (longToEncode <= TwoByteLimit)
{
if (BinaryPrimitives.TryWriteUInt16BigEndian(buffer, (ushort)((uint)longToEncode | TwoByteLengthMask)))
{
bytesWritten = 2;
return true;
}
}
else if (longToEncode <= FourByteLimit)
{
if (BinaryPrimitives.TryWriteUInt32BigEndian(buffer, (uint)longToEncode | FourByteLengthMask))
{
bytesWritten = 4;
return true;
}
}
else // EightByteLimit
{
if (BinaryPrimitives.TryWriteUInt64BigEndian(buffer, (ulong)longToEncode | EightByteLengthMask))
{
bytesWritten = 8;
return true;
}
}
bytesWritten = 0;
return false;
}
public static int WriteInteger(Span<byte> buffer, long longToEncode)
{
bool res = TryWrite(buffer, longToEncode, out int bytesWritten);
Debug.Assert(res);
return bytesWritten;
}
public static int GetByteCount(long value)
{
Debug.Assert(value >= 0);
Debug.Assert(value <= EightByteLimit);
return
value <= OneByteLimit ? 1 :
value <= TwoByteLimit ? 2 :
value <= FourByteLimit ? 4 :
8; // EightByteLimit
}
}
}
|