diff --git a/src/SharpCompress/IO/DataDescriptorStream.cs b/src/SharpCompress/IO/DataDescriptorStream.cs index 235c7a30f..aebff7fcb 100644 --- a/src/SharpCompress/IO/DataDescriptorStream.cs +++ b/src/SharpCompress/IO/DataDescriptorStream.cs @@ -30,6 +30,7 @@ void IStreamStack.SetPosition(long position) { } private int _searchPosition; private bool _isDisposed; private bool _done; + private int _previousSearchPosition; private static byte[] _dataDescriptorMarker = new byte[] { 0x50, 0x4b, 0x07, 0x08 }; private static long _dataDescriptorSize = 24; @@ -116,6 +117,36 @@ public override int Read(byte[] buffer, int offset, int count) var read = _stream.Read(buffer, offset, count); + // Detect infinite loop: if we just rewound and read the exact same bytes again + // (indicated by read == _previousSearchPosition and all bytes matching), + // it means these bytes are data, not a signature. Don't rewind again. + var inInfiniteLoop = + _previousSearchPosition > 0 && read == _previousSearchPosition && read <= 3; + + if (inInfiniteLoop) + { + // Verify all bytes match the pattern + var allMatch = true; + for (var i = 0; i < read && allMatch; i++) + { + if (buffer[offset + i] != _dataDescriptorMarker[i]) + { + allMatch = false; + } + } + + if (allMatch) + { + // This is the infinite loop condition - these bytes are data, not signature + // Reset state and return the bytes as data + _searchPosition = 0; + _previousSearchPosition = 0; + return read; + } + } + + _previousSearchPosition = 0; + for (var i = 0; i < read; i++) { if (buffer[offset + i] == _dataDescriptorMarker[_searchPosition]) @@ -165,6 +196,7 @@ public override int Read(byte[] buffer, int offset, int count) { read -= _searchPosition; _stream.Position -= _searchPosition; + _previousSearchPosition = _searchPosition; _searchPosition = 0; } diff --git a/tests/SharpCompress.Test/Zip/ZipReaderTests.cs b/tests/SharpCompress.Test/Zip/ZipReaderTests.cs index f393e2f4f..75666ef34 100644 --- a/tests/SharpCompress.Test/Zip/ZipReaderTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipReaderTests.cs @@ -530,4 +530,95 @@ public void Archive_Iteration_DoesNotBreak_WhenFlushThrows_LZMA() // 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 DataDescriptorStream_BoundaryBug_ReproduceInfiniteLoop() + { + // Regression test for DataDescriptorStream boundary bug + // Issue: When the first byte of the data descriptor signature (0x50 = 'P') + // appears at the end of a read buffer, the reader would get stuck in an + // infinite loop, causing extraction to fail. + // + // This test reproduces the exact scenario described in the issue: + // - Streaming ZIP reader (non-seekable stream) + // - DataDescriptorStream (triggered by CompressionType.None + non-seekable) + // - Payload filled with 0x50 ('P') bytes + // - Payload size that causes boundary condition + + // Create a payload filled with 0x50 bytes that will trigger the boundary bug + // The bug occurs when partial signature matches fall on buffer boundaries + const int payloadSize = 100000; // Large enough to span multiple read buffers + var payload = new byte[payloadSize]; + for (var i = 0; i < payloadSize; i++) + { + payload[i] = 0x50; // Fill with 'P' bytes (0x50 = first byte of PK signature) + } + + using var memory = new MemoryStream(); + + // Use non-seekable stream to force data descriptor mode + // This triggers the use of DataDescriptorStream + Stream writeStream = new TestStream(memory, read: true, write: true, seek: false); + + // Write ZIP with no compression (this ensures DataDescriptorStream is used) + using ( + var zipWriter = WriterFactory.Open(writeStream, ArchiveType.Zip, CompressionType.None) + ) + { + zipWriter.Write("test.txt", new MemoryStream(payload)); + } + + // Read back the ZIP + var zipBytes = memory.ToArray(); + var readStream = new MemoryStream(zipBytes); + + using var reader = ZipReader.Open(readStream); + + var extracted = false; + var readIterations = 0; + const int maxIterations = 1000; // Safety limit to detect infinite loops + + while (reader.MoveToNextEntry()) + { + using var entryStream = reader.OpenEntryStream(); + var outputStream = new MemoryStream(); + var buffer = new byte[8192]; + + int bytesRead; + while ((bytesRead = entryStream.Read(buffer, 0, buffer.Length)) > 0) + { + outputStream.Write(buffer, 0, bytesRead); + readIterations++; + + // Detect infinite loop - the bug causes read to return very small amounts repeatedly + if (readIterations > maxIterations) + { + Assert.Fail( + $"Detected infinite loop: Read called {readIterations} times, " + + $"extracted {outputStream.Length} of {payloadSize} bytes. " + + "This indicates the DataDescriptorStream boundary bug is present." + ); + } + } + + // Verify we extracted all the data correctly + var extractedData = outputStream.ToArray(); + Assert.Equal(payloadSize, extractedData.Length); + + // Verify content is correct (all 0x50 bytes) + for (var i = 0; i < payloadSize; i++) + { + if (extractedData[i] != 0x50) + { + Assert.Fail( + $"Data corruption at byte {i}: expected 0x50, got 0x{extractedData[i]:X2}" + ); + } + } + + extracted = true; + } + + Assert.True(extracted, "Failed to extract the entry"); + } }