diff --git a/docs/API.md b/docs/API.md index 15511e5dd..5696961d7 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 diff --git a/src/SharpCompress/IO/SharpCompressStream.Async.cs b/src/SharpCompress/IO/SharpCompressStream.Async.cs index e18a47a19..a36a9862e 100644 --- a/src/SharpCompress/IO/SharpCompressStream.Async.cs +++ b/src/SharpCompress/IO/SharpCompressStream.Async.cs @@ -5,7 +5,7 @@ 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 3a886a807..8011641b6 100644 --- a/src/SharpCompress/IO/SharpCompressStream.Create.cs +++ b/src/SharpCompress/IO/SharpCompressStream.Create.cs @@ -4,7 +4,7 @@ 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 a7c09072a..2163e5f8e 100644 --- a/src/SharpCompress/IO/SharpCompressStream.cs +++ b/src/SharpCompress/IO/SharpCompressStream.cs @@ -4,7 +4,19 @@ namespace SharpCompress.IO; -internal partial class SharpCompressStream : Stream, IStreamStack +/// +/// 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; diff --git a/tests/SharpCompress.Test/Xz/XZStreamAsyncTests.cs b/tests/SharpCompress.Test/Xz/XZStreamAsyncTests.cs index a2276602a..9402e7c41 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 async ValueTask CanReadIndexedStreamAsync() 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 02c5020ca..80ad5dc30 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 void CanReadIndexedStream() 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); + } }