From cef6d4603b2b6f91f1ca694526bceedbd5370efd Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 29 Nov 2025 13:25:34 +0000
Subject: [PATCH 1/6] Initial plan
From 75db75a33ac4ce48e21f382460d2988d669a31c3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 29 Nov 2025 13:33:40 +0000
Subject: [PATCH 2/6] Add IByteSource abstraction and documentation for
Volume/SourceStream architecture
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
---
FORMATS.md | 32 ++++++++
src/SharpCompress/Common/IVolume.cs | 52 ++++++++++++
src/SharpCompress/Common/Volume.cs | 31 +++++++
src/SharpCompress/IO/FileByteSource.cs | 39 +++++++++
src/SharpCompress/IO/IByteSource.cs | 100 +++++++++++++++++++++++
src/SharpCompress/IO/SourceStream.cs | 86 +++++++++++++++++++
src/SharpCompress/IO/StreamByteSource.cs | 72 ++++++++++++++++
7 files changed, 412 insertions(+)
create mode 100644 src/SharpCompress/IO/FileByteSource.cs
create mode 100644 src/SharpCompress/IO/IByteSource.cs
create mode 100644 src/SharpCompress/IO/StreamByteSource.cs
diff --git a/FORMATS.md b/FORMATS.md
index 473d8eb90..6aae3340f 100644
--- a/FORMATS.md
+++ b/FORMATS.md
@@ -6,6 +6,38 @@
* Reader classes allow forward-only reading on a stream.
* Writer classes allow forward-only Writing on a stream.
+## Stream and Volume Architecture
+
+SharpCompress uses a layered architecture to handle the complexity of multi-part and multi-volume archives:
+
+### Concepts
+
+1. **ByteSource** (`IByteSource`): The lowest-level abstraction representing a source of raw bytes. This could be a file, a stream, or part of a multi-part archive. ByteSources don't have any archive-specific logic.
+
+2. **SourceStream**: Combines multiple byte sources into a unified stream. It operates in two modes:
+ - **Split Mode** (`IsVolumes=false`): Multiple files/streams are treated as one contiguous byte sequence. Used for split archives (e.g., `.z01`, `.z02`, `.zip`).
+ - **Volume Mode** (`IsVolumes=true`): Each file/stream is treated as an independent unit. Used for multi-volume archives (e.g., `.rar`, `.r00`, `.r01`).
+
+3. **Volume** (`IVolume`): Represents a physical archive container with its own headers and metadata. Each format has its own Volume implementation (ZipVolume, RarVolume, SevenZipVolume, TarVolume).
+
+### Format-Specific Behaviors
+
+| Format | Split Archives | Multi-Volume | SOLID Support | Notes |
+| ------ | -------------- | ------------ | ------------- | ----- |
+| Zip | Yes | Yes | No | Split: `.z01`, `.z02`, `.zip`. Multi-volume uses separate headers per volume. |
+| Rar | Yes | Yes | Yes | Multi-volume: `.rar`, `.r00`, `.r01`. SOLID archives share decompression context. |
+| 7Zip | Yes | No | Yes (Folders) | Uses internal "folders" as compression units. Files in same folder share context. |
+| Tar | Yes | No | No | Split tar files are concatenated. |
+
+### SOLID Archives
+
+Some formats support "SOLID" archives where multiple files share a contiguous stream of compressed bytes:
+
+- **RAR SOLID**: Files are compressed together, and decompressing a file requires decompressing all files before it.
+- **7Zip Folders**: Files grouped in the same folder share a decompression context.
+
+This is why `ExtractAllEntries()` exists for SOLID archives - it allows sequential extraction which is much more efficient than random access.
+
## Supported Format Table
| Archive Format | Compression Format(s) | Compress/Decompress | Archive API | Reader API | Writer API |
diff --git a/src/SharpCompress/Common/IVolume.cs b/src/SharpCompress/Common/IVolume.cs
index aed35fd03..f335e164f 100644
--- a/src/SharpCompress/Common/IVolume.cs
+++ b/src/SharpCompress/Common/IVolume.cs
@@ -2,9 +2,61 @@
namespace SharpCompress.Common;
+///
+/// Represents a physical archive volume (a single archive file or stream).
+///
+///
+/// This interface is distinct from in that:
+///
+///
+/// -
+///
+/// IVolume represents a physical archive container with its own
+/// headers, metadata, and structure. Each volume is a complete or partial
+/// archive unit.
+///
+///
+/// -
+///
+/// IByteSource represents a raw source of bytes without archive-specific
+/// semantics. Multiple byte sources can form a contiguous stream, or each can
+/// be independent.
+///
+///
+///
+///
+///
+/// Archive formats use volumes differently:
+///
+///
+/// -
+///
+/// Multi-volume RAR: Each volume is an independent archive unit with
+/// its own headers. Files can span volumes.
+///
+///
+/// -
+///
+/// Split ZIP: Data is split across multiple files but logically forms
+/// one archive. The central directory is typically in the last part.
+///
+///
+/// -
+///
+/// 7Zip/TAR: Typically single volume, though can be split.
+///
+///
+///
+///
public interface IVolume : IDisposable
{
+ ///
+ /// Gets the zero-based index of this volume within a multi-volume archive.
+ ///
int Index { get; }
+ ///
+ /// Gets the file name of this volume, if it was loaded from a file.
+ ///
string? FileName { get; }
}
diff --git a/src/SharpCompress/Common/Volume.cs b/src/SharpCompress/Common/Volume.cs
index 54dc49953..ec993ff91 100644
--- a/src/SharpCompress/Common/Volume.cs
+++ b/src/SharpCompress/Common/Volume.cs
@@ -5,6 +5,37 @@
namespace SharpCompress.Common;
+///
+/// Base class for archive volumes. A volume represents a single physical
+/// archive file or stream that may contain entries or parts of entries.
+///
+///
+/// The relationship between , ,
+/// and is:
+///
+///
+/// -
+///
+/// IByteSource is the lowest level - it provides access to raw bytes
+/// from a file or stream, with no archive-specific logic.
+///
+///
+/// -
+///
+/// SourceStream combines multiple byte sources into a unified stream,
+/// handling the distinction between split archives (contiguous bytes) and
+/// multi-volume archives (independent units).
+///
+///
+/// -
+///
+/// Volume wraps a stream with archive-specific metadata and behavior.
+/// Format-specific subclasses (ZipVolume, RarVolume, etc.) add format-specific
+/// properties and methods.
+///
+///
+///
+///
public abstract class Volume : IVolume
{
private readonly Stream _baseStream;
diff --git a/src/SharpCompress/IO/FileByteSource.cs b/src/SharpCompress/IO/FileByteSource.cs
new file mode 100644
index 000000000..591148a4f
--- /dev/null
+++ b/src/SharpCompress/IO/FileByteSource.cs
@@ -0,0 +1,39 @@
+using System.IO;
+
+namespace SharpCompress.IO;
+
+///
+/// A byte source backed by a file on the file system.
+///
+public sealed class FileByteSource : IByteSource
+{
+ private readonly FileInfo _fileInfo;
+
+ ///
+ /// Creates a new file-based byte source.
+ ///
+ /// The file to read from.
+ /// The index of this source in a collection.
+ /// Whether this is part of a split archive.
+ public FileByteSource(FileInfo fileInfo, int index = 0, bool isPartOfContiguousSequence = false)
+ {
+ _fileInfo = fileInfo;
+ Index = index;
+ IsPartOfContiguousSequence = isPartOfContiguousSequence;
+ }
+
+ ///
+ public int Index { get; }
+
+ ///
+ public long? Length => _fileInfo.Exists ? _fileInfo.Length : null;
+
+ ///
+ public string? FileName => _fileInfo.FullName;
+
+ ///
+ public Stream OpenRead() => _fileInfo.OpenRead();
+
+ ///
+ public bool IsPartOfContiguousSequence { get; }
+}
diff --git a/src/SharpCompress/IO/IByteSource.cs b/src/SharpCompress/IO/IByteSource.cs
new file mode 100644
index 000000000..30a41a467
--- /dev/null
+++ b/src/SharpCompress/IO/IByteSource.cs
@@ -0,0 +1,100 @@
+using System.IO;
+
+namespace SharpCompress.IO;
+
+///
+/// Represents a source of bytes that can be read as a stream.
+/// This abstraction distinguishes between the "stream of bytes" concept
+/// and the physical container (file, stream, or volume) that holds those bytes.
+///
+///
+/// The key insight is that in archive formats, there are three distinct concepts:
+///
+///
+///
+/// -
+///
+/// ByteSource: A logical source of bytes. This could be:
+///
+/// - A single file or stream
+/// - A contiguous sequence spanning multiple files (split archives)
+/// - A compressed data block that expands to multiple files (SOLID archives)
+///
+///
+///
+/// -
+///
+/// Volume: A physical container representing one archive file/stream.
+/// In multi-volume archives (like RAR volumes), each volume is an independent
+/// archive unit with its own headers and metadata.
+///
+///
+/// -
+///
+/// SourceStream: Manages multiple byte sources and presents them as a
+/// unified stream. Handles both split mode (contiguous bytes across files)
+/// and volume mode (independent archive units).
+///
+///
+///
+///
+///
+/// Format-specific behaviors:
+///
+///
+/// -
+///
+/// 7Zip: Can have a contiguous stream of bytes for a file or files
+/// within a folder (compression unit).
+///
+///
+/// -
+///
+/// RAR SOLID: Uses a contiguous stream of bytes for files, where
+/// decompression depends on previous files in the stream.
+///
+///
+/// -
+///
+/// ZIP Split: File data can be split across multiple disk files,
+/// treated as one contiguous byte stream.
+///
+///
+/// -
+///
+/// RAR Multi-volume: Each volume is an independent archive that
+/// can be opened and read separately.
+///
+///
+///
+///
+public interface IByteSource
+{
+ ///
+ /// Gets the index of this byte source within a collection of sources.
+ ///
+ int Index { get; }
+
+ ///
+ /// Gets the length of bytes available from this source, if known.
+ /// Returns null if the length cannot be determined (e.g., for unseekable streams).
+ ///
+ long? Length { get; }
+
+ ///
+ /// Gets the file name associated with this byte source, if available.
+ ///
+ string? FileName { get; }
+
+ ///
+ /// Opens a stream to read bytes from this source.
+ ///
+ /// A readable stream positioned at the start of the byte source.
+ Stream OpenRead();
+
+ ///
+ /// Indicates whether this byte source represents part of a contiguous
+ /// byte sequence that spans multiple sources (e.g., split archive).
+ ///
+ bool IsPartOfContiguousSequence { get; }
+}
diff --git a/src/SharpCompress/IO/SourceStream.cs b/src/SharpCompress/IO/SourceStream.cs
index 0712d915c..6412f7e89 100644
--- a/src/SharpCompress/IO/SourceStream.cs
+++ b/src/SharpCompress/IO/SourceStream.cs
@@ -8,6 +8,61 @@
namespace SharpCompress.IO;
+///
+/// A stream that unifies multiple byte sources (files or streams) into a single readable stream.
+///
+///
+/// SourceStream handles two distinct modes controlled by :
+///
+///
+/// -
+///
+/// Split Mode (IsVolumes=false): Multiple files/streams are treated as one
+/// contiguous byte sequence. Reading seamlessly transitions from one source to the
+/// next. Position and Length span all sources combined. This is used for split
+/// archives where file data is simply split across multiple physical files.
+///
+///
+/// -
+///
+/// Volume Mode (IsVolumes=true): Each file/stream is treated as an independent
+/// unit. Position and Length refer only to the current source. This is used for
+/// multi-volume archives where each volume has its own headers and structure.
+///
+///
+///
+///
+///
+/// This abstraction works with to provide raw byte access
+/// and with to add archive-specific semantics.
+///
+///
+///
+/// Format-specific behaviors:
+///
+///
+/// -
+///
+/// 7Zip: Uses split mode. The SourceStream presents the entire archive
+/// as one stream, and 7Zip handles internal "folders" (compression units)
+/// that may contain contiguous compressed data for multiple files.
+///
+///
+/// -
+///
+/// RAR: Can use either mode. Multi-volume RAR uses volume mode where
+/// each .rar/.r00/.r01 file is a separate volume. SOLID RAR archives have
+/// internal contiguous byte streams for decompression.
+///
+///
+/// -
+///
+/// ZIP: Can use either mode. Split ZIP (.z01, .z02, .zip) uses split mode.
+/// Multi-volume ZIP uses volume mode.
+///
+///
+///
+///
public class SourceStream : Stream, IStreamStack
{
#if DEBUG_STREAMS
@@ -82,22 +137,53 @@ ReaderOptions options
#endif
}
+ ///
+ /// Loads all available parts/volumes by calling the getPart function
+ /// until it returns null. Resets to the first stream after loading.
+ ///
public void LoadAllParts()
{
for (var i = 1; SetStream(i); i++) { }
SetStream(0);
}
+ ///
+ /// Gets or sets whether this SourceStream operates in volume mode.
+ /// When true, each stream is treated as an independent volume with its own
+ /// position and length. When false (default), all streams are treated as
+ /// one contiguous byte sequence.
+ ///
public bool IsVolumes { get; set; }
+ ///
+ /// Gets the reader options associated with this source stream.
+ ///
public ReaderOptions ReaderOptions { get; }
+
+ ///
+ /// Gets whether this SourceStream was created from a FileInfo (true)
+ /// or from a Stream (false).
+ ///
public bool IsFileMode { get; }
+ ///
+ /// Gets the collection of FileInfo objects for each loaded source.
+ /// May be empty if sources are streams without file associations.
+ ///
public IEnumerable Files => _files;
+
+ ///
+ /// Gets the collection of underlying streams for each loaded source.
+ ///
public IEnumerable Streams => _streams;
private Stream Current => _streams[_stream];
+ ///
+ /// Ensures that streams up to and including the specified index are loaded.
+ ///
+ /// The stream index to load.
+ /// True if the stream at the index was successfully loaded; false otherwise.
public bool LoadStream(int index) //ensure all parts to id are loaded
{
while (_streams.Count <= index)
diff --git a/src/SharpCompress/IO/StreamByteSource.cs b/src/SharpCompress/IO/StreamByteSource.cs
new file mode 100644
index 000000000..ad02ca7da
--- /dev/null
+++ b/src/SharpCompress/IO/StreamByteSource.cs
@@ -0,0 +1,72 @@
+using System.IO;
+
+namespace SharpCompress.IO;
+
+///
+/// A byte source backed by an existing stream.
+///
+public sealed class StreamByteSource : IByteSource
+{
+ private readonly Stream _stream;
+ private readonly bool _leaveOpen;
+
+ ///
+ /// Creates a new stream-based byte source.
+ ///
+ /// The stream to read from.
+ /// The index of this source in a collection.
+ /// Whether this is part of a split archive.
+ /// Whether to leave the stream open when not in use.
+ public StreamByteSource(
+ Stream stream,
+ int index = 0,
+ bool isPartOfContiguousSequence = false,
+ bool leaveOpen = true
+ )
+ {
+ _stream = stream;
+ Index = index;
+ IsPartOfContiguousSequence = isPartOfContiguousSequence;
+ _leaveOpen = leaveOpen;
+
+ if (stream is FileStream fileStream)
+ {
+ FileName = fileStream.Name;
+ }
+ }
+
+ ///
+ public int Index { get; }
+
+ ///
+ public long? Length
+ {
+ get
+ {
+ try
+ {
+ return _stream.CanSeek ? _stream.Length : null;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+ }
+
+ ///
+ public string? FileName { get; }
+
+ ///
+ public Stream OpenRead()
+ {
+ if (_stream.CanSeek)
+ {
+ _stream.Position = 0;
+ }
+ return _stream;
+ }
+
+ ///
+ public bool IsPartOfContiguousSequence { get; }
+}
From 3470f71f831df8cf43735bcea813c0500de0d26d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 29 Nov 2025 13:44:18 +0000
Subject: [PATCH 3/6] Remove unused _leaveOpen field from StreamByteSource
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
---
src/SharpCompress/IO/StreamByteSource.cs | 10 +---------
1 file changed, 1 insertion(+), 9 deletions(-)
diff --git a/src/SharpCompress/IO/StreamByteSource.cs b/src/SharpCompress/IO/StreamByteSource.cs
index ad02ca7da..57f434dbb 100644
--- a/src/SharpCompress/IO/StreamByteSource.cs
+++ b/src/SharpCompress/IO/StreamByteSource.cs
@@ -8,7 +8,6 @@ namespace SharpCompress.IO;
public sealed class StreamByteSource : IByteSource
{
private readonly Stream _stream;
- private readonly bool _leaveOpen;
///
/// Creates a new stream-based byte source.
@@ -16,18 +15,11 @@ public sealed class StreamByteSource : IByteSource
/// The stream to read from.
/// The index of this source in a collection.
/// Whether this is part of a split archive.
- /// Whether to leave the stream open when not in use.
- public StreamByteSource(
- Stream stream,
- int index = 0,
- bool isPartOfContiguousSequence = false,
- bool leaveOpen = true
- )
+ public StreamByteSource(Stream stream, int index = 0, bool isPartOfContiguousSequence = false)
{
_stream = stream;
Index = index;
IsPartOfContiguousSequence = isPartOfContiguousSequence;
- _leaveOpen = leaveOpen;
if (stream is FileStream fileStream)
{
From f2cde3abdc1d22158f52c07745b7bfa854117c6e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 30 Nov 2025 08:50:09 +0000
Subject: [PATCH 4/6] Refactor SourceStream to use IByteSource internally
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
---
src/SharpCompress/IO/SourceStream.cs | 179 ++++++++++++++-------------
1 file changed, 93 insertions(+), 86 deletions(-)
diff --git a/src/SharpCompress/IO/SourceStream.cs b/src/SharpCompress/IO/SourceStream.cs
index 6412f7e89..e78555363 100644
--- a/src/SharpCompress/IO/SourceStream.cs
+++ b/src/SharpCompress/IO/SourceStream.cs
@@ -33,8 +33,9 @@ namespace SharpCompress.IO;
///
///
///
-/// This abstraction works with to provide raw byte access
-/// and with to add archive-specific semantics.
+/// Internally, SourceStream uses to manage its underlying
+/// sources. This allows consistent handling of both file-based and stream-based sources,
+/// while the abstraction adds archive-specific semantics.
///
///
///
@@ -70,7 +71,7 @@ public class SourceStream : Stream, IStreamStack
#endif
int IStreamStack.DefaultBufferSize { get; set; }
- Stream IStreamStack.BaseStream() => _streams[_stream];
+ Stream IStreamStack.BaseStream() => _openStreams[_currentSourceIndex];
int IStreamStack.BufferSize
{
@@ -86,51 +87,65 @@ int IStreamStack.BufferPosition
void IStreamStack.SetPosition(long position) { }
private long _prevSize;
- private readonly List _files;
- private readonly List _streams;
- private readonly Func? _getFilePart;
- private readonly Func? _getStreamPart;
- private int _stream;
+ private readonly List _sources;
+ private readonly List _openStreams;
+ private readonly Func? _getNextSource;
+ private int _currentSourceIndex;
+ ///
+ /// Creates a SourceStream from a file, with a function to get additional file parts.
+ ///
+ /// The initial file to read from.
+ /// Function that returns additional file parts by index, or null when no more parts.
+ /// Reader options.
public SourceStream(FileInfo file, Func getPart, ReaderOptions options)
- : this(null, null, file, getPart, options) { }
+ : this(
+ new FileByteSource(file, 0),
+ index =>
+ {
+ var f = getPart(index);
+ return f != null ? new FileByteSource(f, index) : null;
+ },
+ options
+ ) { }
+ ///
+ /// Creates a SourceStream from a stream, with a function to get additional stream parts.
+ ///
+ /// The initial stream to read from.
+ /// Function that returns additional stream parts by index, or null when no more parts.
+ /// Reader options.
public SourceStream(Stream stream, Func getPart, ReaderOptions options)
- : this(stream, getPart, null, null, options) { }
+ : this(
+ new StreamByteSource(stream, 0),
+ index =>
+ {
+ var s = getPart(index);
+ return s != null ? new StreamByteSource(s, index) : null;
+ },
+ options
+ ) { }
- private SourceStream(
- Stream? stream,
- Func? getStreamPart,
- FileInfo? file,
- Func? getFilePart,
+ ///
+ /// Creates a SourceStream from an initial byte source with a function to get additional sources.
+ ///
+ /// The initial byte source.
+ /// Function that returns additional byte sources by index, or null when no more sources.
+ /// Reader options.
+ public SourceStream(
+ IByteSource initialSource,
+ Func? getNextSource,
ReaderOptions options
)
{
ReaderOptions = options;
- _files = new List();
- _streams = new List();
- IsFileMode = file != null;
- IsVolumes = false;
-
- if (!IsFileMode)
- {
- _streams.Add(stream!);
- _getStreamPart = getStreamPart;
- _getFilePart = _ => null;
- if (stream is FileStream fileStream)
- {
- _files.Add(new FileInfo(fileStream.Name));
- }
- }
- else
- {
- _files.Add(file!);
- _streams.Add(_files[0].OpenRead());
- _getFilePart = getFilePart;
- _getStreamPart = _ => null;
- }
- _stream = 0;
+ _sources = new List { initialSource };
+ _openStreams = new List { initialSource.OpenRead() };
+ _getNextSource = getNextSource;
+ _currentSourceIndex = 0;
_prevSize = 0;
+ IsVolumes = false;
+ IsFileMode = initialSource is FileByteSource;
#if DEBUG_STREAMS
this.DebugConstruct(typeof(SourceStream));
@@ -138,7 +153,7 @@ ReaderOptions options
}
///
- /// Loads all available parts/volumes by calling the getPart function
+ /// Loads all available parts/volumes by calling the getNextSource function
/// until it returns null. Resets to the first stream after loading.
///
public void LoadAllParts()
@@ -166,67 +181,59 @@ public void LoadAllParts()
///
public bool IsFileMode { get; }
+ ///
+ /// Gets the collection of loaded byte sources.
+ ///
+ internal IEnumerable Sources => _sources;
+
///
/// Gets the collection of FileInfo objects for each loaded source.
/// May be empty if sources are streams without file associations.
///
- public IEnumerable Files => _files;
+ public IEnumerable Files =>
+ _sources.Where(s => s.FileName != null).Select(s => new FileInfo(s.FileName!));
///
/// Gets the collection of underlying streams for each loaded source.
///
- public IEnumerable Streams => _streams;
+ public IEnumerable Streams => _openStreams;
- private Stream Current => _streams[_stream];
+ private Stream Current => _openStreams[_currentSourceIndex];
///
- /// Ensures that streams up to and including the specified index are loaded.
+ /// Ensures that sources up to and including the specified index are loaded.
///
- /// The stream index to load.
- /// True if the stream at the index was successfully loaded; false otherwise.
- public bool LoadStream(int index) //ensure all parts to id are loaded
+ /// The source index to load.
+ /// True if the source at the index was successfully loaded; false otherwise.
+ public bool LoadStream(int index)
{
- while (_streams.Count <= index)
+ while (_sources.Count <= index)
{
- if (IsFileMode)
+ var nextSource = _getNextSource?.Invoke(_sources.Count);
+ if (nextSource is null)
{
- var f = _getFilePart.NotNull("GetFilePart is null")(_streams.Count);
- if (f == null)
- {
- _stream = _streams.Count - 1;
- return false;
- }
- //throw new Exception($"File part {idx} not available.");
- _files.Add(f);
- _streams.Add(_files.Last().OpenRead());
- }
- else
- {
- var s = _getStreamPart.NotNull("GetStreamPart is null")(_streams.Count);
- if (s == null)
- {
- _stream = _streams.Count - 1;
- return false;
- }
- //throw new Exception($"Stream part {idx} not available.");
- _streams.Add(s);
- if (s is FileStream stream)
- {
- _files.Add(new FileInfo(stream.Name));
- }
+ _currentSourceIndex = _sources.Count - 1;
+ return false;
}
+ _sources.Add(nextSource);
+ _openStreams.Add(nextSource.OpenRead());
}
return true;
}
- public bool SetStream(int idx) //allow caller to switch part in multipart
+ ///
+ /// Switches to the specified source index.
+ ///
+ /// The source index to switch to.
+ /// True if the switch was successful; false otherwise.
+ public bool SetStream(int idx)
{
if (LoadStream(idx))
{
- _stream = idx;
+ _currentSourceIndex = idx;
}
- return _stream == idx;
+ return _currentSourceIndex == idx;
}
public override bool CanRead => true;
@@ -235,7 +242,7 @@ public bool LoadStream(int index) //ensure all parts to id are loaded
public override bool CanWrite => false;
- public override long Length => !IsVolumes ? _streams.Sum(a => a.Length) : Current.Length;
+ public override long Length => !IsVolumes ? _openStreams.Sum(a => a.Length) : Current.Length;
public override long Position
{
@@ -269,8 +276,8 @@ public override int Read(byte[] buffer, int offset, int count)
{
var length = Current.Length;
- // Load next file if present
- if (!SetStream(_stream + 1))
+ // Load next source if present
+ if (!SetStream(_currentSourceIndex + 1))
{
break;
}
@@ -309,7 +316,7 @@ public override long Seek(long offset, SeekOrigin origin)
while (_prevSize + Current.Length < pos)
{
_prevSize += Current.Length;
- SetStream(_stream + 1);
+ SetStream(_currentSourceIndex + 1);
}
}
@@ -358,8 +365,8 @@ CancellationToken cancellationToken
{
var length = Current.Length;
- // Load next file if present
- if (!SetStream(_stream + 1))
+ // Load next source if present
+ if (!SetStream(_currentSourceIndex + 1))
{
break;
}
@@ -407,8 +414,8 @@ public override async ValueTask ReadAsync(
{
var length = Current.Length;
- // Load next file if present
- if (!SetStream(_stream + 1))
+ // Load next source if present
+ if (!SetStream(_currentSourceIndex + 1))
{
break;
}
@@ -429,7 +436,7 @@ public override void Close()
{
if (IsFileMode || !ReaderOptions.LeaveStreamOpen) //close if file mode or options specify it
{
- foreach (var stream in _streams)
+ foreach (var stream in _openStreams)
{
try
{
@@ -440,8 +447,8 @@ public override void Close()
// ignored
}
}
- _streams.Clear();
- _files.Clear();
+ _openStreams.Clear();
+ _sources.Clear();
}
}
From b2b9e4ef7d75a93d7dc691f8b5a3a35fef3bb8b9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 30 Nov 2025 11:47:08 +0000
Subject: [PATCH 5/6] Add documentation to SharpCompressStream clarifying its
role in the architecture
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
---
src/SharpCompress/IO/SharpCompressStream.cs | 49 +++++++++++++++++++++
1 file changed, 49 insertions(+)
diff --git a/src/SharpCompress/IO/SharpCompressStream.cs b/src/SharpCompress/IO/SharpCompressStream.cs
index 00c0ada2f..9217139a6 100644
--- a/src/SharpCompress/IO/SharpCompressStream.cs
+++ b/src/SharpCompress/IO/SharpCompressStream.cs
@@ -8,6 +8,55 @@
namespace SharpCompress.IO;
+///
+/// A stream wrapper that provides buffering and position tracking for an underlying stream.
+///
+///
+/// SharpCompressStream is part of the stream handling architecture alongside:
+///
+///
+/// -
+///
+/// IByteSource: Represents where bytes come from (file or stream).
+/// An IByteSource.OpenRead() returns a raw stream that may be wrapped by SharpCompressStream.
+///
+///
+/// -
+///
+/// SourceStream: Combines multiple IByteSource instances into a unified stream,
+/// handling split archives and multi-volume scenarios.
+///
+///
+/// -
+///
+/// SharpCompressStream: Wraps a single stream to provide buffering, position tracking,
+/// and implements IStreamStack for hierarchical stream operations.
+///
+///
+///
+///
+///
+/// Key features:
+///
+///
+/// - Optional buffering with configurable buffer size
+/// - Position tracking independent of underlying stream
+/// - Stream lifecycle management (LeaveOpen, ThrowOnDispose)
+/// - IStreamStack implementation for stream stack operations
+///
+///
+///
+/// Usage example:
+///
+///
+/// // Wrap a stream with buffering
+/// var bufferedStream = SharpCompressStream.Create(
+/// stream: rawStream,
+/// leaveOpen: true,
+/// throwOnDispose: false,
+/// bufferSize: 4096);
+///
+///
public class SharpCompressStream : Stream, IStreamStack
{
#if DEBUG_STREAMS
From fb3d32622d618e8923b72076974d549aff057087 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 30 Nov 2025 14:36:44 +0000
Subject: [PATCH 6/6] Document IStreamStack interface and its role in the
architecture
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
---
src/SharpCompress/IO/IStreamStack.cs | 64 ++++++++++++++++++++++++++++
1 file changed, 64 insertions(+)
diff --git a/src/SharpCompress/IO/IStreamStack.cs b/src/SharpCompress/IO/IStreamStack.cs
index 943f7c250..332ba4447 100644
--- a/src/SharpCompress/IO/IStreamStack.cs
+++ b/src/SharpCompress/IO/IStreamStack.cs
@@ -8,6 +8,70 @@
namespace SharpCompress.IO
{
+ ///
+ /// Provides a common interface for streams that wrap other streams, forming a hierarchical "stack"
+ /// of stream processing layers. This interface enables coordinated buffering, position tracking,
+ /// and seeking across multiple stream layers.
+ ///
+ ///
+ /// Purpose in SharpCompress Architecture:
+ ///
+ ///
+ /// SharpCompress uses stacked streams extensively: a raw file/network stream may be wrapped by
+ /// a buffering stream, then by a decompression stream, then by a CRC validation stream, etc.
+ /// IStreamStack provides a uniform way to:
+ ///
+ ///
+ /// - Navigate the stream hierarchy via
+ /// - Coordinate buffering across layers via and
+ /// - Synchronize position state when seeking via
+ /// - Debug stream lifecycle with instance tracking (in DEBUG builds)
+ ///
+ ///
+ ///
+ /// Relationship to Other Abstractions:
+ ///
+ ///
+ /// -
+ ///
+ /// IByteSource: Represents where bytes originate (file, stream). An IByteSource.OpenRead()
+ /// returns a raw stream that may then be wrapped in IStreamStack-implementing streams.
+ ///
+ ///
+ /// -
+ ///
+ /// SourceStream: Combines multiple IByteSource instances into a unified stream.
+ /// SourceStream itself implements IStreamStack.
+ ///
+ ///
+ /// -
+ ///
+ /// SharpCompressStream: A buffering wrapper that implements IStreamStack with actual
+ /// buffering support (BufferSize greater than 0).
+ ///
+ ///
+ /// -
+ ///
+ /// Compression Streams: All decompression streams (DeflateStream, LzmaStream, etc.) implement
+ /// IStreamStack to participate in the stack hierarchy, even if they don't buffer themselves.
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// Extension Methods:
+ ///
+ ///
+ /// The class provides extension methods that operate on the
+ /// entire stack:
+ ///
+ ///
+ /// - SetBuffer(): Configures buffering at the appropriate level
+ /// - Rewind(): Rewinds buffered data when over-read occurs
+ /// - StackSeek(): Seeks efficiently, preferring buffer adjustment
+ /// - GetPosition(): Gets position accounting for buffered reads
+ ///
+ ///
public interface IStreamStack
{
///