From 99cc990277be3c1059e69b25a3b292a013c939f0 Mon Sep 17 00:00:00 2001 From: iremyux Date: Fri, 24 Oct 2025 15:19:38 +0200 Subject: [PATCH 1/5] Rewind the DeflateStream when it is disposed in decompression mode --- .../Compression/DeflateZLib/DeflateStream.cs | 43 +++++++++++++++++++ .../IO/Compression/DeflateZLib/Inflater.cs | 2 + 2 files changed, 45 insertions(+) diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateZLib/DeflateStream.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateZLib/DeflateStream.cs index 9f5360a51a30b9..8bcc4ebc690dbf 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateZLib/DeflateStream.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateZLib/DeflateStream.cs @@ -720,7 +720,28 @@ protected override void Dispose(bool disposing) try { if (disposing && !_leaveOpen) + { + // Auto-rewind the stream if we're in decompression mode and the stream supports seeking + if (_mode == CompressionMode.Decompress && _stream?.CanSeek == true && _inflater != null) + { + // Check if there are unconsumed bytes in the inflater's input buffer + int unconsumedBytes = _inflater.GetAvailableInput(); + if (unconsumedBytes > 0) + { + try + { + // Rewind the stream to the exact end of the compressed data + _stream.Seek(-unconsumedBytes, SeekOrigin.Current); + } + catch + { + // If seeking fails, we don't want to throw during disposal + } + } + } + _stream?.Dispose(); + } } finally { @@ -775,7 +796,29 @@ async ValueTask Core() try { if (!_leaveOpen && stream != null) + { + // Auto-rewind the stream if we're in decompression mode and the stream supports seeking + if (_mode == CompressionMode.Decompress && stream.CanSeek && _inflater != null) + { + // Check if there are unconsumed bytes in the inflater's input buffer + int unconsumedBytes = _inflater.GetAvailableInput(); + if (unconsumedBytes > 0) + { + try + { + // Rewind the stream to the exact end of the compressed data + stream.Seek(-unconsumedBytes, SeekOrigin.Current); + } + catch + { + // If seeking fails, we don't want to throw during disposal + // The stream might have become non-seekable or hit some other issue + } + } + } + await stream.DisposeAsync().ConfigureAwait(false); + } } finally { diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateZLib/Inflater.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateZLib/Inflater.cs index 9c5cae917f2ed8..cc400e2067f786 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateZLib/Inflater.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateZLib/Inflater.cs @@ -173,6 +173,8 @@ private unsafe bool ResetStreamForLeftoverInput() public bool NonEmptyInput() => _nonEmptyInput; + internal int GetAvailableInput() => (int)_zlibStream.AvailIn; + public void SetInput(byte[] inputBuffer, int startIndex, int count) { Debug.Assert(NeedsInput(), "We have something left in previous input!"); From c7b2ed8aaadf315f9bd72af76c655f3542a25381 Mon Sep 17 00:00:00 2001 From: iremyux Date: Fri, 24 Oct 2025 15:39:57 +0200 Subject: [PATCH 2/5] Remove trailing whitespace --- .../src/System/IO/Compression/DeflateZLib/DeflateStream.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateZLib/DeflateStream.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateZLib/DeflateStream.cs index 8bcc4ebc690dbf..6e4276076f3f87 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateZLib/DeflateStream.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateZLib/DeflateStream.cs @@ -739,7 +739,7 @@ protected override void Dispose(bool disposing) } } } - + _stream?.Dispose(); } } @@ -816,7 +816,7 @@ async ValueTask Core() } } } - + await stream.DisposeAsync().ConfigureAwait(false); } } From 2a435c4abb405e25f30da8110e92742094d9ba8c Mon Sep 17 00:00:00 2001 From: iremyux Date: Fri, 24 Oct 2025 15:40:15 +0200 Subject: [PATCH 3/5] Change comment --- .../src/System/IO/Compression/DeflateZLib/DeflateStream.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateZLib/DeflateStream.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateZLib/DeflateStream.cs index 6e4276076f3f87..b348469495fd69 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateZLib/DeflateStream.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateZLib/DeflateStream.cs @@ -812,7 +812,6 @@ async ValueTask Core() catch { // If seeking fails, we don't want to throw during disposal - // The stream might have become non-seekable or hit some other issue } } } From 1357c5dd9f93b930678946c111c05695792bd718 Mon Sep 17 00:00:00 2001 From: iremyux Date: Mon, 27 Oct 2025 11:14:34 +0100 Subject: [PATCH 4/5] Rewind when _leaveOpen is true --- .../Compression/DeflateZLib/DeflateStream.cs | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateZLib/DeflateStream.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateZLib/DeflateStream.cs index b348469495fd69..949f157796255f 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateZLib/DeflateStream.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/DeflateZLib/DeflateStream.cs @@ -719,27 +719,27 @@ protected override void Dispose(bool disposing) // In this case, we still need to clean up internal resources, hence the inner finally blocks. try { - if (disposing && !_leaveOpen) + // Auto-rewind the stream if we're in decompression mode and the stream supports seeking + if (disposing && _mode == CompressionMode.Decompress && _stream?.CanSeek == true && _inflater != null) { - // Auto-rewind the stream if we're in decompression mode and the stream supports seeking - if (_mode == CompressionMode.Decompress && _stream?.CanSeek == true && _inflater != null) + // Check if there are unconsumed bytes in the inflater's input buffer + int unconsumedBytes = _inflater.GetAvailableInput(); + if (unconsumedBytes > 0) { - // Check if there are unconsumed bytes in the inflater's input buffer - int unconsumedBytes = _inflater.GetAvailableInput(); - if (unconsumedBytes > 0) + try { - try - { - // Rewind the stream to the exact end of the compressed data - _stream.Seek(-unconsumedBytes, SeekOrigin.Current); - } - catch - { - // If seeking fails, we don't want to throw during disposal - } + // Rewind the stream to the exact end of the compressed data + _stream.Seek(-unconsumedBytes, SeekOrigin.Current); + } + catch + { + // If seeking fails, we don't want to throw during disposal } } + } + if (disposing && !_leaveOpen) + { _stream?.Dispose(); } } @@ -795,27 +795,27 @@ async ValueTask Core() _stream = null!; try { - if (!_leaveOpen && stream != null) + // Auto-rewind the stream if we're in decompression mode and the stream supports seeking + if (stream != null && _mode == CompressionMode.Decompress && stream.CanSeek && _inflater != null) { - // Auto-rewind the stream if we're in decompression mode and the stream supports seeking - if (_mode == CompressionMode.Decompress && stream.CanSeek && _inflater != null) + // Check if there are unconsumed bytes in the inflater's input buffer + int unconsumedBytes = _inflater.GetAvailableInput(); + if (unconsumedBytes > 0) { - // Check if there are unconsumed bytes in the inflater's input buffer - int unconsumedBytes = _inflater.GetAvailableInput(); - if (unconsumedBytes > 0) + try { - try - { - // Rewind the stream to the exact end of the compressed data - stream.Seek(-unconsumedBytes, SeekOrigin.Current); - } - catch - { - // If seeking fails, we don't want to throw during disposal - } + // Rewind the stream to the exact end of the compressed data + stream.Seek(-unconsumedBytes, SeekOrigin.Current); + } + catch + { + // If seeking fails, we don't want to throw during disposal } } + } + if (!_leaveOpen && stream != null) + { await stream.DisposeAsync().ConfigureAwait(false); } } From 731b4e84c6d4dcf7e1d52ca54a513bb1bc873a27 Mon Sep 17 00:00:00 2001 From: iremyux Date: Mon, 27 Oct 2025 11:14:43 +0100 Subject: [PATCH 5/5] Add tests --- .../CompressionStreamUnitTests.Deflate.cs | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/src/libraries/System.IO.Compression/tests/CompressionStreamUnitTests.Deflate.cs b/src/libraries/System.IO.Compression/tests/CompressionStreamUnitTests.Deflate.cs index 30ab788bab2d42..040ebf13823ce6 100644 --- a/src/libraries/System.IO.Compression/tests/CompressionStreamUnitTests.Deflate.cs +++ b/src/libraries/System.IO.Compression/tests/CompressionStreamUnitTests.Deflate.cs @@ -190,6 +190,124 @@ public void StreamTruncation_IsDetected(TestScenario testScenario) }, testScenario.ToString()).Dispose(); } + [Fact] + public void AutomaticStreamRewinds_WhenDecompressionFinishes() + { + // Create test data: some header bytes + compressed data + some footer bytes + byte[] originalData = Encoding.UTF8.GetBytes("Hello, world! This is a test string for compression."); + byte[] headerBytes = Encoding.UTF8.GetBytes("HEADER"); + byte[] footerBytes = Encoding.UTF8.GetBytes("FOOTER"); + + // Create compressed data + byte[] compressedData; + using (var ms = new MemoryStream()) + { + using (var deflateStream = new DeflateStream(ms, CompressionMode.Compress)) + { + deflateStream.Write(originalData); + } + compressedData = ms.ToArray(); + } + + // Create a stream with: [header][compressed data][footer] + byte[] combinedData = new byte[headerBytes.Length + compressedData.Length + footerBytes.Length]; + Array.Copy(headerBytes, 0, combinedData, 0, headerBytes.Length); + Array.Copy(compressedData, 0, combinedData, headerBytes.Length, compressedData.Length); + Array.Copy(footerBytes, 0, combinedData, headerBytes.Length + compressedData.Length, footerBytes.Length); + + using (var stream = new MemoryStream(combinedData)) + { + // Read the header + byte[] headerBuffer = new byte[headerBytes.Length]; + stream.Read(headerBuffer, 0, headerBytes.Length); + Assert.Equal(headerBytes, headerBuffer); + + // Decompress the data + byte[] decompressedData; + using (var deflateStream = new DeflateStream(stream, CompressionMode.Decompress, leaveOpen: true)) + { + using (var outputStream = new MemoryStream()) + { + deflateStream.CopyTo(outputStream); + decompressedData = outputStream.ToArray(); + } + } + // DeflateStream should have automatically rewound the stream to the end of compressed data + + // Verify decompressed data is correct + Assert.Equal(originalData, decompressedData); + + // Read the footer - if automatic rewinding worked, this should read the footer correctly + byte[] footerBuffer = new byte[footerBytes.Length]; + int bytesRead = stream.Read(footerBuffer, 0, footerBytes.Length); + + Assert.Equal(footerBytes.Length, bytesRead); + Assert.Equal(footerBytes, footerBuffer); + + // Verify we're at the end of the stream + Assert.Equal(combinedData.Length, stream.Position); + } + } + + [Fact] + public async Task AutomaticStreamRewinds_WhenDecompressionFinishes_Async() + { + // Create test data: some header bytes + compressed data + some footer bytes + byte[] originalData = Encoding.UTF8.GetBytes("Hello, world! This is a test string for compression."); + byte[] headerBytes = Encoding.UTF8.GetBytes("HEADER"); + byte[] footerBytes = Encoding.UTF8.GetBytes("FOOTER"); + + // Create compressed data + byte[] compressedData; + using (var ms = new MemoryStream()) + { + using (var deflateStream = new DeflateStream(ms, CompressionMode.Compress)) + { + await deflateStream.WriteAsync(originalData); + } + compressedData = ms.ToArray(); + } + + // Create a stream with: [header][compressed data][footer] + byte[] combinedData = new byte[headerBytes.Length + compressedData.Length + footerBytes.Length]; + Array.Copy(headerBytes, 0, combinedData, 0, headerBytes.Length); + Array.Copy(compressedData, 0, combinedData, headerBytes.Length, compressedData.Length); + Array.Copy(footerBytes, 0, combinedData, headerBytes.Length + compressedData.Length, footerBytes.Length); + + using (var stream = new MemoryStream(combinedData)) + { + // Read the header + byte[] headerBuffer = new byte[headerBytes.Length]; + await stream.ReadAsync(headerBuffer, 0, headerBytes.Length); + Assert.Equal(headerBytes, headerBuffer); + + // Decompress the data + byte[] decompressedData; + await using (var deflateStream = new DeflateStream(stream, CompressionMode.Decompress, leaveOpen: true)) + { + using (var outputStream = new MemoryStream()) + { + await deflateStream.CopyToAsync(outputStream); + decompressedData = outputStream.ToArray(); + } + } + // DeflateStream should have automatically rewound the stream to the end of compressed data + + // Verify decompressed data is correct + Assert.Equal(originalData, decompressedData); + + // Read the footer - if automatic rewinding worked, this should read the footer correctly + byte[] footerBuffer = new byte[footerBytes.Length]; + int bytesRead = await stream.ReadAsync(footerBuffer, 0, footerBytes.Length); + + Assert.Equal(footerBytes.Length, bytesRead); + Assert.Equal(footerBytes, footerBuffer); + + // Verify we're at the end of the stream + Assert.Equal(combinedData.Length, stream.Position); + } + } + private sealed class DerivedDeflateStream : DeflateStream { public bool ReadArrayInvoked = false, WriteArrayInvoked = false;