From e2cb9f39ab939a0ba49ca2175aa39b0835532f2b Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 29 Jan 2026 09:08:40 +0000 Subject: [PATCH] fix up rewindable stream and use it more, add NonDisposingStream --- src/SharpCompress/Common/Volume.cs | 2 +- src/SharpCompress/Factories/Factory.cs | 2 +- src/SharpCompress/Factories/GZipFactory.cs | 4 +- src/SharpCompress/Factories/TarFactory.cs | 13 +- src/SharpCompress/IO/NonDisposingStream.cs | 216 ++++++++++++++++++ src/SharpCompress/IO/RewindableStream.cs | 7 + .../Readers/ReaderFactory.Async.cs | 16 +- src/SharpCompress/Readers/ReaderFactory.cs | 2 +- .../Mocks/ForwardOnlyStream.cs | 176 +++++++++----- tests/SharpCompress.Test/ReaderTests.cs | 7 +- .../Streams/RewindableStreamAsyncTest.cs | 67 ++++++ .../Streams/RewindableStreamTest.cs | 103 +++++++++ 12 files changed, 529 insertions(+), 86 deletions(-) create mode 100644 src/SharpCompress/IO/NonDisposingStream.cs diff --git a/src/SharpCompress/Common/Volume.cs b/src/SharpCompress/Common/Volume.cs index db545865..5ffaf9a0 100644 --- a/src/SharpCompress/Common/Volume.cs +++ b/src/SharpCompress/Common/Volume.cs @@ -18,7 +18,7 @@ public abstract partial class Volume : IVolume, IAsyncDisposable _baseStream = stream; if (ReaderOptions.LeaveStreamOpen) { - stream = SharpCompressStream.Create(stream, leaveOpen: true); + stream = new NonDisposingStream(stream); } if (stream is IStreamStack ss) diff --git a/src/SharpCompress/Factories/Factory.cs b/src/SharpCompress/Factories/Factory.cs index d838173b..589b8c10 100644 --- a/src/SharpCompress/Factories/Factory.cs +++ b/src/SharpCompress/Factories/Factory.cs @@ -87,7 +87,7 @@ public abstract class Factory : IFactory stream.Rewind(); if (IsArchive(stream, options.Password)) { - stream.Rewind(); + stream.StopRecording(); reader = readerFactory.OpenReader(stream, options); return true; } diff --git a/src/SharpCompress/Factories/GZipFactory.cs b/src/SharpCompress/Factories/GZipFactory.cs index c69b9a2a..40907e1d 100644 --- a/src/SharpCompress/Factories/GZipFactory.cs +++ b/src/SharpCompress/Factories/GZipFactory.cs @@ -120,11 +120,11 @@ public class GZipFactory var testStream = new GZipStream(rewindableStream, CompressionMode.Decompress); if (TarArchive.IsTarFile(testStream)) { - rewindableStream.Rewind(); + rewindableStream.StopRecording(); reader = new TarReader(rewindableStream, options, CompressionType.GZip); return true; } - rewindableStream.Rewind(); + rewindableStream.StopRecording(); reader = OpenReader(rewindableStream, options); return true; } diff --git a/src/SharpCompress/Factories/TarFactory.cs b/src/SharpCompress/Factories/TarFactory.cs index 3653f570..189ad3b4 100644 --- a/src/SharpCompress/Factories/TarFactory.cs +++ b/src/SharpCompress/Factories/TarFactory.cs @@ -168,7 +168,7 @@ public class TarFactory var decompressedStream = wrapper.CreateStream(rewindableStream); if (TarArchive.IsTarFile(decompressedStream)) { - rewindableStream.Rewind(); + rewindableStream.StopRecording(); return new TarReader(rewindableStream, options, wrapper.CompressionType); } } @@ -185,18 +185,19 @@ public class TarFactory { cancellationToken.ThrowIfCancellationRequested(); options ??= new ReaderOptions(); - var rewindableStream = new SharpCompressStream(stream); - var pos = rewindableStream.GetPosition(); + var rewindableStream = new RewindableStream(stream); + rewindableStream.StartRecording(); foreach (var wrapper in TarWrapper.Wrappers) { - rewindableStream.StackSeek(pos); + rewindableStream.Rewind(); if (await wrapper.IsMatchAsync(rewindableStream, cancellationToken)) { - rewindableStream.StackSeek(pos); + rewindableStream.Rewind(); var decompressedStream = wrapper.CreateStream(rewindableStream); if (await TarArchive.IsTarFileAsync(decompressedStream, cancellationToken)) { - rewindableStream.StackSeek(pos); + rewindableStream.Rewind(); + rewindableStream.StopRecording(); return new TarReader(rewindableStream, options, wrapper.CompressionType); } } diff --git a/src/SharpCompress/IO/NonDisposingStream.cs b/src/SharpCompress/IO/NonDisposingStream.cs new file mode 100644 index 00000000..35ddeb76 --- /dev/null +++ b/src/SharpCompress/IO/NonDisposingStream.cs @@ -0,0 +1,216 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress.IO; + +/// +/// A stream wrapper that prevents disposal of the underlying stream. +/// This is useful when working with compression streams directly and you want +/// to keep the base stream open after the compression stream is disposed. +/// +internal class NonDisposingStream : Stream +{ + private readonly Stream _stream; + private bool _isDisposed; + + /// + /// Gets or sets a value indicating whether to throw an exception when the stream is disposed. + /// This is useful for testing to ensure streams are not disposed prematurely. + /// + public bool ThrowOnDispose { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The stream to wrap. This stream will NOT be disposed when this wrapper is disposed. + /// Thrown when is null. + public NonDisposingStream(Stream stream) + { + _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + } + + public override bool CanRead => !_isDisposed && _stream.CanRead; + + public override bool CanSeek => !_isDisposed && _stream.CanSeek; + + public override bool CanWrite => !_isDisposed && _stream.CanWrite; + + public override long Length + { + get + { + ThrowIfDisposed(); + return _stream.Length; + } + } + + public override long Position + { + get + { + ThrowIfDisposed(); + return _stream.Position; + } + set + { + ThrowIfDisposed(); + _stream.Position = value; + } + } + + public override void Flush() + { + ThrowIfDisposed(); + _stream.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + ThrowIfDisposed(); + return _stream.Read(buffer, offset, count); + } + +#if !LEGACY_DOTNET + public override int Read(Span buffer) + { + ThrowIfDisposed(); + return _stream.Read(buffer); + } +#endif + + public override long Seek(long offset, SeekOrigin origin) + { + ThrowIfDisposed(); + return _stream.Seek(offset, origin); + } + + public override void SetLength(long value) + { + ThrowIfDisposed(); + _stream.SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + ThrowIfDisposed(); + _stream.Write(buffer, offset, count); + } + +#if !LEGACY_DOTNET + public override void Write(ReadOnlySpan buffer) + { + ThrowIfDisposed(); + _stream.Write(buffer); + } +#endif + + public override Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + ThrowIfDisposed(); + return _stream.ReadAsync(buffer, offset, count, cancellationToken); + } + +#if !LEGACY_DOTNET + public override ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) + { + ThrowIfDisposed(); + return _stream.ReadAsync(buffer, cancellationToken); + } +#endif + + public override Task WriteAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) + { + ThrowIfDisposed(); + return _stream.WriteAsync(buffer, offset, count, cancellationToken); + } + +#if !LEGACY_DOTNET + public override ValueTask WriteAsync( + ReadOnlyMemory buffer, + CancellationToken cancellationToken = default + ) + { + ThrowIfDisposed(); + return _stream.WriteAsync(buffer, cancellationToken); + } +#endif + + public override Task FlushAsync(CancellationToken cancellationToken) + { + ThrowIfDisposed(); + return _stream.FlushAsync(cancellationToken); + } + + public override Task CopyToAsync( + Stream destination, + int bufferSize, + CancellationToken cancellationToken + ) + { + ThrowIfDisposed(); + return _stream.CopyToAsync(destination, bufferSize, cancellationToken); + } + + /// + /// Disposes this wrapper without disposing the underlying stream. + /// + protected override void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (ThrowOnDispose) + { + throw new InvalidOperationException( + $"Attempt to dispose of a {nameof(NonDisposingStream)} when {nameof(ThrowOnDispose)} is true" + ); + } + _isDisposed = true; + // Intentionally do NOT dispose _stream + } + base.Dispose(disposing); + } + +#if !LEGACY_DOTNET + /// + /// Asynchronously disposes this wrapper without disposing the underlying stream. + /// + public override async ValueTask DisposeAsync() + { + if (!_isDisposed) + { + if (ThrowOnDispose) + { + throw new InvalidOperationException( + $"Attempt to dispose of a {nameof(NonDisposingStream)} when {nameof(ThrowOnDispose)} is true" + ); + } + _isDisposed = true; + // Intentionally do NOT dispose _stream + } + await base.DisposeAsync(); + } +#endif + + private void ThrowIfDisposed() + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(NonDisposingStream)); + } + } +} diff --git a/src/SharpCompress/IO/RewindableStream.cs b/src/SharpCompress/IO/RewindableStream.cs index 1688efdb..935972c3 100644 --- a/src/SharpCompress/IO/RewindableStream.cs +++ b/src/SharpCompress/IO/RewindableStream.cs @@ -84,6 +84,13 @@ namespace SharpCompress.IO IsRecording = true; } + public void StopRecording() + { + _isRewound = true; + IsRecording = false; + _bufferPosition = 0; + } + public override bool CanRead => true; public override bool CanSeek => stream.CanSeek; diff --git a/src/SharpCompress/Readers/ReaderFactory.Async.cs b/src/SharpCompress/Readers/ReaderFactory.Async.cs index b879b2ba..f279895e 100644 --- a/src/SharpCompress/Readers/ReaderFactory.Async.cs +++ b/src/SharpCompress/Readers/ReaderFactory.Async.cs @@ -54,8 +54,8 @@ public static partial class ReaderFactory stream.NotNull(nameof(stream)); options ??= new ReaderOptions() { LeaveStreamOpen = false }; - var bStream = new SharpCompressStream(stream, bufferSize: options.BufferSize); - long pos = bStream.GetPosition(); + var bStream = new RewindableStream(stream); + bStream.StartRecording(); var factories = Factory.Factories.OfType(); @@ -68,7 +68,7 @@ public static partial class ReaderFactory ); if (testedFactory is IReaderFactory readerFactory) { - bStream.StackSeek(pos); + bStream.Rewind(); if ( await testedFactory.IsArchiveAsync( bStream, @@ -76,11 +76,12 @@ public static partial class ReaderFactory ) ) { - bStream.StackSeek(pos); + bStream.Rewind(); + bStream.StopRecording(); return await readerFactory.OpenAsyncReader(bStream, options, cancellationToken); } } - bStream.StackSeek(pos); + bStream.Rewind(); } foreach (var factory in factories) @@ -89,13 +90,14 @@ public static partial class ReaderFactory { continue; // Already tested above } - bStream.StackSeek(pos); + bStream.Rewind(); if ( factory is IReaderFactory readerFactory && await factory.IsArchiveAsync(bStream, cancellationToken: cancellationToken) ) { - bStream.StackSeek(pos); + bStream.Rewind(); + bStream.StopRecording(); return await readerFactory.OpenAsyncReader(bStream, options, cancellationToken); } } diff --git a/src/SharpCompress/Readers/ReaderFactory.cs b/src/SharpCompress/Readers/ReaderFactory.cs index ba3a01bd..00f04404 100644 --- a/src/SharpCompress/Readers/ReaderFactory.cs +++ b/src/SharpCompress/Readers/ReaderFactory.cs @@ -52,7 +52,7 @@ public static partial class ReaderFactory && reader != null ) { - bStream.Rewind(); + bStream.StopRecording(); return reader; } } diff --git a/tests/SharpCompress.Test/Mocks/ForwardOnlyStream.cs b/tests/SharpCompress.Test/Mocks/ForwardOnlyStream.cs index 2b80ce2d..32961ebc 100644 --- a/tests/SharpCompress.Test/Mocks/ForwardOnlyStream.cs +++ b/tests/SharpCompress.Test/Mocks/ForwardOnlyStream.cs @@ -2,113 +2,163 @@ using System; using System.IO; using System.Threading; using System.Threading.Tasks; -using SharpCompress.Common; -using SharpCompress.IO; namespace SharpCompress.Test.Mocks; -public class ForwardOnlyStream : SharpCompressStream, IStreamStack +/// +/// A forward-only stream wrapper that delegates directly to the underlying stream +/// without any buffering. Supports reading and writing but not seeking. +/// +public class ForwardOnlyStream : Stream { - private readonly Stream stream; -#if DEBUG_STREAMS - long IStreamStack.InstanceId { get; set; } -#endif - - Stream IStreamStack.BaseStream() => stream; - - int IStreamStack.BufferSize - { - get => 0; - set { } - } - int IStreamStack.BufferPosition - { - get => 0; - set { } - } - - void IStreamStack.SetPosition(long position) { } - - public bool IsDisposed { get; private set; } + private readonly Stream _stream; + private bool _isDisposed; + /// + /// Initializes a new instance of the class. + /// + /// The underlying stream to wrap. + /// Buffer size parameter (ignored - this implementation does not buffer). + /// Thrown when is null. public ForwardOnlyStream(Stream stream, int? bufferSize = null) - : base(stream, bufferSize: bufferSize ?? Constants.BufferSize) { - this.stream = stream; -#if DEBUG_STREAMS - this.DebugConstruct(typeof(ForwardOnlyStream)); -#endif + _stream = stream ?? throw new ArgumentNullException(nameof(stream)); + // bufferSize is ignored - this implementation does not buffer } - protected override void Dispose(bool disposing) - { - if (!IsDisposed) - { - if (disposing) - { -#if DEBUG_STREAMS - this.DebugDispose(typeof(ForwardOnlyStream)); -#endif - stream.Dispose(); - IsDisposed = true; - base.Dispose(disposing); - } - } - } - - public override bool CanRead => true; + public override bool CanRead => !_isDisposed && _stream.CanRead; public override bool CanSeek => false; - public override bool CanWrite => true; - public override void Flush() { } + public override bool CanWrite => !_isDisposed && _stream.CanWrite; - public override long Length => throw new NotSupportedException(); + public override long Length + { + get => throw new NotSupportedException("Length is not supported on a forward-only stream."); + } public override long Position { - get => stream.Position; - set => throw new NotSupportedException(); + get => + throw new NotSupportedException("Position is not supported on a forward-only stream."); + set => + throw new NotSupportedException("Position is not supported on a forward-only stream."); } - public override int Read(byte[] buffer, int offset, int count) => - stream.Read(buffer, offset, count); + public override void Flush() + { + ThrowIfDisposed(); + _stream.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + ThrowIfDisposed(); + return _stream.Read(buffer, offset, count); + } public override Task ReadAsync( byte[] buffer, int offset, int count, CancellationToken cancellationToken - ) => stream.ReadAsync(buffer, offset, count, cancellationToken); + ) + { + ThrowIfDisposed(); + return _stream.ReadAsync(buffer, offset, count, cancellationToken); + } #if !LEGACY_DOTNET public override ValueTask ReadAsync( Memory buffer, CancellationToken cancellationToken = default - ) => stream.ReadAsync(buffer, cancellationToken); + ) + { + ThrowIfDisposed(); + return _stream.ReadAsync(buffer, cancellationToken); + } #endif - public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException("Seek is not supported on a forward-only stream."); - public override void SetLength(long value) => throw new NotSupportedException(); + public override void SetLength(long value) => + throw new NotSupportedException("SetLength is not supported on a forward-only stream."); - public override void Write(byte[] buffer, int offset, int count) => - stream.Write(buffer, offset, count); + public override void Write(byte[] buffer, int offset, int count) + { + ThrowIfDisposed(); + _stream.Write(buffer, offset, count); + } public override Task WriteAsync( byte[] buffer, int offset, int count, CancellationToken cancellationToken - ) => stream.WriteAsync(buffer, offset, count, cancellationToken); + ) + { + ThrowIfDisposed(); + return _stream.WriteAsync(buffer, offset, count, cancellationToken); + } #if !LEGACY_DOTNET public override ValueTask WriteAsync( ReadOnlyMemory buffer, CancellationToken cancellationToken = default - ) => stream.WriteAsync(buffer, cancellationToken); + ) + { + ThrowIfDisposed(); + return _stream.WriteAsync(buffer, cancellationToken); + } #endif - public override Task FlushAsync(CancellationToken cancellationToken) => - stream.FlushAsync(cancellationToken); + public override Task FlushAsync(CancellationToken cancellationToken) + { + ThrowIfDisposed(); + return _stream.FlushAsync(cancellationToken); + } + + public override Task CopyToAsync( + Stream destination, + int bufferSize, + CancellationToken cancellationToken + ) + { + ThrowIfDisposed(); + return _stream.CopyToAsync(destination, bufferSize, cancellationToken); + } + + protected override void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + _stream.Dispose(); + } + _isDisposed = true; + base.Dispose(disposing); + } + } + +#if !LEGACY_DOTNET + public override async ValueTask DisposeAsync() + { + if (!_isDisposed) + { + await _stream.DisposeAsync(); + _isDisposed = true; + } + await base.DisposeAsync(); + } +#endif + + private void ThrowIfDisposed() + { + if (_isDisposed) + { + throw new ObjectDisposedException(nameof(ForwardOnlyStream)); + } + } } diff --git a/tests/SharpCompress.Test/ReaderTests.cs b/tests/SharpCompress.Test/ReaderTests.cs index 84afa636..d357ec3f 100644 --- a/tests/SharpCompress.Test/ReaderTests.cs +++ b/tests/SharpCompress.Test/ReaderTests.cs @@ -164,11 +164,8 @@ public abstract class ReaderTests : TestBase using var file = File.OpenRead(testArchive); #if !LEGACY_DOTNET - await using var protectedStream = SharpCompressStream.Create( - new ForwardOnlyStream(file, options.BufferSize), - leaveOpen: true, - throwOnDispose: true, - bufferSize: options.BufferSize + await using var protectedStream = new NonDisposingStream( + new ForwardOnlyStream(file, options.BufferSize) ); await using var testStream = new TestStream(protectedStream); #else diff --git a/tests/SharpCompress.Test/Streams/RewindableStreamAsyncTest.cs b/tests/SharpCompress.Test/Streams/RewindableStreamAsyncTest.cs index 692b5b01..c5de62ba 100644 --- a/tests/SharpCompress.Test/Streams/RewindableStreamAsyncTest.cs +++ b/tests/SharpCompress.Test/Streams/RewindableStreamAsyncTest.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using SharpCompress.IO; @@ -398,4 +399,70 @@ public class RewindableStreamAsyncTest return result.ToArray(); } #endif + + [Fact] + public async ValueTask TestStopRecordingAsync() + { + var ms = new MemoryStream(); + var bw = new BinaryWriter(ms); + bw.Write(1); + bw.Write(2); + bw.Write(3); + bw.Write(4); + bw.Write(5); + bw.Write(6); + bw.Write(7); + bw.Flush(); + ms.Position = 0; + + var stream = new RewindableStream(ms); + stream.StartRecording(); + + Assert.Equal(1, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(2, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(3, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(4, await ReadInt32Async(stream).ConfigureAwait(false)); + + stream.StopRecording(); + + Assert.Equal(1, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(2, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(3, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(4, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(5, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(6, await ReadInt32Async(stream).ConfigureAwait(false)); + Assert.Equal(7, await ReadInt32Async(stream).ConfigureAwait(false)); + + Assert.False(stream.IsRecording); + } + + [Fact] + public async ValueTask TestStopRecordingNoFurtherBufferingAsync() + { + var ms = new MemoryStream(); + var bw = new BinaryWriter(ms); + bw.Write(1); + bw.Write(2); + bw.Write(3); + bw.Write(4); + bw.Flush(); + ms.Position = 0; + + var stream = new RewindableStream(ms); + stream.StartRecording(); + + var buffer = new byte[8]; + await stream.ReadAsync(buffer, 0, 8).ConfigureAwait(false); + + stream.StopRecording(); + + await stream.ReadAsync(buffer, 0, 8).ConfigureAwait(false); + Assert.Equal(BitConverter.GetBytes(1), buffer.Take(4).ToArray()); + Assert.Equal(BitConverter.GetBytes(2), buffer.Skip(4).Take(4).ToArray()); + + int bytesRead = await stream.ReadAsync(buffer, 0, 8).ConfigureAwait(false); + Assert.Equal(8, bytesRead); + + Assert.False(stream.IsRecording); + } } diff --git a/tests/SharpCompress.Test/Streams/RewindableStreamTest.cs b/tests/SharpCompress.Test/Streams/RewindableStreamTest.cs index bf6a74a2..138daf42 100644 --- a/tests/SharpCompress.Test/Streams/RewindableStreamTest.cs +++ b/tests/SharpCompress.Test/Streams/RewindableStreamTest.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Linq; using SharpCompress.IO; using Xunit; @@ -194,4 +195,106 @@ public class RewindableStreamTest stream.Dispose(); Assert.Throws(() => stream.Read(new byte[4], 0, 4)); } + + [Fact] + public void TestStopRecordingBasic() + { + var ms = new MemoryStream(); + var bw = new BinaryWriter(ms); + bw.Write(1); + bw.Write(2); + bw.Write(3); + bw.Write(4); + bw.Write(5); + bw.Write(6); + bw.Write(7); + bw.Flush(); + ms.Position = 0; + + var stream = new RewindableStream(ms); + stream.StartRecording(); + var br = new BinaryReader(stream); + + Assert.Equal(1, br.ReadInt32()); + Assert.Equal(2, br.ReadInt32()); + Assert.Equal(3, br.ReadInt32()); + Assert.Equal(4, br.ReadInt32()); + + stream.StopRecording(); + + Assert.Equal(1, br.ReadInt32()); + Assert.Equal(2, br.ReadInt32()); + Assert.Equal(3, br.ReadInt32()); + Assert.Equal(4, br.ReadInt32()); + Assert.Equal(5, br.ReadInt32()); + Assert.Equal(6, br.ReadInt32()); + Assert.Equal(7, br.ReadInt32()); + + Assert.False(stream.IsRecording); + } + + [Fact] + public void TestStopRecordingNoFurtherBuffering() + { + var ms = new MemoryStream(); + var bw = new BinaryWriter(ms); + bw.Write(1); + bw.Write(2); + bw.Write(3); + bw.Write(4); + bw.Flush(); + ms.Position = 0; + + var stream = new RewindableStream(ms); + stream.StartRecording(); + + var buffer = new byte[8]; + stream.Read(buffer, 0, 8); + + stream.StopRecording(); + + stream.Read(buffer, 0, 8); + Assert.Equal(BitConverter.GetBytes(1), buffer.Take(4).ToArray()); + Assert.Equal(BitConverter.GetBytes(2), buffer.Skip(4).Take(4).ToArray()); + + int bytesRead = stream.Read(buffer, 0, 8); + Assert.Equal(8, bytesRead); + + Assert.False(stream.IsRecording); + + bytesRead = stream.Read(buffer, 0, 8); + Assert.Equal(0, bytesRead); + } + +#if !LEGACY_DOTNET + [Fact] + public void TestStopRecordingWithSpan() + { + var ms = new MemoryStream(); + var bw = new BinaryWriter(ms); + bw.Write(1); + bw.Write(2); + bw.Write(3); + bw.Write(4); + bw.Flush(); + ms.Position = 0; + + var stream = new RewindableStream(ms); + stream.StartRecording(); + + var buffer = new byte[8]; + stream.Read(buffer); + + stream.StopRecording(); + + stream.Read(buffer); + Assert.Equal(BitConverter.GetBytes(1), buffer.Take(4).ToArray()); + Assert.Equal(BitConverter.GetBytes(2), buffer.Skip(4).Take(4).ToArray()); + + int bytesRead = stream.Read(buffer); + Assert.Equal(8, bytesRead); + Assert.Equal(BitConverter.GetBytes(3), buffer.Take(4).ToArray()); + Assert.Equal(BitConverter.GetBytes(4), buffer.Skip(4).Take(4).ToArray()); + } +#endif }