diff --git a/src/SharpCompress/Compressors/BZip2/BZip2Stream.cs b/src/SharpCompress/Compressors/BZip2/BZip2Stream.cs index 898839bc0..03a9f6b86 100644 --- a/src/SharpCompress/Compressors/BZip2/BZip2Stream.cs +++ b/src/SharpCompress/Compressors/BZip2/BZip2Stream.cs @@ -30,6 +30,7 @@ void IStreamStack.SetPosition(long position) { } private readonly Stream stream; private bool isDisposed; + private readonly bool leaveOpen; /// /// Create a BZip2Stream @@ -37,19 +38,30 @@ void IStreamStack.SetPosition(long position) { } /// The stream to read from /// Compression Mode /// Decompress Concatenated - public BZip2Stream(Stream stream, CompressionMode compressionMode, bool decompressConcatenated) + /// Leave the stream open after disposing + 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 + ); } } diff --git a/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs b/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs index e466cc07d..e31044089 100644 --- a/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs +++ b/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.cs @@ -168,6 +168,7 @@ during decompression. private int computedBlockCRC, computedCombinedCRC; private readonly bool decompressConcatenated; + private readonly bool leaveOpen; private int i2, count, @@ -181,9 +182,10 @@ during decompression. 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 @@ protected override void Dispose(bool disposing) 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 @@ private bool Complete() private void BsFinishedWithStream() { - bsStream?.Dispose(); + if (!leaveOpen) + { + bsStream?.Dispose(); + } bsStream = null; } diff --git a/src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.cs b/src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.cs index 7ee38b81a..e0500adc1 100644 --- a/src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.cs +++ b/src/SharpCompress/Compressors/BZip2/CBZip2OutputStream.cs @@ -341,12 +341,14 @@ The current block size is 100000 * this number. 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 @@ protected override void Dispose(bool disposing) this.DebugDispose(typeof(CBZip2OutputStream)); #endif Dispose(); - bsStream?.Dispose(); + if (!leaveOpen) + { + bsStream?.Dispose(); + } bsStream = null; } } diff --git a/src/SharpCompress/Compressors/LZMA/LZipStream.cs b/src/SharpCompress/Compressors/LZMA/LZipStream.cs index ddf39e01b..d03353f77 100644 --- a/src/SharpCompress/Compressors/LZMA/LZipStream.cs +++ b/src/SharpCompress/Compressors/LZMA/LZipStream.cs @@ -46,11 +46,13 @@ void IStreamStack.SetPosition(long position) { } 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 LZipStream(Stream stream, CompressionMode mode) 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 @@ protected override void Dispose(bool disposing) { Finish(); _stream.Dispose(); - if (Mode == CompressionMode.Compress) + if (Mode == CompressionMode.Compress && !_leaveOpen) { _originalStream?.Dispose(); } diff --git a/src/SharpCompress/Compressors/LZMA/LzmaStream.cs b/src/SharpCompress/Compressors/LZMA/LzmaStream.cs index 1399c0bf7..2ddfdd8cc 100644 --- a/src/SharpCompress/Compressors/LZMA/LzmaStream.cs +++ b/src/SharpCompress/Compressors/LZMA/LzmaStream.cs @@ -35,6 +35,7 @@ void IStreamStack.SetPosition(long position) { } 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 @@ void IStreamStack.SetPosition(long position) { } 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 LzmaStream( 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 @@ protected override void Dispose(bool disposing) { _position = _encoder.Code(null, true); } - _inputStream?.Dispose(); + if (!_leaveOpen) + { + _inputStream?.Dispose(); + } _outWindow.Dispose(); } base.Dispose(disposing); diff --git a/tests/SharpCompress.Test/Streams/DisposalTests.cs b/tests/SharpCompress.Test/Streams/DisposalTests.cs index 9d30ab76f..089958929 100644 --- a/tests/SharpCompress.Test/Streams/DisposalTests.cs +++ b/tests/SharpCompress.Test/Streams/DisposalTests.cs @@ -2,6 +2,7 @@ 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 void LzmaStream_Disposal() [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] diff --git a/tests/SharpCompress.Test/Streams/LeaveOpenBehaviorTests.cs b/tests/SharpCompress.Test/Streams/LeaveOpenBehaviorTests.cs new file mode 100644 index 000000000..76e0565e0 --- /dev/null +++ b/tests/SharpCompress.Test/Streams/LeaveOpenBehaviorTests.cs @@ -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); + } +}