Split DI services into focused workflow components

This commit is contained in:
Adam Hathcock
2026-10-21 09:27:47 +01:00
parent 4cc8d6b257
commit e21da75de2
29 changed files with 597 additions and 430 deletions

View File

@@ -149,8 +149,20 @@ Roots and constructor dependencies use internal interfaces generated by Pure.DI'
for composition binding and supplements the sync signatures produced by Zomp, since source
generators cannot inspect one another's generated members.
Public extension classes remain beside their public abstractions. Simple overloads supply defaults
locally; extraction and filesystem workflows delegate to `ArchiveExtractor` and `ArchiveFileWriter`,
including reader extraction, writable-archive entry creation, and saving archives to files.
locally; workflows delegate directly to the appropriate typed root:
| Services | Responsibility |
| --- | --- |
| `ArchiveService`, `ArchiveDetector`, `ArchiveInspector` | Open/create archives, recognize formats/select factories, and collect metadata respectively |
| `ArchiveExtractor` | Whole-archive extraction, including sequential solid-archive decoding |
| `ReaderExtractor`, `ArchiveEntryExtractor` | Forward-only reader extraction and individual archive-entry extraction |
| `WriterFileOperations`, `WritableArchiveFileOperations` | Writer filesystem operations and writable-archive entry ownership/saving respectively |
| `ReaderService`, `WriterService` | Open forward-only readers and writers |
The inspector depends on detection and opening, not the other way around. Archive extraction
depends on the reader and entry extractors. Shared stream decoration lives in the dependency-free
`ExtractionStreamHelper`; inspection accumulators remain fresh per-operation objects, never
singleton services. There is no aggregate service routing layer.
An injected `FactoryCatalog` supplies factories and TAR wrappers. The default catalog reads
through to `Factory.Factories` and the existing `TarWrapper.Wrappers` array rather than freezing

View File

@@ -14,6 +14,8 @@ namespace SharpCompress.Archives;
public static partial class ArchiveFactory
{
private static IArchiveService Service => LibraryDefaults.Composition.Archives;
private static IArchiveDetector Detector => LibraryDefaults.Composition.Detector;
private static IArchiveInspector Inspector => LibraryDefaults.Composition.Inspector;
public static IArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null) =>
Service.OpenArchive(stream, readerOptions);
@@ -71,81 +73,86 @@ public static partial class ArchiveFactory
string sourceArchive,
string destinationDirectory,
ExtractionOptions? options = null
) => Service.WriteToDirectory(sourceArchive, destinationDirectory, options);
) =>
LibraryDefaults.Composition.Extractor.WriteToDirectory(
sourceArchive,
destinationDirectory,
options
);
public static T FindFactory<T>(string filePath)
where T : IFactory => Service.FindFactory<T>(filePath);
where T : IFactory => Detector.FindFactory<T>(filePath);
public static T FindFactory<T>(FileInfo finfo)
where T : IFactory => Service.FindFactory<T>(finfo);
where T : IFactory => Detector.FindFactory<T>(finfo);
public static T FindFactory<T>(Stream stream)
where T : IFactory => Service.FindFactory<T>(stream);
where T : IFactory => Detector.FindFactory<T>(stream);
internal static ValueTask<T> FindFactoryAsync<T>(
string filePath,
CancellationToken cancellationToken = default
)
where T : IFactory => Service.FindFactoryAsync<T>(filePath, cancellationToken);
where T : IFactory => Detector.FindFactoryAsync<T>(filePath, cancellationToken);
internal static ValueTask<T> FindFactoryAsync<T>(
FileInfo fileInfo,
CancellationToken cancellationToken = default
)
where T : IFactory => Service.FindFactoryAsync<T>(fileInfo, cancellationToken);
where T : IFactory => Detector.FindFactoryAsync<T>(fileInfo, cancellationToken);
internal static ValueTask<T> FindFactoryAsync<T>(
Stream stream,
CancellationToken cancellationToken = default
)
where T : IFactory => Service.FindFactoryAsync<T>(stream, cancellationToken);
where T : IFactory => Detector.FindFactoryAsync<T>(stream, cancellationToken);
public static bool IsArchive(string filePath, out ArchiveType? type) =>
Service.IsArchive(filePath, out type);
Detector.IsArchive(filePath, out type);
public static bool IsArchive(
string filePath,
ReaderOptions? readerOptions,
out ArchiveType? type
) => Service.IsArchive(filePath, readerOptions, out type);
) => Detector.IsArchive(filePath, readerOptions, out type);
public static bool IsArchive(Stream stream, out ArchiveType? type) =>
Service.IsArchive(stream, out type);
Detector.IsArchive(stream, out type);
public static bool IsArchive(
Stream stream,
ReaderOptions? readerOptions,
out ArchiveType? type
) => Service.IsArchive(stream, readerOptions, out type);
) => Detector.IsArchive(stream, readerOptions, out type);
public static ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
string filePath,
CancellationToken cancellationToken = default
) => Service.IsArchiveAsync(filePath, cancellationToken);
) => Detector.IsArchiveAsync(filePath, cancellationToken);
public static ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
string filePath,
ReaderOptions? readerOptions,
CancellationToken cancellationToken = default
) => Service.IsArchiveAsync(filePath, readerOptions, cancellationToken);
) => Detector.IsArchiveAsync(filePath, readerOptions, cancellationToken);
public static ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
Stream stream,
CancellationToken cancellationToken = default
) => Service.IsArchiveAsync(stream, cancellationToken);
) => Detector.IsArchiveAsync(stream, cancellationToken);
public static ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
Stream stream,
ReaderOptions? readerOptions,
CancellationToken cancellationToken = default
) => Service.IsArchiveAsync(stream, readerOptions, cancellationToken);
) => Detector.IsArchiveAsync(stream, readerOptions, cancellationToken);
/// <summary>Identify the archive without enumerating its entries.</summary>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public static ValueTask<ArchiveDetection?> DetectArchiveAsync(
string filePath,
CancellationToken cancellationToken = default
) => Service.DetectArchiveAsync(filePath, cancellationToken);
) => Detector.DetectArchiveAsync(filePath, cancellationToken);
/// <summary>Identify the archive without enumerating its entries.</summary>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
@@ -153,14 +160,14 @@ public static partial class ArchiveFactory
string filePath,
ReaderOptions? readerOptions,
CancellationToken cancellationToken = default
) => Service.DetectArchiveAsync(filePath, readerOptions, cancellationToken);
) => Detector.DetectArchiveAsync(filePath, readerOptions, cancellationToken);
/// <summary>Identify the archive without enumerating its entries. Restore the stream position.</summary>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public static ValueTask<ArchiveDetection?> DetectArchiveAsync(
Stream stream,
CancellationToken cancellationToken = default
) => Service.DetectArchiveAsync(stream, cancellationToken);
) => Detector.DetectArchiveAsync(stream, cancellationToken);
/// <summary>Identify the archive without enumerating its entries. Restore the stream position.</summary>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
@@ -168,14 +175,14 @@ public static partial class ArchiveFactory
Stream stream,
ReaderOptions? readerOptions,
CancellationToken cancellationToken = default
) => Service.DetectArchiveAsync(stream, readerOptions, cancellationToken);
) => Detector.DetectArchiveAsync(stream, readerOptions, cancellationToken);
/// <summary>Inspect archive metadata.</summary>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public static ValueTask<ArchiveInformation?> InspectArchiveAsync(
string filePath,
CancellationToken cancellationToken = default
) => Service.InspectArchiveAsync(filePath, cancellationToken);
) => Inspector.InspectArchiveAsync(filePath, cancellationToken);
/// <summary>Inspect archive metadata.</summary>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
@@ -183,14 +190,14 @@ public static partial class ArchiveFactory
string filePath,
ReaderOptions? readerOptions,
CancellationToken cancellationToken = default
) => Service.InspectArchiveAsync(filePath, readerOptions, cancellationToken);
) => Inspector.InspectArchiveAsync(filePath, readerOptions, cancellationToken);
/// <summary>Inspect archive metadata, leaving the stream open at its original position.</summary>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public static ValueTask<ArchiveInformation?> InspectArchiveAsync(
Stream stream,
CancellationToken cancellationToken = default
) => Service.InspectArchiveAsync(stream, cancellationToken);
) => Inspector.InspectArchiveAsync(stream, cancellationToken);
/// <summary>Inspect archive metadata, leaving the stream open at its original position.</summary>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
@@ -198,7 +205,7 @@ public static partial class ArchiveFactory
Stream stream,
ReaderOptions? readerOptions,
CancellationToken cancellationToken = default
) => Service.InspectArchiveAsync(stream, readerOptions, cancellationToken);
) => Inspector.InspectArchiveAsync(stream, readerOptions, cancellationToken);
/// <summary>Inspect multipart archive metadata.</summary>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
@@ -206,7 +213,7 @@ public static partial class ArchiveFactory
IReadOnlyList<FileInfo> fileInfos,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
) => Service.InspectArchiveAsync(fileInfos, readerOptions, cancellationToken);
) => Inspector.InspectArchiveAsync(fileInfos, readerOptions, cancellationToken);
/// <summary>Inspect multipart archive metadata, leaving the streams open at their original positions.</summary>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
@@ -214,7 +221,7 @@ public static partial class ArchiveFactory
IReadOnlyList<Stream> streams,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
) => Service.InspectArchiveAsync(streams, readerOptions, cancellationToken);
) => Inspector.InspectArchiveAsync(streams, readerOptions, cancellationToken);
public static IEnumerable<string> GetFileParts(string part1) => Service.GetFileParts(part1);

View File

@@ -10,7 +10,7 @@ namespace SharpCompress.Archives;
public static partial class IArchiveEntryExtensions
{
// Give the sync generator a known receiver type before Pure.DI generates the root property.
private static IArchiveExtractor Extractor => LibraryDefaults.Composition.Extractor;
private static IArchiveEntryExtractor Extractor => LibraryDefaults.Composition.EntryExtractor;
/// <param name="archiveEntry">The archive entry to extract.</param>
extension(IArchiveEntry archiveEntry)

View File

@@ -14,7 +14,7 @@ public static class IWritableArchiveExtensions
string searchPattern = "*.*",
SearchOption searchOption = SearchOption.AllDirectories
) =>
LibraryDefaults.Composition.FileWriter.AddAllFromDirectory(
LibraryDefaults.Composition.ArchiveFiles.AddAllFromDirectory(
writableArchive,
directoryPath,
searchPattern,
@@ -32,7 +32,7 @@ public static class IWritableArchiveExtensions
) => writableArchive.AddEntry(key, source, false, size, modified);
public IArchiveEntry AddEntry(string key, FileInfo fileInfo) =>
LibraryDefaults.Composition.FileWriter.AddEntry(writableArchive, key, fileInfo);
LibraryDefaults.Composition.ArchiveFiles.AddEntry(writableArchive, key, fileInfo);
}
public static void SaveTo<TOptions>(
@@ -48,5 +48,5 @@ public static class IWritableArchiveExtensions
TOptions options
)
where TOptions : IWriterOptions =>
LibraryDefaults.Composition.FileWriter.SaveTo(writableArchive, fileInfo, options);
LibraryDefaults.Composition.ArchiveFiles.SaveTo(writableArchive, fileInfo, options);
}

View File

@@ -16,7 +16,7 @@ public static class IWritableAsyncArchiveExtensions
string searchPattern = "*.*",
SearchOption searchOption = SearchOption.AllDirectories
) =>
LibraryDefaults.Composition.FileWriter.AddAllFromDirectoryAsync(
LibraryDefaults.Composition.ArchiveFiles.AddAllFromDirectoryAsync(
writableArchive,
directoryPath,
searchPattern,
@@ -34,7 +34,7 @@ public static class IWritableAsyncArchiveExtensions
) => writableArchive.AddEntryAsync(key, source, false, size, modified);
public ValueTask<IArchiveEntry> AddEntryAsync(string key, FileInfo fileInfo) =>
LibraryDefaults.Composition.FileWriter.AddEntryAsync(writableArchive, key, fileInfo);
LibraryDefaults.Composition.ArchiveFiles.AddEntryAsync(writableArchive, key, fileInfo);
}
public static ValueTask SaveToAsync<TOptions>(
@@ -54,7 +54,7 @@ public static class IWritableAsyncArchiveExtensions
)
where TOptions : IWriterOptions =>
await LibraryDefaults
.Composition.FileWriter.SaveToAsync(
.Composition.ArchiveFiles.SaveToAsync(
writableArchive,
fileInfo,
options,

View File

@@ -16,7 +16,7 @@ public static class IAsyncReaderExtensions
ExtractionOptions? options = null,
CancellationToken cancellationToken = default
) =>
LibraryDefaults.Composition.Extractor.WriteEntryToDirectoryAsync(
LibraryDefaults.Composition.ReaderExtractor.WriteEntryToDirectoryAsync(
reader,
destinationDirectory,
options,
@@ -29,7 +29,7 @@ public static class IAsyncReaderExtensions
ExtractionOptions? options = null,
CancellationToken cancellationToken = default
) =>
LibraryDefaults.Composition.Extractor.WriteEntryToFileAsync(
LibraryDefaults.Composition.ReaderExtractor.WriteEntryToFileAsync(
reader,
destinationFileName,
options,
@@ -42,7 +42,7 @@ public static class IAsyncReaderExtensions
ExtractionOptions? options = null,
CancellationToken cancellationToken = default
) =>
LibraryDefaults.Composition.Extractor.WriteAllToDirectoryAsync(
LibraryDefaults.Composition.ReaderExtractor.WriteAllToDirectoryAsync(
reader,
destinationDirectory,
options,
@@ -54,7 +54,7 @@ public static class IAsyncReaderExtensions
ExtractionOptions? options = null,
CancellationToken cancellationToken = default
) =>
LibraryDefaults.Composition.Extractor.WriteEntryToFileAsync(
LibraryDefaults.Composition.ReaderExtractor.WriteEntryToFileAsync(
reader,
destinationFileName,
options,

View File

@@ -9,17 +9,17 @@ public static class IReaderExtensions
extension(IReader reader)
{
public void WriteEntryTo(string filePath) =>
LibraryDefaults.Composition.Extractor.WriteEntryTo(reader, filePath);
LibraryDefaults.Composition.ReaderExtractor.WriteEntryTo(reader, filePath);
public void WriteEntryTo(FileInfo filePath) =>
LibraryDefaults.Composition.Extractor.WriteEntryTo(reader, filePath);
LibraryDefaults.Composition.ReaderExtractor.WriteEntryTo(reader, filePath);
/// <summary>Extract all remaining unread entries to specific directory, retaining filename.</summary>
public void WriteAllToDirectory(
string destinationDirectory,
ExtractionOptions? options = null
) =>
LibraryDefaults.Composition.Extractor.WriteAllToDirectory(
LibraryDefaults.Composition.ReaderExtractor.WriteAllToDirectory(
reader,
destinationDirectory,
options
@@ -30,7 +30,7 @@ public static class IReaderExtensions
string destinationDirectory,
ExtractionOptions? options = null
) =>
LibraryDefaults.Composition.Extractor.WriteEntryToDirectory(
LibraryDefaults.Composition.ReaderExtractor.WriteEntryToDirectory(
reader,
destinationDirectory,
options
@@ -41,7 +41,7 @@ public static class IReaderExtensions
string destinationFileName,
ExtractionOptions? options = null
) =>
LibraryDefaults.Composition.Extractor.WriteEntryToFile(
LibraryDefaults.Composition.ReaderExtractor.WriteEntryToFile(
reader,
destinationFileName,
options

View File

@@ -13,7 +13,7 @@ using SharpCompress.Readers;
namespace SharpCompress.Services;
internal sealed partial class ArchiveService
internal sealed partial class ArchiveDetector
{
/// <summary>
/// Identifies the archive at the given file path without enumerating its entries.
@@ -110,7 +110,7 @@ internal sealed partial class ArchiveService
}
[Zomp.SyncMethodGenerator.CreateSyncVersion]
private async ValueTask<T> FindFactoryAsync<T>(
public async ValueTask<T> FindFactoryAsync<T>(
FileInfo fileInfo,
ReaderOptions readerOptions,
CancellationToken cancellationToken
@@ -141,7 +141,7 @@ internal sealed partial class ArchiveService
}
[Zomp.SyncMethodGenerator.CreateSyncVersion]
private async ValueTask<T> FindFactoryAsync<T>(
public async ValueTask<T> FindFactoryAsync<T>(
Stream stream,
ReaderOptions readerOptions,
CancellationToken cancellationToken
@@ -171,7 +171,7 @@ internal sealed partial class ArchiveService
/// Stream position is restored to its value at entry on both success and failure.
/// </summary>
[Zomp.SyncMethodGenerator.CreateSyncVersion]
private async ValueTask<IFactory?> TryFindFactoryAsync(
public async ValueTask<IFactory?> TryFindFactoryAsync(
Stream stream,
ReaderOptions readerOptions,
CancellationToken cancellationToken
@@ -270,7 +270,7 @@ internal sealed partial class ArchiveService
}
[Zomp.SyncMethodGenerator.CreateSyncVersion]
private async ValueTask<ArchiveDetection?> TryDetectArchiveAsync(
public async ValueTask<ArchiveDetection?> TryDetectArchiveAsync(
Stream stream,
ReaderOptions readerOptions,
CancellationToken cancellationToken

View File

@@ -7,7 +7,7 @@ using SharpCompress.Readers;
namespace SharpCompress.Services;
internal sealed partial class ArchiveService
internal sealed partial class ArchiveDetector
{
private static void DisposeProbeStream(Stream stream)
{

View File

@@ -0,0 +1,101 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Pure.DI;
using SharpCompress.Common;
using SharpCompress.Factories;
using SharpCompress.Readers;
namespace SharpCompress.Services;
[GenerateInterface(asInternal: true)]
internal sealed partial class ArchiveDetector(IFactoryCatalog catalog) : IArchiveDetector
{
public T FindFactory<T>(string filePath)
where T : IFactory
{
filePath.NotNullOrEmpty(nameof(filePath));
using Stream stream = File.OpenRead(filePath);
return FindFactory<T>(stream);
}
public T FindFactory<T>(FileInfo finfo)
where T : IFactory
{
finfo.NotNull(nameof(finfo));
using Stream stream = finfo.OpenRead();
return FindFactory<T>(stream);
}
public T FindFactory<T>(Stream stream)
where T : IFactory => FindFactory<T>(stream, ReaderOptions.ForExternalStream);
public bool IsArchive(string filePath, out ArchiveType? type) =>
IsArchive(filePath, ReaderOptions.ForFilePath, out type);
public bool IsArchive(string filePath, ReaderOptions? readerOptions, out ArchiveType? type)
{
filePath.NotNullOrEmpty(nameof(filePath));
using Stream stream = File.OpenRead(filePath);
return IsArchive(stream, readerOptions ?? ReaderOptions.ForFilePath, out type);
}
public bool IsArchive(Stream stream, out ArchiveType? type) =>
IsArchive(stream, ReaderOptions.ForExternalStream, out type);
public bool IsArchive(Stream stream, ReaderOptions? readerOptions, out ArchiveType? type)
{
stream.RequireReadable();
stream.RequireSeekable();
var factory = TryFindFactory(stream, readerOptions ?? ReaderOptions.ForExternalStream);
type = factory?.KnownArchiveType;
return factory is not null;
}
public async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
string filePath,
CancellationToken cancellationToken = default
) =>
await IsArchiveAsync(filePath, ReaderOptions.ForFilePath, cancellationToken)
.ConfigureAwait(false);
public async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
string filePath,
ReaderOptions? readerOptions,
CancellationToken cancellationToken = default
)
{
filePath.NotNullOrEmpty(nameof(filePath));
using Stream stream = File.OpenRead(filePath);
return await IsArchiveAsync(
stream,
readerOptions ?? ReaderOptions.ForFilePath,
cancellationToken
)
.ConfigureAwait(false);
}
public async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
Stream stream,
CancellationToken cancellationToken = default
) =>
await IsArchiveAsync(stream, ReaderOptions.ForExternalStream, cancellationToken)
.ConfigureAwait(false);
public async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
Stream stream,
ReaderOptions? readerOptions,
CancellationToken cancellationToken = default
)
{
stream.RequireReadable();
stream.RequireSeekable();
var factory = await TryFindFactoryAsync(
stream,
readerOptions ?? ReaderOptions.ForExternalStream,
cancellationToken
)
.ConfigureAwait(false);
return (factory is not null, factory?.KnownArchiveType);
}
}

View File

@@ -6,7 +6,7 @@ using SharpCompress.Common;
namespace SharpCompress.Services;
internal sealed partial class ArchiveExtractor
internal sealed partial class ArchiveEntryExtractor
{
[Zomp.SyncMethodGenerator.CreateSyncVersion]
public async ValueTask WriteToDirectoryAsync(

View File

@@ -0,0 +1,50 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Pure.DI;
using SharpCompress.Archives;
using SharpCompress.Common;
namespace SharpCompress.Services;
// Destination streams are borrowed, not owned by the reusable extraction service.
[GenerateInterface(asInternal: true)]
internal sealed partial class ArchiveEntryExtractor : IArchiveEntryExtractor
{
[Zomp.SyncMethodGenerator.CreateSyncVersion(PreserveProgress = true)]
public async ValueTask WriteToAsync(
IArchiveEntry archiveEntry,
Stream streamToWriteTo,
int? bufferSize,
ExtractionOptions? options = null,
IProgress<ProgressReport>? progress = null,
CancellationToken cancellationToken = default
)
{
if (archiveEntry.IsDirectory)
{
throw new ExtractionException("Entry is a file directory and cannot be extracted.");
}
#if SYNC_ONLY
using var entryStream = archiveEntry.OpenEntryStream();
#else
var entryStream = await archiveEntry
.OpenEntryStreamAsync(cancellationToken)
.ConfigureAwait(false);
await using var entryStreamScope = entryStream.DisposeAsyncScope().ConfigureAwait(false);
#endif
var checkedStream = options is null
? entryStream
: IEntryExtensions.WrapWithChecksumValidation(archiveEntry, entryStream, options);
var sourceStream = ExtractionStreamHelper.WrapWithProgress(
checkedStream,
archiveEntry,
progress
);
await sourceStream
.CopyToAsync(streamToWriteTo, bufferSize ?? Constants.BufferSize, cancellationToken)
.ConfigureAwait(false);
}
}

View File

@@ -1,67 +0,0 @@
using System;
using SharpCompress.Archives;
using SharpCompress.Common;
using SharpCompress.Readers;
namespace SharpCompress.Services;
internal sealed partial class ArchiveExtractor
{
/// <summary>
/// Extract to specific directory with progress reporting
/// </summary>
/// <param name="destinationDirectory">The folder to extract into.</param>
/// <param name="options">Extraction options.</param>
/// <param name="progress">Optional progress reporter for tracking extraction progress.</param>
public void WriteToDirectory(
IArchive archive,
string destinationDirectory,
ExtractionOptions? options = null,
IProgress<ProgressReport>? progress = null
)
{
if (archive.IsSolid || archive.Type == ArchiveType.SevenZip)
{
using var reader = archive.ExtractAllEntries();
WriteAllToDirectory(reader, destinationDirectory, options);
}
else
{
WriteToDirectoryInternal(archive, destinationDirectory, options, progress);
}
}
private void WriteToDirectoryInternal(
IArchive archive,
string destinationDirectory,
ExtractionOptions? options,
IProgress<ProgressReport>? progress
)
{
options ??= new ExtractionOptions();
var fullDestinationDirectoryPath = DirectoryManagement.GetFullDestinationDirectoryPath(
destinationDirectory
);
var totalBytes = archive.TotalUncompressedSize;
var bytesRead = 0L;
foreach (var entry in archive.Entries)
{
if (entry.IsDirectory)
{
entry.WriteEntryToDirectoryCore(fullDestinationDirectoryPath, options, null);
continue;
}
entry.WriteEntryToDirectoryCore(
fullDestinationDirectoryPath,
options,
path => WriteToFile(entry, path, options)
);
bytesRead += entry.Size;
progress?.Report(new ProgressReport(entry.Key ?? string.Empty, bytesRead, totalBytes));
}
}
}

View File

@@ -36,7 +36,8 @@ internal sealed partial class ArchiveExtractor
{
cancellationToken.ThrowIfCancellationRequested();
await WriteEntryToDirectoryAsync(
await readers
.WriteEntryToDirectoryAsync(
reader,
destinationDirectory,
options,
@@ -110,7 +111,9 @@ internal sealed partial class ArchiveExtractor
fullDestinationDirectoryPath,
options,
async (path, ct) =>
await WriteToFileAsync(entry, path, options, ct).ConfigureAwait(false),
await entries
.WriteToFileAsync(entry, path, options, ct)
.ConfigureAwait(false),
cancellationToken
)
.ConfigureAwait(false);

View File

@@ -1,82 +1,83 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Pure.DI;
using SharpCompress.Archives;
using SharpCompress.Common;
using SharpCompress.IO;
using SharpCompress.Readers;
namespace SharpCompress.Services;
// Destination streams are borrowed, not owned by the reusable extraction service.
[GenerateInterface(asInternal: true)]
internal sealed partial class ArchiveExtractor : IArchiveExtractor
internal sealed partial class ArchiveExtractor(
IArchiveService archives,
IReaderExtractor readers,
IArchiveEntryExtractor entries
) : IArchiveExtractor
{
[Zomp.SyncMethodGenerator.CreateSyncVersion(PreserveProgress = true)]
public async ValueTask WriteToAsync(
IArchiveEntry archiveEntry,
Stream streamToWriteTo,
int? bufferSize,
ExtractionOptions? options = null,
IProgress<ProgressReport>? progress = null,
CancellationToken cancellationToken = default
public void WriteToDirectory(
string sourceArchive,
string destinationDirectory,
ExtractionOptions? options = null
)
{
if (archiveEntry.IsDirectory)
{
throw new ExtractionException("Entry is a file directory and cannot be extracted.");
}
#if SYNC_ONLY
using var entryStream = archiveEntry.OpenEntryStream();
#else
var entryStream = await archiveEntry
.OpenEntryStreamAsync(cancellationToken)
.ConfigureAwait(false);
await using var entryStreamScope = entryStream.DisposeAsyncScope().ConfigureAwait(false);
#endif
var checkedStream = options is null
? entryStream
: IEntryExtensions.WrapWithChecksumValidation(archiveEntry, entryStream, options);
var sourceStream = WrapWithProgress(checkedStream, archiveEntry, progress);
await sourceStream
.CopyToAsync(streamToWriteTo, bufferSize ?? Constants.BufferSize, cancellationToken)
.ConfigureAwait(false);
using var archive = archives.OpenArchive(sourceArchive);
WriteToDirectory(archive, destinationDirectory, options);
}
private static Stream WrapWithProgress(
Stream source,
IEntry entry,
/// <summary>
/// Extract to specific directory with progress reporting
/// </summary>
/// <param name="destinationDirectory">The folder to extract into.</param>
/// <param name="options">Extraction options.</param>
/// <param name="progress">Optional progress reporter for tracking extraction progress.</param>
public void WriteToDirectory(
IArchive archive,
string destinationDirectory,
ExtractionOptions? options = null,
IProgress<ProgressReport>? progress = null
)
{
if (archive.IsSolid || archive.Type == ArchiveType.SevenZip)
{
using var reader = archive.ExtractAllEntries();
readers.WriteAllToDirectory(reader, destinationDirectory, options);
}
else
{
WriteToDirectoryInternal(archive, destinationDirectory, options, progress);
}
}
private void WriteToDirectoryInternal(
IArchive archive,
string destinationDirectory,
ExtractionOptions? options,
IProgress<ProgressReport>? progress
)
{
if (progress is null)
{
return source;
}
var entryPath = entry.Key ?? string.Empty;
var totalBytes = GetEntrySizeSafe(entry);
return new ProgressReportingStream(
source,
progress,
entryPath,
totalBytes,
leaveOpen: true
options ??= new ExtractionOptions();
var fullDestinationDirectoryPath = DirectoryManagement.GetFullDestinationDirectoryPath(
destinationDirectory
);
}
private static long? GetEntrySizeSafe(IEntry entry)
{
try
var totalBytes = archive.TotalUncompressedSize;
var bytesRead = 0L;
foreach (var entry in archive.Entries)
{
var size = entry.Size;
return size >= 0 ? size : null;
}
catch (NotImplementedException)
{
return null;
if (entry.IsDirectory)
{
entry.WriteEntryToDirectoryCore(fullDestinationDirectoryPath, options, null);
continue;
}
entry.WriteEntryToDirectoryCore(
fullDestinationDirectoryPath,
options,
path => entries.WriteToFile(entry, path, options)
);
bytesRead += entry.Size;
progress?.Report(new ProgressReport(entry.Key ?? string.Empty, bytesRead, totalBytes));
}
}
}

View File

@@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Pure.DI;
using SharpCompress.Archives;
using SharpCompress.Archives.Rar;
using SharpCompress.Archives.SevenZip;
@@ -18,7 +19,12 @@ using SharpCompress.Readers;
namespace SharpCompress.Services;
internal sealed partial class ArchiveService
[GenerateInterface(asInternal: true)]
internal sealed partial class ArchiveInspector(
IArchiveDetector detector,
IArchiveService archives,
IReaderService readers
) : IArchiveInspector
{
/// <summary>
/// Collects metadata for the archive at the given file path.
@@ -50,7 +56,8 @@ internal sealed partial class ArchiveService
{
filePath.NotNullOrEmpty(nameof(filePath));
var options = readerOptions ?? ReaderOptions.ForFilePath;
var detection = await DetectArchiveAsync(filePath, options, cancellationToken)
var detection = await detector
.DetectArchiveAsync(filePath, options, cancellationToken)
.ConfigureAwait(false);
if (detection is null)
{
@@ -113,11 +120,8 @@ internal sealed partial class ArchiveService
{
using var archiveStream = new ArchiveOffsetStream(stream);
var inspectionOptions = options with { LeaveStreamOpen = true };
var detection = await TryDetectArchiveAsync(
archiveStream,
inspectionOptions,
cancellationToken
)
var detection = await detector
.TryDetectArchiveAsync(archiveStream, inspectionOptions, cancellationToken)
.ConfigureAwait(false);
if (detection is null)
{
@@ -127,14 +131,11 @@ internal sealed partial class ArchiveService
if ((detection.SupportedApis & ArchiveAccessMode.Archive) != 0)
{
#if SYNC_ONLY
using var archive = OpenArchive(archiveStream, inspectionOptions);
using var archive = archives.OpenArchive(archiveStream, inspectionOptions);
return InspectOpenedArchive(archive, detection, physicalSize, 1);
#else
await using var archive = await OpenAsyncArchive(
archiveStream,
inspectionOptions,
cancellationToken
)
await using var archive = await archives
.OpenAsyncArchive(archiveStream, inspectionOptions, cancellationToken)
.ConfigureAwait(false);
return await InspectOpenedArchiveAsync(
archive,
@@ -245,7 +246,8 @@ internal sealed partial class ArchiveService
}
var options = readerOptions ?? ReaderOptions.ForFilePath;
var detection = await DetectArchiveAsync(fileInfos[0].FullName, options, cancellationToken)
var detection = await detector
.DetectArchiveAsync(fileInfos[0].FullName, options, cancellationToken)
.ConfigureAwait(false);
if (detection is null)
{
@@ -262,10 +264,11 @@ internal sealed partial class ArchiveService
try
{
#if SYNC_ONLY
using var archive = OpenArchive(fileInfos, options);
using var archive = archives.OpenArchive(fileInfos, options);
return InspectOpenedArchive(archive, detection, physicalSize, fileInfos.Count);
#else
await using var archive = await OpenAsyncArchive(fileInfos, options, cancellationToken)
await using var archive = await archives
.OpenAsyncArchive(fileInfos, options, cancellationToken)
.ConfigureAwait(false);
return await InspectOpenedArchiveAsync(
archive,
@@ -321,11 +324,8 @@ internal sealed partial class ArchiveService
{
using var firstArchiveStream = new ArchiveOffsetStream(streams[0]);
var inspectionOptions = options with { LeaveStreamOpen = true };
var detection = await TryDetectArchiveAsync(
firstArchiveStream,
inspectionOptions,
cancellationToken
)
var detection = await detector
.TryDetectArchiveAsync(firstArchiveStream, inspectionOptions, cancellationToken)
.ConfigureAwait(false);
if (detection is null)
{
@@ -343,14 +343,11 @@ internal sealed partial class ArchiveService
streams.Skip(1).Select(stream => new ArchiveOffsetStream(stream))
);
#if SYNC_ONLY
using var archive = OpenArchive(archiveStreams, inspectionOptions);
using var archive = archives.OpenArchive(archiveStreams, inspectionOptions);
return InspectOpenedArchive(archive, detection, physicalSize, streams.Count);
#else
await using var archive = await OpenAsyncArchive(
archiveStreams,
inspectionOptions,
cancellationToken
)
await using var archive = await archives
.OpenAsyncArchive(archiveStreams, inspectionOptions, cancellationToken)
.ConfigureAwait(false);
return await InspectOpenedArchiveAsync(
archive,
@@ -607,7 +604,8 @@ internal sealed partial class ArchiveService
{
stream.Seek(startPosition, SeekOrigin.Begin);
using var archiveStream = new ArchiveOffsetStream(stream);
return await TryDetectArchiveAsync(archiveStream, options, cancellationToken)
return await detector
.TryDetectArchiveAsync(archiveStream, options, cancellationToken)
.ConfigureAwait(false);
}
}

View File

@@ -16,7 +16,7 @@ using AceMainHeader = SharpCompress.Common.Ace.Headers.AceMainHeader;
namespace SharpCompress.Services;
internal sealed partial class ArchiveService
internal sealed partial class ArchiveInspector
{
private static ArchiveInformation InspectOpenedArchive(
IArchive archive,
@@ -235,7 +235,7 @@ internal sealed partial class ArchiveService
private FileInfo[] GetArchiveFileParts(FileInfo firstPart, ReaderOptions options)
{
using Stream stream = firstPart.OpenRead();
var factory = TryFindFactory(stream, options);
var factory = detector.TryFindFactory(stream, options);
if (factory is null)
{
return [firstPart];

View File

@@ -18,11 +18,8 @@ internal sealed partial class ArchiveService
)
{
readerOptions ??= ReaderOptions.ForExternalStream;
var factory = await FindFactoryAsync<IArchiveFactory>(
stream,
readerOptions,
cancellationToken
)
var factory = await detector
.FindFactoryAsync<IArchiveFactory>(stream, readerOptions, cancellationToken)
.ConfigureAwait(false);
return await factory
.OpenAsyncArchive(stream, readerOptions, cancellationToken)
@@ -51,7 +48,8 @@ internal sealed partial class ArchiveService
{
options ??= ReaderOptions.ForFilePath;
var factory = await FindFactoryAsync<IArchiveFactory>(fileInfo, options, cancellationToken)
var factory = await detector
.FindFactoryAsync<IArchiveFactory>(fileInfo, options, cancellationToken)
.ConfigureAwait(false);
return await factory
.OpenAsyncArchive(fileInfo, options, cancellationToken)
@@ -81,11 +79,8 @@ internal sealed partial class ArchiveService
fileInfo.NotNull(nameof(fileInfo));
options ??= ReaderOptions.ForFilePath;
var factory = await FindFactoryAsync<IMultiArchiveFactory>(
fileInfo,
options,
cancellationToken
)
var factory = await detector
.FindFactoryAsync<IMultiArchiveFactory>(fileInfo, options, cancellationToken)
.ConfigureAwait(false);
return await factory
.OpenAsyncArchive(filesArray, options, cancellationToken)
@@ -110,11 +105,8 @@ internal sealed partial class ArchiveService
firstStream.NotNull(nameof(firstStream));
options ??= ReaderOptions.ForExternalStream;
var factory = await FindFactoryAsync<IMultiArchiveFactory>(
firstStream,
options,
cancellationToken
)
var factory = await detector
.FindFactoryAsync<IMultiArchiveFactory>(firstStream, options, cancellationToken)
.ConfigureAwait(false);
return await factory
.OpenAsyncArchive(streamsArray, options, cancellationToken)

View File

@@ -2,8 +2,6 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Pure.DI;
using SharpCompress.Archives;
using SharpCompress.Common;
@@ -14,16 +12,14 @@ using SharpCompress.Readers;
namespace SharpCompress.Services;
[GenerateInterface(asInternal: true)]
internal sealed partial class ArchiveService(
IFactoryCatalog catalog,
IReaderService readers,
IArchiveExtractor extractor
) : IArchiveService
internal sealed partial class ArchiveService(IFactoryCatalog catalog, IArchiveDetector detector)
: IArchiveService
{
public IArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null)
{
readerOptions ??= ReaderOptions.ForExternalStream;
return FindFactory<IArchiveFactory>(stream, readerOptions)
return detector
.FindFactory<IArchiveFactory>(stream, readerOptions)
.OpenArchive(stream, readerOptions);
}
@@ -52,7 +48,9 @@ internal sealed partial class ArchiveService(
{
options ??= ReaderOptions.ForFilePath;
return FindFactory<IArchiveFactory>(fileInfo, options).OpenArchive(fileInfo, options);
return detector
.FindFactory<IArchiveFactory>(fileInfo, options)
.OpenArchive(fileInfo, options);
}
public IArchive OpenArchive(IReadOnlyList<FileInfo> fileInfos, ReaderOptions? options = null)
@@ -73,7 +71,8 @@ internal sealed partial class ArchiveService(
fileInfo.NotNull(nameof(fileInfo));
options ??= ReaderOptions.ForFilePath;
return FindFactory<IMultiArchiveFactory>(fileInfo, options)
return detector
.FindFactory<IMultiArchiveFactory>(fileInfo, options)
.OpenArchive(filesArray, options);
}
@@ -94,114 +93,11 @@ internal sealed partial class ArchiveService(
firstStream.NotNull(nameof(firstStream));
options ??= ReaderOptions.ForExternalStream;
return FindFactory<IMultiArchiveFactory>(firstStream, options)
return detector
.FindFactory<IMultiArchiveFactory>(firstStream, options)
.OpenArchive(streamsArray, options);
}
public void WriteToDirectory(
string sourceArchive,
string destinationDirectory,
ExtractionOptions? options = null
)
{
using var archive = OpenArchive(sourceArchive);
extractor.WriteToDirectory(archive, destinationDirectory, options);
}
public T FindFactory<T>(string filePath)
where T : IFactory
{
filePath.NotNullOrEmpty(nameof(filePath));
using Stream stream = File.OpenRead(filePath);
return FindFactory<T>(stream);
}
public T FindFactory<T>(FileInfo finfo)
where T : IFactory
{
finfo.NotNull(nameof(finfo));
using Stream stream = finfo.OpenRead();
return FindFactory<T>(stream);
}
public T FindFactory<T>(Stream stream)
where T : IFactory => FindFactory<T>(stream, ReaderOptions.ForExternalStream);
public bool IsArchive(string filePath, out ArchiveType? type)
{
return IsArchive(filePath, ReaderOptions.ForFilePath, out type);
}
public bool IsArchive(string filePath, ReaderOptions? readerOptions, out ArchiveType? type)
{
filePath.NotNullOrEmpty(nameof(filePath));
using Stream s = File.OpenRead(filePath);
return IsArchive(s, readerOptions ?? ReaderOptions.ForFilePath, out type);
}
public bool IsArchive(Stream stream, out ArchiveType? type)
{
return IsArchive(stream, ReaderOptions.ForExternalStream, out type);
}
public bool IsArchive(Stream stream, ReaderOptions? readerOptions, out ArchiveType? type)
{
stream.RequireReadable();
stream.RequireSeekable();
var factory = TryFindFactory(stream, readerOptions ?? ReaderOptions.ForExternalStream);
type = factory?.KnownArchiveType;
return factory is not null;
}
public async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
string filePath,
CancellationToken cancellationToken = default
) =>
await IsArchiveAsync(filePath, ReaderOptions.ForFilePath, cancellationToken)
.ConfigureAwait(false);
public async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
string filePath,
ReaderOptions? readerOptions,
CancellationToken cancellationToken = default
)
{
filePath.NotNullOrEmpty(nameof(filePath));
using Stream stream = File.OpenRead(filePath);
return await IsArchiveAsync(
stream,
readerOptions ?? ReaderOptions.ForFilePath,
cancellationToken
)
.ConfigureAwait(false);
}
public async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
Stream stream,
CancellationToken cancellationToken = default
) =>
await IsArchiveAsync(stream, ReaderOptions.ForExternalStream, cancellationToken)
.ConfigureAwait(false);
public async ValueTask<(bool IsArchive, ArchiveType? Type)> IsArchiveAsync(
Stream stream,
ReaderOptions? readerOptions,
CancellationToken cancellationToken = default
)
{
stream.RequireReadable();
stream.RequireSeekable();
var factory = await TryFindFactoryAsync(
stream,
readerOptions ?? ReaderOptions.ForExternalStream,
cancellationToken
)
.ConfigureAwait(false);
return (factory is not null, factory?.KnownArchiveType);
}
public IEnumerable<string> GetFileParts(string part1)
{
part1.NotNullOrEmpty(nameof(part1));

View File

@@ -0,0 +1,42 @@
using System;
using System.IO;
using SharpCompress.Common;
using SharpCompress.IO;
namespace SharpCompress.Services;
// Pure stream decoration shared by entry and reader workflows; it never takes stream ownership.
internal static class ExtractionStreamHelper
{
public static Stream WrapWithProgress(
Stream source,
IEntry entry,
IProgress<ProgressReport>? progress
)
{
if (progress is null)
{
return source;
}
return new ProgressReportingStream(
source,
progress,
entry.Key ?? string.Empty,
GetEntrySizeSafe(entry),
leaveOpen: true
);
}
private static long? GetEntrySizeSafe(IEntry entry)
{
try
{
var size = entry.Size;
return size >= 0 ? size : null;
}
catch (NotImplementedException)
{
return null;
}
}
}

View File

@@ -1,14 +1,12 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.IO;
using SharpCompress.Readers;
namespace SharpCompress.Services;
internal sealed partial class ArchiveExtractor
internal sealed partial class ReaderExtractor
{
/// <summary>
/// Extract to specific directory asynchronously, retaining filename
@@ -92,7 +90,11 @@ internal sealed partial class ArchiveExtractor
entryStream,
options
);
var sourceStream = WrapWithProgress(checkedStream, reader.Entry);
var sourceStream = ExtractionStreamHelper.WrapWithProgress(
checkedStream,
reader.Entry,
reader.Entry.Options.Progress
);
await sourceStream
.CopyToAsync(writableStream, options.BufferSize, cancellationToken)
.ConfigureAwait(false);

View File

@@ -1,12 +1,12 @@
using System;
using System.IO;
using Pure.DI;
using SharpCompress.Common;
using SharpCompress.IO;
using SharpCompress.Readers;
namespace SharpCompress.Services;
internal sealed partial class ArchiveExtractor
[GenerateInterface(asInternal: true)]
internal sealed partial class ReaderExtractor : IReaderExtractor
{
public void WriteEntryTo(IReader reader, string filePath)
{
@@ -82,12 +82,11 @@ internal sealed partial class ArchiveExtractor
entryStream,
options
);
var sourceStream = WrapWithProgress(checkedStream, reader.Entry);
var sourceStream = ExtractionStreamHelper.WrapWithProgress(
checkedStream,
reader.Entry,
reader.Entry.Options.Progress
);
sourceStream.CopyTo(writableStream, options.BufferSize);
}
private static Stream WrapWithProgress(Stream source, IEntry entry)
{
return WrapWithProgress(source, entry, entry.Options.Progress);
}
}

View File

@@ -16,20 +16,40 @@ internal partial class ServiceComposition
.Bind<IArchiveService>()
.As(Lifetime.Singleton)
.To<ArchiveService>()
.Bind<IArchiveDetector>()
.As(Lifetime.Singleton)
.To<ArchiveDetector>()
.Bind<IArchiveInspector>()
.As(Lifetime.Singleton)
.To<ArchiveInspector>()
.Bind<IReaderExtractor>()
.As(Lifetime.Singleton)
.To<ReaderExtractor>()
.Bind<IArchiveEntryExtractor>()
.As(Lifetime.Singleton)
.To<ArchiveEntryExtractor>()
.Bind<IWriterService>()
.As(Lifetime.Singleton)
.To<WriterService>()
.Bind<IArchiveExtractor>()
.As(Lifetime.Singleton)
.To<ArchiveExtractor>()
.Bind<IArchiveFileWriter>()
.Bind<IWriterFileOperations>()
.As(Lifetime.Singleton)
.To<ArchiveFileWriter>()
.To<WriterFileOperations>()
.Bind<IWritableArchiveFileOperations>()
.As(Lifetime.Singleton)
.To<WritableArchiveFileOperations>()
.Root<IArchiveService>("Archives")
.Root<IReaderService>("Readers")
.Root<IWriterService>("Writers")
.Root<IArchiveExtractor>("Extractor")
.Root<IArchiveFileWriter>("FileWriter");
.Root<IArchiveDetector>("Detector")
.Root<IArchiveInspector>("Inspector")
.Root<IReaderExtractor>("ReaderExtractor")
.Root<IArchiveEntryExtractor>("EntryExtractor")
.Root<IWriterFileOperations>("WriterFiles")
.Root<IWritableArchiveFileOperations>("ArchiveFiles");
}
internal static class LibraryDefaults

View File

@@ -16,16 +16,34 @@ internal partial interface IReaderService;
internal partial interface IWriterService;
internal partial interface IArchiveFileWriter;
internal partial interface IWriterFileOperations;
internal partial interface IWritableArchiveFileOperations;
internal partial interface IReaderExtractor;
internal partial interface IArchiveExtractor;
internal partial interface IArchiveService;
// Pure.DI cannot see methods emitted by another source generator. Only the sync signatures
// generated by Zomp are declared here; Pure.DI generates the rest from the implementations.
internal partial interface IArchiveService
internal partial interface IArchiveDetector
{
ArchiveDetection? DetectArchive(string filePath);
ArchiveDetection? DetectArchive(string filePath, ReaderOptions? readerOptions);
ArchiveDetection? DetectArchive(Stream stream);
ArchiveDetection? DetectArchive(Stream stream, ReaderOptions? readerOptions);
T FindFactory<T>(FileInfo fileInfo, ReaderOptions readerOptions)
where T : SharpCompress.Factories.IFactory;
T FindFactory<T>(Stream stream, ReaderOptions readerOptions)
where T : SharpCompress.Factories.IFactory;
SharpCompress.Factories.IFactory? TryFindFactory(Stream stream, ReaderOptions readerOptions);
ArchiveDetection? TryDetectArchive(Stream stream, ReaderOptions readerOptions);
}
internal partial interface IArchiveInspector
{
ArchiveInformation? InspectArchive(string filePath);
ArchiveInformation? InspectArchive(string filePath, ReaderOptions? readerOptions);
ArchiveInformation? InspectArchive(Stream stream);
@@ -40,7 +58,7 @@ internal partial interface IArchiveService
);
}
internal partial interface IArchiveExtractor
internal partial interface IArchiveEntryExtractor
{
void WriteTo(
IArchiveEntry archiveEntry,

View File

@@ -2,12 +2,14 @@ using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Pure.DI;
using SharpCompress.Archives;
using SharpCompress.Common.Options;
namespace SharpCompress.Services;
internal sealed partial class ArchiveFileWriter
[GenerateInterface(asInternal: true)]
internal sealed class WritableArchiveFileOperations : IWritableArchiveFileOperations
{
public void AddAllFromDirectory(
IWritableArchive writableArchive,

View File

@@ -9,7 +9,7 @@ using SharpCompress.Writers;
namespace SharpCompress.Services;
[GenerateInterface(asInternal: true)]
internal sealed partial class ArchiveFileWriter : IArchiveFileWriter
internal sealed class WriterFileOperations : IWriterFileOperations
{
public void Write(IWriter writer, string entryPath, FileInfo source)
{

View File

@@ -13,7 +13,7 @@ public static class IWriterExtensions
public void Write(string entryPath, Stream source) => writer.Write(entryPath, source, null);
public void Write(string entryPath, FileInfo source) =>
LibraryDefaults.Composition.FileWriter.Write(writer, entryPath, source);
LibraryDefaults.Composition.WriterFiles.Write(writer, entryPath, source);
public void Write(string entryPath, string source) =>
writer.Write(entryPath, new FileInfo(source));
@@ -30,7 +30,7 @@ public static class IWriterExtensions
Func<string, bool>? fileSearchFunc = null,
SearchOption option = SearchOption.TopDirectoryOnly
) =>
LibraryDefaults.Composition.FileWriter.WriteAll(
LibraryDefaults.Composition.WriterFiles.WriteAll(
writer,
directory,
searchPattern,
@@ -56,7 +56,7 @@ public static class IWriterExtensions
CancellationToken cancellationToken = default
) =>
await LibraryDefaults
.Composition.FileWriter.WriteAsync(writer, entryPath, source, cancellationToken)
.Composition.WriterFiles.WriteAsync(writer, entryPath, source, cancellationToken)
.ConfigureAwait(false);
public ValueTask WriteAsync(
@@ -80,7 +80,7 @@ public static class IWriterExtensions
CancellationToken cancellationToken = default
) =>
await LibraryDefaults
.Composition.FileWriter.WriteAllAsync(
.Composition.WriterFiles.WriteAllAsync(
writer,
directory,
searchPattern,

View File

@@ -8,6 +8,7 @@ using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Common;
using SharpCompress.Detection;
using SharpCompress.Factories;
using SharpCompress.Providers;
using SharpCompress.Providers.Default;
@@ -24,6 +25,20 @@ public class ServiceCompositionTests : TestBase
private static ServiceComposition CreateComposition(params IFactory[] factories) =>
new(new FactoryCatalog(() => factories, () => TarWrapper.Wrappers));
private static object[] GetRoots(ServiceComposition composition) =>
[
composition.Archives,
composition.Detector,
composition.Inspector,
composition.Readers,
composition.Writers,
composition.Extractor,
composition.ReaderExtractor,
composition.EntryExtractor,
composition.WriterFiles,
composition.ArchiveFiles,
];
[Fact]
public void Composition_RemainsAnInternalBuildTimeDetail()
{
@@ -36,13 +51,24 @@ public class ServiceCompositionTests : TestBase
typeof(ReaderService),
typeof(WriterService),
typeof(ArchiveExtractor),
typeof(ArchiveFileWriter),
typeof(ExtractionStreamHelper),
typeof(ArchiveDetector),
typeof(ArchiveInspector),
typeof(ReaderExtractor),
typeof(ArchiveEntryExtractor),
typeof(WriterFileOperations),
typeof(WritableArchiveFileOperations),
typeof(IFactoryCatalog),
typeof(IArchiveService),
typeof(IReaderService),
typeof(IWriterService),
typeof(IArchiveExtractor),
typeof(IArchiveFileWriter),
typeof(IArchiveDetector),
typeof(IArchiveInspector),
typeof(IReaderExtractor),
typeof(IArchiveEntryExtractor),
typeof(IWriterFileOperations),
typeof(IWritableArchiveFileOperations),
];
Assert.All(
serviceTypes,
@@ -69,7 +95,12 @@ public class ServiceCompositionTests : TestBase
(typeof(IReaderService), typeof(ReaderService)),
(typeof(IWriterService), typeof(WriterService)),
(typeof(IArchiveExtractor), typeof(ArchiveExtractor)),
(typeof(IArchiveFileWriter), typeof(ArchiveFileWriter)),
(typeof(IArchiveDetector), typeof(ArchiveDetector)),
(typeof(IArchiveInspector), typeof(ArchiveInspector)),
(typeof(IReaderExtractor), typeof(ReaderExtractor)),
(typeof(IArchiveEntryExtractor), typeof(ArchiveEntryExtractor)),
(typeof(IWriterFileOperations), typeof(WriterFileOperations)),
(typeof(IWritableArchiveFileOperations), typeof(WritableArchiveFileOperations)),
];
foreach (var (contract, implementation) in services)
{
@@ -86,25 +117,9 @@ public class ServiceCompositionTests : TestBase
);
}
}
Assert.Equal(
typeof(IArchiveService),
typeof(ServiceComposition).GetProperty("Archives")!.PropertyType
);
Assert.Equal(
typeof(IReaderService),
typeof(ServiceComposition).GetProperty("Readers")!.PropertyType
);
Assert.Equal(
typeof(IWriterService),
typeof(ServiceComposition).GetProperty("Writers")!.PropertyType
);
Assert.Equal(
typeof(IArchiveExtractor),
typeof(ServiceComposition).GetProperty("Extractor")!.PropertyType
);
Assert.Equal(
typeof(IArchiveFileWriter),
typeof(ServiceComposition).GetProperty("FileWriter")!.PropertyType
Assert.All(
typeof(ServiceComposition).GetProperties(),
root => Assert.Contains(services, service => service.Contract == root.PropertyType)
);
}
@@ -114,7 +129,7 @@ public class ServiceCompositionTests : TestBase
IFactoryCatalog catalog = new TestFactoryCatalog();
var composition = new ServiceComposition(catalog);
using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip"));
Assert.Equal(ArchiveType.Zip, composition.Archives.DetectArchive(stream)?.ContainerType);
Assert.Equal(ArchiveType.Zip, composition.Detector.DetectArchive(stream)?.ContainerType);
Assert.Equal(0, stream.Position);
}
@@ -130,36 +145,59 @@ public class ServiceCompositionTests : TestBase
var composition = CreateComposition(new ZipFactory());
var other = CreateComposition(new ZipFactory());
var roots = await Task.WhenAll(
Enumerable
.Range(0, 10)
.Select(_ =>
Task.Run(() =>
(
composition.Archives,
composition.Readers,
composition.Writers,
composition.Extractor,
composition.FileWriter
)
Enumerable.Range(0, 10).Select(_ => Task.Run(() => GetRoots(composition)))
);
var expected = GetRoots(composition);
var separate = GetRoots(other);
for (var i = 0; i < expected.Length; i++)
{
Assert.All(roots, root => Assert.Same(expected[i], root[i]));
Assert.NotSame(expected[i], separate[i]);
}
}
[Fact]
public async Task Inspector_KeepsMetadataStateLocalToEachOperation()
{
var composition = CreateComposition(new ZipFactory(), new AceFactory());
var expected = new[] { "Zip.deflate.zip", "Ace.store.ace" }
.Select(name => Path.Combine(TEST_ARCHIVES_PATH, name))
.Select(path =>
(
Path: path,
Information: Assert.IsType<ArchiveInformation>(
composition.Inspector.InspectArchive(path)
)
)
)
.ToArray();
await Task.WhenAll(
Enumerable
.Range(0, 20)
.Select(index =>
Task.Run(async () =>
{
var sample = expected[index % expected.Length];
using var stream = File.OpenRead(sample.Path);
var information = Assert.IsType<ArchiveInformation>(
await composition.Inspector.InspectArchiveAsync(
stream,
CancellationToken.None
)
);
Assert.Equal(sample.Information.EntryCount, information.EntryCount);
Assert.Equal(
sample.Information.UncompressedPayloadSize,
information.UncompressedPayloadSize
);
Assert.Equal(
sample.Information.CompressedPayloadSize,
information.CompressedPayloadSize
);
Assert.Equal(0, stream.Position);
})
)
);
Assert.All(
roots,
root =>
{
Assert.Same(composition.Archives, root.Archives);
Assert.Same(composition.Readers, root.Readers);
Assert.Same(composition.Writers, root.Writers);
Assert.Same(composition.Extractor, root.Extractor);
Assert.Same(composition.FileWriter, root.FileWriter);
}
);
Assert.NotSame(composition.Archives, other.Archives);
Assert.NotSame(composition.Readers, other.Readers);
Assert.NotSame(composition.Writers, other.Writers);
Assert.NotSame(composition.Extractor, other.Extractor);
Assert.NotSame(composition.FileWriter, other.FileWriter);
}
[Fact]
@@ -178,15 +216,15 @@ public class ServiceCompositionTests : TestBase
using var stream = File.OpenRead(path);
Assert.Equal(
ArchiveType.Zip,
zip.Archives.DetectArchive(stream)?.ContainerType
zip.Detector.DetectArchive(stream)?.ContainerType
);
Assert.Null(empty.Archives.DetectArchive(stream));
Assert.Null(empty.Detector.DetectArchive(stream));
Assert.Equal(0, stream.Position);
Assert.NotNull(
await zip.Archives.InspectArchiveAsync(stream, CancellationToken.None)
await zip.Inspector.InspectArchiveAsync(stream, CancellationToken.None)
);
Assert.Null(
await empty.Archives.InspectArchiveAsync(stream, CancellationToken.None)
await empty.Inspector.InspectArchiveAsync(stream, CancellationToken.None)
);
Assert.Equal(0, stream.Position);
using var archive = zip.Archives.OpenArchive(stream);
@@ -241,7 +279,7 @@ public class ServiceCompositionTests : TestBase
);
await foreach (var entry in archive.EntriesAsync)
{
await services.Extractor.WriteToAsync(
await services.EntryExtractor.WriteToAsync(
entry,
output,
null,
@@ -252,7 +290,7 @@ public class ServiceCompositionTests : TestBase
else
{
using var archive = services.Archives.OpenArchive(stream, readerOptions);
services.Extractor.WriteTo(Assert.Single(archive.Entries), output, null);
services.EntryExtractor.WriteTo(Assert.Single(archive.Entries), output, null);
}
Assert.True(provider.DecompressionCalls > 0);
Assert.Equal("instance services", Encoding.UTF8.GetString(output.ToArray()));
@@ -289,7 +327,7 @@ public class ServiceCompositionTests : TestBase
cancellation.Cancel();
using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip"));
await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
services.Archives.DetectArchiveAsync(stream, cancellation.Token).AsTask()
services.Detector.DetectArchiveAsync(stream, cancellation.Token).AsTask()
);
Assert.Equal(0, stream.Position);
}

View File

@@ -8,6 +8,7 @@ using SharpCompress.Common;
using SharpCompress.Factories;
using SharpCompress.Readers;
using SharpCompress.Services;
using SharpCompress.Writers;
using SharpCompress.Writers.Zip;
using Xunit;
@@ -15,6 +16,58 @@ namespace SharpCompress.Test;
public class ServiceWorkflowTests : TestBase
{
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task WriterFilesystemOperations_LeaveBorrowedOutputOpen(bool useAsync)
{
var composition = new ServiceComposition(
new FactoryCatalog(() => [new ZipFactory()], () => TarWrapper.Wrappers)
);
var directory = CreateScratchDirectory("writer-source") + Path.DirectorySeparatorChar;
File.WriteAllText(Path.Combine(directory, "payload.txt"), "writer payload");
using var output = new MemoryStream();
var options = new WriterOptions(CompressionType.Deflate) { LeaveStreamOpen = true };
if (useAsync)
{
await using var writer = await composition.Writers.OpenAsyncWriter(
output,
ArchiveType.Zip,
options,
CancellationToken.None
);
await composition.WriterFiles.WriteAllAsync(
writer,
directory,
"*",
null,
SearchOption.TopDirectoryOnly,
CancellationToken.None
);
}
else
{
using var writer = composition.Writers.OpenWriter(output, ArchiveType.Zip, options);
composition.WriterFiles.WriteAll(
writer,
directory,
"*",
null,
SearchOption.TopDirectoryOnly
);
}
Assert.True(output.CanWrite);
output.Position = 0;
using var archive = composition.Archives.OpenArchive(
output,
ReaderOptions.ForExternalStream
);
var entry = Assert.Single(archive.Entries);
using var content = entry.OpenEntryStream();
using var text = new StreamReader(content);
Assert.Equal("writer payload", text.ReadToEnd());
}
[Theory]
[InlineData(false, false)]
[InlineData(false, true)]
@@ -44,20 +97,20 @@ public class ServiceWorkflowTests : TestBase
}
else
{
await composition.FileWriter.AddAllFromDirectoryAsync(
await composition.ArchiveFiles.AddAllFromDirectoryAsync(
archive,
sourceDirectory,
"*.*",
SearchOption.AllDirectories,
CancellationToken.None
);
await composition.FileWriter.AddEntryAsync(
await composition.ArchiveFiles.AddEntryAsync(
archive,
"extra.txt",
source,
CancellationToken.None
);
await composition.FileWriter.SaveToAsync(
await composition.ArchiveFiles.SaveToAsync(
archive,
destination,
options,
@@ -76,14 +129,14 @@ public class ServiceWorkflowTests : TestBase
}
else
{
composition.FileWriter.AddAllFromDirectory(
composition.ArchiveFiles.AddAllFromDirectory(
archive,
sourceDirectory,
"*.*",
SearchOption.AllDirectories
);
composition.FileWriter.AddEntry(archive, "extra.txt", source);
composition.FileWriter.SaveTo(archive, destination, options);
composition.ArchiveFiles.AddEntry(archive, "extra.txt", source);
composition.ArchiveFiles.SaveTo(archive, destination, options);
}
}
@@ -112,7 +165,7 @@ public class ServiceWorkflowTests : TestBase
}
else
{
await composition.Extractor.WriteAllToDirectoryAsync(
await composition.ReaderExtractor.WriteAllToDirectoryAsync(
reader,
readerDirectory,
cancellationToken: CancellationToken.None
@@ -135,7 +188,7 @@ public class ServiceWorkflowTests : TestBase
}
else
{
composition.Extractor.WriteAllToDirectory(reader, readerDirectory);
composition.ReaderExtractor.WriteAllToDirectory(reader, readerDirectory);
composition.Extractor.WriteToDirectory(archive, archiveDirectory);
}
}