Regression in SharpCompress 0.41.0: archive iteration breaks if input stream throws in Flush() #769

Closed
opened 2026-01-29 22:17:17 +00:00 by claunia · 3 comments
Owner

Originally created by @rleroux-regnology on GitHub (Jan 23, 2026).

Originally assigned to: @adamhathcock, @Copilot on GitHub.

Summary

Since SharpCompress 0.41.0, archive iteration silently breaks when the input stream throws in Flush().

Only the first entry is returned, then iteration stops without exception.

This can be reproduced using a simple wrapper stream around a FileStream.


Minimal reproduction

public sealed class ForwardOnlyThrowOnFlushStream : Stream
{
	private readonly Stream inner;

	public ForwardOnlyThrowOnFlushStream(Stream inner)
	{
		this.inner = inner;
	}

	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 int Read(byte[] buffer, int offset, int count) => inner.Read(buffer, offset, count);

	public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) => inner.ReadAsync(buffer, cancellationToken);

	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);
	}
}

Test program:

using var fs = File.OpenRead("multi-entry.zip");
using var stream = new ForwardOnlyThrowOnFlushStream(fs);

var reader = ReaderFactory.OpenAsyncReader(stream);

int count = 0;

while (await reader.MoveToNextEntryAsync())
{
	if (reader.Entry.IsDirectory)
	{
		continue;
	}

	count++;
}

Console.WriteLine(count);

Expected behavior

If the archive contains N entries, count == N.


Actual behavior (0.44.1)

count == 1

Iteration stops after the first entry.
No exception is thrown.


Workaround

If Flush() is implemented as a no-op:

public override void Flush() { }

Then all entries are iterated correctly.


Why this is a problem

Flush() has no semantic meaning for read-only input streams and should not be required for reading archives.

Any library stream that:

  • is read-only
  • forward-only
  • or explicitly does not support flushing

can silently break SharpCompress iteration.

This includes:

  • ASP.NET Core MultipartReaderStream
  • custom streaming pipelines
  • network streams

Regression info

  • Works with: SharpCompress 0.40.0 (or previous)
  • Fails with: SharpCompress 0.41.0 (or higher)

Suspected cause

SharpCompress 0.41.0 introduced a call to Stream.Flush() somewhere in the read / entry iteration pipeline.
If Flush() throws, the internal state becomes inconsistent and iteration stops early.


This looks closely related to #1150.

In #1150, EntryStream.Dispose() started calling Flush() on internal decompression streams (Deflate/LZMA), which breaks valid streaming scenarios (non-seekable / forward-only streams). In my case, beyond the exception scenario discussed there, I now also observe a silent regression: iteration stops after the first entry.

Both issues share the same underlying theme: Flush() (or its side-effects) should not be required nor relied upon in a read-only streaming pipeline, especially for forward-only/non-seekable streams.

Originally created by @rleroux-regnology on GitHub (Jan 23, 2026). Originally assigned to: @adamhathcock, @Copilot on GitHub. ### Summary Since SharpCompress **0.41.0**, archive iteration silently breaks when the input stream throws in `Flush()`. Only the **first entry** is returned, then iteration stops without exception. This can be reproduced using a simple wrapper stream around a `FileStream`. --- ### Minimal reproduction ```cs public sealed class ForwardOnlyThrowOnFlushStream : Stream { private readonly Stream inner; public ForwardOnlyThrowOnFlushStream(Stream inner) { this.inner = inner; } 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 int Read(byte[] buffer, int offset, int count) => inner.Read(buffer, offset, count); public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) => inner.ReadAsync(buffer, cancellationToken); 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); } } ``` Test program: ```cs using var fs = File.OpenRead("multi-entry.zip"); using var stream = new ForwardOnlyThrowOnFlushStream(fs); var reader = ReaderFactory.OpenAsyncReader(stream); int count = 0; while (await reader.MoveToNextEntryAsync()) { if (reader.Entry.IsDirectory) { continue; } count++; } Console.WriteLine(count); ``` --- ### Expected behavior If the archive contains N entries, `count == N`. --- ### Actual behavior (0.44.1) `count == 1` Iteration stops after the first entry. No exception is thrown. --- ### Workaround If `Flush()` is implemented as a no-op: ```cs public override void Flush() { } ``` Then all entries are iterated correctly. --- ### Why this is a problem `Flush()` **has no semantic meaning for read-only input streams** and should not be required for reading archives. Any library stream that: - is read-only - forward-only - or explicitly does not support flushing can silently break SharpCompress iteration. This includes: - ASP.NET Core `MultipartReaderStream` - custom streaming pipelines - network streams --- ### Regression info - Works with: SharpCompress 0.40.0 (or previous) - Fails with: SharpCompress 0.41.0 (or higher) --- ### Suspected cause SharpCompress 0.41.0 introduced a call to `Stream.Flush()` somewhere in the read / entry iteration pipeline. If `Flush()` throws, the internal state becomes inconsistent and iteration stops early. --- ### Related issue This looks closely related to #1150. In #1150, `EntryStream.Dispose()` started calling `Flush()` on internal decompression streams (Deflate/LZMA), which breaks valid streaming scenarios (non-seekable / forward-only streams). In my case, beyond the exception scenario discussed there, I now also observe a **silent regression**: iteration stops after the first entry. Both issues share the same underlying theme: `Flush()` (or its side-effects) should not be required nor relied upon in a read-only streaming pipeline, especially for forward-only/non-seekable streams.
Author
Owner

@adamhathcock commented on GitHub (Jan 24, 2026):

Try https://www.nuget.org/packages/SharpCompress/0.45.0-beta.144 and I can do a real release of 0.44.3

The API is slightly different for this beta though

@adamhathcock commented on GitHub (Jan 24, 2026): Try https://www.nuget.org/packages/SharpCompress/0.45.0-beta.144 and I can do a real release of 0.44.3 The API is slightly different for this beta though
Author
Owner

@rleroux-regnology commented on GitHub (Jan 25, 2026):

Try https://www.nuget.org/packages/SharpCompress/0.45.0-beta.144 and I can do a real release of 0.44.3

The API is slightly different for this beta though

After testing, I've found that the reported issue no longer exists in version 0.45.0-beta.144. Thank you very much!

PS: I have another problem now, but I first need to find a minimal reproduction scenario. I'll probably open a new issue fairly soon.

@rleroux-regnology commented on GitHub (Jan 25, 2026): > Try https://www.nuget.org/packages/SharpCompress/0.45.0-beta.144 and I can do a real release of 0.44.3 > > The API is slightly different for this beta though After testing, I've found that the reported issue no longer exists in version 0.45.0-beta.144. Thank you very much! PS: I have another problem now, but I first need to find a minimal reproduction scenario. I'll probably open a new issue fairly soon.
Author
Owner

@adamhathcock commented on GitHub (Jan 25, 2026):

Try https://www.nuget.org/packages/SharpCompress/0.45.0-beta.144 and I can do a real release of 0.44.3

The API is slightly different for this beta though

After testing, I've found that the reported issue no longer exists in version 0.45.0-beta.144. Thank you very much!

PS: I have another problem now, but I first need to find a minimal reproduction scenario. I'll probably open a new issue fairly soon.

No problem. Please report. Hopefully something different!

@adamhathcock commented on GitHub (Jan 25, 2026): > > Try https://www.nuget.org/packages/SharpCompress/0.45.0-beta.144 and I can do a real release of 0.44.3 > > > > The API is slightly different for this beta though > > After testing, I've found that the reported issue no longer exists in version 0.45.0-beta.144. Thank you very much! > > PS: I have another problem now, but I first need to find a minimal reproduction scenario. I'll probably open a new issue fairly soon. No problem. Please report. Hopefully something different!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/sharpcompress#769