File: FormatterBinaryReader.cs
Web Access
Project: src\src\Middleware\OutputCaching\src\Microsoft.AspNetCore.OutputCaching.csproj (Microsoft.AspNetCore.OutputCaching)
// 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.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
 
namespace Microsoft.AspNetCore.OutputCaching;
 
internal ref struct FormatterBinaryReader
{
    // this is effectively a cut-down re-implementation of BinaryReader
    // from https://github.com/dotnet/runtime/blob/3689fbec921418e496962dc0ee252bdc9eafa3de/src/libraries/System.Private.CoreLib/src/System/IO/BinaryReader.cs
    // and is byte-compatible; however, instead of working against a Stream, we work against a ReadOnlyMemory<byte>
    //
    // additionally, we add support for reading a string with length specified by the caller (rather than handled automatically),
    // and in-place (zero-copy) BLOB reads
 
    private readonly ReadOnlyMemory<byte> _original; // used to allow us to zero-copy chunks out of the payload
    private readonly ref byte _root;
    private readonly int _length;
    private int _offset;
 
    public bool IsEOF => _offset >= _length;
 
    public FormatterBinaryReader(ReadOnlyMemory<byte> content)
    {
        _original = content;
        _length = content.Length;
        _root = ref MemoryMarshal.GetReference(content.Span);
    }
 
    public byte ReadByte()
    {
        if (IsEOF)
        {
            ThrowEndOfStream();
        }
        return Unsafe.Add(ref _root, _offset++);
    }
 
    public int Read7BitEncodedInt()
    {
        // Unlike writing, we can't delegate to the 64-bit read on
        // 64-bit platforms. The reason for this is that we want to
        // stop consuming bytes if we encounter an integer overflow.
 
        uint result = 0;
        byte byteReadJustNow;
 
        // Read the integer 7 bits at a time. The high bit
        // of the byte when on means to continue reading more bytes.
        //
        // There are two failure cases: we've read more than 5 bytes,
        // or the fifth byte is about to cause integer overflow.
        // This means that we can read the first 4 bytes without
        // worrying about integer overflow.
 
        const int MaxBytesWithoutOverflow = 4;
        for (int shift = 0; shift < MaxBytesWithoutOverflow * 7; shift += 7)
        {
            // ReadByte handles end of stream cases for us.
            byteReadJustNow = ReadByte();
            result |= (byteReadJustNow & 0x7Fu) << shift;
 
            if (byteReadJustNow <= 0x7Fu)
            {
                return (int)result; // early exit
            }
        }
 
        // Read the 5th byte. Since we already read 28 bits,
        // the value of this byte must fit within 4 bits (32 - 28),
        // and it must not have the high bit set.
 
        byteReadJustNow = ReadByte();
        if (byteReadJustNow > 0b_1111u)
        {
            ThrowOverflowException();
        }
 
        result |= (uint)byteReadJustNow << (MaxBytesWithoutOverflow * 7);
        return (int)result;
    }
 
    public long Read7BitEncodedInt64()
    {
        ulong result = 0;
        byte byteReadJustNow;
 
        // Read the integer 7 bits at a time. The high bit
        // of the byte when on means to continue reading more bytes.
        //
        // There are two failure cases: we've read more than 10 bytes,
        // or the tenth byte is about to cause integer overflow.
        // This means that we can read the first 9 bytes without
        // worrying about integer overflow.
 
        const int MaxBytesWithoutOverflow = 9;
        for (int shift = 0; shift < MaxBytesWithoutOverflow * 7; shift += 7)
        {
            // ReadByte handles end of stream cases for us.
            byteReadJustNow = ReadByte();
            result |= (byteReadJustNow & 0x7Ful) << shift;
 
            if (byteReadJustNow <= 0x7Fu)
            {
                return (long)result; // early exit
            }
        }
 
        // Read the 10th byte. Since we already read 63 bits,
        // the value of this byte must fit within 1 bit (64 - 63),
        // and it must not have the high bit set.
 
        byteReadJustNow = ReadByte();
        if (byteReadJustNow > 0b_1u)
        {
            ThrowOverflowException();
        }
 
        result |= (ulong)byteReadJustNow << (MaxBytesWithoutOverflow * 7);
        return (long)result;
    }
 
    public string ReadString() => ReadString(Read7BitEncodedInt());
 
    public void SkipString() => Skip(Read7BitEncodedInt());
 
    public string ReadString(int bytes)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(bytes);
 
        if (_offset > _length - bytes)
        {
            ThrowEndOfStream();
        }
        if (bytes == 0)
        {
            return "";
        }
        var s = Encoding.UTF8.GetString(MemoryMarshal.CreateReadOnlySpan(ref Unsafe.Add(ref _root, _offset), bytes));
        _offset += bytes;
        return s;
    }
 
    public void Skip(int bytes)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(bytes);
        if (_offset > _length - bytes)
        {
            ThrowEndOfStream();
        }
        _offset += bytes;
    }
 
    public ReadOnlySpan<byte> ReadBytesSpan(int count)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(count);
 
        if (_offset > _length - count)
        {
            ThrowEndOfStream();
        }
        if (count == 0)
        {
            return default;
        }
        var result = MemoryMarshal.CreateReadOnlySpan(ref Unsafe.Add(ref _root, _offset), count);
        _offset += count;
        return result;
    }
 
    public ReadOnlyMemory<byte> ReadBytesMemory(int count)
    {
        ArgumentOutOfRangeException.ThrowIfNegative(count);
 
        if (_offset > _length - count)
        {
            ThrowEndOfStream();
        }
        if (count == 0)
        {
            return default;
        }
        var result = _original.Slice(_offset, count);
        _offset += count;
        return result;
    }
 
    [DoesNotReturn]
    private static void ThrowEndOfStream() => throw new EndOfStreamException();
 
    [DoesNotReturn]
    private static void ThrowOverflowException() => throw new OverflowException();
}