diff --git a/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.Async.cs b/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.Async.cs index b1ddb576b..febd59b57 100644 --- a/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.Async.cs +++ b/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.Async.cs @@ -72,9 +72,9 @@ CancellationToken cancellationToken ) { _headerFactory = headerFactory; - // Use EnsureSeekable to avoid double-wrapping if stream is already a SharpCompressStream, + // Use Create to avoid double-wrapping if stream is already a SharpCompressStream, // and to preserve seekability for DataDescriptorStream which needs to seek backward - _sharpCompressStream = SharpCompressStream.EnsureSeekable(stream); + _sharpCompressStream = SharpCompressStream.Create(stream); _reader = new AsyncBinaryReader(_sharpCompressStream, leaveOpen: true); _cancellationToken = cancellationToken; } diff --git a/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs b/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs index cc91e1845..d68e7177c 100644 --- a/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs +++ b/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs @@ -20,9 +20,9 @@ internal StreamingZipHeaderFactory( internal IEnumerable ReadStreamHeader(Stream stream) { - // Use EnsureSeekable to avoid double-wrapping if stream is already a SharpCompressStream, + // Use Create to avoid double-wrapping if stream is already a SharpCompressStream, // and to preserve seekability for DataDescriptorStream which needs to seek backward - var sharpCompressStream = SharpCompressStream.EnsureSeekable(stream); + var sharpCompressStream = SharpCompressStream.Create(stream); var reader = new BinaryReader( sharpCompressStream, System.Text.Encoding.Default, diff --git a/src/SharpCompress/IO/SeekableSharpCompressStream.Async.cs b/src/SharpCompress/IO/SeekableSharpCompressStream.Async.cs index 34a9ec0eb..f0e4f1dfc 100644 --- a/src/SharpCompress/IO/SeekableSharpCompressStream.Async.cs +++ b/src/SharpCompress/IO/SeekableSharpCompressStream.Async.cs @@ -12,18 +12,18 @@ public override Task ReadAsync( int offset, int count, CancellationToken cancellationToken - ) => _underlyingStream.ReadAsync(buffer, offset, count, cancellationToken); + ) => _stream.ReadAsync(buffer, offset, count, cancellationToken); #if !LEGACY_DOTNET public override ValueTask ReadAsync( Memory buffer, CancellationToken cancellationToken = default - ) => _underlyingStream.ReadAsync(buffer, cancellationToken); + ) => _stream.ReadAsync(buffer, cancellationToken); public override ValueTask WriteAsync( ReadOnlyMemory buffer, CancellationToken cancellationToken = default - ) => _underlyingStream.WriteAsync(buffer, cancellationToken); + ) => _stream.WriteAsync(buffer, cancellationToken); public override ValueTask DisposeAsync() { @@ -40,7 +40,7 @@ public override ValueTask DisposeAsync() _isDisposed = true; if (!LeaveStreamOpen) { - _underlyingStream.Dispose(); + _stream.Dispose(); } return base.DisposeAsync(); } @@ -51,14 +51,14 @@ public override Task WriteAsync( int offset, int count, CancellationToken cancellationToken - ) => _underlyingStream.WriteAsync(buffer, offset, count, cancellationToken); + ) => _stream.WriteAsync(buffer, offset, count, cancellationToken); public override Task FlushAsync(CancellationToken cancellationToken) => - _underlyingStream.FlushAsync(cancellationToken); + _stream.FlushAsync(cancellationToken); public override Task CopyToAsync( Stream destination, int bufferSize, CancellationToken cancellationToken - ) => _underlyingStream.CopyToAsync(destination, bufferSize, cancellationToken); + ) => _stream.CopyToAsync(destination, bufferSize, cancellationToken); } diff --git a/src/SharpCompress/IO/SeekableSharpCompressStream.cs b/src/SharpCompress/IO/SeekableSharpCompressStream.cs index 0e8d83d27..28b67c4e2 100644 --- a/src/SharpCompress/IO/SeekableSharpCompressStream.cs +++ b/src/SharpCompress/IO/SeekableSharpCompressStream.cs @@ -5,25 +5,25 @@ namespace SharpCompress.IO; internal sealed partial class SeekableSharpCompressStream : SharpCompressStream { - public override Stream BaseStream() => _underlyingStream; + public override Stream BaseStream() => _stream; - private readonly Stream _underlyingStream; + private readonly Stream _stream; private long? _recordedPosition; private bool _isDisposed; /// /// Gets or sets whether to leave the underlying stream open when disposed. /// - public new bool LeaveStreamOpen { get; set; } + public override bool LeaveStreamOpen { get; } /// /// Gets or sets whether to throw an exception when Dispose is called. /// Useful for testing to ensure streams are not disposed prematurely. /// - public new bool ThrowOnDispose { get; set; } + public override bool ThrowOnDispose { get; set; } - public SeekableSharpCompressStream(Stream stream) - : base(new NullStream()) + public SeekableSharpCompressStream(Stream stream, bool leaveStreamOpen = false) + : base(Null, true, false, null) { if (stream is null) { @@ -33,44 +33,45 @@ public SeekableSharpCompressStream(Stream stream) { throw new ArgumentException("Stream must be seekable", nameof(stream)); } - _underlyingStream = stream; + + LeaveStreamOpen = leaveStreamOpen; + _stream = stream; } - public override bool CanRead => _underlyingStream.CanRead; + public override bool CanRead => _stream.CanRead; - public override bool CanSeek => _underlyingStream.CanSeek; + public override bool CanSeek => _stream.CanSeek; - public override bool CanWrite => _underlyingStream.CanWrite; + public override bool CanWrite => _stream.CanWrite; - public override long Length => _underlyingStream.Length; + public override long Length => _stream.Length; public override long Position { - get => _underlyingStream.Position; - set => _underlyingStream.Position = value; + get => _stream.Position; + set => _stream.Position = value; } internal override bool IsRecording => _recordedPosition.HasValue; - public override void Flush() => _underlyingStream.Flush(); + public override void Flush() => _stream.Flush(); public override int Read(byte[] buffer, int offset, int count) => - _underlyingStream.Read(buffer, offset, count); + _stream.Read(buffer, offset, count); #if !LEGACY_DOTNET - public override int Read(Span buffer) => _underlyingStream.Read(buffer); + public override int Read(Span buffer) => _stream.Read(buffer); #endif - public override long Seek(long offset, SeekOrigin origin) => - _underlyingStream.Seek(offset, origin); + public override long Seek(long offset, SeekOrigin origin) => _stream.Seek(offset, origin); - public override void SetLength(long value) => _underlyingStream.SetLength(value); + public override void SetLength(long value) => _stream.SetLength(value); public override void Write(byte[] buffer, int offset, int count) => - _underlyingStream.Write(buffer, offset, count); + _stream.Write(buffer, offset, count); #if !LEGACY_DOTNET - public override void Write(ReadOnlySpan buffer) => _underlyingStream.Write(buffer); + public override void Write(ReadOnlySpan buffer) => _stream.Write(buffer); #endif public override void Rewind(bool stopRecording = false) @@ -80,22 +81,16 @@ public override void Rewind(bool stopRecording = false) return; } - _underlyingStream.Seek(_recordedPosition.Value, SeekOrigin.Begin); + _stream.Seek(_recordedPosition.Value, SeekOrigin.Begin); if (stopRecording) { _recordedPosition = null; } } - public override void StartRecording() - { - _recordedPosition = _underlyingStream.Position; - } + public override void StartRecording() => _recordedPosition = _stream.Position; - public override void StopRecording() - { - _recordedPosition = null; - } + public override void StopRecording() => _recordedPosition = null; protected override void Dispose(bool disposing) { @@ -112,45 +107,8 @@ protected override void Dispose(bool disposing) _isDisposed = true; if (disposing && !LeaveStreamOpen) { - _underlyingStream.Dispose(); + _stream.Dispose(); } base.Dispose(disposing); } - - private sealed class NullStream : Stream - { - public override bool CanRead => true; - - public override bool CanSeek => false; - - public override bool CanWrite => false; - - public override long Length => throw new NotSupportedException(); - - public override long Position - { - get => throw new NotSupportedException(); - set => throw new NotSupportedException(); - } - - public override void Flush() { } - - public override int Read(byte[] buffer, int offset, int count) => 0; - -#if !LEGACY_DOTNET - public override int Read(Span buffer) => 0; -#endif - - public override long Seek(long offset, SeekOrigin origin) => - throw new NotSupportedException(); - - public override void SetLength(long value) => throw new NotSupportedException(); - - public override void Write(byte[] buffer, int offset, int count) => - throw new NotSupportedException(); - -#if !LEGACY_DOTNET - public override void Write(ReadOnlySpan buffer) => throw new NotSupportedException(); -#endif - } } diff --git a/src/SharpCompress/IO/SharpCompressStream.Create.cs b/src/SharpCompress/IO/SharpCompressStream.Create.cs new file mode 100644 index 000000000..3a886a807 --- /dev/null +++ b/src/SharpCompress/IO/SharpCompressStream.Create.cs @@ -0,0 +1,59 @@ +using System; +using System.IO; +using SharpCompress.Common; + +namespace SharpCompress.IO; + +internal partial class SharpCompressStream +{ + /// + /// Creates a SharpCompressStream that acts as a passthrough wrapper. + /// No buffering is performed; CanSeek delegates to the underlying stream. + /// The underlying stream will not be disposed when this stream is disposed. + /// + public static SharpCompressStream CreateNonDisposing(Stream stream) => + new(stream, leaveStreamOpen: true, passthrough: true, bufferSize: null); + + public static SharpCompressStream Create(Stream stream, int? bufferSize = null) + { + var rewindableBufferSize = bufferSize ?? Constants.RewindableBufferSize; + + // If it's a passthrough SharpCompressStream, unwrap it and create proper seekable wrapper + if (stream is SharpCompressStream sharpCompressStream) + { + if (sharpCompressStream._isPassthrough) + { + // Unwrap the passthrough and create appropriate wrapper + var underlying = sharpCompressStream.stream; + if (underlying.CanSeek) + { + // Create SeekableSharpCompressStream that preserves LeaveStreamOpen + return new SeekableSharpCompressStream(underlying, true); + } + // Non-seekable underlying stream - wrap with rolling buffer + return new SharpCompressStream(underlying, true, false, rewindableBufferSize); + } + // Not passthrough - return as-is + return sharpCompressStream; + } + + // Check if stream is wrapping a SharpCompressStream (e.g., via IStreamStack) + if (stream is IStreamStack streamStack) + { + var underlying = streamStack.GetStream(); + if (underlying is not null) + { + return underlying; + } + } + + if (stream.CanSeek) + { + return new SeekableSharpCompressStream(stream); + } + + // For non-seekable streams, create a SharpCompressStream with rolling buffer + // to allow limited backward seeking (required by decompressors that over-read) + return new SharpCompressStream(stream, false, false, bufferSize); + } +} diff --git a/src/SharpCompress/IO/SharpCompressStream.cs b/src/SharpCompress/IO/SharpCompressStream.cs index eae6217f5..b47b70313 100644 --- a/src/SharpCompress/IO/SharpCompressStream.cs +++ b/src/SharpCompress/IO/SharpCompressStream.cs @@ -30,20 +30,15 @@ internal partial class SharpCompressStream : Stream, IStreamStack internal bool IsPassthrough => _isPassthrough; /// - /// Default size for rolling buffer (same as .NET Stream.CopyTo default) + /// Gets whether to leave the underlying stream open when disposed. /// - public const int DefaultRollingBufferSize = 81920; - - /// - /// Gets or sets whether to leave the underlying stream open when disposed. - /// - public bool LeaveStreamOpen { get; set; } + public virtual bool LeaveStreamOpen { get; } /// /// Gets or sets whether to throw an exception when Dispose is called. /// Useful for testing to ensure streams are not disposed prematurely. /// - public bool ThrowOnDispose { get; set; } + public virtual bool ThrowOnDispose { get; set; } public SharpCompressStream(Stream stream) { @@ -51,38 +46,26 @@ public SharpCompressStream(Stream stream) _logicalPosition = 0; } - /// - /// Creates a SharpCompressStream with a rolling buffer that enables limited backward seeking. - /// - /// The underlying stream to wrap. - /// Size of the rolling buffer in bytes. - public SharpCompressStream(Stream stream, int rollingBufferSize) - : this(stream) - { - if (rollingBufferSize > 0) - { - _ringBuffer = new RingBuffer(rollingBufferSize); - } - } - /// /// Private constructor for passthrough mode. /// - private SharpCompressStream(Stream stream, bool leaveStreamOpen, bool passthrough) + protected SharpCompressStream( + Stream stream, + bool leaveStreamOpen, + bool passthrough, + int? bufferSize + ) { this.stream = stream; LeaveStreamOpen = leaveStreamOpen; _isPassthrough = passthrough; _logicalPosition = 0; - } - /// - /// Creates a SharpCompressStream that acts as a passthrough wrapper. - /// No buffering is performed; CanSeek delegates to the underlying stream. - /// The underlying stream will not be disposed when this stream is disposed. - /// - public static SharpCompressStream CreateNonDisposing(Stream stream) => - new(stream, leaveStreamOpen: true, passthrough: true); + if (bufferSize.HasValue && bufferSize.Value > 0) + { + _ringBuffer = new RingBuffer(bufferSize.Value); + } + } /// /// Gets whether the stream is actively recording reads to the ring buffer. @@ -121,7 +104,7 @@ public virtual void Rewind(bool stopRecording) if (_isPassthrough) { throw new InvalidOperationException( - "Rewind cannot be called on a passthrough stream. Use EnsureSeekable() first." + "Rewind cannot be called on a passthrough stream. Use Create() first." ); } @@ -160,7 +143,7 @@ public virtual void StopRecording() if (_isPassthrough) { throw new InvalidOperationException( - "StopRecording cannot be called on a passthrough stream. Use EnsureSeekable() first." + "StopRecording cannot be called on a passthrough stream. Use Create() first." ); } if (!IsRecording) @@ -180,61 +163,12 @@ public virtual void StopRecording() // (frozen recording mode) until Rewind(stopRecording: true) is called } - public static SharpCompressStream EnsureSeekable( - Stream stream, - int? rewindableBufferSize = null - ) - { - int bufferSize = rewindableBufferSize ?? Constants.RewindableBufferSize; - - // If it's a passthrough SharpCompressStream, unwrap it and create proper seekable wrapper - if (stream is SharpCompressStream sharpCompressStream) - { - if (sharpCompressStream._isPassthrough) - { - // Unwrap the passthrough and create appropriate wrapper - var underlying = sharpCompressStream.stream; - if (underlying.CanSeek) - { - // Create SeekableSharpCompressStream that preserves LeaveStreamOpen - return new SeekableSharpCompressStream(underlying) - { - LeaveStreamOpen = true, // Preserve non-disposing behavior - }; - } - // Non-seekable underlying stream - wrap with rolling buffer - return new SharpCompressStream(underlying, bufferSize) { LeaveStreamOpen = true }; - } - // Not passthrough - return as-is - return sharpCompressStream; - } - - // Check if stream is wrapping a SharpCompressStream (e.g., via IStreamStack) - if (stream is IStreamStack streamStack) - { - var underlying = streamStack.GetStream(); - if (underlying is not null) - { - return underlying; - } - } - - if (stream.CanSeek) - { - return new SeekableSharpCompressStream(stream); - } - - // For non-seekable streams, create a SharpCompressStream with rolling buffer - // to allow limited backward seeking (required by decompressors that over-read) - return new SharpCompressStream(stream, bufferSize); - } - public virtual void StartRecording() { if (_isPassthrough) { throw new InvalidOperationException( - "StartRecording cannot be called on a passthrough stream. Use EnsureSeekable() first." + "StartRecording cannot be called on a passthrough stream. Use Create() first." ); } if (IsRecording) @@ -247,7 +181,7 @@ public virtual void StartRecording() // Ensure ring buffer exists if (_ringBuffer is null) { - _ringBuffer = new RingBuffer(DefaultRollingBufferSize); + _ringBuffer = new RingBuffer(Constants.BufferSize); } // Mark current position as recording anchor @@ -258,7 +192,7 @@ public virtual void StartRecording() public override bool CanRead => true; - public override bool CanSeek => _isPassthrough ? stream.CanSeek : true; + public override bool CanSeek => !_isPassthrough || stream.CanSeek; public override bool CanWrite => _isPassthrough && stream.CanWrite; diff --git a/src/SharpCompress/Readers/ReaderFactory.Async.cs b/src/SharpCompress/Readers/ReaderFactory.Async.cs index 2293ea076..cf51e5601 100644 --- a/src/SharpCompress/Readers/ReaderFactory.Async.cs +++ b/src/SharpCompress/Readers/ReaderFactory.Async.cs @@ -54,9 +54,9 @@ public static async ValueTask OpenAsyncReader( stream.NotNull(nameof(stream)); options ??= new ReaderOptions() { LeaveStreamOpen = false }; - var sharpCompressStream = SharpCompressStream.EnsureSeekable( + var sharpCompressStream = SharpCompressStream.Create( stream, - options.RewindableBufferSize + bufferSize: options.RewindableBufferSize ); sharpCompressStream.StartRecording(); diff --git a/src/SharpCompress/Readers/ReaderFactory.cs b/src/SharpCompress/Readers/ReaderFactory.cs index 180c28092..186efb64f 100644 --- a/src/SharpCompress/Readers/ReaderFactory.cs +++ b/src/SharpCompress/Readers/ReaderFactory.cs @@ -34,9 +34,9 @@ public static IReader OpenReader(Stream stream, ReaderOptions? options = null) stream.NotNull(nameof(stream)); options ??= new ReaderOptions() { LeaveStreamOpen = false }; - var sharpCompressStream = SharpCompressStream.EnsureSeekable( + var sharpCompressStream = SharpCompressStream.Create( stream, - options.RewindableBufferSize + bufferSize: options.RewindableBufferSize ); sharpCompressStream.StartRecording(); diff --git a/src/SharpCompress/Readers/Tar/TarReader.cs b/src/SharpCompress/Readers/Tar/TarReader.cs index c20687892..b29c820d1 100644 --- a/src/SharpCompress/Readers/Tar/TarReader.cs +++ b/src/SharpCompress/Readers/Tar/TarReader.cs @@ -57,9 +57,9 @@ public static IReader OpenReader(Stream stream, ReaderOptions? options = null) { stream.NotNull(nameof(stream)); options = options ?? new ReaderOptions(); - var sharpCompressStream = SharpCompressStream.EnsureSeekable( + var sharpCompressStream = SharpCompressStream.Create( stream, - options.RewindableBufferSize + bufferSize: options.RewindableBufferSize ); long pos = sharpCompressStream.Position; if (GZipArchive.IsGZipFile(sharpCompressStream))