From ed442de8b0a4cbef3b0343c897791f8506205e5a Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Mon, 13 Apr 2026 15:22:16 +0200 Subject: [PATCH 1/6] This fixes a missing overload for `ReadByte()` that caused the library to return incorrect results when reading a stream with `ReadByte()`. As the use of `ReadByte()` on a compressed stream is arguably seldom used, I opted for a simple "read into 1-byte array" solution that is not terribly resource efficient, but prevents having code replicated from the main `Read()` method into `ReadByte()`. Unfortunately, the main `Read()` method also had issues when reading into a 1-byte array, so that was fixed as well. Added a set of tests that explain the issue and fail before the fix, but succeeds after. These are clearly edge case, but if anyone passes a compressed stream to another library, event these edge cases should work correctly. --- .../Compressors/Deflate/ZlibBaseStream.cs | 40 ++- .../Zip/ZipCompressionRoundtripTests.cs | 252 ++++++++++++++++++ 2 files changed, 281 insertions(+), 11 deletions(-) create mode 100644 tests/SharpCompress.Test/Zip/ZipCompressionRoundtripTests.cs diff --git a/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs b/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs index ea96f0656..d8590cb36 100644 --- a/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs +++ b/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs @@ -630,17 +630,17 @@ public override Int64 Seek(Int64 offset, SeekOrigin origin) => //_outStream.Seek(offset, origin); public override void SetLength(Int64 value) => _stream.SetLength(value); -#if NOT - public int Read() + public override int ReadByte() { - if (Read(_buf1, 0, 1) == 0) - return 0; - // calculate CRC after reading - if (crc != null) - crc.SlurpBlock(_buf1, 0, 1); - return (_buf1[0] & 0xFF); + var buffer = _buf1; + var n = Read(buffer, 0, 1); + if (n == 0) + { + return -1; + } + + return buffer[0]; } -#endif private bool nomoreinput; private bool isDisposed; @@ -987,6 +987,11 @@ public override Int32 Read(Byte[] buffer, Int32 offset, Int32 count) } } + // Save the inflate/deflate result code before overwriting rc with the + // byte count. Z_STREAM_END == 1, so when count == 1 and all bytes were + // produced, (count - AvailableBytesOut) == 1 which would falsely match + // Z_STREAM_END and trigger a spurious rewind. + var inflateResult = rc; rc = (count - _z.AvailableBytesOut); // calculate CRC after reading @@ -995,7 +1000,11 @@ public override Int32 Read(Byte[] buffer, Int32 offset, Int32 count) crc.SlurpBlock(buffer, offset, rc); } - if (rc == ZlibConstants.Z_STREAM_END && z.AvailableBytesIn != 0 && !_wantCompress) + if ( + inflateResult == ZlibConstants.Z_STREAM_END + && z.AvailableBytesIn != 0 + && !_wantCompress + ) { //rewind the buffer this.Rewind(z.AvailableBytesIn); @@ -1181,6 +1190,11 @@ CancellationToken cancellationToken } } + // Save the inflate/deflate result code before overwriting rc with the + // byte count. Z_STREAM_END == 1, so when count == 1 and all bytes were + // produced, (count - AvailableBytesOut) == 1 which would falsely match + // Z_STREAM_END and trigger a spurious rewind. + var inflateResult = rc; rc = (count - _z.AvailableBytesOut); // calculate CRC after reading @@ -1189,7 +1203,11 @@ CancellationToken cancellationToken crc.SlurpBlock(buffer, offset, rc); } - if (rc == ZlibConstants.Z_STREAM_END && z.AvailableBytesIn != 0 && !_wantCompress) + if ( + inflateResult == ZlibConstants.Z_STREAM_END + && z.AvailableBytesIn != 0 + && !_wantCompress + ) { //rewind the buffer this.Rewind(z.AvailableBytesIn); diff --git a/tests/SharpCompress.Test/Zip/ZipCompressionRoundtripTests.cs b/tests/SharpCompress.Test/Zip/ZipCompressionRoundtripTests.cs new file mode 100644 index 000000000..35e14795c --- /dev/null +++ b/tests/SharpCompress.Test/Zip/ZipCompressionRoundtripTests.cs @@ -0,0 +1,252 @@ +using System; +using System.IO; +using System.Linq; +using SharpCompress.Archives.Zip; +using SharpCompress.Common; +using SharpCompress.Readers; +using SharpCompress.Writers; +using SharpCompress.Writers.Zip; +using Xunit; + +namespace SharpCompress.Test.Zip; + +public class ZipCompressionRoundtripTests : TestBase +{ + private const int TESTSIZE = 1024 * 1024; + + [Fact] + public void Zip_Deflate_Roundtrip_ArchiveApi_BufferedRead_Succeeds() + { + // Create test data with specific patterns + var testset1 = Enumerable + .Range(0, TESTSIZE) + .Select(i => (byte)((i * 22695477) % 257)) + .ToArray(); + var testset2 = Enumerable + .Range(0, TESTSIZE) + .Select(i => (byte)((i * 48271) % 257)) + .ToArray(); + + using var stream = new MemoryStream(); + + // Compress test streams + var writerOptions = new ZipWriterOptions(CompressionType.Deflate, compressionLevel: 9); + + using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Zip, writerOptions)) + { + writer.Write("sample1", new MemoryStream(testset1)); + writer.Write("sample2", new MemoryStream(testset2)); + } + + // Decompress and verify using Archive API with buffered read (CopyTo) + stream.Position = 0; + using (var archive = ZipArchive.OpenArchive(stream)) + { + var files = archive.Entries.Where(e => !e.IsDirectory).OrderBy(e => e.Key).ToList(); + Assert.Equal(2, files.Count); + + // Read using CopyTo pattern (buffered - should succeed) + using (var entryStream = files[1].OpenEntryStream()) + { + using var extracted = new MemoryStream(); + entryStream.CopyTo(extracted); + Assert.Equal(testset2, extracted.ToArray()); + } + + using (var entryStream = files[0].OpenEntryStream()) + { + using var extracted = new MemoryStream(); + entryStream.CopyTo(extracted); + Assert.Equal(testset1, extracted.ToArray()); + } + } + } + + [Fact] + public void Zip_Deflate_Roundtrip_ArchiveApi_ByteByByteRead_Succeeds() + { + // Create test data with specific patterns + var testset1 = Enumerable + .Range(0, TESTSIZE) + .Select(i => (byte)((i * 22695477) % 257)) + .ToArray(); + var testset2 = Enumerable + .Range(0, TESTSIZE) + .Select(i => (byte)((i * 48271) % 257)) + .ToArray(); + + using var stream = new MemoryStream(); + + // Compress test streams + var writerOptions = new ZipWriterOptions(CompressionType.Deflate, compressionLevel: 9); + + using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Zip, writerOptions)) + { + writer.Write("sample1", new MemoryStream(testset1)); + writer.Write("sample2", new MemoryStream(testset2)); + } + + // Decompress and verify using byte-by-byte read + stream.Position = 0; + using (var archive = ZipArchive.OpenArchive(stream)) + { + var files = archive.Entries.Where(e => !e.IsDirectory).OrderBy(e => e.Key).ToList(); + Assert.Equal(2, files.Count); + + // Read second file byte by byte + using (var entryStream = files[1].OpenEntryStream()) + { + var buffer = new byte[testset2.Length]; + for (var i = 0; i < buffer.Length; i++) + { + var b = entryStream.ReadByte(); + if (b == -1) + { + throw new InvalidOperationException($"Unexpected EOF at offset {i}"); + } + + buffer[i] = (byte)b; + } + Assert.Equal(testset2, buffer); + + // Verify EOF + Assert.Equal(-1, entryStream.ReadByte()); + } + + // Read first file byte by byte using All pattern + using (var entryStream = files[0].OpenEntryStream()) + { + var match = + testset1.All(b => b == entryStream.ReadByte()) && entryStream.ReadByte() == -1; + Assert.True( + match, + "Decompressed file sample1 contents do not match the source file." + ); + } + } + } + + [Fact] + public void Zip_Deflate_Roundtrip_ReaderApi_BufferedRead_Succeeds() + { + // Create test data with specific patterns + var testset1 = Enumerable + .Range(0, TESTSIZE) + .Select(i => (byte)((i * 22695477) % 257)) + .ToArray(); + var testset2 = Enumerable + .Range(0, TESTSIZE) + .Select(i => (byte)((i * 48271) % 257)) + .ToArray(); + + using var stream = new MemoryStream(); + + // Compress test streams + var writerOptions = new ZipWriterOptions(CompressionType.Deflate, compressionLevel: 9); + + using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Zip, writerOptions)) + { + writer.Write("sample1", new MemoryStream(testset1)); + writer.Write("sample2", new MemoryStream(testset2)); + } + + // Decompress and verify using Reader API with buffered read (CopyTo) + stream.Position = 0; + using (var reader = ReaderFactory.OpenReader(stream)) + { + // Read first entry + Assert.True(reader.MoveToNextEntry()); + Assert.Equal("sample1", reader.Entry.Key); + using (var entryStream = reader.OpenEntryStream()) + { + using var extracted = new MemoryStream(); + entryStream.CopyTo(extracted); + Assert.Equal(testset1, extracted.ToArray()); + } + + // Read second entry + Assert.True(reader.MoveToNextEntry()); + Assert.Equal("sample2", reader.Entry.Key); + using (var entryStream = reader.OpenEntryStream()) + { + using var extracted = new MemoryStream(); + entryStream.CopyTo(extracted); + Assert.Equal(testset2, extracted.ToArray()); + } + + // No more entries + Assert.False(reader.MoveToNextEntry()); + } + } + + [Fact] + public void Zip_Deflate_Roundtrip_ReaderApi_ByteByByteRead_Succeeds() + { + // Create test data with specific patterns + var testset1 = Enumerable + .Range(0, TESTSIZE) + .Select(i => (byte)((i * 22695477) % 257)) + .ToArray(); + var testset2 = Enumerable + .Range(0, TESTSIZE) + .Select(i => (byte)((i * 48271) % 257)) + .ToArray(); + + using var stream = new MemoryStream(); + + // Compress test streams + var writerOptions = new ZipWriterOptions(CompressionType.Deflate, compressionLevel: 9); + + using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Zip, writerOptions)) + { + writer.Write("sample1", new MemoryStream(testset1)); + writer.Write("sample2", new MemoryStream(testset2)); + } + + // Decompress and verify using Reader API with byte-by-byte read + stream.Position = 0; + using (var reader = ReaderFactory.OpenReader(stream)) + { + // Read first entry byte by byte + Assert.True(reader.MoveToNextEntry()); + Assert.Equal("sample1", reader.Entry.Key); + using (var entryStream = reader.OpenEntryStream()) + { + var buffer = new byte[testset1.Length]; + for (var i = 0; i < buffer.Length; i++) + { + var b = entryStream.ReadByte(); + if (b == -1) + { + throw new InvalidOperationException($"Unexpected EOF at offset {i}"); + } + buffer[i] = (byte)b; + } + Assert.Equal(testset1, buffer); + Assert.Equal(-1, entryStream.ReadByte()); + } + + // Read second entry byte by byte + Assert.True(reader.MoveToNextEntry()); + Assert.Equal("sample2", reader.Entry.Key); + using (var entryStream = reader.OpenEntryStream()) + { + var buffer = new byte[testset2.Length]; + for (var i = 0; i < buffer.Length; i++) + { + var b = entryStream.ReadByte(); + if (b == -1) + { + throw new InvalidOperationException($"Unexpected EOF at offset {i}"); + } + buffer[i] = (byte)b; + } + Assert.Equal(testset2, buffer); + Assert.Equal(-1, entryStream.ReadByte()); + } + + // No more entries + Assert.False(reader.MoveToNextEntry()); + } + } +} From e2dac6724d4c4808ccff33e4e70f086d6fa7d7e0 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Mon, 13 Apr 2026 15:58:10 +0200 Subject: [PATCH 2/6] Addressed review concerns in test code. --- .../Zip/ZipCompressionRoundtripTests.cs | 94 ++++--------------- 1 file changed, 17 insertions(+), 77 deletions(-) diff --git a/tests/SharpCompress.Test/Zip/ZipCompressionRoundtripTests.cs b/tests/SharpCompress.Test/Zip/ZipCompressionRoundtripTests.cs index 35e14795c..402783b56 100644 --- a/tests/SharpCompress.Test/Zip/ZipCompressionRoundtripTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipCompressionRoundtripTests.cs @@ -12,24 +12,21 @@ namespace SharpCompress.Test.Zip; public class ZipCompressionRoundtripTests : TestBase { - private const int TESTSIZE = 1024 * 1024; + private const int TestSize = 1024 * 1024; - [Fact] - public void Zip_Deflate_Roundtrip_ArchiveApi_BufferedRead_Succeeds() + private static (byte[] TestSet1, byte[] TestSet2, MemoryStream Stream) CreateTestZip() { - // Create test data with specific patterns var testset1 = Enumerable - .Range(0, TESTSIZE) + .Range(0, TestSize) .Select(i => (byte)((i * 22695477) % 257)) .ToArray(); var testset2 = Enumerable - .Range(0, TESTSIZE) + .Range(0, TestSize) .Select(i => (byte)((i * 48271) % 257)) .ToArray(); - using var stream = new MemoryStream(); + var stream = new MemoryStream(); - // Compress test streams var writerOptions = new ZipWriterOptions(CompressionType.Deflate, compressionLevel: 9); using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Zip, writerOptions)) @@ -38,6 +35,15 @@ public void Zip_Deflate_Roundtrip_ArchiveApi_BufferedRead_Succeeds() writer.Write("sample2", new MemoryStream(testset2)); } + stream.Position = 0; + return (testset1, testset2, stream); + } + + [Fact] + public void Zip_Deflate_Roundtrip_ArchiveApi_BufferedRead_Succeeds() + { + var (testset1, testset2, stream) = CreateTestZip(); + // Decompress and verify using Archive API with buffered read (CopyTo) stream.Position = 0; using (var archive = ZipArchive.OpenArchive(stream)) @@ -65,29 +71,7 @@ public void Zip_Deflate_Roundtrip_ArchiveApi_BufferedRead_Succeeds() [Fact] public void Zip_Deflate_Roundtrip_ArchiveApi_ByteByByteRead_Succeeds() { - // Create test data with specific patterns - var testset1 = Enumerable - .Range(0, TESTSIZE) - .Select(i => (byte)((i * 22695477) % 257)) - .ToArray(); - var testset2 = Enumerable - .Range(0, TESTSIZE) - .Select(i => (byte)((i * 48271) % 257)) - .ToArray(); - - using var stream = new MemoryStream(); - - // Compress test streams - var writerOptions = new ZipWriterOptions(CompressionType.Deflate, compressionLevel: 9); - - using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Zip, writerOptions)) - { - writer.Write("sample1", new MemoryStream(testset1)); - writer.Write("sample2", new MemoryStream(testset2)); - } - - // Decompress and verify using byte-by-byte read - stream.Position = 0; + var (testset1, testset2, stream) = CreateTestZip(); using (var archive = ZipArchive.OpenArchive(stream)) { var files = archive.Entries.Where(e => !e.IsDirectory).OrderBy(e => e.Key).ToList(); @@ -129,29 +113,7 @@ public void Zip_Deflate_Roundtrip_ArchiveApi_ByteByByteRead_Succeeds() [Fact] public void Zip_Deflate_Roundtrip_ReaderApi_BufferedRead_Succeeds() { - // Create test data with specific patterns - var testset1 = Enumerable - .Range(0, TESTSIZE) - .Select(i => (byte)((i * 22695477) % 257)) - .ToArray(); - var testset2 = Enumerable - .Range(0, TESTSIZE) - .Select(i => (byte)((i * 48271) % 257)) - .ToArray(); - - using var stream = new MemoryStream(); - - // Compress test streams - var writerOptions = new ZipWriterOptions(CompressionType.Deflate, compressionLevel: 9); - - using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Zip, writerOptions)) - { - writer.Write("sample1", new MemoryStream(testset1)); - writer.Write("sample2", new MemoryStream(testset2)); - } - - // Decompress and verify using Reader API with buffered read (CopyTo) - stream.Position = 0; + var (testset1, testset2, stream) = CreateTestZip(); using (var reader = ReaderFactory.OpenReader(stream)) { // Read first entry @@ -182,29 +144,7 @@ public void Zip_Deflate_Roundtrip_ReaderApi_BufferedRead_Succeeds() [Fact] public void Zip_Deflate_Roundtrip_ReaderApi_ByteByByteRead_Succeeds() { - // Create test data with specific patterns - var testset1 = Enumerable - .Range(0, TESTSIZE) - .Select(i => (byte)((i * 22695477) % 257)) - .ToArray(); - var testset2 = Enumerable - .Range(0, TESTSIZE) - .Select(i => (byte)((i * 48271) % 257)) - .ToArray(); - - using var stream = new MemoryStream(); - - // Compress test streams - var writerOptions = new ZipWriterOptions(CompressionType.Deflate, compressionLevel: 9); - - using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Zip, writerOptions)) - { - writer.Write("sample1", new MemoryStream(testset1)); - writer.Write("sample2", new MemoryStream(testset2)); - } - - // Decompress and verify using Reader API with byte-by-byte read - stream.Position = 0; + var (testset1, testset2, stream) = CreateTestZip(); using (var reader = ReaderFactory.OpenReader(stream)) { // Read first entry byte by byte From 1fadf3620eb923e9faccf813e47780ff7a78a294 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Mon, 13 Apr 2026 16:32:24 +0200 Subject: [PATCH 3/6] Fixed follow-up failing tests. It does not look like these failures are related to the fixes though. --- .../Compressors/Deflate/GZipStream.cs | 18 ++++++++++++++++- .../Compressors/Deflate/ZlibBaseStream.cs | 20 +++++++++++++------ src/SharpCompress/IO/IStreamStack.cs | 13 ++++++++++-- .../Default/GZipCompressionProvider.cs | 8 +++++++- 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/src/SharpCompress/Compressors/Deflate/GZipStream.cs b/src/SharpCompress/Compressors/Deflate/GZipStream.cs index 5085cd145..96384e172 100644 --- a/src/SharpCompress/Compressors/Deflate/GZipStream.cs +++ b/src/SharpCompress/Compressors/Deflate/GZipStream.cs @@ -78,9 +78,25 @@ public GZipStream( CompressionMode mode, CompressionLevel level, Encoding encoding + ) + : this(stream, mode, level, leaveOpen: false, encoding) { } + + public GZipStream( + Stream stream, + CompressionMode mode, + CompressionLevel level, + bool leaveOpen, + Encoding encoding ) { - BaseStream = new ZlibBaseStream(stream, mode, level, ZlibStreamFlavor.GZIP, encoding); + BaseStream = new ZlibBaseStream( + stream, + mode, + level, + ZlibStreamFlavor.GZIP, + leaveOpen, + encoding + ); _encoding = encoding; } diff --git a/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs b/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs index d8590cb36..8a91dea03 100644 --- a/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs +++ b/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs @@ -1006,9 +1006,13 @@ public override Int32 Read(Byte[] buffer, Int32 offset, Int32 count) && !_wantCompress ) { - //rewind the buffer - this.Rewind(z.AvailableBytesIn); - z.AvailableBytesIn = 0; + // Rewind the buffer. Only clear AvailableBytesIn if rewind succeeded. + // If rewind fails (e.g., non-seekable stream), the trailer bytes are + // still in the working buffer and will be needed by finish(). + if (this.Rewind(z.AvailableBytesIn)) + { + z.AvailableBytesIn = 0; + } } return rc; @@ -1209,9 +1213,13 @@ CancellationToken cancellationToken && !_wantCompress ) { - //rewind the buffer - this.Rewind(z.AvailableBytesIn); - z.AvailableBytesIn = 0; + // Rewind the buffer. Only clear AvailableBytesIn if rewind succeeded. + // If rewind fails (e.g., non-seekable stream), the trailer bytes are + // still in the working buffer and will be needed by finish(). + if (this.Rewind(z.AvailableBytesIn)) + { + z.AvailableBytesIn = 0; + } } return rc; diff --git a/src/SharpCompress/IO/IStreamStack.cs b/src/SharpCompress/IO/IStreamStack.cs index c24a9c90a..baa56f8e8 100644 --- a/src/SharpCompress/IO/IStreamStack.cs +++ b/src/SharpCompress/IO/IStreamStack.cs @@ -49,7 +49,7 @@ public static Stream GetRootStream(this IStreamStack stack) return current; } - internal static void Rewind(this IStreamStack stream, int count) + internal static bool Rewind(this IStreamStack stream, int count) { IStreamStack? current = stream; @@ -57,6 +57,12 @@ internal static void Rewind(this IStreamStack stream, int count) { if (current is SharpCompressStream sharpCompressStream) { + // Check if stream supports seeking before attempting rewind + if (!sharpCompressStream.CanSeek) + { + return false; + } + // Try to rewind within the buffer. If the position is outside the buffered // region, silently ignore (matching release behavior where streams without // buffering simply didn't rewind). @@ -66,15 +72,18 @@ internal static void Rewind(this IStreamStack stream, int count) try { sharpCompressStream.Position = targetPosition; + return true; } catch (NotSupportedException) { // Cannot seek outside buffered region - silently ignore + return false; } } - return; + return false; } current = current.BaseStream() as IStreamStack; } + return false; } } diff --git a/src/SharpCompress/Providers/Default/GZipCompressionProvider.cs b/src/SharpCompress/Providers/Default/GZipCompressionProvider.cs index 1365e0a77..256b3e90b 100644 --- a/src/SharpCompress/Providers/Default/GZipCompressionProvider.cs +++ b/src/SharpCompress/Providers/Default/GZipCompressionProvider.cs @@ -20,7 +20,13 @@ public sealed class GZipCompressionProvider : CompressionProviderBase public override Stream CreateCompressStream(Stream destination, int compressionLevel) { var level = (CompressionLevel)compressionLevel; - return new GZipStream(destination, CompressionMode.Compress, level, Encoding.UTF8); + return new GZipStream( + destination, + CompressionMode.Compress, + level, + leaveOpen: true, + Encoding.UTF8 + ); } public override Stream CreateDecompressStream(Stream source) From 08ed55c6caa583decaddd43deaa4b1700a56ae75 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Mon, 13 Apr 2026 17:08:49 +0200 Subject: [PATCH 4/6] Adhere to previous defaults --- src/SharpCompress/Providers/Default/GZipCompressionProvider.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/SharpCompress/Providers/Default/GZipCompressionProvider.cs b/src/SharpCompress/Providers/Default/GZipCompressionProvider.cs index 256b3e90b..55d9c6d9b 100644 --- a/src/SharpCompress/Providers/Default/GZipCompressionProvider.cs +++ b/src/SharpCompress/Providers/Default/GZipCompressionProvider.cs @@ -24,7 +24,6 @@ public override Stream CreateCompressStream(Stream destination, int compressionL destination, CompressionMode.Compress, level, - leaveOpen: true, Encoding.UTF8 ); } From 4f919f2bd1d2caecb768264eeeced0b02de22c92 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Mon, 13 Apr 2026 17:13:43 +0200 Subject: [PATCH 5/6] Fixed formatting, comments and restored default auto-close behavior --- src/SharpCompress/IO/IStreamStack.cs | 4 ++-- .../Providers/Default/GZipCompressionProvider.cs | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/SharpCompress/IO/IStreamStack.cs b/src/SharpCompress/IO/IStreamStack.cs index baa56f8e8..6c397b420 100644 --- a/src/SharpCompress/IO/IStreamStack.cs +++ b/src/SharpCompress/IO/IStreamStack.cs @@ -64,8 +64,8 @@ internal static bool Rewind(this IStreamStack stream, int count) } // Try to rewind within the buffer. If the position is outside the buffered - // region, silently ignore (matching release behavior where streams without - // buffering simply didn't rewind). + // region, silently ignore (non-seekable/non-buffered streams cannot rewind, + // but passthrough streams with seekable underlying streams can). var targetPosition = sharpCompressStream.Position - count; if (targetPosition >= 0) { diff --git a/src/SharpCompress/Providers/Default/GZipCompressionProvider.cs b/src/SharpCompress/Providers/Default/GZipCompressionProvider.cs index 55d9c6d9b..cdd0d3d7b 100644 --- a/src/SharpCompress/Providers/Default/GZipCompressionProvider.cs +++ b/src/SharpCompress/Providers/Default/GZipCompressionProvider.cs @@ -13,6 +13,16 @@ namespace SharpCompress.Providers.Default; /// public sealed class GZipCompressionProvider : CompressionProviderBase { + private readonly bool _leaveOpen; + + public GZipCompressionProvider() + : this(leaveOpen: false) { } + + public GZipCompressionProvider(bool leaveOpen) + { + _leaveOpen = leaveOpen; + } + public override CompressionType CompressionType => CompressionType.GZip; public override bool SupportsCompression => true; public override bool SupportsDecompression => true; @@ -24,6 +34,7 @@ public override Stream CreateCompressStream(Stream destination, int compressionL destination, CompressionMode.Compress, level, + _leaveOpen, Encoding.UTF8 ); } From 00ed4779f2000498c5367e0275cc966bb7811d27 Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Mon, 13 Apr 2026 17:53:28 +0200 Subject: [PATCH 6/6] Reduced tests to minimum for testing the issue --- .../Zip/ZipCompressionRoundtripTests.cs | 94 ++++--------------- 1 file changed, 20 insertions(+), 74 deletions(-) diff --git a/tests/SharpCompress.Test/Zip/ZipCompressionRoundtripTests.cs b/tests/SharpCompress.Test/Zip/ZipCompressionRoundtripTests.cs index 402783b56..4ccb2539f 100644 --- a/tests/SharpCompress.Test/Zip/ZipCompressionRoundtripTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipCompressionRoundtripTests.cs @@ -12,18 +12,14 @@ namespace SharpCompress.Test.Zip; public class ZipCompressionRoundtripTests : TestBase { - private const int TestSize = 1024 * 1024; + private const int TestSize = 1024 * 100; - private static (byte[] TestSet1, byte[] TestSet2, MemoryStream Stream) CreateTestZip() + private static (byte[] TestSet, MemoryStream Stream) CreateTestZip() { - var testset1 = Enumerable + var testset = Enumerable .Range(0, TestSize) .Select(i => (byte)((i * 22695477) % 257)) .ToArray(); - var testset2 = Enumerable - .Range(0, TestSize) - .Select(i => (byte)((i * 48271) % 257)) - .ToArray(); var stream = new MemoryStream(); @@ -31,39 +27,30 @@ private static (byte[] TestSet1, byte[] TestSet2, MemoryStream Stream) CreateTes using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Zip, writerOptions)) { - writer.Write("sample1", new MemoryStream(testset1)); - writer.Write("sample2", new MemoryStream(testset2)); + writer.Write("sample1", new MemoryStream(testset)); } stream.Position = 0; - return (testset1, testset2, stream); + return (testset, stream); } [Fact] public void Zip_Deflate_Roundtrip_ArchiveApi_BufferedRead_Succeeds() { - var (testset1, testset2, stream) = CreateTestZip(); + var (testset, stream) = CreateTestZip(); // Decompress and verify using Archive API with buffered read (CopyTo) stream.Position = 0; using (var archive = ZipArchive.OpenArchive(stream)) { - var files = archive.Entries.Where(e => !e.IsDirectory).OrderBy(e => e.Key).ToList(); - Assert.Equal(2, files.Count); - - // Read using CopyTo pattern (buffered - should succeed) - using (var entryStream = files[1].OpenEntryStream()) - { - using var extracted = new MemoryStream(); - entryStream.CopyTo(extracted); - Assert.Equal(testset2, extracted.ToArray()); - } + var files = archive.Entries.Where(e => !e.IsDirectory).ToList(); + Assert.Single(files); using (var entryStream = files[0].OpenEntryStream()) { using var extracted = new MemoryStream(); entryStream.CopyTo(extracted); - Assert.Equal(testset1, extracted.ToArray()); + Assert.Equal(testset, extracted.ToArray()); } } } @@ -71,16 +58,15 @@ public void Zip_Deflate_Roundtrip_ArchiveApi_BufferedRead_Succeeds() [Fact] public void Zip_Deflate_Roundtrip_ArchiveApi_ByteByByteRead_Succeeds() { - var (testset1, testset2, stream) = CreateTestZip(); + var (testset, stream) = CreateTestZip(); using (var archive = ZipArchive.OpenArchive(stream)) { - var files = archive.Entries.Where(e => !e.IsDirectory).OrderBy(e => e.Key).ToList(); - Assert.Equal(2, files.Count); + var files = archive.Entries.Where(e => !e.IsDirectory).ToList(); + Assert.Single(files); - // Read second file byte by byte - using (var entryStream = files[1].OpenEntryStream()) + using (var entryStream = files[0].OpenEntryStream()) { - var buffer = new byte[testset2.Length]; + var buffer = new byte[testset.Length]; for (var i = 0; i < buffer.Length; i++) { var b = entryStream.ReadByte(); @@ -91,29 +77,18 @@ public void Zip_Deflate_Roundtrip_ArchiveApi_ByteByByteRead_Succeeds() buffer[i] = (byte)b; } - Assert.Equal(testset2, buffer); + Assert.Equal(testset, buffer); // Verify EOF Assert.Equal(-1, entryStream.ReadByte()); } - - // Read first file byte by byte using All pattern - using (var entryStream = files[0].OpenEntryStream()) - { - var match = - testset1.All(b => b == entryStream.ReadByte()) && entryStream.ReadByte() == -1; - Assert.True( - match, - "Decompressed file sample1 contents do not match the source file." - ); - } } } [Fact] public void Zip_Deflate_Roundtrip_ReaderApi_BufferedRead_Succeeds() { - var (testset1, testset2, stream) = CreateTestZip(); + var (testset, stream) = CreateTestZip(); using (var reader = ReaderFactory.OpenReader(stream)) { // Read first entry @@ -123,17 +98,7 @@ public void Zip_Deflate_Roundtrip_ReaderApi_BufferedRead_Succeeds() { using var extracted = new MemoryStream(); entryStream.CopyTo(extracted); - Assert.Equal(testset1, extracted.ToArray()); - } - - // Read second entry - Assert.True(reader.MoveToNextEntry()); - Assert.Equal("sample2", reader.Entry.Key); - using (var entryStream = reader.OpenEntryStream()) - { - using var extracted = new MemoryStream(); - entryStream.CopyTo(extracted); - Assert.Equal(testset2, extracted.ToArray()); + Assert.Equal(testset, extracted.ToArray()); } // No more entries @@ -144,7 +109,7 @@ public void Zip_Deflate_Roundtrip_ReaderApi_BufferedRead_Succeeds() [Fact] public void Zip_Deflate_Roundtrip_ReaderApi_ByteByByteRead_Succeeds() { - var (testset1, testset2, stream) = CreateTestZip(); + var (testset, stream) = CreateTestZip(); using (var reader = ReaderFactory.OpenReader(stream)) { // Read first entry byte by byte @@ -152,26 +117,7 @@ public void Zip_Deflate_Roundtrip_ReaderApi_ByteByByteRead_Succeeds() Assert.Equal("sample1", reader.Entry.Key); using (var entryStream = reader.OpenEntryStream()) { - var buffer = new byte[testset1.Length]; - for (var i = 0; i < buffer.Length; i++) - { - var b = entryStream.ReadByte(); - if (b == -1) - { - throw new InvalidOperationException($"Unexpected EOF at offset {i}"); - } - buffer[i] = (byte)b; - } - Assert.Equal(testset1, buffer); - Assert.Equal(-1, entryStream.ReadByte()); - } - - // Read second entry byte by byte - Assert.True(reader.MoveToNextEntry()); - Assert.Equal("sample2", reader.Entry.Key); - using (var entryStream = reader.OpenEntryStream()) - { - var buffer = new byte[testset2.Length]; + var buffer = new byte[testset.Length]; for (var i = 0; i < buffer.Length; i++) { var b = entryStream.ReadByte(); @@ -181,7 +127,7 @@ public void Zip_Deflate_Roundtrip_ReaderApi_ByteByByteRead_Succeeds() } buffer[i] = (byte)b; } - Assert.Equal(testset2, buffer); + Assert.Equal(testset, buffer); Assert.Equal(-1, entryStream.ReadByte()); }