Compare commits

...

6 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
fb3d32622d Document IStreamStack interface and its role in the architecture
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-30 14:36:44 +00:00
copilot-swe-agent[bot]
b2b9e4ef7d Add documentation to SharpCompressStream clarifying its role in the architecture
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-30 11:47:08 +00:00
copilot-swe-agent[bot]
f2cde3abdc Refactor SourceStream to use IByteSource internally
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-30 08:50:09 +00:00
copilot-swe-agent[bot]
3470f71f83 Remove unused _leaveOpen field from StreamByteSource
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-29 13:44:18 +00:00
copilot-swe-agent[bot]
75db75a33a Add IByteSource abstraction and documentation for Volume/SourceStream architecture
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
2025-11-29 13:33:40 +00:00
copilot-swe-agent[bot]
cef6d4603b Initial plan 2025-11-29 13:25:34 +00:00
9 changed files with 604 additions and 80 deletions

View File

@@ -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 |

View File

@@ -2,9 +2,61 @@ using System;
namespace SharpCompress.Common;
/// <summary>
/// Represents a physical archive volume (a single archive file or stream).
///
/// <para>
/// This interface is distinct from <see cref="IO.IByteSource"/> in that:
/// </para>
/// <list type="bullet">
/// <item>
/// <description>
/// <b>IVolume</b> represents a physical archive container with its own
/// headers, metadata, and structure. Each volume is a complete or partial
/// archive unit.
/// </description>
/// </item>
/// <item>
/// <description>
/// <b>IByteSource</b> represents a raw source of bytes without archive-specific
/// semantics. Multiple byte sources can form a contiguous stream, or each can
/// be independent.
/// </description>
/// </item>
/// </list>
///
/// <para>
/// Archive formats use volumes differently:
/// </para>
/// <list type="bullet">
/// <item>
/// <description>
/// <b>Multi-volume RAR</b>: Each volume is an independent archive unit with
/// its own headers. Files can span volumes.
/// </description>
/// </item>
/// <item>
/// <description>
/// <b>Split ZIP</b>: Data is split across multiple files but logically forms
/// one archive. The central directory is typically in the last part.
/// </description>
/// </item>
/// <item>
/// <description>
/// <b>7Zip/TAR</b>: Typically single volume, though can be split.
/// </description>
/// </item>
/// </list>
/// </summary>
public interface IVolume : IDisposable
{
/// <summary>
/// Gets the zero-based index of this volume within a multi-volume archive.
/// </summary>
int Index { get; }
/// <summary>
/// Gets the file name of this volume, if it was loaded from a file.
/// </summary>
string? FileName { get; }
}

View File

@@ -5,6 +5,37 @@ using SharpCompress.Readers;
namespace SharpCompress.Common;
/// <summary>
/// Base class for archive volumes. A volume represents a single physical
/// archive file or stream that may contain entries or parts of entries.
///
/// <para>
/// The relationship between <see cref="Volume"/>, <see cref="SourceStream"/>,
/// and <see cref="IByteSource"/> is:
/// </para>
/// <list type="bullet">
/// <item>
/// <description>
/// <b>IByteSource</b> is the lowest level - it provides access to raw bytes
/// from a file or stream, with no archive-specific logic.
/// </description>
/// </item>
/// <item>
/// <description>
/// <b>SourceStream</b> combines multiple byte sources into a unified stream,
/// handling the distinction between split archives (contiguous bytes) and
/// multi-volume archives (independent units).
/// </description>
/// </item>
/// <item>
/// <description>
/// <b>Volume</b> wraps a stream with archive-specific metadata and behavior.
/// Format-specific subclasses (ZipVolume, RarVolume, etc.) add format-specific
/// properties and methods.
/// </description>
/// </item>
/// </list>
/// </summary>
public abstract class Volume : IVolume
{
private readonly Stream _baseStream;

View File

@@ -0,0 +1,39 @@
using System.IO;
namespace SharpCompress.IO;
/// <summary>
/// A byte source backed by a file on the file system.
/// </summary>
public sealed class FileByteSource : IByteSource
{
private readonly FileInfo _fileInfo;
/// <summary>
/// Creates a new file-based byte source.
/// </summary>
/// <param name="fileInfo">The file to read from.</param>
/// <param name="index">The index of this source in a collection.</param>
/// <param name="isPartOfContiguousSequence">Whether this is part of a split archive.</param>
public FileByteSource(FileInfo fileInfo, int index = 0, bool isPartOfContiguousSequence = false)
{
_fileInfo = fileInfo;
Index = index;
IsPartOfContiguousSequence = isPartOfContiguousSequence;
}
/// <inheritdoc />
public int Index { get; }
/// <inheritdoc />
public long? Length => _fileInfo.Exists ? _fileInfo.Length : null;
/// <inheritdoc />
public string? FileName => _fileInfo.FullName;
/// <inheritdoc />
public Stream OpenRead() => _fileInfo.OpenRead();
/// <inheritdoc />
public bool IsPartOfContiguousSequence { get; }
}

View File

@@ -0,0 +1,100 @@
using System.IO;
namespace SharpCompress.IO;
/// <summary>
/// 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.
///
/// <para>
/// The key insight is that in archive formats, there are three distinct concepts:
/// </para>
///
/// <list type="number">
/// <item>
/// <description>
/// <b>ByteSource</b>: A logical source of bytes. This could be:
/// <list type="bullet">
/// <item>A single file or stream</item>
/// <item>A contiguous sequence spanning multiple files (split archives)</item>
/// <item>A compressed data block that expands to multiple files (SOLID archives)</item>
/// </list>
/// </description>
/// </item>
/// <item>
/// <description>
/// <b>Volume</b>: 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.
/// </description>
/// </item>
/// <item>
/// <description>
/// <b>SourceStream</b>: 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).
/// </description>
/// </item>
/// </list>
///
/// <para>
/// Format-specific behaviors:
/// </para>
/// <list type="bullet">
/// <item>
/// <description>
/// <b>7Zip</b>: Can have a contiguous stream of bytes for a file or files
/// within a folder (compression unit).
/// </description>
/// </item>
/// <item>
/// <description>
/// <b>RAR SOLID</b>: Uses a contiguous stream of bytes for files, where
/// decompression depends on previous files in the stream.
/// </description>
/// </item>
/// <item>
/// <description>
/// <b>ZIP Split</b>: File data can be split across multiple disk files,
/// treated as one contiguous byte stream.
/// </description>
/// </item>
/// <item>
/// <description>
/// <b>RAR Multi-volume</b>: Each volume is an independent archive that
/// can be opened and read separately.
/// </description>
/// </item>
/// </list>
/// </summary>
public interface IByteSource
{
/// <summary>
/// Gets the index of this byte source within a collection of sources.
/// </summary>
int Index { get; }
/// <summary>
/// Gets the length of bytes available from this source, if known.
/// Returns null if the length cannot be determined (e.g., for unseekable streams).
/// </summary>
long? Length { get; }
/// <summary>
/// Gets the file name associated with this byte source, if available.
/// </summary>
string? FileName { get; }
/// <summary>
/// Opens a stream to read bytes from this source.
/// </summary>
/// <returns>A readable stream positioned at the start of the byte source.</returns>
Stream OpenRead();
/// <summary>
/// Indicates whether this byte source represents part of a contiguous
/// byte sequence that spans multiple sources (e.g., split archive).
/// </summary>
bool IsPartOfContiguousSequence { get; }
}

View File

@@ -8,6 +8,70 @@ using System.Text;
namespace SharpCompress.IO
{
/// <summary>
/// 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.
///
/// <para>
/// <b>Purpose in SharpCompress Architecture:</b>
/// </para>
/// <para>
/// 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.
/// <c>IStreamStack</c> provides a uniform way to:
/// </para>
/// <list type="bullet">
/// <item><description>Navigate the stream hierarchy via <see cref="BaseStream()"/></description></item>
/// <item><description>Coordinate buffering across layers via <see cref="BufferSize"/> and <see cref="BufferPosition"/></description></item>
/// <item><description>Synchronize position state when seeking via <see cref="SetPosition"/></description></item>
/// <item><description>Debug stream lifecycle with instance tracking (in DEBUG builds)</description></item>
/// </list>
///
/// <para>
/// <b>Relationship to Other Abstractions:</b>
/// </para>
/// <list type="bullet">
/// <item>
/// <description>
/// <b>IByteSource</b>: Represents where bytes originate (file, stream). An <c>IByteSource.OpenRead()</c>
/// returns a raw stream that may then be wrapped in <c>IStreamStack</c>-implementing streams.
/// </description>
/// </item>
/// <item>
/// <description>
/// <b>SourceStream</b>: Combines multiple <c>IByteSource</c> instances into a unified stream.
/// <c>SourceStream</c> itself implements <c>IStreamStack</c>.
/// </description>
/// </item>
/// <item>
/// <description>
/// <b>SharpCompressStream</b>: A buffering wrapper that implements <c>IStreamStack</c> with actual
/// buffering support (BufferSize greater than 0).
/// </description>
/// </item>
/// <item>
/// <description>
/// <b>Compression Streams</b>: All decompression streams (DeflateStream, LzmaStream, etc.) implement
/// <c>IStreamStack</c> to participate in the stack hierarchy, even if they don't buffer themselves.
/// </description>
/// </item>
/// </list>
///
/// <para>
/// <b>Extension Methods:</b>
/// </para>
/// <para>
/// The <see cref="StackStreamExtensions"/> class provides extension methods that operate on the
/// entire stack:
/// </para>
/// <list type="bullet">
/// <item><description><c>SetBuffer()</c>: Configures buffering at the appropriate level</description></item>
/// <item><description><c>Rewind()</c>: Rewinds buffered data when over-read occurs</description></item>
/// <item><description><c>StackSeek()</c>: Seeks efficiently, preferring buffer adjustment</description></item>
/// <item><description><c>GetPosition()</c>: Gets position accounting for buffered reads</description></item>
/// </list>
/// </summary>
public interface IStreamStack
{
/// <summary>

View File

@@ -8,6 +8,55 @@ using System.Threading.Tasks;
namespace SharpCompress.IO;
/// <summary>
/// A stream wrapper that provides buffering and position tracking for an underlying stream.
///
/// <para>
/// SharpCompressStream is part of the stream handling architecture alongside:
/// </para>
/// <list type="bullet">
/// <item>
/// <description>
/// <b>IByteSource</b>: Represents where bytes come from (file or stream).
/// An IByteSource.OpenRead() returns a raw stream that may be wrapped by SharpCompressStream.
/// </description>
/// </item>
/// <item>
/// <description>
/// <b>SourceStream</b>: Combines multiple IByteSource instances into a unified stream,
/// handling split archives and multi-volume scenarios.
/// </description>
/// </item>
/// <item>
/// <description>
/// <b>SharpCompressStream</b>: Wraps a single stream to provide buffering, position tracking,
/// and implements IStreamStack for hierarchical stream operations.
/// </description>
/// </item>
/// </list>
///
/// <para>
/// Key features:
/// </para>
/// <list type="bullet">
/// <item><description>Optional buffering with configurable buffer size</description></item>
/// <item><description>Position tracking independent of underlying stream</description></item>
/// <item><description>Stream lifecycle management (LeaveOpen, ThrowOnDispose)</description></item>
/// <item><description>IStreamStack implementation for stream stack operations</description></item>
/// </list>
///
/// <para>
/// Usage example:
/// </para>
/// <code>
/// // Wrap a stream with buffering
/// var bufferedStream = SharpCompressStream.Create(
/// stream: rawStream,
/// leaveOpen: true,
/// throwOnDispose: false,
/// bufferSize: 4096);
/// </code>
/// </summary>
public class SharpCompressStream : Stream, IStreamStack
{
#if DEBUG_STREAMS

View File

@@ -8,6 +8,62 @@ using SharpCompress.Readers;
namespace SharpCompress.IO;
/// <summary>
/// A stream that unifies multiple byte sources (files or streams) into a single readable stream.
///
/// <para>
/// SourceStream handles two distinct modes controlled by <see cref="IsVolumes"/>:
/// </para>
/// <list type="bullet">
/// <item>
/// <description>
/// <b>Split Mode (IsVolumes=false)</b>: 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.
/// </description>
/// </item>
/// <item>
/// <description>
/// <b>Volume Mode (IsVolumes=true)</b>: 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.
/// </description>
/// </item>
/// </list>
///
/// <para>
/// Internally, SourceStream uses <see cref="IByteSource"/> to manage its underlying
/// sources. This allows consistent handling of both file-based and stream-based sources,
/// while the <see cref="Common.Volume"/> abstraction adds archive-specific semantics.
/// </para>
///
/// <para>
/// Format-specific behaviors:
/// </para>
/// <list type="bullet">
/// <item>
/// <description>
/// <b>7Zip</b>: 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.
/// </description>
/// </item>
/// <item>
/// <description>
/// <b>RAR</b>: 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.
/// </description>
/// </item>
/// <item>
/// <description>
/// <b>ZIP</b>: Can use either mode. Split ZIP (.z01, .z02, .zip) uses split mode.
/// Multi-volume ZIP uses volume mode.
/// </description>
/// </item>
/// </list>
/// </summary>
public class SourceStream : Stream, IStreamStack
{
#if DEBUG_STREAMS
@@ -15,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
{
@@ -31,116 +87,153 @@ public class SourceStream : Stream, IStreamStack
void IStreamStack.SetPosition(long position) { }
private long _prevSize;
private readonly List<FileInfo> _files;
private readonly List<Stream> _streams;
private readonly Func<int, FileInfo?>? _getFilePart;
private readonly Func<int, Stream?>? _getStreamPart;
private int _stream;
private readonly List<IByteSource> _sources;
private readonly List<Stream> _openStreams;
private readonly Func<int, IByteSource?>? _getNextSource;
private int _currentSourceIndex;
/// <summary>
/// Creates a SourceStream from a file, with a function to get additional file parts.
/// </summary>
/// <param name="file">The initial file to read from.</param>
/// <param name="getPart">Function that returns additional file parts by index, or null when no more parts.</param>
/// <param name="options">Reader options.</param>
public SourceStream(FileInfo file, Func<int, FileInfo?> 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
) { }
/// <summary>
/// Creates a SourceStream from a stream, with a function to get additional stream parts.
/// </summary>
/// <param name="stream">The initial stream to read from.</param>
/// <param name="getPart">Function that returns additional stream parts by index, or null when no more parts.</param>
/// <param name="options">Reader options.</param>
public SourceStream(Stream stream, Func<int, Stream?> 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<int, Stream?>? getStreamPart,
FileInfo? file,
Func<int, FileInfo?>? getFilePart,
/// <summary>
/// Creates a SourceStream from an initial byte source with a function to get additional sources.
/// </summary>
/// <param name="initialSource">The initial byte source.</param>
/// <param name="getNextSource">Function that returns additional byte sources by index, or null when no more sources.</param>
/// <param name="options">Reader options.</param>
public SourceStream(
IByteSource initialSource,
Func<int, IByteSource?>? getNextSource,
ReaderOptions options
)
{
ReaderOptions = options;
_files = new List<FileInfo>();
_streams = new List<Stream>();
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<IByteSource> { initialSource };
_openStreams = new List<Stream> { initialSource.OpenRead() };
_getNextSource = getNextSource;
_currentSourceIndex = 0;
_prevSize = 0;
IsVolumes = false;
IsFileMode = initialSource is FileByteSource;
#if DEBUG_STREAMS
this.DebugConstruct(typeof(SourceStream));
#endif
}
/// <summary>
/// Loads all available parts/volumes by calling the getNextSource function
/// until it returns null. Resets to the first stream after loading.
/// </summary>
public void LoadAllParts()
{
for (var i = 1; SetStream(i); i++) { }
SetStream(0);
}
/// <summary>
/// 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.
/// </summary>
public bool IsVolumes { get; set; }
/// <summary>
/// Gets the reader options associated with this source stream.
/// </summary>
public ReaderOptions ReaderOptions { get; }
/// <summary>
/// Gets whether this SourceStream was created from a FileInfo (true)
/// or from a Stream (false).
/// </summary>
public bool IsFileMode { get; }
public IEnumerable<FileInfo> Files => _files;
public IEnumerable<Stream> Streams => _streams;
/// <summary>
/// Gets the collection of loaded byte sources.
/// </summary>
internal IEnumerable<IByteSource> Sources => _sources;
private Stream Current => _streams[_stream];
/// <summary>
/// Gets the collection of FileInfo objects for each loaded source.
/// May be empty if sources are streams without file associations.
/// </summary>
public IEnumerable<FileInfo> Files =>
_sources.Where(s => s.FileName != null).Select(s => new FileInfo(s.FileName!));
public bool LoadStream(int index) //ensure all parts to id are loaded
/// <summary>
/// Gets the collection of underlying streams for each loaded source.
/// </summary>
public IEnumerable<Stream> Streams => _openStreams;
private Stream Current => _openStreams[_currentSourceIndex];
/// <summary>
/// Ensures that sources up to and including the specified index are loaded.
/// </summary>
/// <param name="index">The source index to load.</param>
/// <returns>True if the source at the index was successfully loaded; false otherwise.</returns>
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
/// <summary>
/// Switches to the specified source index.
/// </summary>
/// <param name="idx">The source index to switch to.</param>
/// <returns>True if the switch was successful; false otherwise.</returns>
public bool SetStream(int idx)
{
if (LoadStream(idx))
{
_stream = idx;
_currentSourceIndex = idx;
}
return _stream == idx;
return _currentSourceIndex == idx;
}
public override bool CanRead => true;
@@ -149,7 +242,7 @@ public class SourceStream : Stream, IStreamStack
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
{
@@ -183,8 +276,8 @@ public class SourceStream : Stream, IStreamStack
{
var length = Current.Length;
// Load next file if present
if (!SetStream(_stream + 1))
// Load next source if present
if (!SetStream(_currentSourceIndex + 1))
{
break;
}
@@ -223,7 +316,7 @@ public class SourceStream : Stream, IStreamStack
while (_prevSize + Current.Length < pos)
{
_prevSize += Current.Length;
SetStream(_stream + 1);
SetStream(_currentSourceIndex + 1);
}
}
@@ -272,8 +365,8 @@ public class SourceStream : Stream, IStreamStack
{
var length = Current.Length;
// Load next file if present
if (!SetStream(_stream + 1))
// Load next source if present
if (!SetStream(_currentSourceIndex + 1))
{
break;
}
@@ -321,8 +414,8 @@ public class SourceStream : Stream, IStreamStack
{
var length = Current.Length;
// Load next file if present
if (!SetStream(_stream + 1))
// Load next source if present
if (!SetStream(_currentSourceIndex + 1))
{
break;
}
@@ -343,7 +436,7 @@ public class SourceStream : Stream, IStreamStack
{
if (IsFileMode || !ReaderOptions.LeaveStreamOpen) //close if file mode or options specify it
{
foreach (var stream in _streams)
foreach (var stream in _openStreams)
{
try
{
@@ -354,8 +447,8 @@ public class SourceStream : Stream, IStreamStack
// ignored
}
}
_streams.Clear();
_files.Clear();
_openStreams.Clear();
_sources.Clear();
}
}

View File

@@ -0,0 +1,64 @@
using System.IO;
namespace SharpCompress.IO;
/// <summary>
/// A byte source backed by an existing stream.
/// </summary>
public sealed class StreamByteSource : IByteSource
{
private readonly Stream _stream;
/// <summary>
/// Creates a new stream-based byte source.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <param name="index">The index of this source in a collection.</param>
/// <param name="isPartOfContiguousSequence">Whether this is part of a split archive.</param>
public StreamByteSource(Stream stream, int index = 0, bool isPartOfContiguousSequence = false)
{
_stream = stream;
Index = index;
IsPartOfContiguousSequence = isPartOfContiguousSequence;
if (stream is FileStream fileStream)
{
FileName = fileStream.Name;
}
}
/// <inheritdoc />
public int Index { get; }
/// <inheritdoc />
public long? Length
{
get
{
try
{
return _stream.CanSeek ? _stream.Length : null;
}
catch
{
return null;
}
}
}
/// <inheritdoc />
public string? FileName { get; }
/// <inheritdoc />
public Stream OpenRead()
{
if (_stream.CanSeek)
{
_stream.Position = 0;
}
return _stream;
}
/// <inheritdoc />
public bool IsPartOfContiguousSequence { get; }
}