mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-09-22 23:15:20 +00:00
Merge remote-tracking branch 'origin/master' into copilot/add-performance-benchmarks
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
59
src/SharpCompress/IO/SharpCompressStream.Create.cs
Normal file
59
src/SharpCompress/IO/SharpCompressStream.Create.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user