diff --git a/src/SharpCompress/Compressors/PPMd/PpmdStream.cs b/src/SharpCompress/Compressors/PPMd/PpmdStream.cs index b48cf4cf..744682c4 100644 --- a/src/SharpCompress/Compressors/PPMd/PpmdStream.cs +++ b/src/SharpCompress/Compressors/PPMd/PpmdStream.cs @@ -28,7 +28,16 @@ public class PpmdStream : Stream, IAsyncDisposable _stream = stream; _compress = compress; - InitializeSync(stream, compress); + try + { + InitializeSync(stream, compress); + } + catch + { + // Construction has not transferred ownership to a caller yet. + _modelH?.Dispose(); + throw; + } } private PpmdStream( @@ -47,6 +56,13 @@ public class PpmdStream : Stream, IAsyncDisposable private void InitializeSync(Stream stream, bool compress) { + if ( + compress + && (_properties.Version == PpmdVersion.H || _properties.Version == PpmdVersion.H7Z) + ) + { + throw new NotImplementedException("PPMd H/H7Z compression not supported"); + } if (_properties.Version == PpmdVersion.I1) { _model = new Model(); @@ -62,19 +78,11 @@ public class PpmdStream : Stream, IAsyncDisposable if (_properties.Version == PpmdVersion.H) { _modelH = new ModelPpm(); - if (compress) - { - throw new NotImplementedException(); - } _modelH.DecodeInit(stream, _properties.ModelOrder, _properties.AllocatorSize); } if (_properties.Version == PpmdVersion.H7Z) { _modelH = new ModelPpm(); - if (compress) - { - throw new NotImplementedException(); - } _modelH.DecodeInit(null, _properties.ModelOrder, _properties.AllocatorSize); _decoder = new Decoder(); _decoder.Init(stream); diff --git a/src/SharpCompress/Compressors/Squeezed/SqueezedStream.Async.cs b/src/SharpCompress/Compressors/Squeezed/SqueezedStream.Async.cs index cfdd0e52..c4fd5854 100644 --- a/src/SharpCompress/Compressors/Squeezed/SqueezedStream.Async.cs +++ b/src/SharpCompress/Compressors/Squeezed/SqueezedStream.Async.cs @@ -17,11 +17,20 @@ public partial class SqueezeStream ) { var squeezeStream = new SqueezeStream(stream, compressedSize); - squeezeStream._decodedStream = await squeezeStream - .BuildDecodedStreamAsync(cancellationToken) - .ConfigureAwait(false); - - return squeezeStream; + try + { + squeezeStream._decodedStream = await squeezeStream + .BuildDecodedStreamAsync(cancellationToken) + .ConfigureAwait(false); + return squeezeStream; + } + catch + { +#pragma warning disable VSTHRD103 // Cleanup only releases in-memory buffers; the source remains open. + squeezeStream.Dispose(); +#pragma warning restore VSTHRD103 + throw; + } } public override async Task ReadAsync( @@ -83,7 +92,7 @@ public partial class SqueezeStream } var bitReader = new BitReader(_stream); - var huffmanDecoded = new PooledMemoryStream(); + var huffmanDecoded = _huffmanDecoded = new PooledMemoryStream(); int i = 0; while (true) diff --git a/src/SharpCompress/Compressors/Squeezed/SqueezedStream.cs b/src/SharpCompress/Compressors/Squeezed/SqueezedStream.cs index f6a40f1c..b3c116b2 100644 --- a/src/SharpCompress/Compressors/Squeezed/SqueezedStream.cs +++ b/src/SharpCompress/Compressors/Squeezed/SqueezedStream.cs @@ -16,6 +16,7 @@ public partial class SqueezeStream : Stream private const int SPEOF = 256; private Stream _decodedStream = null!; + private PooledMemoryStream? _huffmanDecoded; private SqueezeStream(Stream stream, int compressedSize) { @@ -25,15 +26,36 @@ public partial class SqueezeStream : Stream public static SqueezeStream Create(Stream stream, int compressedSize) { var squeezeStream = new SqueezeStream(stream, compressedSize); - squeezeStream._decodedStream = squeezeStream.BuildDecodedStream(); - - return squeezeStream; + try + { + squeezeStream._decodedStream = squeezeStream.BuildDecodedStream(); + return squeezeStream; + } + catch + { + squeezeStream.Dispose(); + throw; + } } protected override void Dispose(bool disposing) { - _decodedStream?.Dispose(); - base.Dispose(disposing); + try + { + if (disposing) + { + _decodedStream?.Dispose(); + } + } + finally + { + if (disposing) + { + // RunLength90Stream deliberately does not own its input. + _huffmanDecoded?.Dispose(); + } + base.Dispose(disposing); + } } public override bool CanRead => true; @@ -79,7 +101,7 @@ public partial class SqueezeStream : Stream } var bitReader = new BitReader(_stream); - var huffmanDecoded = new PooledMemoryStream(); + var huffmanDecoded = _huffmanDecoded = new PooledMemoryStream(); int i = 0; while (true) diff --git a/tests/SharpCompress.Test/Streams/SqueezeLifetimeTests.cs b/tests/SharpCompress.Test/Streams/SqueezeLifetimeTests.cs new file mode 100644 index 00000000..b5a58aa5 --- /dev/null +++ b/tests/SharpCompress.Test/Streams/SqueezeLifetimeTests.cs @@ -0,0 +1,42 @@ +using System; +using System.IO; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Compressors.PPMd; +using SharpCompress.Compressors.Squeezed; +using Xunit; + +namespace SharpCompress.Test.Streams; + +public class SqueezeLifetimeTests +{ + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task DisposeReleasesOwnedHuffmanStream(bool asyncOpen) + { + // One Huffman node: literal A on zero, end marker on one. + using var source = new MemoryStream(new byte[] { 1, 0, 190, 255, 255, 254, 2 }); + var stream = asyncOpen + ? await SqueezeStream.CreateAsync(source, 7, CancellationToken.None) + : SqueezeStream.Create(source, 7); + var owned = (Stream) + typeof(SqueezeStream) + .GetField("_huffmanDecoded", BindingFlags.Instance | BindingFlags.NonPublic)! + .GetValue(stream)!; + Assert.True(owned.CanRead); + stream.Dispose(); + Assert.False(owned.CanRead); + Assert.True(source.CanRead); + } + + [Fact] + public void UnsupportedPpmdCompressionLeavesInputOpen() + { + using var source = new MemoryStream(); + var properties = new PpmdProperties(new byte[] { 2, 0, 0, 16, 0 }); + Assert.Throws(() => PpmdStream.Create(properties, source, true)); + Assert.True(source.CanRead); + } +}