From 7231b7b35cc1cf72b3e50ec25652048ce6fda779 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Wed, 4 Feb 2026 17:03:56 +0000 Subject: [PATCH 1/8] move static methods on SCStream --- .../Zip/StreamingZipHeaderFactory.Async.cs | 4 +- .../Common/Zip/StreamingZipHeaderFactory.cs | 4 +- .../IO/SharpCompressStream.Create.cs | 65 +++++++++++++++++++ src/SharpCompress/IO/SharpCompressStream.cs | 63 +----------------- .../Readers/ReaderFactory.Async.cs | 2 +- src/SharpCompress/Readers/ReaderFactory.cs | 2 +- src/SharpCompress/Readers/Tar/TarReader.cs | 2 +- 7 files changed, 75 insertions(+), 67 deletions(-) create mode 100644 src/SharpCompress/IO/SharpCompressStream.Create.cs diff --git a/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.Async.cs b/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.Async.cs index b1ddb576..febd59b5 100644 --- a/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.Async.cs +++ b/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.Async.cs @@ -72,9 +72,9 @@ internal sealed partial class StreamingZipHeaderFactory ) { _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 cc91e184..d68e7177 100644 --- a/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs +++ b/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs @@ -20,9 +20,9 @@ internal partial class StreamingZipHeaderFactory : ZipHeaderFactory 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/SharpCompressStream.Create.cs b/src/SharpCompress/IO/SharpCompressStream.Create.cs new file mode 100644 index 00000000..5f30a585 --- /dev/null +++ b/src/SharpCompress/IO/SharpCompressStream.Create.cs @@ -0,0 +1,65 @@ +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); + + public static SharpCompressStream Create( + 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); + } +} diff --git a/src/SharpCompress/IO/SharpCompressStream.cs b/src/SharpCompress/IO/SharpCompressStream.cs index eae6217f..adeca170 100644 --- a/src/SharpCompress/IO/SharpCompressStream.cs +++ b/src/SharpCompress/IO/SharpCompressStream.cs @@ -76,14 +76,6 @@ internal partial class SharpCompressStream : Stream, IStreamStack _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); - /// /// Gets whether the stream is actively recording reads to the ring buffer. /// @@ -121,7 +113,7 @@ internal partial class SharpCompressStream : Stream, IStreamStack 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 +152,7 @@ internal partial class SharpCompressStream : Stream, IStreamStack 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 +172,12 @@ internal partial class SharpCompressStream : Stream, IStreamStack // (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) diff --git a/src/SharpCompress/Readers/ReaderFactory.Async.cs b/src/SharpCompress/Readers/ReaderFactory.Async.cs index 2293ea07..a1302fc0 100644 --- a/src/SharpCompress/Readers/ReaderFactory.Async.cs +++ b/src/SharpCompress/Readers/ReaderFactory.Async.cs @@ -54,7 +54,7 @@ public static partial class ReaderFactory stream.NotNull(nameof(stream)); options ??= new ReaderOptions() { LeaveStreamOpen = false }; - var sharpCompressStream = SharpCompressStream.EnsureSeekable( + var sharpCompressStream = SharpCompressStream.Create( stream, options.RewindableBufferSize ); diff --git a/src/SharpCompress/Readers/ReaderFactory.cs b/src/SharpCompress/Readers/ReaderFactory.cs index 180c2809..7aad0c00 100644 --- a/src/SharpCompress/Readers/ReaderFactory.cs +++ b/src/SharpCompress/Readers/ReaderFactory.cs @@ -34,7 +34,7 @@ public static partial class ReaderFactory stream.NotNull(nameof(stream)); options ??= new ReaderOptions() { LeaveStreamOpen = false }; - var sharpCompressStream = SharpCompressStream.EnsureSeekable( + var sharpCompressStream = SharpCompressStream.Create( stream, options.RewindableBufferSize ); diff --git a/src/SharpCompress/Readers/Tar/TarReader.cs b/src/SharpCompress/Readers/Tar/TarReader.cs index c2068789..562ec362 100644 --- a/src/SharpCompress/Readers/Tar/TarReader.cs +++ b/src/SharpCompress/Readers/Tar/TarReader.cs @@ -57,7 +57,7 @@ public partial class TarReader : AbstractReader { stream.NotNull(nameof(stream)); options = options ?? new ReaderOptions(); - var sharpCompressStream = SharpCompressStream.EnsureSeekable( + var sharpCompressStream = SharpCompressStream.Create( stream, options.RewindableBufferSize ); From d26db95aff4e890894a3d7470fd0af779aee25d4 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 5 Feb 2026 08:30:52 +0000 Subject: [PATCH 2/8] change some construction --- .../IO/SharpCompressStream.Create.cs | 10 +++++----- src/SharpCompress/IO/SharpCompressStream.cs | 20 ++++++------------- .../Readers/ReaderFactory.Async.cs | 2 +- src/SharpCompress/Readers/ReaderFactory.cs | 2 +- src/SharpCompress/Readers/Tar/TarReader.cs | 2 +- 5 files changed, 14 insertions(+), 22 deletions(-) diff --git a/src/SharpCompress/IO/SharpCompressStream.Create.cs b/src/SharpCompress/IO/SharpCompressStream.Create.cs index 5f30a585..7cf40095 100644 --- a/src/SharpCompress/IO/SharpCompressStream.Create.cs +++ b/src/SharpCompress/IO/SharpCompressStream.Create.cs @@ -12,14 +12,14 @@ internal partial class SharpCompressStream /// The underlying stream will not be disposed when this stream is disposed. /// public static SharpCompressStream CreateNonDisposing(Stream stream) => - new(stream, leaveStreamOpen: true, passthrough: true); + new(stream, leaveStreamOpen: true, passthrough: true, bufferSize: null); public static SharpCompressStream Create( Stream stream, - int? rewindableBufferSize = null + int? bufferSize = null ) { - int bufferSize = rewindableBufferSize ?? Constants.RewindableBufferSize; + int rewindableBufferSize = bufferSize ?? Constants.RewindableBufferSize; // If it's a passthrough SharpCompressStream, unwrap it and create proper seekable wrapper if (stream is SharpCompressStream sharpCompressStream) @@ -37,7 +37,7 @@ internal partial class SharpCompressStream }; } // Non-seekable underlying stream - wrap with rolling buffer - return new SharpCompressStream(underlying, bufferSize) { LeaveStreamOpen = true }; + return new SharpCompressStream(underlying, true, false, rewindableBufferSize); } // Not passthrough - return as-is return sharpCompressStream; @@ -60,6 +60,6 @@ internal partial class SharpCompressStream // 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); + return new SharpCompressStream(stream, false,false, bufferSize); } } diff --git a/src/SharpCompress/IO/SharpCompressStream.cs b/src/SharpCompress/IO/SharpCompressStream.cs index adeca170..f0aec887 100644 --- a/src/SharpCompress/IO/SharpCompressStream.cs +++ b/src/SharpCompress/IO/SharpCompressStream.cs @@ -51,29 +51,21 @@ internal partial class SharpCompressStream : Stream, IStreamStack _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) + private SharpCompressStream(Stream stream, bool leaveStreamOpen, bool passthrough, int? bufferSize) { this.stream = stream; LeaveStreamOpen = leaveStreamOpen; _isPassthrough = passthrough; _logicalPosition = 0; + + if (bufferSize.HasValue && bufferSize.Value > 0) + { + _ringBuffer = new RingBuffer(bufferSize.Value); + } } /// diff --git a/src/SharpCompress/Readers/ReaderFactory.Async.cs b/src/SharpCompress/Readers/ReaderFactory.Async.cs index a1302fc0..c0b3f623 100644 --- a/src/SharpCompress/Readers/ReaderFactory.Async.cs +++ b/src/SharpCompress/Readers/ReaderFactory.Async.cs @@ -56,7 +56,7 @@ public static partial class ReaderFactory 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 7aad0c00..6cafb063 100644 --- a/src/SharpCompress/Readers/ReaderFactory.cs +++ b/src/SharpCompress/Readers/ReaderFactory.cs @@ -36,7 +36,7 @@ public static partial class ReaderFactory 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 562ec362..8a79e49f 100644 --- a/src/SharpCompress/Readers/Tar/TarReader.cs +++ b/src/SharpCompress/Readers/Tar/TarReader.cs @@ -59,7 +59,7 @@ public partial class TarReader : AbstractReader options = options ?? new ReaderOptions(); var sharpCompressStream = SharpCompressStream.Create( stream, - options.RewindableBufferSize + bufferSize:options.RewindableBufferSize ); long pos = sharpCompressStream.Position; if (GZipArchive.IsGZipFile(sharpCompressStream)) From d12b5397202c4804d68d6d104cd4a62bbc4d4fba Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 5 Feb 2026 08:52:00 +0000 Subject: [PATCH 3/8] some clean up --- .../IO/SeekableSharpCompressStream.Async.cs | 14 +-- .../IO/SeekableSharpCompressStream.cs | 85 +++++-------------- .../IO/SharpCompressStream.Create.cs | 7 +- src/SharpCompress/IO/SharpCompressStream.cs | 13 +-- 4 files changed, 34 insertions(+), 85 deletions(-) diff --git a/src/SharpCompress/IO/SeekableSharpCompressStream.Async.cs b/src/SharpCompress/IO/SeekableSharpCompressStream.Async.cs index 34a9ec0e..f0e4f1df 100644 --- a/src/SharpCompress/IO/SeekableSharpCompressStream.Async.cs +++ b/src/SharpCompress/IO/SeekableSharpCompressStream.Async.cs @@ -12,18 +12,18 @@ internal sealed partial class SeekableSharpCompressStream 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 @@ internal sealed partial class SeekableSharpCompressStream _isDisposed = true; if (!LeaveStreamOpen) { - _underlyingStream.Dispose(); + _stream.Dispose(); } return base.DisposeAsync(); } @@ -51,14 +51,14 @@ internal sealed partial class SeekableSharpCompressStream 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 0e8d83d2..20348f35 100644 --- a/src/SharpCompress/IO/SeekableSharpCompressStream.cs +++ b/src/SharpCompress/IO/SeekableSharpCompressStream.cs @@ -5,9 +5,9 @@ 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; @@ -23,7 +23,7 @@ internal sealed partial class SeekableSharpCompressStream : SharpCompressStream public new bool ThrowOnDispose { get; set; } public SeekableSharpCompressStream(Stream stream) - : base(new NullStream()) + : base(Null, true, false, null) { if (stream is null) { @@ -33,44 +33,44 @@ internal sealed partial class SeekableSharpCompressStream : SharpCompressStream { throw new ArgumentException("Stream must be seekable", nameof(stream)); } - _underlyingStream = stream; + _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); + _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 +80,16 @@ internal sealed partial class SeekableSharpCompressStream : SharpCompressStream 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 +106,8 @@ internal sealed partial class SeekableSharpCompressStream : SharpCompressStream _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 index 7cf40095..294a1033 100644 --- a/src/SharpCompress/IO/SharpCompressStream.Create.cs +++ b/src/SharpCompress/IO/SharpCompressStream.Create.cs @@ -19,7 +19,7 @@ internal partial class SharpCompressStream int? bufferSize = null ) { - int rewindableBufferSize = bufferSize ?? Constants.RewindableBufferSize; + var rewindableBufferSize = bufferSize ?? Constants.RewindableBufferSize; // If it's a passthrough SharpCompressStream, unwrap it and create proper seekable wrapper if (stream is SharpCompressStream sharpCompressStream) @@ -31,10 +31,7 @@ internal partial class SharpCompressStream if (underlying.CanSeek) { // Create SeekableSharpCompressStream that preserves LeaveStreamOpen - return new SeekableSharpCompressStream(underlying) - { - LeaveStreamOpen = true, // Preserve non-disposing behavior - }; + return new SeekableSharpCompressStream(underlying); } // Non-seekable underlying stream - wrap with rolling buffer return new SharpCompressStream(underlying, true, false, rewindableBufferSize); diff --git a/src/SharpCompress/IO/SharpCompressStream.cs b/src/SharpCompress/IO/SharpCompressStream.cs index f0aec887..4972ceff 100644 --- a/src/SharpCompress/IO/SharpCompressStream.cs +++ b/src/SharpCompress/IO/SharpCompressStream.cs @@ -29,15 +29,10 @@ internal partial class SharpCompressStream : Stream, IStreamStack /// internal bool IsPassthrough => _isPassthrough; - /// - /// Default size for rolling buffer (same as .NET Stream.CopyTo default) - /// - public const int DefaultRollingBufferSize = 81920; - /// /// Gets or sets whether to leave the underlying stream open when disposed. /// - public bool LeaveStreamOpen { get; set; } + public bool LeaveStreamOpen { get; } /// /// Gets or sets whether to throw an exception when Dispose is called. @@ -55,7 +50,7 @@ internal partial class SharpCompressStream : Stream, IStreamStack /// /// Private constructor for passthrough mode. /// - private SharpCompressStream(Stream stream, bool leaveStreamOpen, bool passthrough, int? bufferSize) + protected SharpCompressStream(Stream stream, bool leaveStreamOpen, bool passthrough, int? bufferSize) { this.stream = stream; LeaveStreamOpen = leaveStreamOpen; @@ -182,7 +177,7 @@ internal partial class SharpCompressStream : Stream, IStreamStack // Ensure ring buffer exists if (_ringBuffer is null) { - _ringBuffer = new RingBuffer(DefaultRollingBufferSize); + _ringBuffer = new RingBuffer(Constants.BufferSize); } // Mark current position as recording anchor @@ -193,7 +188,7 @@ internal partial class SharpCompressStream : Stream, IStreamStack 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; From db544211f578ed154b2c94ecd60b5d6ffe797d3f Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 5 Feb 2026 09:07:53 +0000 Subject: [PATCH 4/8] fmt --- src/SharpCompress/IO/SeekableSharpCompressStream.cs | 3 +-- src/SharpCompress/IO/SharpCompressStream.Create.cs | 7 ++----- src/SharpCompress/IO/SharpCompressStream.cs | 8 ++++++-- src/SharpCompress/Readers/ReaderFactory.Async.cs | 2 +- src/SharpCompress/Readers/ReaderFactory.cs | 2 +- src/SharpCompress/Readers/Tar/TarReader.cs | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/SharpCompress/IO/SeekableSharpCompressStream.cs b/src/SharpCompress/IO/SeekableSharpCompressStream.cs index 20348f35..5a66cbfb 100644 --- a/src/SharpCompress/IO/SeekableSharpCompressStream.cs +++ b/src/SharpCompress/IO/SeekableSharpCompressStream.cs @@ -61,8 +61,7 @@ internal sealed partial class SeekableSharpCompressStream : SharpCompressStream public override int Read(Span buffer) => _stream.Read(buffer); #endif - public override long Seek(long offset, SeekOrigin origin) => - _stream.Seek(offset, origin); + public override long Seek(long offset, SeekOrigin origin) => _stream.Seek(offset, origin); public override void SetLength(long value) => _stream.SetLength(value); diff --git a/src/SharpCompress/IO/SharpCompressStream.Create.cs b/src/SharpCompress/IO/SharpCompressStream.Create.cs index 294a1033..40db0bee 100644 --- a/src/SharpCompress/IO/SharpCompressStream.Create.cs +++ b/src/SharpCompress/IO/SharpCompressStream.Create.cs @@ -14,10 +14,7 @@ internal partial class SharpCompressStream public static SharpCompressStream CreateNonDisposing(Stream stream) => new(stream, leaveStreamOpen: true, passthrough: true, bufferSize: null); - public static SharpCompressStream Create( - Stream stream, - int? bufferSize = null - ) + public static SharpCompressStream Create(Stream stream, int? bufferSize = null) { var rewindableBufferSize = bufferSize ?? Constants.RewindableBufferSize; @@ -57,6 +54,6 @@ internal partial class SharpCompressStream // 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); + return new SharpCompressStream(stream, false, false, bufferSize); } } diff --git a/src/SharpCompress/IO/SharpCompressStream.cs b/src/SharpCompress/IO/SharpCompressStream.cs index 4972ceff..8c16fa4b 100644 --- a/src/SharpCompress/IO/SharpCompressStream.cs +++ b/src/SharpCompress/IO/SharpCompressStream.cs @@ -46,11 +46,15 @@ internal partial class SharpCompressStream : Stream, IStreamStack _logicalPosition = 0; } - /// /// Private constructor for passthrough mode. /// - protected SharpCompressStream(Stream stream, bool leaveStreamOpen, bool passthrough, int? bufferSize) + protected SharpCompressStream( + Stream stream, + bool leaveStreamOpen, + bool passthrough, + int? bufferSize + ) { this.stream = stream; LeaveStreamOpen = leaveStreamOpen; diff --git a/src/SharpCompress/Readers/ReaderFactory.Async.cs b/src/SharpCompress/Readers/ReaderFactory.Async.cs index c0b3f623..cf51e560 100644 --- a/src/SharpCompress/Readers/ReaderFactory.Async.cs +++ b/src/SharpCompress/Readers/ReaderFactory.Async.cs @@ -56,7 +56,7 @@ public static partial class ReaderFactory var sharpCompressStream = SharpCompressStream.Create( stream, - bufferSize:options.RewindableBufferSize + bufferSize: options.RewindableBufferSize ); sharpCompressStream.StartRecording(); diff --git a/src/SharpCompress/Readers/ReaderFactory.cs b/src/SharpCompress/Readers/ReaderFactory.cs index 6cafb063..186efb64 100644 --- a/src/SharpCompress/Readers/ReaderFactory.cs +++ b/src/SharpCompress/Readers/ReaderFactory.cs @@ -36,7 +36,7 @@ public static partial class ReaderFactory var sharpCompressStream = SharpCompressStream.Create( stream, - bufferSize:options.RewindableBufferSize + bufferSize: options.RewindableBufferSize ); sharpCompressStream.StartRecording(); diff --git a/src/SharpCompress/Readers/Tar/TarReader.cs b/src/SharpCompress/Readers/Tar/TarReader.cs index 8a79e49f..b29c820d 100644 --- a/src/SharpCompress/Readers/Tar/TarReader.cs +++ b/src/SharpCompress/Readers/Tar/TarReader.cs @@ -59,7 +59,7 @@ public partial class TarReader : AbstractReader options = options ?? new ReaderOptions(); var sharpCompressStream = SharpCompressStream.Create( stream, - bufferSize:options.RewindableBufferSize + bufferSize: options.RewindableBufferSize ); long pos = sharpCompressStream.Position; if (GZipArchive.IsGZipFile(sharpCompressStream)) From 778a930266e5f946ddedcd55caf59f800cfad002 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 5 Feb 2026 09:16:45 +0000 Subject: [PATCH 5/8] fix inheritance --- src/SharpCompress/IO/SeekableSharpCompressStream.cs | 4 ++-- src/SharpCompress/IO/SharpCompressStream.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/SharpCompress/IO/SeekableSharpCompressStream.cs b/src/SharpCompress/IO/SeekableSharpCompressStream.cs index 5a66cbfb..2f94b641 100644 --- a/src/SharpCompress/IO/SeekableSharpCompressStream.cs +++ b/src/SharpCompress/IO/SeekableSharpCompressStream.cs @@ -14,13 +14,13 @@ internal sealed partial class SeekableSharpCompressStream : SharpCompressStream /// /// 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(Null, true, false, null) diff --git a/src/SharpCompress/IO/SharpCompressStream.cs b/src/SharpCompress/IO/SharpCompressStream.cs index 8c16fa4b..bcc36318 100644 --- a/src/SharpCompress/IO/SharpCompressStream.cs +++ b/src/SharpCompress/IO/SharpCompressStream.cs @@ -32,13 +32,13 @@ internal partial class SharpCompressStream : Stream, IStreamStack /// /// Gets or sets whether to leave the underlying stream open when disposed. /// - public bool LeaveStreamOpen { get; } + 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) { From 240468f968446a170b9a13ed305b39e95fbeb99b Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 5 Feb 2026 09:30:36 +0000 Subject: [PATCH 6/8] Update src/SharpCompress/IO/SharpCompressStream.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/SharpCompress/IO/SharpCompressStream.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SharpCompress/IO/SharpCompressStream.cs b/src/SharpCompress/IO/SharpCompressStream.cs index bcc36318..b47b7031 100644 --- a/src/SharpCompress/IO/SharpCompressStream.cs +++ b/src/SharpCompress/IO/SharpCompressStream.cs @@ -30,7 +30,7 @@ internal partial class SharpCompressStream : Stream, IStreamStack internal bool IsPassthrough => _isPassthrough; /// - /// Gets or sets whether to leave the underlying stream open when disposed. + /// Gets whether to leave the underlying stream open when disposed. /// public virtual bool LeaveStreamOpen { get; } From 3fb8387419fa1ca1907179e5ddfee39a3d03e7ec Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 5 Feb 2026 09:34:54 +0000 Subject: [PATCH 7/8] leave stream open --- src/SharpCompress/IO/SeekableSharpCompressStream.cs | 4 +++- src/SharpCompress/IO/SharpCompressStream.Create.cs | 12 ++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/SharpCompress/IO/SeekableSharpCompressStream.cs b/src/SharpCompress/IO/SeekableSharpCompressStream.cs index 2f94b641..28b67c4e 100644 --- a/src/SharpCompress/IO/SeekableSharpCompressStream.cs +++ b/src/SharpCompress/IO/SeekableSharpCompressStream.cs @@ -22,7 +22,7 @@ internal sealed partial class SeekableSharpCompressStream : SharpCompressStream /// public override bool ThrowOnDispose { get; set; } - public SeekableSharpCompressStream(Stream stream) + public SeekableSharpCompressStream(Stream stream, bool leaveStreamOpen = false) : base(Null, true, false, null) { if (stream is null) @@ -33,6 +33,8 @@ internal sealed partial class SeekableSharpCompressStream : SharpCompressStream { throw new ArgumentException("Stream must be seekable", nameof(stream)); } + + LeaveStreamOpen = leaveStreamOpen; _stream = stream; } diff --git a/src/SharpCompress/IO/SharpCompressStream.Create.cs b/src/SharpCompress/IO/SharpCompressStream.Create.cs index 40db0bee..ae71003b 100644 --- a/src/SharpCompress/IO/SharpCompressStream.Create.cs +++ b/src/SharpCompress/IO/SharpCompressStream.Create.cs @@ -28,10 +28,18 @@ internal partial class SharpCompressStream if (underlying.CanSeek) { // Create SeekableSharpCompressStream that preserves LeaveStreamOpen - return new SeekableSharpCompressStream(underlying); + return new SeekableSharpCompressStream( + underlying, + sharpCompressStream.LeaveStreamOpen + ); } // Non-seekable underlying stream - wrap with rolling buffer - return new SharpCompressStream(underlying, true, false, rewindableBufferSize); + return new SharpCompressStream( + underlying, + sharpCompressStream.LeaveStreamOpen, + false, + rewindableBufferSize + ); } // Not passthrough - return as-is return sharpCompressStream; From c21c7eb5eea7b6d6265620a525d3cdf8715e2b83 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Thu, 5 Feb 2026 09:48:29 +0000 Subject: [PATCH 8/8] always leave open --- src/SharpCompress/IO/SharpCompressStream.Create.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/SharpCompress/IO/SharpCompressStream.Create.cs b/src/SharpCompress/IO/SharpCompressStream.Create.cs index ae71003b..3a886a80 100644 --- a/src/SharpCompress/IO/SharpCompressStream.Create.cs +++ b/src/SharpCompress/IO/SharpCompressStream.Create.cs @@ -28,18 +28,10 @@ internal partial class SharpCompressStream if (underlying.CanSeek) { // Create SeekableSharpCompressStream that preserves LeaveStreamOpen - return new SeekableSharpCompressStream( - underlying, - sharpCompressStream.LeaveStreamOpen - ); + return new SeekableSharpCompressStream(underlying, true); } // Non-seekable underlying stream - wrap with rolling buffer - return new SharpCompressStream( - underlying, - sharpCompressStream.LeaveStreamOpen, - false, - rewindableBufferSize - ); + return new SharpCompressStream(underlying, true, false, rewindableBufferSize); } // Not passthrough - return as-is return sharpCompressStream;