From eb7e55ac7d20970589006f68f118d71576d06af9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Feb 2026 10:08:28 +0000 Subject: [PATCH 1/7] Initial plan From 31c6eb3b5c300fc80cb27f7685a80f7fa222f973 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Feb 2026 10:14:01 +0000 Subject: [PATCH 2/7] Fix NullReferenceException for 7z empty-stream entries Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- .../Archives/SevenZip/SevenZipArchive.cs | 6 +++ src/SharpCompress/packages.lock.json | 12 +++--- .../SevenZip/SevenZipArchiveTests.cs | 35 ++++++++++++++++++ .../TestArchives/Archives/7Zip.EmptyStream.7z | Bin 0 -> 182 bytes 4 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 tests/TestArchives/Archives/7Zip.EmptyStream.7z diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs index de0adc98..710e490c 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs @@ -164,6 +164,12 @@ public partial class SevenZipArchive : AbstractArchive>-?#Ja{C)8(;&GMGc8{R_-V>VH8U}YvoSDomNKw& dGcqtLGcz)>#xe*nFz_^TaWSest$4w}007~-KhppJ literal 0 HcmV?d00001 From fb70f06fd47768699f0a8229e6b5f5a50468475e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Feb 2026 10:15:55 +0000 Subject: [PATCH 3/7] Improve empty-stream test to verify HasStream == false entries Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- .../SevenZip/SevenZipArchiveTests.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs index 9947168e..0b77c040 100644 --- a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs +++ b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs @@ -4,6 +4,7 @@ using System.Linq; using SharpCompress.Archives; using SharpCompress.Archives.SevenZip; using SharpCompress.Common; +using SharpCompress.Common.SevenZip; using SharpCompress.Factories; using SharpCompress.Readers; using Xunit; @@ -355,15 +356,29 @@ public class SevenZipArchiveTests : ArchiveTests var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "7Zip.EmptyStream.7z"); using var archive = SevenZipArchive.OpenArchive(testArchive); + var emptyStreamFileCount = 0; foreach (var entry in archive.Entries) { if (!entry.IsDirectory) { + // Verify this is actually an empty-stream entry (HasStream == false) + var sevenZipEntry = entry as SevenZipEntry; + if (sevenZipEntry?.FilePart.Header.HasStream == false) + { + emptyStreamFileCount++; + } + // This should not throw NullReferenceException entry.WriteToDirectory(SCRATCH_FILES_PATH); } } + // Ensure we actually tested empty-stream entries + Assert.True( + emptyStreamFileCount > 0, + "Test archive should contain at least one empty-stream entry" + ); + // Verify that empty files were created var extractedFiles = Directory.GetFiles( SCRATCH_FILES_PATH, From a2a1b2e8fd436719c13d6467e9bbfe4572cfe331 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sat, 14 Feb 2026 10:16:01 +0000 Subject: [PATCH 4/7] Expose SharpCompressStream to allow ringbuffer to be used on non-seekable streams --- .../IO/SharpCompressStream.Async.cs | 2 +- .../IO/SharpCompressStream.Create.cs | 2 +- src/SharpCompress/IO/SharpCompressStream.cs | 2 +- .../Xz/XZStreamAsyncTests.cs | 21 ++++++++++++++++++ tests/SharpCompress.Test/Xz/XZStreamTests.cs | 22 +++++++++++++++++++ 5 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/SharpCompress/IO/SharpCompressStream.Async.cs b/src/SharpCompress/IO/SharpCompressStream.Async.cs index e18a47a1..a36a9862 100644 --- a/src/SharpCompress/IO/SharpCompressStream.Async.cs +++ b/src/SharpCompress/IO/SharpCompressStream.Async.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; namespace SharpCompress.IO; -internal partial class SharpCompressStream +public partial class SharpCompressStream { public override Task ReadAsync( byte[] buffer, diff --git a/src/SharpCompress/IO/SharpCompressStream.Create.cs b/src/SharpCompress/IO/SharpCompressStream.Create.cs index 3a886a80..8011641b 100644 --- a/src/SharpCompress/IO/SharpCompressStream.Create.cs +++ b/src/SharpCompress/IO/SharpCompressStream.Create.cs @@ -4,7 +4,7 @@ using SharpCompress.Common; namespace SharpCompress.IO; -internal partial class SharpCompressStream +public partial class SharpCompressStream { /// /// Creates a SharpCompressStream that acts as a passthrough wrapper. diff --git a/src/SharpCompress/IO/SharpCompressStream.cs b/src/SharpCompress/IO/SharpCompressStream.cs index a7c09072..d5a7dfcc 100644 --- a/src/SharpCompress/IO/SharpCompressStream.cs +++ b/src/SharpCompress/IO/SharpCompressStream.cs @@ -4,7 +4,7 @@ using SharpCompress.Common; namespace SharpCompress.IO; -internal partial class SharpCompressStream : Stream, IStreamStack +public partial class SharpCompressStream : Stream, IStreamStack { public virtual Stream BaseStream() => stream; diff --git a/tests/SharpCompress.Test/Xz/XZStreamAsyncTests.cs b/tests/SharpCompress.Test/Xz/XZStreamAsyncTests.cs index a2276602..9402e7c4 100644 --- a/tests/SharpCompress.Test/Xz/XZStreamAsyncTests.cs +++ b/tests/SharpCompress.Test/Xz/XZStreamAsyncTests.cs @@ -1,6 +1,7 @@ using System.IO; using System.Threading.Tasks; using SharpCompress.Compressors.Xz; +using SharpCompress.IO; using SharpCompress.Test.Mocks; using Xunit; @@ -34,4 +35,24 @@ public class XzStreamAsyncTests : XzTestsBase var uncompressed = await sr.ReadToEndAsync().ConfigureAwait(false); Assert.Equal(OriginalIndexed, uncompressed); } + + [Fact] + public async ValueTask CanReadNonSeekableStreamAsync() + { + var nonSeekable = new ForwardOnlyStream(new MemoryStream(Compressed)); + var xz = new XZStream(SharpCompressStream.Create(nonSeekable)); + using var sr = new StreamReader(new AsyncOnlyStream(xz)); + var uncompressed = await sr.ReadToEndAsync().ConfigureAwait(false); + Assert.Equal(Original, uncompressed); + } + + [Fact] + public async ValueTask CanReadNonSeekableEmptyStreamAsync() + { + var nonSeekable = new ForwardOnlyStream(new MemoryStream(CompressedEmpty)); + var xz = new XZStream(SharpCompressStream.Create(nonSeekable)); + using var sr = new StreamReader(new AsyncOnlyStream(xz)); + var uncompressed = await sr.ReadToEndAsync().ConfigureAwait(false); + Assert.Equal(OriginalEmpty, uncompressed); + } } diff --git a/tests/SharpCompress.Test/Xz/XZStreamTests.cs b/tests/SharpCompress.Test/Xz/XZStreamTests.cs index 02c5020c..80ad5dc3 100644 --- a/tests/SharpCompress.Test/Xz/XZStreamTests.cs +++ b/tests/SharpCompress.Test/Xz/XZStreamTests.cs @@ -1,5 +1,7 @@ using System.IO; using SharpCompress.Compressors.Xz; +using SharpCompress.IO; +using SharpCompress.Test.Mocks; using Xunit; namespace SharpCompress.Test.Xz; @@ -32,4 +34,24 @@ public class XzStreamTests : XzTestsBase var uncompressed = sr.ReadToEnd(); Assert.Equal(OriginalIndexed, uncompressed); } + + [Fact] + public void CanReadNonSeekableStream() + { + var nonSeekable = new ForwardOnlyStream(new MemoryStream(Compressed)); + var xz = new XZStream(SharpCompressStream.Create(nonSeekable)); + using var sr = new StreamReader(xz); + var uncompressed = sr.ReadToEnd(); + Assert.Equal(Original, uncompressed); + } + + [Fact] + public void CanReadNonSeekableEmptyStream() + { + var nonSeekable = new ForwardOnlyStream(new MemoryStream(CompressedEmpty)); + var xz = new XZStream(SharpCompressStream.Create(nonSeekable)); + using var sr = new StreamReader(xz); + var uncompressed = sr.ReadToEnd(); + Assert.Equal(OriginalEmpty, uncompressed); + } } From 4546a3acd2268325c5a3dc6453400466177f5976 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sat, 14 Feb 2026 10:26:33 +0000 Subject: [PATCH 5/7] add some docs --- docs/API.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/API.md b/docs/API.md index 15511e5d..5696961d 100644 --- a/docs/API.md +++ b/docs/API.md @@ -485,6 +485,31 @@ using (var archive = ZipArchive.CreateArchive()) } ``` +### Buffered Forward-Only Streams + +`SharpCompressStream` can wrap streams with buffering for forward-only scenarios: + +```csharp +// Wrap a non-seekable stream with buffering +using (var bufferedStream = new SharpCompressStream(rawStream)) +{ + // Provides ring buffer functionality for reading ahead + // and seeking within buffered data + using (var reader = ReaderFactory.OpenReader(bufferedStream)) + { + while (reader.MoveToNextEntry()) + { + reader.WriteEntryToDirectory(@"C:\output"); + } + } +} +``` + +Useful for: +- Non-seekable streams (network streams, pipes) +- Forward-only reading with limited look-ahead +- Buffering unbuffered streams for better performance + ### Extract Specific Files ```csharp From 13fdb4d7ca31d3d5d954cf6f39eef9d5b5989c51 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sat, 14 Feb 2026 10:34:12 +0000 Subject: [PATCH 6/7] add better length from master --- src/SharpCompress/IO/SharpCompressStream.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/SharpCompress/IO/SharpCompressStream.cs b/src/SharpCompress/IO/SharpCompressStream.cs index b47b7031..a7c09072 100644 --- a/src/SharpCompress/IO/SharpCompressStream.cs +++ b/src/SharpCompress/IO/SharpCompressStream.cs @@ -206,8 +206,22 @@ internal partial class SharpCompressStream : Stream, IStreamStack throw new NotSupportedException(); } - public override long Length => - _isPassthrough ? stream.Length : throw new NotSupportedException(); + public override long Length + { + get + { + if (_isPassthrough) + { + return stream.Length; + } + + if (_ringBuffer is not null) + { + return _ringBuffer.Length; + } + throw new NotSupportedException(); + } + } public override long Position { From 71d993fab1300bb040295a404385c1a0e037809b Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sat, 14 Feb 2026 10:35:02 +0000 Subject: [PATCH 7/7] Update src/SharpCompress/IO/SharpCompressStream.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/SharpCompress/IO/SharpCompressStream.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/SharpCompress/IO/SharpCompressStream.cs b/src/SharpCompress/IO/SharpCompressStream.cs index d5a7dfcc..2163e5f8 100644 --- a/src/SharpCompress/IO/SharpCompressStream.cs +++ b/src/SharpCompress/IO/SharpCompressStream.cs @@ -4,6 +4,18 @@ using SharpCompress.Common; namespace SharpCompress.IO; +/// +/// Stream wrapper that provides optional ring-buffered reading for non-seekable +/// or forward-only streams, enabling limited backward seeking required by some +/// decompressors and archive formats. +/// +/// +/// In most cases, callers should obtain an instance via the static +/// SharpCompressStream.Create(...) methods rather than constructing this +/// class directly. The Create methods select an appropriate configuration +/// (such as passthrough vs buffered mode and buffer size) for the underlying +/// stream and usage scenario. +/// public partial class SharpCompressStream : Stream, IStreamStack { public virtual Stream BaseStream() => stream;