314 lines
8.6 KiB
C#
314 lines
8.6 KiB
C#
// Copyright (c) Harry Pierson. All rights reserved.
|
|
// Licensed under the MIT license.
|
|
// See LICENSE file in the project root for full license information.
|
|
|
|
using System.Diagnostics;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Threading;
|
|
|
|
namespace System.Buffers
|
|
{
|
|
public ref struct BufferReader<T>
|
|
{
|
|
private readonly bool usingSequence;
|
|
private readonly ReadOnlySequence<T> sequence;
|
|
private SequencePosition currentPosition;
|
|
private SequencePosition nextPosition;
|
|
private bool moreData;
|
|
private readonly long length;
|
|
|
|
public BufferReader(ReadOnlySpan<T> span)
|
|
{
|
|
usingSequence = false;
|
|
CurrentSpanIndex = 0;
|
|
Consumed = 0;
|
|
sequence = default;
|
|
currentPosition = default;
|
|
length = span.Length;
|
|
|
|
CurrentSpan = span;
|
|
nextPosition = default;
|
|
moreData = span.Length > 0;
|
|
}
|
|
|
|
public BufferReader(in ReadOnlySequence<T> sequence)
|
|
{
|
|
usingSequence = true;
|
|
CurrentSpanIndex = 0;
|
|
Consumed = 0;
|
|
this.sequence = sequence;
|
|
currentPosition = sequence.Start;
|
|
length = -1;
|
|
|
|
var first = sequence.First.Span;
|
|
CurrentSpan = first;
|
|
nextPosition = sequence.GetPosition(first.Length);
|
|
moreData = first.Length > 0;
|
|
|
|
if (!moreData && !sequence.IsSingleSegment)
|
|
{
|
|
moreData = true;
|
|
GetNextSpan();
|
|
}
|
|
}
|
|
|
|
public readonly bool End => !moreData;
|
|
|
|
public ReadOnlySpan<T> CurrentSpan { get; private set; }
|
|
|
|
public int CurrentSpanIndex { get; private set; }
|
|
|
|
public readonly ReadOnlySpan<T> UnreadSpan
|
|
{
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
get => CurrentSpan.Slice(CurrentSpanIndex);
|
|
}
|
|
|
|
public long Consumed { get; private set; }
|
|
|
|
public readonly long Remaining => Length - Consumed;
|
|
|
|
public readonly long Length
|
|
{
|
|
get
|
|
{
|
|
if (length < 0)
|
|
{
|
|
Debug.Assert(usingSequence, "usingSequence");
|
|
// Cast-away readonly to initialize lazy field
|
|
Volatile.Write(ref Unsafe.AsRef(length), sequence.Length);
|
|
}
|
|
return length;
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public readonly bool TryPeek([MaybeNullWhen(false)] out T value)
|
|
{
|
|
if (moreData)
|
|
{
|
|
value = CurrentSpan[CurrentSpanIndex];
|
|
return true;
|
|
}
|
|
|
|
value = default!;
|
|
return false;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public bool TryRead([MaybeNullWhen(false)] out T value)
|
|
{
|
|
if (End)
|
|
{
|
|
value = default!;
|
|
return false;
|
|
}
|
|
|
|
value = CurrentSpan[CurrentSpanIndex];
|
|
CurrentSpanIndex++;
|
|
Consumed++;
|
|
|
|
if (CurrentSpanIndex >= CurrentSpan.Length)
|
|
{
|
|
if (usingSequence)
|
|
GetNextSpan();
|
|
else
|
|
moreData = false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Rewind(long count)
|
|
{
|
|
if ((ulong)count > (ulong)Consumed) throw new ArgumentOutOfRangeException(nameof(count));
|
|
|
|
Consumed -= count;
|
|
|
|
if (CurrentSpanIndex >= count)
|
|
{
|
|
CurrentSpanIndex -= (int)count;
|
|
moreData = true;
|
|
}
|
|
else if (usingSequence)
|
|
{
|
|
// Current segment doesn't have enough data, scan backward through segments
|
|
RetreatToPreviousSpan(Consumed);
|
|
}
|
|
else
|
|
{
|
|
throw new ArgumentOutOfRangeException($"Rewind went past the start of the memory by {count}.", nameof(count));
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
private void RetreatToPreviousSpan(long consumed)
|
|
{
|
|
Debug.Assert(usingSequence, "usingSequence");
|
|
ResetReader();
|
|
Advance(consumed);
|
|
}
|
|
|
|
private void ResetReader()
|
|
{
|
|
Debug.Assert(usingSequence, "usingSequence");
|
|
CurrentSpanIndex = 0;
|
|
Consumed = 0;
|
|
currentPosition = sequence.Start;
|
|
nextPosition = currentPosition;
|
|
|
|
if (sequence.TryGet(ref nextPosition, out ReadOnlyMemory<T> memory))
|
|
{
|
|
moreData = true;
|
|
|
|
if (memory.Length == 0)
|
|
{
|
|
CurrentSpan = default;
|
|
// No data in the first span, move to one with data
|
|
GetNextSpan();
|
|
}
|
|
else
|
|
{
|
|
CurrentSpan = memory.Span;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// No data in any spans and at end of sequence
|
|
moreData = false;
|
|
CurrentSpan = default;
|
|
}
|
|
}
|
|
|
|
private void GetNextSpan()
|
|
{
|
|
Debug.Assert(usingSequence, "usingSequence");
|
|
if (!sequence.IsSingleSegment)
|
|
{
|
|
SequencePosition previousNextPosition = nextPosition;
|
|
while (sequence.TryGet(ref nextPosition, out ReadOnlyMemory<T> memory))
|
|
{
|
|
currentPosition = previousNextPosition;
|
|
if (memory.Length > 0)
|
|
{
|
|
CurrentSpan = memory.Span;
|
|
CurrentSpanIndex = 0;
|
|
return;
|
|
}
|
|
|
|
CurrentSpan = default;
|
|
CurrentSpanIndex = 0;
|
|
previousNextPosition = nextPosition;
|
|
}
|
|
}
|
|
moreData = false;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void Advance(long count)
|
|
{
|
|
const long TooBigOrNegative = unchecked((long)0xFFFFFFFF80000000);
|
|
if ((count & TooBigOrNegative) == 0 && CurrentSpan.Length - CurrentSpanIndex > (int)count)
|
|
{
|
|
CurrentSpanIndex += (int)count;
|
|
Consumed += count;
|
|
}
|
|
else if (usingSequence)
|
|
{
|
|
// Can't satisfy from the current span
|
|
AdvanceToNextSpan(count);
|
|
}
|
|
else if (CurrentSpan.Length - CurrentSpanIndex == (int)count)
|
|
{
|
|
CurrentSpanIndex += (int)count;
|
|
Consumed += count;
|
|
moreData = false;
|
|
}
|
|
else
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(count));
|
|
}
|
|
}
|
|
|
|
private void AdvanceToNextSpan(long count)
|
|
{
|
|
Debug.Assert(usingSequence, "usingSequence");
|
|
if (count < 0) throw new ArgumentOutOfRangeException(nameof(count));
|
|
|
|
Consumed += count;
|
|
while (moreData)
|
|
{
|
|
int remaining = CurrentSpan.Length - CurrentSpanIndex;
|
|
|
|
if (remaining > count)
|
|
{
|
|
CurrentSpanIndex += (int)count;
|
|
count = 0;
|
|
break;
|
|
}
|
|
|
|
// As there may not be any further segments we need to
|
|
// push the current index to the end of the span.
|
|
CurrentSpanIndex += remaining;
|
|
count -= remaining;
|
|
Debug.Assert(count >= 0);
|
|
|
|
GetNextSpan();
|
|
|
|
if (count == 0) break;
|
|
}
|
|
|
|
if (count != 0)
|
|
{
|
|
// Not enough data left- adjust for where we actually ended and throw
|
|
Consumed -= count;
|
|
throw new ArgumentOutOfRangeException(nameof(count));
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public readonly bool TryCopyTo(Span<T> destination)
|
|
{
|
|
// This API doesn't advance to facilitate conditional advancement based on the data returned.
|
|
// We don't provide an advance option to allow easier utilizing of stack allocated destination spans.
|
|
// (Because we can make this method readonly we can guarantee that we won't capture the span.)
|
|
|
|
ReadOnlySpan<T> firstSpan = UnreadSpan;
|
|
if (firstSpan.Length >= destination.Length)
|
|
{
|
|
firstSpan.Slice(0, destination.Length).CopyTo(destination);
|
|
return true;
|
|
}
|
|
|
|
// Not enough in the current span to satisfy the request, fall through to the slow path
|
|
return TryCopyMultisegment(destination);
|
|
}
|
|
|
|
internal readonly bool TryCopyMultisegment(Span<T> destination)
|
|
{
|
|
// If we don't have enough to fill the requested buffer, return false
|
|
if (Remaining < destination.Length)
|
|
return false;
|
|
|
|
ReadOnlySpan<T> firstSpan = UnreadSpan;
|
|
Debug.Assert(firstSpan.Length < destination.Length);
|
|
firstSpan.CopyTo(destination);
|
|
int copied = firstSpan.Length;
|
|
|
|
SequencePosition next = nextPosition;
|
|
while (sequence.TryGet(ref next, out ReadOnlyMemory<T> nextSegment))
|
|
if (nextSegment.Length > 0)
|
|
{
|
|
ReadOnlySpan<T> nextSpan = nextSegment.Span;
|
|
int toCopy = Math.Min(nextSpan.Length, destination.Length - copied);
|
|
nextSpan.Slice(0, toCopy).CopyTo(destination.Slice(copied));
|
|
copied += toCopy;
|
|
if (copied >= destination.Length) break;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|