Compare commits

...

17 Commits

Author SHA1 Message Date
Adam Hathcock
b9fc680548 Merge pull request #1160 from adamhathcock/adam/check-if-seek
add check to see if we need to seek before hand
2026-01-26 12:24:39 +00:00
Adam Hathcock
7dcc13c1f0 Merge pull request #1161 from adamhathcock/copilot/sub-pr-1160
Fix ArrayPool corruption from double-disposal in BufferedSubStream
2026-01-26 12:15:55 +00:00
copilot-swe-agent[bot]
56d3091688 Fix condition order to check CanSeek before Position
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-26 12:12:08 +00:00
copilot-swe-agent[bot]
a0af0604d1 Add disposal checks to RefillCache methods
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-26 12:11:16 +00:00
copilot-swe-agent[bot]
875c2d7694 Fix BufferedSubStream double-dispose issue with ArrayPool
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2026-01-26 12:10:19 +00:00
Adam Hathcock
8c95f863cb do CanSeek first 2026-01-26 12:06:57 +00:00
copilot-swe-agent[bot]
ddf37e82c2 Initial plan 2026-01-26 12:06:38 +00:00
Adam Hathcock
a82fda98d7 more testing and add pooling to cache 2026-01-26 11:45:25 +00:00
Adam Hathcock
44e4b1804e add check to see if we need to seek before hand 2026-01-26 09:41:13 +00:00
Adam Hathcock
4ca1a7713e Merge pull request #1157 from adamhathcock/adam/1154-release
Merge pull request #1156 from adamhathcock/copilot/fix-sharpcompress-…
2026-01-25 11:36:59 +00:00
Adam Hathcock
9caf7be928 Revert testing 2026-01-24 10:23:02 +00:00
Adam Hathcock
bf4217fde6 Merge pull request #1156 from adamhathcock/copilot/fix-sharpcompress-archive-iteration
Fix silent iteration failure when input stream throws on Flush()
# Conflicts:
#	src/SharpCompress/packages.lock.json
2026-01-24 10:18:02 +00:00
Adam Hathcock
d5a8c37113 Merge pull request #1154 from adamhathcock/adam/1151-release
Adam/1151 release cherry pick
2026-01-23 09:31:03 +00:00
Adam Hathcock
21ce9a38e6 fix up tests 2026-01-23 09:04:55 +00:00
Adam Hathcock
7732fbb698 Merge pull request #1151 from adamhathcock/copilot/fix-entrystream-flush-issue
Fix EntryStream.Dispose() throwing NotSupportedException on non-seekable streams
2026-01-23 08:59:56 +00:00
Adam Hathcock
97879f18b6 Merge pull request #1146 from adamhathcock/adam/pr-1145-release
Merge pull request #1145 from adamhathcock/copilot/add-leaveopen-para…
2026-01-19 10:35:33 +00:00
Adam Hathcock
d74454f7e9 Merge pull request #1145 from adamhathcock/copilot/add-leaveopen-parameter-lzipstream
Add leaveOpen parameter to LZipStream and BZip2Stream
2026-01-19 09:58:10 +00:00
15 changed files with 670 additions and 38 deletions

View File

@@ -30,6 +30,7 @@ public sealed class BZip2Stream : Stream, IStreamStack
private readonly Stream stream;
private bool isDisposed;
private readonly bool leaveOpen;
/// <summary>
/// Create a BZip2Stream
@@ -37,19 +38,30 @@ public sealed class BZip2Stream : Stream, IStreamStack
/// <param name="stream">The stream to read from</param>
/// <param name="compressionMode">Compression Mode</param>
/// <param name="decompressConcatenated">Decompress Concatenated</param>
public BZip2Stream(Stream stream, CompressionMode compressionMode, bool decompressConcatenated)
/// <param name="leaveOpen">Leave the stream open after disposing</param>
public BZip2Stream(
Stream stream,
CompressionMode compressionMode,
bool decompressConcatenated,
bool leaveOpen = false
)
{
#if DEBUG_STREAMS
this.DebugConstruct(typeof(BZip2Stream));
#endif
this.leaveOpen = leaveOpen;
Mode = compressionMode;
if (Mode == CompressionMode.Compress)
{
this.stream = new CBZip2OutputStream(stream);
this.stream = new CBZip2OutputStream(stream, 9, leaveOpen);
}
else
{
this.stream = new CBZip2InputStream(stream, decompressConcatenated);
this.stream = new CBZip2InputStream(
stream,
decompressConcatenated,
leaveOpen: leaveOpen
);
}
}

View File

@@ -168,6 +168,7 @@ internal class CBZip2InputStream : Stream, IStreamStack
private int computedBlockCRC,
computedCombinedCRC;
private readonly bool decompressConcatenated;
private readonly bool leaveOpen;
private int i2,
count,
@@ -181,9 +182,10 @@ internal class CBZip2InputStream : Stream, IStreamStack
private char z;
private bool isDisposed;
public CBZip2InputStream(Stream zStream, bool decompressConcatenated)
public CBZip2InputStream(Stream zStream, bool decompressConcatenated, bool leaveOpen = false)
{
this.decompressConcatenated = decompressConcatenated;
this.leaveOpen = leaveOpen;
ll8 = null;
tt = null;
BsSetStream(zStream);
@@ -207,7 +209,10 @@ internal class CBZip2InputStream : Stream, IStreamStack
this.DebugDispose(typeof(CBZip2InputStream));
#endif
base.Dispose(disposing);
bsStream?.Dispose();
if (!leaveOpen)
{
bsStream?.Dispose();
}
}
internal static int[][] InitIntArray(int n1, int n2)
@@ -398,7 +403,10 @@ internal class CBZip2InputStream : Stream, IStreamStack
private void BsFinishedWithStream()
{
bsStream?.Dispose();
if (!leaveOpen)
{
bsStream?.Dispose();
}
bsStream = null;
}

View File

@@ -341,12 +341,14 @@ internal sealed class CBZip2OutputStream : Stream, IStreamStack
private int currentChar = -1;
private int runLength;
private readonly bool leaveOpen;
public CBZip2OutputStream(Stream inStream)
: this(inStream, 9) { }
public CBZip2OutputStream(Stream inStream, bool leaveOpen = false)
: this(inStream, 9, leaveOpen) { }
public CBZip2OutputStream(Stream inStream, int inBlockSize)
public CBZip2OutputStream(Stream inStream, int inBlockSize, bool leaveOpen = false)
{
this.leaveOpen = leaveOpen;
block = null;
quadrant = null;
zptr = null;
@@ -481,7 +483,10 @@ internal sealed class CBZip2OutputStream : Stream, IStreamStack
this.DebugDispose(typeof(CBZip2OutputStream));
#endif
Dispose();
bsStream?.Dispose();
if (!leaveOpen)
{
bsStream?.Dispose();
}
bsStream = null;
}
}

View File

@@ -586,7 +586,13 @@ internal class ZlibBaseStream : Stream, IStreamStack
public override void Flush()
{
_stream.Flush();
// Only flush the underlying stream when in write mode
// Flushing input streams during read operations is not meaningful
// and can cause issues with forward-only/non-seekable streams
if (_streamMode == StreamMode.Writer)
{
_stream.Flush();
}
//rewind the buffer
((IStreamStack)this).Rewind(z.AvailableBytesIn); //unused
z.AvailableBytesIn = 0;
@@ -594,7 +600,13 @@ internal class ZlibBaseStream : Stream, IStreamStack
public override async Task FlushAsync(CancellationToken cancellationToken)
{
await _stream.FlushAsync(cancellationToken).ConfigureAwait(false);
// Only flush the underlying stream when in write mode
// Flushing input streams during read operations is not meaningful
// and can cause issues with forward-only/non-seekable streams
if (_streamMode == StreamMode.Writer)
{
await _stream.FlushAsync(cancellationToken).ConfigureAwait(false);
}
//rewind the buffer
((IStreamStack)this).Rewind(z.AvailableBytesIn); //unused
z.AvailableBytesIn = 0;

View File

@@ -46,11 +46,13 @@ public sealed class LZipStream : Stream, IStreamStack
private long _writeCount;
private readonly Stream? _originalStream;
private readonly bool _leaveOpen;
public LZipStream(Stream stream, CompressionMode mode)
public LZipStream(Stream stream, CompressionMode mode, bool leaveOpen = false)
{
Mode = mode;
_originalStream = stream;
_leaveOpen = leaveOpen;
if (mode == CompressionMode.Decompress)
{
@@ -60,7 +62,7 @@ public sealed class LZipStream : Stream, IStreamStack
throw new InvalidFormatException("Not an LZip stream");
}
var properties = GetProperties(dSize);
_stream = new LzmaStream(properties, stream);
_stream = new LzmaStream(properties, stream, leaveOpen: leaveOpen);
}
else
{
@@ -127,7 +129,7 @@ public sealed class LZipStream : Stream, IStreamStack
{
Finish();
_stream.Dispose();
if (Mode == CompressionMode.Compress)
if (Mode == CompressionMode.Compress && !_leaveOpen)
{
_originalStream?.Dispose();
}

View File

@@ -35,6 +35,7 @@ public class LzmaStream : Stream, IStreamStack
private readonly Stream _inputStream;
private readonly long _inputSize;
private readonly long _outputSize;
private readonly bool _leaveOpen;
private readonly int _dictionarySize;
private readonly OutWindow _outWindow = new();
@@ -56,14 +57,28 @@ public class LzmaStream : Stream, IStreamStack
private readonly Encoder _encoder;
private bool _isDisposed;
public LzmaStream(byte[] properties, Stream inputStream)
: this(properties, inputStream, -1, -1, null, properties.Length < 5) { }
public LzmaStream(byte[] properties, Stream inputStream, bool leaveOpen = false)
: this(properties, inputStream, -1, -1, null, properties.Length < 5, leaveOpen) { }
public LzmaStream(byte[] properties, Stream inputStream, long inputSize)
: this(properties, inputStream, inputSize, -1, null, properties.Length < 5) { }
public LzmaStream(byte[] properties, Stream inputStream, long inputSize, bool leaveOpen = false)
: this(properties, inputStream, inputSize, -1, null, properties.Length < 5, leaveOpen) { }
public LzmaStream(byte[] properties, Stream inputStream, long inputSize, long outputSize)
: this(properties, inputStream, inputSize, outputSize, null, properties.Length < 5) { }
public LzmaStream(
byte[] properties,
Stream inputStream,
long inputSize,
long outputSize,
bool leaveOpen = false
)
: this(
properties,
inputStream,
inputSize,
outputSize,
null,
properties.Length < 5,
leaveOpen
) { }
public LzmaStream(
byte[] properties,
@@ -71,13 +86,15 @@ public class LzmaStream : Stream, IStreamStack
long inputSize,
long outputSize,
Stream presetDictionary,
bool isLzma2
bool isLzma2,
bool leaveOpen = false
)
{
_inputStream = inputStream;
_inputSize = inputSize;
_outputSize = outputSize;
_isLzma2 = isLzma2;
_leaveOpen = leaveOpen;
#if DEBUG_STREAMS
this.DebugConstruct(typeof(LzmaStream));
@@ -179,7 +196,10 @@ public class LzmaStream : Stream, IStreamStack
{
_position = _encoder.Code(null, true);
}
_inputStream?.Dispose();
if (!_leaveOpen)
{
_inputStream?.Dispose();
}
_outWindow.Dispose();
}
base.Dispose(disposing);

View File

@@ -1,4 +1,5 @@
using System;
using System.Buffers;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
@@ -28,14 +29,25 @@ internal class BufferedSubStream : SharpCompressStream, IStreamStack
#if DEBUG_STREAMS
this.DebugDispose(typeof(BufferedSubStream));
#endif
if (disposing) { }
if (_isDisposed)
{
return;
}
_isDisposed = true;
if (disposing && _cache is not null)
{
ArrayPool<byte>.Shared.Return(_cache);
_cache = null;
}
base.Dispose(disposing);
}
private int _cacheOffset;
private int _cacheLength;
private readonly byte[] _cache = new byte[32 << 10];
private byte[]? _cache = ArrayPool<byte>.Shared.Rent(81920);
private long origin;
private bool _isDisposed;
private long BytesLeftToRead { get; set; }
@@ -57,14 +69,26 @@ internal class BufferedSubStream : SharpCompressStream, IStreamStack
private void RefillCache()
{
var count = (int)Math.Min(BytesLeftToRead, _cache.Length);
if (_isDisposed)
{
throw new ObjectDisposedException(nameof(BufferedSubStream));
}
var count = (int)Math.Min(BytesLeftToRead, _cache!.Length);
_cacheOffset = 0;
if (count == 0)
{
_cacheLength = 0;
return;
}
Stream.Position = origin;
// Only seek if we're not already at the correct position
// This avoids expensive seek operations when reading sequentially
if (Stream.CanSeek && Stream.Position != origin)
{
Stream.Position = origin;
}
_cacheLength = Stream.Read(_cache, 0, count);
origin += _cacheLength;
BytesLeftToRead -= _cacheLength;
@@ -72,14 +96,24 @@ internal class BufferedSubStream : SharpCompressStream, IStreamStack
private async ValueTask RefillCacheAsync(CancellationToken cancellationToken)
{
var count = (int)Math.Min(BytesLeftToRead, _cache.Length);
if (_isDisposed)
{
throw new ObjectDisposedException(nameof(BufferedSubStream));
}
var count = (int)Math.Min(BytesLeftToRead, _cache!.Length);
_cacheOffset = 0;
if (count == 0)
{
_cacheLength = 0;
return;
}
Stream.Position = origin;
// Only seek if we're not already at the correct position
// This avoids expensive seek operations when reading sequentially
if (Stream.CanSeek && Stream.Position != origin)
{
Stream.Position = origin;
}
_cacheLength = await Stream
.ReadAsync(_cache, 0, count, cancellationToken)
.ConfigureAwait(false);
@@ -102,7 +136,7 @@ internal class BufferedSubStream : SharpCompressStream, IStreamStack
}
count = Math.Min(count, _cacheLength - _cacheOffset);
Buffer.BlockCopy(_cache, _cacheOffset, buffer, offset, count);
Buffer.BlockCopy(_cache!, _cacheOffset, buffer, offset, count);
_cacheOffset += count;
}
@@ -120,7 +154,7 @@ internal class BufferedSubStream : SharpCompressStream, IStreamStack
}
}
return _cache[_cacheOffset++];
return _cache![_cacheOffset++];
}
public override async Task<int> ReadAsync(
@@ -143,7 +177,7 @@ internal class BufferedSubStream : SharpCompressStream, IStreamStack
}
count = Math.Min(count, _cacheLength - _cacheOffset);
Buffer.BlockCopy(_cache, _cacheOffset, buffer, offset, count);
Buffer.BlockCopy(_cache!, _cacheOffset, buffer, offset, count);
_cacheOffset += count;
}
@@ -170,7 +204,7 @@ internal class BufferedSubStream : SharpCompressStream, IStreamStack
}
count = Math.Min(count, _cacheLength - _cacheOffset);
_cache.AsSpan(_cacheOffset, count).CopyTo(buffer.Span);
_cache!.AsSpan(_cacheOffset, count).CopyTo(buffer.Span);
_cacheOffset += count;
}

View File

@@ -257,7 +257,6 @@ public class SharpCompressStream : Stream, IStreamStack
ValidateBufferState();
}
long orig = _internalPosition;
long targetPos;
// Calculate the absolute target position based on origin
switch (origin)

View File

@@ -0,0 +1,73 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace SharpCompress.Test.Mocks;
/// <summary>
/// A stream wrapper that throws NotSupportedException on Flush() calls.
/// This is used to test that archive iteration handles streams that don't support flushing.
/// </summary>
public class ThrowOnFlushStream : Stream
{
private readonly Stream inner;
public ThrowOnFlushStream(Stream inner)
{
this.inner = inner;
}
public override bool CanRead => inner.CanRead;
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() => throw new NotSupportedException("Flush not supported");
public override Task FlushAsync(CancellationToken cancellationToken) =>
throw new NotSupportedException("FlushAsync not supported");
public override int Read(byte[] buffer, int offset, int count) =>
inner.Read(buffer, offset, count);
public override Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
) => inner.ReadAsync(buffer, offset, count, cancellationToken);
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
) => inner.ReadAsync(buffer, cancellationToken);
#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();
protected override void Dispose(bool disposing)
{
if (disposing)
{
inner.Dispose();
}
base.Dispose(disposing);
}
}

View File

@@ -9,6 +9,9 @@
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net10.0|AnyCPU'">
<DefineConstants>$(DefineConstants);DEBUG_STREAMS</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'net48' ">
<DefineConstants>$(DefineConstants);LEGACY_DOTNET</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))">
<DefineConstants>$(DefineConstants);WINDOWS</DefineConstants>
</PropertyGroup>
@@ -25,7 +28,7 @@
<PackageReference Include="xunit" />
<PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" PrivateAssets="All" />
</ItemGroup>
<ItemGroup Condition=" '$(VersionlessImplicitFrameworkDefine)' != 'NETFRAMEWORK' ">
<ItemGroup Condition="$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))">
<PackageReference Include="Mono.Posix.NETStandard" />
</ItemGroup>
</Project>

View File

@@ -2,6 +2,7 @@ using System;
using System.IO;
using SharpCompress.Common;
using SharpCompress.Compressors;
using SharpCompress.Compressors.BZip2;
using SharpCompress.Compressors.Deflate;
using SharpCompress.Compressors.LZMA;
using SharpCompress.Compressors.Lzw;
@@ -152,9 +153,21 @@ public class DisposalTests
[Fact]
public void LZipStream_Disposal()
{
// LZipStream always disposes inner stream
// LZipStream now supports leaveOpen parameter
// Use Compress mode to avoid need for valid input header
VerifyAlwaysDispose(stream => new LZipStream(stream, CompressionMode.Compress));
VerifyStreamDisposal(
(stream, leaveOpen) => new LZipStream(stream, CompressionMode.Compress, leaveOpen)
);
}
[Fact]
public void BZip2Stream_Disposal()
{
// BZip2Stream now supports leaveOpen parameter
VerifyStreamDisposal(
(stream, leaveOpen) =>
new BZip2Stream(stream, CompressionMode.Compress, false, leaveOpen)
);
}
[Fact]

View File

@@ -0,0 +1,226 @@
using System;
using System.IO;
using System.Text;
using SharpCompress.Compressors;
using SharpCompress.Compressors.BZip2;
using SharpCompress.Compressors.LZMA;
using SharpCompress.Test.Mocks;
using Xunit;
namespace SharpCompress.Test.Streams;
public class LeaveOpenBehaviorTests
{
private static byte[] CreateTestData() =>
Encoding.UTF8.GetBytes("The quick brown fox jumps over the lazy dog");
[Fact]
public void BZip2Stream_Compress_LeaveOpen_False()
{
using var innerStream = new TestStream(new MemoryStream());
using (
var bzip2 = new BZip2Stream(
innerStream,
CompressionMode.Compress,
false,
leaveOpen: false
)
)
{
bzip2.Write(CreateTestData(), 0, CreateTestData().Length);
bzip2.Finish();
}
Assert.True(innerStream.IsDisposed, "Inner stream should be disposed when leaveOpen=false");
}
[Fact]
public void BZip2Stream_Compress_LeaveOpen_True()
{
using var innerStream = new TestStream(new MemoryStream());
byte[] compressed;
using (
var bzip2 = new BZip2Stream(
innerStream,
CompressionMode.Compress,
false,
leaveOpen: true
)
)
{
bzip2.Write(CreateTestData(), 0, CreateTestData().Length);
bzip2.Finish();
}
Assert.False(
innerStream.IsDisposed,
"Inner stream should NOT be disposed when leaveOpen=true"
);
// Should be able to read the compressed data
innerStream.Position = 0;
compressed = new byte[innerStream.Length];
innerStream.Read(compressed, 0, compressed.Length);
Assert.True(compressed.Length > 0);
}
[Fact]
public void BZip2Stream_Decompress_LeaveOpen_False()
{
// First compress some data
var memStream = new MemoryStream();
using (var bzip2 = new BZip2Stream(memStream, CompressionMode.Compress, false, true))
{
bzip2.Write(CreateTestData(), 0, CreateTestData().Length);
bzip2.Finish();
}
memStream.Position = 0;
using var innerStream = new TestStream(memStream);
var decompressed = new byte[CreateTestData().Length];
using (
var bzip2 = new BZip2Stream(
innerStream,
CompressionMode.Decompress,
false,
leaveOpen: false
)
)
{
bzip2.Read(decompressed, 0, decompressed.Length);
}
Assert.True(innerStream.IsDisposed, "Inner stream should be disposed when leaveOpen=false");
Assert.Equal(CreateTestData(), decompressed);
}
[Fact]
public void BZip2Stream_Decompress_LeaveOpen_True()
{
// First compress some data
var memStream = new MemoryStream();
using (var bzip2 = new BZip2Stream(memStream, CompressionMode.Compress, false, true))
{
bzip2.Write(CreateTestData(), 0, CreateTestData().Length);
bzip2.Finish();
}
memStream.Position = 0;
using var innerStream = new TestStream(memStream);
var decompressed = new byte[CreateTestData().Length];
using (
var bzip2 = new BZip2Stream(
innerStream,
CompressionMode.Decompress,
false,
leaveOpen: true
)
)
{
bzip2.Read(decompressed, 0, decompressed.Length);
}
Assert.False(
innerStream.IsDisposed,
"Inner stream should NOT be disposed when leaveOpen=true"
);
Assert.Equal(CreateTestData(), decompressed);
// Should still be able to use the stream
innerStream.Position = 0;
Assert.True(innerStream.CanRead);
}
[Fact]
public void LZipStream_Compress_LeaveOpen_False()
{
using var innerStream = new TestStream(new MemoryStream());
using (var lzip = new LZipStream(innerStream, CompressionMode.Compress, leaveOpen: false))
{
lzip.Write(CreateTestData(), 0, CreateTestData().Length);
lzip.Finish();
}
Assert.True(innerStream.IsDisposed, "Inner stream should be disposed when leaveOpen=false");
}
[Fact]
public void LZipStream_Compress_LeaveOpen_True()
{
using var innerStream = new TestStream(new MemoryStream());
byte[] compressed;
using (var lzip = new LZipStream(innerStream, CompressionMode.Compress, leaveOpen: true))
{
lzip.Write(CreateTestData(), 0, CreateTestData().Length);
lzip.Finish();
}
Assert.False(
innerStream.IsDisposed,
"Inner stream should NOT be disposed when leaveOpen=true"
);
// Should be able to read the compressed data
innerStream.Position = 0;
compressed = new byte[innerStream.Length];
innerStream.Read(compressed, 0, compressed.Length);
Assert.True(compressed.Length > 0);
}
[Fact]
public void LZipStream_Decompress_LeaveOpen_False()
{
// First compress some data
var memStream = new MemoryStream();
using (var lzip = new LZipStream(memStream, CompressionMode.Compress, true))
{
lzip.Write(CreateTestData(), 0, CreateTestData().Length);
lzip.Finish();
}
memStream.Position = 0;
using var innerStream = new TestStream(memStream);
var decompressed = new byte[CreateTestData().Length];
using (var lzip = new LZipStream(innerStream, CompressionMode.Decompress, leaveOpen: false))
{
lzip.Read(decompressed, 0, decompressed.Length);
}
Assert.True(innerStream.IsDisposed, "Inner stream should be disposed when leaveOpen=false");
Assert.Equal(CreateTestData(), decompressed);
}
[Fact]
public void LZipStream_Decompress_LeaveOpen_True()
{
// First compress some data
var memStream = new MemoryStream();
using (var lzip = new LZipStream(memStream, CompressionMode.Compress, true))
{
lzip.Write(CreateTestData(), 0, CreateTestData().Length);
lzip.Finish();
}
memStream.Position = 0;
using var innerStream = new TestStream(memStream);
var decompressed = new byte[CreateTestData().Length];
using (var lzip = new LZipStream(innerStream, CompressionMode.Decompress, leaveOpen: true))
{
lzip.Read(decompressed, 0, decompressed.Length);
}
Assert.False(
innerStream.IsDisposed,
"Inner stream should NOT be disposed when leaveOpen=true"
);
Assert.Equal(CreateTestData(), decompressed);
// Should still be able to use the stream
innerStream.Position = 0;
Assert.True(innerStream.CanRead);
}
}

View File

@@ -5,6 +5,7 @@ using System.Linq;
using System.Text;
using SharpCompress.Compressors.LZMA;
using SharpCompress.IO;
using SharpCompress.Test.Mocks;
using Xunit;
namespace SharpCompress.Test.Streams;
@@ -64,7 +65,14 @@ public class SharpCompressStreamTests
{
createData(ms);
using (SharpCompressStream scs = new SharpCompressStream(ms, true, false, 0x10000))
using (
SharpCompressStream scs = new SharpCompressStream(
new ForwardOnlyStream(ms),
true,
false,
0x10000
)
)
{
IStreamStack stack = (IStreamStack)scs;
@@ -89,4 +97,25 @@ public class SharpCompressStreamTests
}
}
}
[Fact]
public void BufferedSubStream_DoubleDispose_DoesNotCorruptArrayPool()
{
// This test verifies that calling Dispose multiple times on BufferedSubStream
// doesn't return the same array to the pool twice, which would cause pool corruption
byte[] data = new byte[0x10000];
using (MemoryStream ms = new MemoryStream(data))
{
var stream = new BufferedSubStream(ms, 0, data.Length);
// First disposal
stream.Dispose();
// Second disposal should not throw or corrupt the pool
stream.Dispose();
}
// If we got here without an exception, the test passed
Assert.True(true);
}
}

View File

@@ -251,4 +251,106 @@ public class ZipReaderAsyncTests : ReaderTests
}
Assert.Equal(8, count);
}
[Fact]
public async ValueTask EntryStream_Dispose_DoesNotThrow_OnNonSeekableStream_Deflate_Async()
{
// Since version 0.41.0: EntryStream.DisposeAsync() should not throw NotSupportedException
// when FlushAsync() fails on non-seekable streams (Deflate compression)
var path = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip");
using Stream stream = new ForwardOnlyStream(File.OpenRead(path));
using var reader = ReaderFactory.Open(stream);
// This should not throw, even if internal FlushAsync() fails
while (await reader.MoveToNextEntryAsync())
{
if (!reader.Entry.IsDirectory)
{
#if LEGACY_DOTNET
using var entryStream = await reader.OpenEntryStreamAsync();
#else
await using var entryStream = await reader.OpenEntryStreamAsync();
#endif
// Read some data
var buffer = new byte[1024];
await entryStream.ReadAsync(buffer, 0, buffer.Length);
// DisposeAsync should not throw NotSupportedException
}
}
}
[Fact]
public async ValueTask EntryStream_Dispose_DoesNotThrow_OnNonSeekableStream_LZMA_Async()
{
// Since version 0.41.0: EntryStream.DisposeAsync() should not throw NotSupportedException
// when FlushAsync() fails on non-seekable streams (LZMA compression)
var path = Path.Combine(TEST_ARCHIVES_PATH, "Zip.lzma.dd.zip");
using Stream stream = new ForwardOnlyStream(File.OpenRead(path));
using var reader = ReaderFactory.Open(stream);
// This should not throw, even if internal FlushAsync() fails
while (await reader.MoveToNextEntryAsync())
{
if (!reader.Entry.IsDirectory)
{
#if LEGACY_DOTNET
using var entryStream = await reader.OpenEntryStreamAsync();
#else
await using var entryStream = await reader.OpenEntryStreamAsync();
#endif
// Read some data
var buffer = new byte[1024];
await entryStream.ReadAsync(buffer, 0, buffer.Length);
// DisposeAsync should not throw NotSupportedException
}
}
}
[Fact]
public async ValueTask Archive_Iteration_DoesNotBreak_WhenFlushThrows_Deflate_Async()
{
// Regression test: since 0.41.0, archive iteration would silently break
// when the input stream throws NotSupportedException in Flush().
// Only the first entry would be returned, then iteration would stop without exception.
var path = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip");
using var fileStream = File.OpenRead(path);
using Stream stream = new ThrowOnFlushStream(fileStream);
using var reader = ReaderFactory.Open(stream);
var count = 0;
while (await reader.MoveToNextEntryAsync())
{
if (!reader.Entry.IsDirectory)
{
count++;
}
}
// Should iterate through all entries, not just the first one
Assert.True(count > 1, $"Expected more than 1 entry, but got {count}");
}
[Fact]
public async ValueTask Archive_Iteration_DoesNotBreak_WhenFlushThrows_LZMA_Async()
{
// Regression test: since 0.41.0, archive iteration would silently break
// when the input stream throws NotSupportedException in Flush().
// Only the first entry would be returned, then iteration would stop without exception.
var path = Path.Combine(TEST_ARCHIVES_PATH, "Zip.lzma.dd.zip");
using var fileStream = File.OpenRead(path);
using Stream stream = new ThrowOnFlushStream(fileStream);
using var reader = ReaderFactory.Open(stream);
var count = 0;
while (await reader.MoveToNextEntryAsync())
{
if (!reader.Entry.IsDirectory)
{
count++;
}
}
// Should iterate through all entries, not just the first one
Assert.True(count > 1, $"Expected more than 1 entry, but got {count}");
}
}

View File

@@ -436,4 +436,98 @@ public class ZipReaderTests : ReaderTests
Assert.Equal(archiveKeys.OrderBy(k => k), readerKeys.OrderBy(k => k));
}
}
[Fact]
public void EntryStream_Dispose_DoesNotThrow_OnNonSeekableStream_Deflate()
{
// Since version 0.41.0: EntryStream.Dispose() should not throw NotSupportedException
// when Flush() fails on non-seekable streams (Deflate compression)
var path = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip");
using Stream stream = new ForwardOnlyStream(File.OpenRead(path));
using var reader = ReaderFactory.Open(stream);
// This should not throw, even if internal Flush() fails
while (reader.MoveToNextEntry())
{
if (!reader.Entry.IsDirectory)
{
using var entryStream = reader.OpenEntryStream();
// Read some data
var buffer = new byte[1024];
entryStream.Read(buffer, 0, buffer.Length);
// Dispose should not throw NotSupportedException
}
}
}
[Fact]
public void EntryStream_Dispose_DoesNotThrow_OnNonSeekableStream_LZMA()
{
// Since version 0.41.0: EntryStream.Dispose() should not throw NotSupportedException
// when Flush() fails on non-seekable streams (LZMA compression)
var path = Path.Combine(TEST_ARCHIVES_PATH, "Zip.lzma.dd.zip");
using Stream stream = new ForwardOnlyStream(File.OpenRead(path));
using var reader = ReaderFactory.Open(stream);
// This should not throw, even if internal Flush() fails
while (reader.MoveToNextEntry())
{
if (!reader.Entry.IsDirectory)
{
using var entryStream = reader.OpenEntryStream();
// Read some data
var buffer = new byte[1024];
entryStream.Read(buffer, 0, buffer.Length);
// Dispose should not throw NotSupportedException
}
}
}
[Fact]
public void Archive_Iteration_DoesNotBreak_WhenFlushThrows_Deflate()
{
// Regression test: since 0.41.0, archive iteration would silently break
// when the input stream throws NotSupportedException in Flush().
// Only the first entry would be returned, then iteration would stop without exception.
var path = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip");
using var fileStream = File.OpenRead(path);
using Stream stream = new ThrowOnFlushStream(fileStream);
using var reader = ReaderFactory.Open(stream);
var count = 0;
while (reader.MoveToNextEntry())
{
if (!reader.Entry.IsDirectory)
{
count++;
}
}
// Should iterate through all entries, not just the first one
Assert.True(count > 1, $"Expected more than 1 entry, but got {count}");
}
[Fact]
public void Archive_Iteration_DoesNotBreak_WhenFlushThrows_LZMA()
{
// Regression test: since 0.41.0, archive iteration would silently break
// when the input stream throws NotSupportedException in Flush().
// Only the first entry would be returned, then iteration would stop without exception.
var path = Path.Combine(TEST_ARCHIVES_PATH, "Zip.lzma.dd.zip");
using var fileStream = File.OpenRead(path);
using Stream stream = new ThrowOnFlushStream(fileStream);
using var reader = ReaderFactory.Open(stream);
var count = 0;
while (reader.MoveToNextEntry())
{
if (!reader.Entry.IsDirectory)
{
count++;
}
}
// Should iterate through all entries, not just the first one
Assert.True(count > 1, $"Expected more than 1 entry, but got {count}");
}
}