Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
4 changes: 2 additions & 2 deletions src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ internal StreamingZipHeaderFactory(

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,
Expand Down
14 changes: 7 additions & 7 deletions src/SharpCompress/IO/SeekableSharpCompressStream.Async.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ public override Task<int> 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<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()
{
Expand All @@ -40,7 +40,7 @@ public override ValueTask DisposeAsync()
_isDisposed = true;
if (!LeaveStreamOpen)
{
_underlyingStream.Dispose();
_stream.Dispose();
}
return base.DisposeAsync();
}
Expand All @@ -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);
}
94 changes: 26 additions & 68 deletions src/SharpCompress/IO/SeekableSharpCompressStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documentation mismatch: The comment says "Gets or sets" but the property is get-only. Since the property cannot be set after construction, the documentation should say "Gets whether to leave the underlying stream open when disposed."

Suggested change
/// Gets or sets whether to leave the underlying stream open when disposed.
/// Gets whether to leave the underlying stream open when disposed.

Copilot uses AI. Check for mistakes.
/// </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)
{
Expand All @@ -33,44 +33,45 @@ public SeekableSharpCompressStream(Stream stream)
{
throw new ArgumentException("Stream must be seekable", nameof(stream));
}
_underlyingStream = stream;

LeaveStreamOpen = leaveStreamOpen;
Comment on lines 25 to +37
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SeekableSharpCompressStream constructor should accept a leaveStreamOpen parameter to support the use case in Create() where LeaveStreamOpen behavior needs to be preserved when unwrapping passthrough streams. Currently, there's no way to control whether the underlying stream is disposed, which breaks the intended behavior described in the comment on line 30.

Copilot uses AI. Check for mistakes.
_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)
Expand All @@ -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)
{
Expand All @@ -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<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 changes: 59 additions & 0 deletions src/SharpCompress/IO/SharpCompressStream.Create.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}

Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent buffer size handling: Line 34 uses rewindableBufferSize (which is never null and defaults to Constants.RewindableBufferSize), but line 57 uses bufferSize which can be null. This means that unwrapping a passthrough stream will always create a buffer, while creating from a raw non-seekable stream might not create a buffer if bufferSize is null. This inconsistency could lead to unexpected behavior. Consider using rewindableBufferSize here for consistency.

Copilot uses AI. Check for mistakes.
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);
}
}
Loading