diff --git a/src/SharpCompress/Common/EntryStream.cs b/src/SharpCompress/Common/EntryStream.cs index d265be00..e4de4ca9 100644 --- a/src/SharpCompress/Common/EntryStream.cs +++ b/src/SharpCompress/Common/EntryStream.cs @@ -79,25 +79,11 @@ public class EntryStream : Stream, IStreamStack { if (ss.BaseStream() is SharpCompress.Compressors.Deflate.DeflateStream deflateStream) { - try - { - deflateStream.Flush(); //Deflate over reads. Knock it back - } - catch (NotSupportedException) - { - // Ignore: underlying stream does not support required operations for Flush - } + deflateStream.Flush(); //Deflate over reads. Knock it back } else if (ss.BaseStream() is SharpCompress.Compressors.LZMA.LzmaStream lzmaStream) { - try - { - lzmaStream.Flush(); //Lzma over reads. Knock it back - } - catch (NotSupportedException) - { - // Ignore: underlying stream does not support required operations for Flush - } + lzmaStream.Flush(); //Lzma over reads. Knock it back } } #if DEBUG_STREAMS @@ -125,25 +111,11 @@ public class EntryStream : Stream, IStreamStack { if (ss.BaseStream() is SharpCompress.Compressors.Deflate.DeflateStream deflateStream) { - try - { - await deflateStream.FlushAsync().ConfigureAwait(false); - } - catch (NotSupportedException) - { - // Ignore: underlying stream does not support required operations for Flush - } + await deflateStream.FlushAsync().ConfigureAwait(false); } else if (ss.BaseStream() is SharpCompress.Compressors.LZMA.LzmaStream lzmaStream) { - try - { - await lzmaStream.FlushAsync().ConfigureAwait(false); - } - catch (NotSupportedException) - { - // Ignore: underlying stream does not support required operations for Flush - } + await lzmaStream.FlushAsync().ConfigureAwait(false); } } #if DEBUG_STREAMS diff --git a/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs b/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs index dd6590b7..5099de54 100644 --- a/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs +++ b/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs @@ -586,7 +586,13 @@ internal class ZlibBaseStream : Stream, IStreamStack public override void Flush() { - _stream.Flush(); + // Only flush the underlying stream when in write mode + // Flushing input streams during read operations is not meaningful + // and can cause issues with forward-only/non-seekable streams + if (_streamMode == StreamMode.Writer) + { + _stream.Flush(); + } //rewind the buffer ((IStreamStack)this).Rewind(z.AvailableBytesIn); //unused z.AvailableBytesIn = 0; @@ -594,7 +600,13 @@ internal class ZlibBaseStream : Stream, IStreamStack public override async Task FlushAsync(CancellationToken cancellationToken) { - await _stream.FlushAsync(cancellationToken).ConfigureAwait(false); + // Only flush the underlying stream when in write mode + // Flushing input streams during read operations is not meaningful + // and can cause issues with forward-only/non-seekable streams + if (_streamMode == StreamMode.Writer) + { + await _stream.FlushAsync(cancellationToken).ConfigureAwait(false); + } //rewind the buffer ((IStreamStack)this).Rewind(z.AvailableBytesIn); //unused z.AvailableBytesIn = 0; diff --git a/src/SharpCompress/packages.lock.json b/src/SharpCompress/packages.lock.json index 032c15c4..5e7ece33 100644 --- a/src/SharpCompress/packages.lock.json +++ b/src/SharpCompress/packages.lock.json @@ -216,9 +216,9 @@ "net10.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[10.0.1, )", - "resolved": "10.0.1", - "contentHash": "ISahzLHsHY7vrwqr2p1YWZ+gsxoBRtH7gWRDK8fDUst9pp2He0GiesaqEfeX0V8QMCJM3eNEHGGpnIcPjFo2NQ==" + "requested": "[10.0.2, )", + "resolved": "10.0.2", + "contentHash": "sXdDtMf2qcnbygw9OdE535c2lxSxrZP8gO4UhDJ0xiJbl1wIqXS1OTcTDFTIJPOFd6Mhcm8gPEthqWGUxBsTqw==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", @@ -264,9 +264,9 @@ "net8.0": { "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[8.0.22, )", - "resolved": "8.0.22", - "contentHash": "MhcMithKEiyyNkD2ZfbDZPmcOdi0GheGfg8saEIIEfD/fol3iHmcV8TsZkD4ZYz5gdUuoX4YtlVySUU7Sxl9SQ==" + "requested": "[8.0.23, )", + "resolved": "8.0.23", + "contentHash": "GqHiB1HbbODWPbY/lc5xLQH8siEEhNA0ptpJCC6X6adtAYNEzu5ZlqV3YHA3Gh7fuEwgA8XqVwMtH2KNtuQM1Q==" }, "Microsoft.NETFramework.ReferenceAssemblies": { "type": "Direct", diff --git a/tests/SharpCompress.Test/Mocks/ThrowOnFlushStream.cs b/tests/SharpCompress.Test/Mocks/ThrowOnFlushStream.cs new file mode 100644 index 00000000..2cf1a84c --- /dev/null +++ b/tests/SharpCompress.Test/Mocks/ThrowOnFlushStream.cs @@ -0,0 +1,73 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress.Test.Mocks; + +/// +/// A stream wrapper that throws NotSupportedException on Flush() calls. +/// This is used to test that archive iteration handles streams that don't support flushing. +/// +public class ThrowOnFlushStream : Stream +{ + private readonly Stream inner; + + public ThrowOnFlushStream(Stream inner) + { + this.inner = inner; + } + + public override bool CanRead => inner.CanRead; + + 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("Flush not supported"); + + public override Task FlushAsync(CancellationToken cancellationToken) => + throw new NotSupportedException("FlushAsync not supported"); + + public override int Read(byte[] buffer, int offset, int count) => + inner.Read(buffer, offset, count); + + public override Task ReadAsync( + byte[] buffer, + int offset, + int count, + CancellationToken cancellationToken + ) => inner.ReadAsync(buffer, offset, count, cancellationToken); + +#if !NETFRAMEWORK && !NETSTANDARD2_0 + public override ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) => inner.ReadAsync(buffer, cancellationToken); +#endif + + 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(); + + protected override void Dispose(bool disposing) + { + if (disposing) + { + inner.Dispose(); + } + + base.Dispose(disposing); + } +} diff --git a/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs index c4c5bb3a..aaa345ad 100644 --- a/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs @@ -337,4 +337,52 @@ public class ZipReaderAsyncTests : ReaderTests } } } + + [Fact] + public async ValueTask Archive_Iteration_DoesNotBreak_WhenFlushThrows_Deflate_Async() + { + // Regression test: since 0.41.0, archive iteration would silently break + // when the input stream throws NotSupportedException in Flush(). + // Only the first entry would be returned, then iteration would stop without exception. + var path = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip"); + using var fileStream = File.OpenRead(path); + using Stream stream = new ThrowOnFlushStream(fileStream); + await using var reader = ReaderFactory.OpenAsyncReader(new AsyncOnlyStream(stream)); + + var count = 0; + while (await reader.MoveToNextEntryAsync()) + { + if (!reader.Entry.IsDirectory) + { + count++; + } + } + + // Should iterate through all entries, not just the first one + Assert.True(count > 1, $"Expected more than 1 entry, but got {count}"); + } + + [Fact] + public async ValueTask Archive_Iteration_DoesNotBreak_WhenFlushThrows_LZMA_Async() + { + // Regression test: since 0.41.0, archive iteration would silently break + // when the input stream throws NotSupportedException in Flush(). + // Only the first entry would be returned, then iteration would stop without exception. + var path = Path.Combine(TEST_ARCHIVES_PATH, "Zip.lzma.dd.zip"); + using var fileStream = File.OpenRead(path); + using Stream stream = new ThrowOnFlushStream(fileStream); + await using var reader = ReaderFactory.OpenAsyncReader(new AsyncOnlyStream(stream)); + + var count = 0; + while (await reader.MoveToNextEntryAsync()) + { + if (!reader.Entry.IsDirectory) + { + count++; + } + } + + // Should iterate through all entries, not just the first one + Assert.True(count > 1, $"Expected more than 1 entry, but got {count}"); + } } diff --git a/tests/SharpCompress.Test/Zip/ZipReaderTests.cs b/tests/SharpCompress.Test/Zip/ZipReaderTests.cs index 30f1f16f..ab9b3afa 100644 --- a/tests/SharpCompress.Test/Zip/ZipReaderTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipReaderTests.cs @@ -490,4 +490,52 @@ public class ZipReaderTests : ReaderTests } } } + + [Fact] + public void Archive_Iteration_DoesNotBreak_WhenFlushThrows_Deflate() + { + // Regression test: since 0.41.0, archive iteration would silently break + // when the input stream throws NotSupportedException in Flush(). + // Only the first entry would be returned, then iteration would stop without exception. + var path = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip"); + using var fileStream = File.OpenRead(path); + using Stream stream = new ThrowOnFlushStream(fileStream); + using var reader = ReaderFactory.OpenReader(stream); + + var count = 0; + while (reader.MoveToNextEntry()) + { + if (!reader.Entry.IsDirectory) + { + count++; + } + } + + // 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 Archive_Iteration_DoesNotBreak_WhenFlushThrows_LZMA() + { + // Regression test: since 0.41.0, archive iteration would silently break + // when the input stream throws NotSupportedException in Flush(). + // Only the first entry would be returned, then iteration would stop without exception. + var path = Path.Combine(TEST_ARCHIVES_PATH, "Zip.lzma.dd.zip"); + using var fileStream = File.OpenRead(path); + using Stream stream = new ThrowOnFlushStream(fileStream); + using var reader = ReaderFactory.OpenReader(stream); + + var count = 0; + while (reader.MoveToNextEntry()) + { + if (!reader.Entry.IsDirectory) + { + count++; + } + } + + // Should iterate through all entries, not just the first one + Assert.True(count > 1, $"Expected more than 1 entry, but got {count}"); + } }