using System; using System.Collections.Generic; using System.IO; using SharpCompress.Readers; using Xunit; namespace SharpCompress.Test.Zip; /// /// Tests for ZIP reading with streams that return short reads. /// Reproduces the regression where ZIP parsing fails depending on Stream.Read chunking patterns. /// public class ZipShortReadTests : ReaderTests { /// /// A non-seekable stream that returns controlled short reads. /// Simulates real-world network/multipart streams that legally return fewer bytes than requested. /// private sealed class PatternReadStream : Stream { private readonly MemoryStream _inner; private readonly int _firstReadSize; private readonly int _chunkSize; private bool _firstReadDone; public PatternReadStream(byte[] bytes, int firstReadSize, int chunkSize) { _inner = new MemoryStream(bytes, writable: false); _firstReadSize = firstReadSize; _chunkSize = chunkSize; } public override int Read(byte[] buffer, int offset, int count) { int limit = !_firstReadDone ? _firstReadSize : _chunkSize; _firstReadDone = true; int toRead = Math.Min(count, limit); return _inner.Read(buffer, offset, toRead); } public override bool CanRead => true; public override bool CanSeek => false; public override bool CanWrite => false; public override long Length => throw new NotSupportedException(); public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } public override void Flush() => throw new NotSupportedException(); public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); } /// /// Test that ZIP reading works correctly with short reads on non-seekable streams. /// Uses a test archive and different chunking patterns. /// [Theory] [InlineData("Zip.deflate.zip", 1000, 4096)] [InlineData("Zip.deflate.zip", 999, 4096)] [InlineData("Zip.deflate.zip", 100, 4096)] [InlineData("Zip.deflate.zip", 50, 512)] [InlineData("Zip.deflate.zip", 1, 1)] // Extreme case: 1 byte at a time [InlineData("Zip.deflate.dd.zip", 1000, 4096)] [InlineData("Zip.deflate.dd.zip", 999, 4096)] [InlineData("Zip.zip64.zip", 3816, 4096)] [InlineData("Zip.zip64.zip", 3815, 4096)] // Similar to the issue pattern public void Zip_Reader_Handles_Short_Reads(string zipFile, int firstReadSize, int chunkSize) { // Use an existing test ZIP file var zipPath = Path.Combine(TEST_ARCHIVES_PATH, zipFile); if (!File.Exists(zipPath)) { return; // Skip if file doesn't exist } var bytes = File.ReadAllBytes(zipPath); // Baseline with MemoryStream (seekable, no short reads) var baseline = ReadEntriesFromStream(new MemoryStream(bytes, writable: false)); Assert.NotEmpty(baseline); // Non-seekable stream with controlled short read pattern var chunked = ReadEntriesFromStream(new PatternReadStream(bytes, firstReadSize, chunkSize)); Assert.Equal(baseline, chunked); } private List ReadEntriesFromStream(Stream stream) { var names = new List(); using var reader = ReaderFactory.OpenReader( stream, new ReaderOptions { LeaveStreamOpen = true } ); while (reader.MoveToNextEntry()) { if (reader.Entry.IsDirectory) { continue; } names.Add(reader.Entry.Key!); using var entryStream = reader.OpenEntryStream(); entryStream.CopyTo(Stream.Null); } return names; } }