Merge remote-tracking branch 'origin/master' into copilot/add-performance-benchmarks

This commit is contained in:
Adam Hathcock
2026-02-06 11:51:29 +00:00
9 changed files with 121 additions and 170 deletions

View File

@@ -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;
}

View File

@@ -20,9 +20,9 @@ internal partial class StreamingZipHeaderFactory : ZipHeaderFactory
internal IEnumerable<ZipHeader> 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,

View File

@@ -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<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
) => _underlyingStream.ReadAsync(buffer, cancellationToken);
) => _stream.ReadAsync(buffer, cancellationToken);
public override ValueTask WriteAsync(
ReadOnlyMemory<byte> 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);
}

View File

@@ -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;
/// <summary>
/// Gets or sets whether to leave the underlying stream open when disposed.
/// </summary>
public new bool LeaveStreamOpen { get; set; }
public override bool LeaveStreamOpen { get; }
/// <summary>
/// Gets or sets whether to throw an exception when Dispose is called.
/// Useful for testing to ensure streams are not disposed prematurely.
/// </summary>
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 @@ internal sealed partial class SeekableSharpCompressStream : SharpCompressStream
{
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<byte> buffer) => _underlyingStream.Read(buffer);
public override int Read(Span<byte> 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<byte> buffer) => _underlyingStream.Write(buffer);
public override void Write(ReadOnlySpan<byte> buffer) => _stream.Write(buffer);
#endif
public override void Rewind(bool stopRecording = false)
@@ -80,22 +81,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 +107,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<byte> 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<byte> buffer) => throw new NotSupportedException();
#endif
}
}

View File

@@ -0,0 +1,59 @@
using System;
using System.IO;
using SharpCompress.Common;
namespace SharpCompress.IO;
internal partial class SharpCompressStream
{
/// <summary>
/// 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.
/// </summary>
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<SharpCompressStream>();
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);
}
}

View File

@@ -30,20 +30,15 @@ internal partial class SharpCompressStream : Stream, IStreamStack
internal bool IsPassthrough => _isPassthrough;
/// <summary>
/// Default size for rolling buffer (same as .NET Stream.CopyTo default)
/// Gets whether to leave the underlying stream open when disposed.
/// </summary>
public const int DefaultRollingBufferSize = 81920;
/// <summary>
/// Gets or sets whether to leave the underlying stream open when disposed.
/// </summary>
public bool LeaveStreamOpen { get; set; }
public virtual bool LeaveStreamOpen { get; }
/// <summary>
/// Gets or sets whether to throw an exception when Dispose is called.
/// Useful for testing to ensure streams are not disposed prematurely.
/// </summary>
public bool ThrowOnDispose { get; set; }
public virtual bool ThrowOnDispose { get; set; }
public SharpCompressStream(Stream stream)
{
@@ -51,38 +46,26 @@ internal partial class SharpCompressStream : Stream, IStreamStack
_logicalPosition = 0;
}
/// <summary>
/// Creates a SharpCompressStream with a rolling buffer that enables limited backward seeking.
/// </summary>
/// <param name="stream">The underlying stream to wrap.</param>
/// <param name="rollingBufferSize">Size of the rolling buffer in bytes.</param>
public SharpCompressStream(Stream stream, int rollingBufferSize)
: this(stream)
{
if (rollingBufferSize > 0)
{
_ringBuffer = new RingBuffer(rollingBufferSize);
}
}
/// <summary>
/// Private constructor for passthrough mode.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
public static SharpCompressStream CreateNonDisposing(Stream stream) =>
new(stream, leaveStreamOpen: true, passthrough: true);
if (bufferSize.HasValue && bufferSize.Value > 0)
{
_ringBuffer = new RingBuffer(bufferSize.Value);
}
}
/// <summary>
/// Gets whether the stream is actively recording reads to the ring buffer.
@@ -121,7 +104,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 +143,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 +163,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<SharpCompressStream>();
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 @@ 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
@@ -258,7 +192,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;

View File

@@ -54,9 +54,9 @@ 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
bufferSize: options.RewindableBufferSize
);
sharpCompressStream.StartRecording();

View File

@@ -34,9 +34,9 @@ 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
bufferSize: options.RewindableBufferSize
);
sharpCompressStream.StartRecording();

View File

@@ -57,9 +57,9 @@ public partial class TarReader : AbstractReader<TarEntry, TarVolume>
{
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))