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 { ///