| File: System\Buffers\SequenceReader.cs | Web Access |
| Project: src\runtime\src\libraries\System.Memory\src\System.Memory.csproj (System.Memory) |
// 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.Runtime.CompilerServices; namespace System.Buffers { public ref partial struct SequenceReader<T> where T : unmanaged, IEquatable<T> { private SequencePosition _currentPosition; private SequencePosition _nextPosition; private bool _moreData; private readonly long _length; /// <summary> /// Create a <see cref="SequenceReader{T}"/> over the given <see cref="ReadOnlySequence{T}"/>. /// </summary> [MethodImpl(MethodImplOptions.AggressiveInlining)] public SequenceReader(ReadOnlySequence<T> sequence) { CurrentSpanIndex = 0; Consumed = 0; Sequence = sequence; _currentPosition = sequence.Start; _length = -1; sequence.GetFirstSpan(out ReadOnlySpan<T> first, out _nextPosition); CurrentSpan = first; _moreData = first.Length > 0; if (!_moreData && !sequence.IsSingleSegment) { _moreData = true; GetNextSpan(); } } /// <summary> /// True when there is no more data in the <see cref="Sequence"/>. /// </summary> public readonly bool End => !_moreData; /// <summary> /// The underlying <see cref="ReadOnlySequence{T}"/> for the reader. /// </summary> public ReadOnlySequence<T> Sequence { get; } /// <summary> /// Gets the unread portion of the <see cref="Sequence"/>. /// </summary> /// <value> /// The unread portion of the <see cref="Sequence"/>. /// </value> public readonly ReadOnlySequence<T> UnreadSequence => Sequence.Slice(Position); /// <summary> /// The current position in the <see cref="Sequence"/>. /// </summary> public readonly SequencePosition Position => Sequence.GetPosition(CurrentSpanIndex, _currentPosition); /// <summary> /// The current segment in the <see cref="Sequence"/> as a span. /// </summary> public ReadOnlySpan<T> CurrentSpan { get; private set; } /// <summary> /// The index in the <see cref="CurrentSpan"/>. /// </summary> public int CurrentSpanIndex { get; private set; } /// <summary> /// The unread portion of the <see cref="CurrentSpan"/>. /// </summary> public readonly ReadOnlySpan<T> UnreadSpan { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => CurrentSpan.Slice(CurrentSpanIndex); } /// <summary> /// The total number of <typeparamref name="T"/>'s processed by the reader. /// </summary> public long Consumed { get; private set; } /// <summary> /// Remaining <typeparamref name="T"/>'s in the reader's <see cref="Sequence"/>. /// </summary> public readonly long Remaining => Length - Consumed; /// <summary> /// Count of <typeparamref name="T"/> in the reader's <see cref="Sequence"/>. /// </summary> public readonly long Length { get { if (_length < 0) { // Cast-away readonly to initialize lazy field Unsafe.AsRef(in _length) = Sequence.Length; } return _length; } } /// <summary> /// Peeks at the next value without advancing the reader. /// </summary> /// <param name="value">The next value or default if at the end.</param> /// <returns>False if at the end of the reader.</returns> [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly bool TryPeek(out T value) { if (_moreData) { value = CurrentSpan[CurrentSpanIndex]; return true; } else { value = default; return false; } } /// <summary> /// Peeks at the next value at specific offset without advancing the reader. /// </summary> /// <param name="offset">The offset from current position.</param> /// <param name="value">The next value, or the default value if at the end of the reader.</param> /// <returns><c>true</c> if the reader is not at its end and the peek operation succeeded; <c>false</c> if at the end of the reader.</returns> public readonly bool TryPeek(long offset, out T value) { if (offset < 0) ThrowHelper.ThrowArgumentOutOfRangeException_OffsetOutOfRange(); // If we've got data and offset is not out of bounds if (!_moreData || Remaining <= offset) { value = default; return false; } // Sum CurrentSpanIndex + offset could overflow as is but the value of offset should be very large // because we check Remaining <= offset above so to overflow we should have a ReadOnlySequence close to 8 exabytes Debug.Assert(CurrentSpanIndex + offset >= 0); // If offset doesn't fall inside current segment move to next until we find correct one if ((CurrentSpanIndex + offset) <= CurrentSpan.Length - 1) { Debug.Assert(offset <= int.MaxValue); value = CurrentSpan[CurrentSpanIndex + (int)offset]; return true; } else { long remainingOffset = offset - (CurrentSpan.Length - CurrentSpanIndex); SequencePosition nextPosition = _nextPosition; ReadOnlyMemory<T> currentMemory; while (Sequence.TryGet(ref nextPosition, out currentMemory, advance: true)) { // Skip empty segment if (currentMemory.Length > 0) { if (remainingOffset >= currentMemory.Length) { // Subtract current non consumed data remainingOffset -= currentMemory.Length; } else { break; } } } value = currentMemory.Span[(int)remainingOffset]; return true; } } /// <summary> /// Read the next value and advance the reader. /// </summary> /// <param name="value">The next value or default if at the end.</param> /// <returns>False if at the end of the reader.</returns> [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryRead(out T value) { if (End) { value = default; return false; } value = CurrentSpan[CurrentSpanIndex]; CurrentSpanIndex++; Consumed++; if (CurrentSpanIndex >= CurrentSpan.Length) { GetNextSpan(); } return true; } /// <summary> /// Move the reader back the specified number of items. /// </summary> /// <exception cref="ArgumentOutOfRangeException"> /// Thrown if trying to rewind a negative amount or more than <see cref="Consumed"/>. /// </exception> [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Rewind(long count) { if ((ulong)count > (ulong)Consumed) { ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count); } if (count == 0) { return; } Consumed -= count; if (CurrentSpanIndex >= count) { CurrentSpanIndex -= (int)count; _moreData = true; } else { // Current segment doesn't have enough data, scan backward through segments RetreatToPreviousSpan(Consumed); } } [MethodImpl(MethodImplOptions.NoInlining)] private void RetreatToPreviousSpan(long consumed) { ResetReader(); Advance(consumed); } private void ResetReader() { CurrentSpanIndex = 0; Consumed = 0; _currentPosition = Sequence.Start; _nextPosition = _currentPosition; if (Sequence.TryGet(ref _nextPosition, out ReadOnlyMemory<T> memory, advance: true)) { _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; } } /// <summary> /// Get the next segment with available data, if any. /// </summary> private void GetNextSpan() { if (!Sequence.IsSingleSegment) { SequencePosition previousNextPosition = _nextPosition; while (Sequence.TryGet(ref _nextPosition, out ReadOnlyMemory<T> memory, advance: true)) { _currentPosition = previousNextPosition; if (memory.Length > 0) { CurrentSpan = memory.Span; CurrentSpanIndex = 0; return; } else { CurrentSpan = default; CurrentSpanIndex = 0; previousNextPosition = _nextPosition; } } } _moreData = false; } /// <summary> /// Move the reader ahead the specified number of items. /// </summary> [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 { // Can't satisfy from the current span AdvanceToNextSpan(count); } } /// <summary> /// Unchecked helper to avoid unnecessary checks where you know count is valid. /// </summary> [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void AdvanceCurrentSpan(long count) { Debug.Assert(count >= 0); Consumed += count; CurrentSpanIndex += (int)count; if (CurrentSpanIndex >= CurrentSpan.Length) GetNextSpan(); } /// <summary> /// Only call this helper if you know that you are advancing in the current span /// with valid count and there is no need to fetch the next one. /// </summary> [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void AdvanceWithinSpan(long count) { Debug.Assert(count >= 0); Consumed += count; CurrentSpanIndex += (int)count; Debug.Assert(CurrentSpanIndex < CurrentSpan.Length); } private void AdvanceToNextSpan(long count) { if (count < 0) { ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.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; ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count); } } /// <summary> /// Copies data from the current <see cref="Position"/> to the given <paramref name="destination"/> span if there /// is enough data to fill it. /// </summary> /// <remarks> /// This API is used to copy a fixed amount of data out of the sequence if possible. It does not advance /// the reader. To look ahead for a specific stream of data <see cref="IsNext(ReadOnlySpan{T}, bool)"/> can be used. /// </remarks> /// <param name="destination">Destination span to copy to.</param> /// <returns>True if there is enough data to completely fill the <paramref name="destination"/> span.</returns> [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, true)) { 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; } } }