SevenZipReader

This commit is contained in:
Adam Hathcock
2026-02-17 11:54:32 +00:00
parent fb5b5e9a6b
commit 9c47eaf447
11 changed files with 286 additions and 118 deletions

View File

@@ -2,6 +2,8 @@
Quick reference for commonly used SharpCompress APIs.
Refer to [INTERFACES.md](INTERFACES.md) for more details.
## Factory Methods
### Opening Archives

View File

@@ -537,7 +537,7 @@ await writer.WriteAsync("file.txt", contentStream, DateTime.Now);
| **Zip** | ✅ | ✅ | ✅ | ✅ |
| **Tar** | ✅ | ✅ | ✅ | ✅ |
| **GZip** | ✅ | ✅ | ✅ | ✅ |
| **7Zip** | ✅ | ❌ (sequential only) | | ❌ |
| **7Zip** | ✅ | ❌ (sequential only) | ✅ (sequential only) | ❌ |
| **Rar** | ✅ | ✅ | ✅ (read-only) | ❌ |
| **Ace** | ❌ | ❌ | ✅ (read-only) | ❌ |
| **Arc** | ❌ | ❌ | ✅ (read-only) | ❌ |

View File

@@ -7,6 +7,7 @@ using SharpCompress.Common;
using SharpCompress.Common.SevenZip;
using SharpCompress.IO;
using SharpCompress.Readers;
using SharpCompress.Readers.SevenZip;
namespace SharpCompress.Archives.SevenZip;

View File

@@ -9,6 +9,7 @@ using SharpCompress.Common.SevenZip;
using SharpCompress.Compressors.LZMA.Utilities;
using SharpCompress.IO;
using SharpCompress.Readers;
using SharpCompress.Readers.SevenZip;
namespace SharpCompress.Archives.SevenZip;
@@ -18,6 +19,7 @@ public partial class SevenZipArchive
ISevenZipAsyncArchive
{
private ArchiveDatabase? _database;
internal ArchiveDatabase? Database => _database;
/// <summary>
/// Constructor with a SourceStream able to handle FileInfo and Streams.
@@ -106,111 +108,6 @@ public partial class SevenZipArchive
public override long TotalSize =>
_database?._packSizes.Aggregate(0L, (total, packSize) => total + packSize) ?? 0;
internal sealed class SevenZipReader
: AbstractReader<SevenZipEntry, SevenZipVolume>,
ISevenZipReader,
ISevenZipAsyncReader
{
private readonly SevenZipArchive _archive;
private SevenZipEntry? _currentEntry;
private Stream? _currentFolderStream;
private CFolder? _currentFolder;
/// <summary>
/// Enables internal diagnostics for tests.
/// When disabled (default), diagnostics properties return null to avoid exposing internal state.
/// </summary>
internal bool DiagnosticsEnabled { get; set; }
/// <summary>
/// Current folder instance used to decide whether the solid folder stream should be reused.
/// Only available when <see cref="DiagnosticsEnabled"/> is true.
/// </summary>
internal object? DiagnosticsCurrentFolder => DiagnosticsEnabled ? _currentFolder : null;
/// <summary>
/// Current shared folder stream instance.
/// Only available when <see cref="DiagnosticsEnabled"/> is true.
/// </summary>
internal Stream? DiagnosticsCurrentFolderStream =>
DiagnosticsEnabled ? _currentFolderStream : null;
internal SevenZipReader(ReaderOptions readerOptions, SevenZipArchive archive)
: base(readerOptions, ArchiveType.SevenZip, false) => this._archive = archive;
public override SevenZipVolume Volume => _archive.Volumes.Single();
protected override IEnumerable<SevenZipEntry> GetEntries(Stream stream)
{
var entries = _archive.Entries.ToList();
stream.Position = 0;
foreach (var dir in entries.Where(x => x.IsDirectory))
{
_currentEntry = dir;
yield return dir;
}
// For solid archives (entries in the same folder share a compressed stream),
// we must iterate entries sequentially and maintain the folder stream state
// across entries in the same folder to avoid recreating the decompression
// stream for each file, which breaks contiguous streaming.
foreach (var entry in entries.Where(x => !x.IsDirectory))
{
_currentEntry = entry;
yield return entry;
}
}
protected override EntryStream GetEntryStream()
{
var entry = _currentEntry.NotNull("currentEntry is not null");
if (entry.IsDirectory)
{
return CreateEntryStream(Stream.Null);
}
var folder = entry.FilePart.Folder;
// If folder is null (empty stream entry), return empty stream
if (folder is null)
{
return CreateEntryStream(Stream.Null);
}
// Check if we're starting a new folder - dispose old folder stream if needed
if (folder != _currentFolder)
{
_currentFolderStream?.Dispose();
_currentFolderStream = null;
_currentFolder = folder;
}
// Create the folder stream once per folder
if (_currentFolderStream is null)
{
_currentFolderStream = _archive._database!.GetFolderStream(
_archive.Volumes.Single().Stream,
folder!,
_archive._database.PasswordProvider
);
}
return CreateEntryStream(
new ReadOnlySubStream(_currentFolderStream, entry.Size, leaveOpen: true)
);
}
protected override ValueTask<EntryStream> GetEntryStreamAsync(
CancellationToken cancellationToken = default
) => new(GetEntryStream());
public override void Dispose()
{
_currentFolderStream?.Dispose();
_currentFolderStream = null;
base.Dispose();
}
}
/// <summary>
/// WORKAROUND: Forces async operations to use synchronous equivalents.
/// This is necessary because the LZMA decoder has bugs in its async implementation

View File

@@ -7,13 +7,14 @@ using SharpCompress.Archives.SevenZip;
using SharpCompress.Common;
using SharpCompress.IO;
using SharpCompress.Readers;
using SharpCompress.Readers.SevenZip;
namespace SharpCompress.Factories;
/// <summary>
/// Represents the foundation factory of 7Zip archive.
/// </summary>
public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory
public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReaderFactory
{
#region IFactory
@@ -110,11 +111,18 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory
SharpCompressStream sharpCompressStream,
ReaderOptions options,
out IReader? reader
)
{
reader = null;
return false;
}
) => base.TryOpenReader(sharpCompressStream, options, out reader);
/// <inheritdoc/>
public IReader OpenReader(Stream stream, ReaderOptions? options) =>
SevenZipReader.OpenReader(stream, options);
/// <inheritdoc/>
public ValueTask<IAsyncReader> OpenAsyncReader(
Stream stream,
ReaderOptions? options,
CancellationToken cancellationToken = default
) => SevenZipReader.OpenAsyncReader(stream, options, cancellationToken);
#endregion
}

View File

@@ -98,7 +98,7 @@ public static partial class ReaderFactory
}
throw new InvalidFormatException(
"Cannot determine compressed stream type. Supported Reader Formats: Arc, Arj, Zip, GZip, BZip2, Tar, Rar, LZip, XZ, ZStandard"
"Cannot determine compressed stream type. Supported Reader Formats: Arc, Arj, Zip, GZip, BZip2, Tar, Rar, SevenZip, LZip, XZ, ZStandard"
);
}
}

View File

@@ -75,7 +75,7 @@ public static partial class ReaderFactory
}
throw new InvalidFormatException(
"Cannot determine compressed stream type. Supported Reader Formats: Ace, Arc, Arj, Zip, GZip, BZip2, Tar, Rar, LZip, Lzw, XZ, ZStandard"
"Cannot determine compressed stream type. Supported Reader Formats: Ace, Arc, Arj, Zip, GZip, BZip2, Tar, Rar, SevenZip, LZip, Lzw, XZ, ZStandard"
);
}
}

View File

@@ -1,6 +1,6 @@
using SharpCompress.Readers;
namespace SharpCompress.Archives.SevenZip;
namespace SharpCompress.Readers.SevenZip;
/// <summary>
/// Reader for 7Zip archives - supports sequential extraction only.

View File

@@ -0,0 +1,221 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Archives.SevenZip;
using SharpCompress.Common;
using SharpCompress.Common.SevenZip;
using SharpCompress.IO;
namespace SharpCompress.Readers.SevenZip;
/// <summary>
/// Public 7Zip reader entry point for sequential extraction.
/// </summary>
public sealed class SevenZipReader
: AbstractReader<SevenZipEntry, SevenZipVolume>,
ISevenZipReader,
ISevenZipAsyncReader
{
private readonly SevenZipArchive _archive;
private readonly bool _disposeArchive;
private SevenZipEntry? _currentEntry;
private Stream? _currentFolderStream;
private CFolder? _currentFolder;
/// <summary>
/// Enables internal diagnostics for tests.
/// When disabled (default), diagnostics properties return null to avoid exposing internal state.
/// </summary>
internal bool DiagnosticsEnabled { get; set; }
/// <summary>
/// Current folder instance used to decide whether the solid folder stream should be reused.
/// Only available when <see cref="DiagnosticsEnabled"/> is true.
/// </summary>
internal object? DiagnosticsCurrentFolder => DiagnosticsEnabled ? _currentFolder : null;
/// <summary>
/// Current shared folder stream instance.
/// Only available when <see cref="DiagnosticsEnabled"/> is true.
/// </summary>
internal Stream? DiagnosticsCurrentFolderStream =>
DiagnosticsEnabled ? _currentFolderStream : null;
internal SevenZipReader(
ReaderOptions readerOptions,
SevenZipArchive archive,
bool disposeArchive = false
)
: base(readerOptions, ArchiveType.SevenZip, disposeVolume: false)
{
_archive = archive;
_disposeArchive = disposeArchive;
}
/// <summary>
/// Opens a 7Zip reader from a file path.
/// </summary>
public static ISevenZipReader OpenReader(string filePath, ReaderOptions? readerOptions = null)
{
filePath.NotNullOrEmpty(nameof(filePath));
return OpenReader(new FileInfo(filePath), readerOptions);
}
/// <summary>
/// Opens a 7Zip reader from a file.
/// </summary>
public static ISevenZipReader OpenReader(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
fileInfo.NotNull(nameof(fileInfo));
var options = readerOptions ?? ReaderOptions.ForOwnedFile;
return OpenReader(fileInfo.OpenRead(), options);
}
/// <summary>
/// Opens a 7Zip reader from a stream.
/// </summary>
public static ISevenZipReader OpenReader(Stream stream, ReaderOptions? readerOptions = null)
{
stream.NotNull(nameof(stream));
var options = readerOptions ?? ReaderOptions.ForExternalStream;
return new SevenZipReader(
options,
(SevenZipArchive)SevenZipArchive.OpenArchive(stream, options),
disposeArchive: true
);
}
/// <summary>
/// Opens a 7Zip reader from a file path asynchronously.
/// </summary>
public static ValueTask<IAsyncReader> OpenAsyncReader(
string filePath,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
filePath.NotNullOrEmpty(nameof(filePath));
return OpenAsyncReader(new FileInfo(filePath), readerOptions, cancellationToken);
}
/// <summary>
/// Opens a 7Zip reader from a file asynchronously.
/// </summary>
public static ValueTask<IAsyncReader> OpenAsyncReader(
FileInfo fileInfo,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
fileInfo.NotNull(nameof(fileInfo));
return OpenAsyncReader(
fileInfo.OpenRead(),
readerOptions ?? ReaderOptions.ForOwnedFile,
cancellationToken
);
}
/// <summary>
/// Opens a 7Zip reader from a stream asynchronously.
/// </summary>
public static ValueTask<IAsyncReader> OpenAsyncReader(
Stream stream,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return new((IAsyncReader)OpenReader(stream, readerOptions));
}
public override SevenZipVolume Volume => _archive.Volumes.Single();
protected override IEnumerable<SevenZipEntry> GetEntries(Stream stream)
{
var entries = _archive.Entries.ToList();
stream.Position = 0;
foreach (var dir in entries.Where(x => x.IsDirectory))
{
_currentEntry = dir;
yield return dir;
}
// For solid archives (entries in the same folder share a compressed stream),
// we must iterate entries sequentially and maintain the folder stream state
// across entries in the same folder to avoid recreating the decompression
// stream for each file, which breaks contiguous streaming.
foreach (var entry in entries.Where(x => !x.IsDirectory))
{
_currentEntry = entry;
yield return entry;
}
}
protected override EntryStream GetEntryStream()
{
var entry = _currentEntry.NotNull("currentEntry is not null");
if (entry.IsDirectory)
{
return CreateEntryStream(Stream.Null);
}
var folder = entry.FilePart.Folder;
// If folder is null (empty stream entry), return empty stream
if (folder is null)
{
return CreateEntryStream(Stream.Null);
}
// Check if we're starting a new folder - dispose old folder stream if needed
if (folder != _currentFolder)
{
_currentFolderStream?.Dispose();
_currentFolderStream = null;
_currentFolder = folder;
}
// Create the folder stream once per folder
if (_currentFolderStream is null)
{
var database = _archive.Database.NotNull("database is not loaded");
_currentFolderStream = database.GetFolderStream(
_archive.Volumes.Single().Stream,
folder,
database.PasswordProvider
);
}
return CreateEntryStream(
new ReadOnlySubStream(_currentFolderStream, entry.Size, leaveOpen: true)
);
}
protected override ValueTask<EntryStream> GetEntryStreamAsync(
CancellationToken cancellationToken = default
) => new(GetEntryStream());
public override void Dispose()
{
_currentFolderStream?.Dispose();
_currentFolderStream = null;
base.Dispose();
if (_disposeArchive)
{
_archive.Dispose();
}
}
public override async ValueTask DisposeAsync()
{
_currentFolderStream?.Dispose();
_currentFolderStream = null;
await base.DisposeAsync().ConfigureAwait(false);
if (_disposeArchive)
{
await _archive.DisposeAsync().ConfigureAwait(false);
}
}
}

View File

@@ -6,6 +6,8 @@ using SharpCompress.Archives;
using SharpCompress.Archives.SevenZip;
using SharpCompress.Readers;
using Xunit;
using PublicISevenZipAsyncReader = SharpCompress.Readers.SevenZip.ISevenZipAsyncReader;
using PublicSevenZipReader = SharpCompress.Readers.SevenZip.SevenZipReader;
namespace SharpCompress.Test.SevenZip;
@@ -39,6 +41,24 @@ public class SevenZipArchiveAsyncTests : ArchiveTests
await ExtractAllEntriesSequentiallyAsync(testArchive);
}
[Fact]
public async Task SevenZipReader_OpenAsyncReader_ReturnsSevenZipReader()
{
using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "7Zip.LZMA.7z"));
await using var reader = await PublicSevenZipReader.OpenAsyncReader(stream);
Assert.IsAssignableFrom<PublicISevenZipAsyncReader>(reader);
}
[Fact]
public async Task ReaderFactory_OpenAsyncReader_SevenZip_ReturnsSevenZipReader()
{
using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "7Zip.LZMA.7z"));
await using var reader = await ReaderFactory.OpenAsyncReader(stream);
Assert.IsAssignableFrom<PublicISevenZipAsyncReader>(reader);
}
[Fact]
public async Task SevenZipArchive_PPMd_AsyncStreamExtraction()
{
@@ -78,7 +98,7 @@ public class SevenZipArchiveAsyncTests : ArchiveTests
await using var reader = await archive.ExtractAllEntriesAsync();
var sevenZipReader = Assert.IsType<SevenZipArchive.SevenZipReader>(reader);
var sevenZipReader = Assert.IsType<PublicSevenZipReader>(reader);
sevenZipReader.DiagnosticsEnabled = true;
Stream? currentFolderStreamInstance = null;

View File

@@ -1,4 +1,3 @@
using System;
using System.IO;
using System.Linq;
using SharpCompress.Archives;
@@ -7,6 +6,7 @@ using SharpCompress.Common;
using SharpCompress.Common.SevenZip;
using SharpCompress.Factories;
using SharpCompress.Readers;
using SharpCompress.Readers.SevenZip;
using Xunit;
namespace SharpCompress.Test.SevenZip;
@@ -63,6 +63,24 @@ public class SevenZipArchiveTests : ArchiveTests
public void SevenZipArchive_LZMA2_EXE_PathRead() =>
ArchiveFileRead("7Zip.LZMA2.exe", new() { LookForHeader = true }, new SevenZipFactory());
[Fact]
public void SevenZipReader_OpenReader_StreamRead()
{
using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "7Zip.LZMA.7z"));
using var reader = SevenZipReader.OpenReader(stream);
Assert.IsAssignableFrom<ISevenZipReader>(reader);
}
[Fact]
public void ReaderFactory_OpenReader_SevenZip_ReturnsSevenZipReader()
{
using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "7Zip.LZMA.7z"));
using var reader = ReaderFactory.OpenReader(stream);
Assert.IsAssignableFrom<ISevenZipReader>(reader);
}
[Fact]
public void SevenZipArchive_LZMA2AES_StreamRead() =>
ArchiveStreamRead("7Zip.LZMA2.Aes.7z", new ReaderOptions { Password = "testpassword" });
@@ -289,7 +307,8 @@ public class SevenZipArchiveTests : ArchiveTests
using var reader = archive.ExtractAllEntries();
var sevenZipReader = Assert.IsType<SevenZipArchive.SevenZipReader>(reader);
Assert.IsType<ISevenZipReader>(reader);
SevenZipReader sevenZipReader = (SevenZipReader)reader;
sevenZipReader.DiagnosticsEnabled = true;
Stream? currentFolderStreamInstance = null;