From e21da75de235dda7fe1360d4abf708217b3deb55 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Wed, 21 Oct 2026 09:27:47 +0100 Subject: [PATCH] Split DI services into focused workflow components --- docs/ARCHITECTURE.md | 16 +- src/SharpCompress/Archives/ArchiveFactory.cs | 57 ++++--- .../Archives/IArchiveEntryExtensions.cs | 2 +- .../Archives/IWritableArchiveExtensions.cs | 6 +- .../IWritableAsyncArchiveExtensions.cs | 6 +- .../Readers/IAsyncReaderExtensions.cs | 8 +- .../Readers/IReaderExtensions.cs | 10 +- ...tion.Async.cs => ArchiveDetector.Async.cs} | 10 +- ...Detection.cs => ArchiveDetector.Probes.cs} | 2 +- src/SharpCompress/Services/ArchiveDetector.cs | 101 ++++++++++++ ...ntry.cs => ArchiveEntryExtractor.Files.cs} | 2 +- .../Services/ArchiveEntryExtractor.cs | 50 ++++++ .../Services/ArchiveExtractor.Archive.cs | 67 -------- ...ive.Async.cs => ArchiveExtractor.Async.cs} | 7 +- .../Services/ArchiveExtractor.cs | 121 +++++++------- ...ion.Async.cs => ArchiveInspector.Async.cs} | 54 +++---- ...mation.cs => ArchiveInspector.Metadata.cs} | 4 +- .../Services/ArchiveService.Async.cs | 24 +-- src/SharpCompress/Services/ArchiveService.cs | 126 ++------------- .../Services/ExtractionStreamHelper.cs | 42 +++++ ...ader.Async.cs => ReaderExtractor.Async.cs} | 10 +- ...Extractor.Reader.cs => ReaderExtractor.cs} | 17 +- .../Services/ServiceComposition.cs | 26 ++- .../Services/ServiceContracts.cs | 24 ++- ...ve.cs => WritableArchiveFileOperations.cs} | 4 +- ...eFileWriter.cs => WriterFileOperations.cs} | 2 +- .../Writers/IWriterExtensions.cs | 8 +- .../ServiceCompositionTests.cs | 152 +++++++++++------- .../ServiceWorkflowTests.cs | 69 +++++++- 29 files changed, 597 insertions(+), 430 deletions(-) rename src/SharpCompress/Services/{ArchiveService.Detection.Async.cs => ArchiveDetector.Async.cs} (97%) rename src/SharpCompress/Services/{ArchiveService.Detection.cs => ArchiveDetector.Probes.cs} (96%) create mode 100644 src/SharpCompress/Services/ArchiveDetector.cs rename src/SharpCompress/Services/{ArchiveExtractor.Entry.cs => ArchiveEntryExtractor.Files.cs} (97%) create mode 100644 src/SharpCompress/Services/ArchiveEntryExtractor.cs delete mode 100644 src/SharpCompress/Services/ArchiveExtractor.Archive.cs rename src/SharpCompress/Services/{ArchiveExtractor.Archive.Async.cs => ArchiveExtractor.Async.cs} (94%) rename src/SharpCompress/Services/{ArchiveService.Information.Async.cs => ArchiveInspector.Async.cs} (93%) rename src/SharpCompress/Services/{ArchiveService.Information.cs => ArchiveInspector.Metadata.cs} (99%) create mode 100644 src/SharpCompress/Services/ExtractionStreamHelper.cs rename src/SharpCompress/Services/{ArchiveExtractor.Reader.Async.cs => ReaderExtractor.Async.cs} (93%) rename src/SharpCompress/Services/{ArchiveExtractor.Reader.cs => ReaderExtractor.cs} (87%) rename src/SharpCompress/Services/{ArchiveFileWriter.Archive.cs => WritableArchiveFileOperations.cs} (96%) rename src/SharpCompress/Services/{ArchiveFileWriter.cs => WriterFileOperations.cs} (97%) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 99c80e18..aa181929 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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 diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index 5d1998c8..9ac01719 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -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(string filePath) - where T : IFactory => Service.FindFactory(filePath); + where T : IFactory => Detector.FindFactory(filePath); public static T FindFactory(FileInfo finfo) - where T : IFactory => Service.FindFactory(finfo); + where T : IFactory => Detector.FindFactory(finfo); public static T FindFactory(Stream stream) - where T : IFactory => Service.FindFactory(stream); + where T : IFactory => Detector.FindFactory(stream); internal static ValueTask FindFactoryAsync( string filePath, CancellationToken cancellationToken = default ) - where T : IFactory => Service.FindFactoryAsync(filePath, cancellationToken); + where T : IFactory => Detector.FindFactoryAsync(filePath, cancellationToken); internal static ValueTask FindFactoryAsync( FileInfo fileInfo, CancellationToken cancellationToken = default ) - where T : IFactory => Service.FindFactoryAsync(fileInfo, cancellationToken); + where T : IFactory => Detector.FindFactoryAsync(fileInfo, cancellationToken); internal static ValueTask FindFactoryAsync( Stream stream, CancellationToken cancellationToken = default ) - where T : IFactory => Service.FindFactoryAsync(stream, cancellationToken); + where T : IFactory => Detector.FindFactoryAsync(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); /// Identify the archive without enumerating its entries. [Zomp.SyncMethodGenerator.CreateSyncVersion] public static ValueTask DetectArchiveAsync( string filePath, CancellationToken cancellationToken = default - ) => Service.DetectArchiveAsync(filePath, cancellationToken); + ) => Detector.DetectArchiveAsync(filePath, cancellationToken); /// Identify the archive without enumerating its entries. [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); /// Identify the archive without enumerating its entries. Restore the stream position. [Zomp.SyncMethodGenerator.CreateSyncVersion] public static ValueTask DetectArchiveAsync( Stream stream, CancellationToken cancellationToken = default - ) => Service.DetectArchiveAsync(stream, cancellationToken); + ) => Detector.DetectArchiveAsync(stream, cancellationToken); /// Identify the archive without enumerating its entries. Restore the stream position. [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); /// Inspect archive metadata. [Zomp.SyncMethodGenerator.CreateSyncVersion] public static ValueTask InspectArchiveAsync( string filePath, CancellationToken cancellationToken = default - ) => Service.InspectArchiveAsync(filePath, cancellationToken); + ) => Inspector.InspectArchiveAsync(filePath, cancellationToken); /// Inspect archive metadata. [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); /// Inspect archive metadata, leaving the stream open at its original position. [Zomp.SyncMethodGenerator.CreateSyncVersion] public static ValueTask InspectArchiveAsync( Stream stream, CancellationToken cancellationToken = default - ) => Service.InspectArchiveAsync(stream, cancellationToken); + ) => Inspector.InspectArchiveAsync(stream, cancellationToken); /// Inspect archive metadata, leaving the stream open at its original position. [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); /// Inspect multipart archive metadata. [Zomp.SyncMethodGenerator.CreateSyncVersion] @@ -206,7 +213,7 @@ public static partial class ArchiveFactory IReadOnlyList fileInfos, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default - ) => Service.InspectArchiveAsync(fileInfos, readerOptions, cancellationToken); + ) => Inspector.InspectArchiveAsync(fileInfos, readerOptions, cancellationToken); /// Inspect multipart archive metadata, leaving the streams open at their original positions. [Zomp.SyncMethodGenerator.CreateSyncVersion] @@ -214,7 +221,7 @@ public static partial class ArchiveFactory IReadOnlyList streams, ReaderOptions? readerOptions = null, CancellationToken cancellationToken = default - ) => Service.InspectArchiveAsync(streams, readerOptions, cancellationToken); + ) => Inspector.InspectArchiveAsync(streams, readerOptions, cancellationToken); public static IEnumerable GetFileParts(string part1) => Service.GetFileParts(part1); diff --git a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs index d9529300..9b75d6b7 100644 --- a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs +++ b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs @@ -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; /// The archive entry to extract. extension(IArchiveEntry archiveEntry) diff --git a/src/SharpCompress/Archives/IWritableArchiveExtensions.cs b/src/SharpCompress/Archives/IWritableArchiveExtensions.cs index e8be445f..b8dac179 100644 --- a/src/SharpCompress/Archives/IWritableArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IWritableArchiveExtensions.cs @@ -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( @@ -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); } diff --git a/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs b/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs index c7b61bba..264f34b2 100644 --- a/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs @@ -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 AddEntryAsync(string key, FileInfo fileInfo) => - LibraryDefaults.Composition.FileWriter.AddEntryAsync(writableArchive, key, fileInfo); + LibraryDefaults.Composition.ArchiveFiles.AddEntryAsync(writableArchive, key, fileInfo); } public static ValueTask SaveToAsync( @@ -54,7 +54,7 @@ public static class IWritableAsyncArchiveExtensions ) where TOptions : IWriterOptions => await LibraryDefaults - .Composition.FileWriter.SaveToAsync( + .Composition.ArchiveFiles.SaveToAsync( writableArchive, fileInfo, options, diff --git a/src/SharpCompress/Readers/IAsyncReaderExtensions.cs b/src/SharpCompress/Readers/IAsyncReaderExtensions.cs index 42ff399e..1b8ee933 100644 --- a/src/SharpCompress/Readers/IAsyncReaderExtensions.cs +++ b/src/SharpCompress/Readers/IAsyncReaderExtensions.cs @@ -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, diff --git a/src/SharpCompress/Readers/IReaderExtensions.cs b/src/SharpCompress/Readers/IReaderExtensions.cs index 9c71c549..b653b47c 100644 --- a/src/SharpCompress/Readers/IReaderExtensions.cs +++ b/src/SharpCompress/Readers/IReaderExtensions.cs @@ -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); /// Extract all remaining unread entries to specific directory, retaining filename. 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 diff --git a/src/SharpCompress/Services/ArchiveService.Detection.Async.cs b/src/SharpCompress/Services/ArchiveDetector.Async.cs similarity index 97% rename from src/SharpCompress/Services/ArchiveService.Detection.Async.cs rename to src/SharpCompress/Services/ArchiveDetector.Async.cs index f7a91a20..23c90b78 100644 --- a/src/SharpCompress/Services/ArchiveService.Detection.Async.cs +++ b/src/SharpCompress/Services/ArchiveDetector.Async.cs @@ -13,7 +13,7 @@ using SharpCompress.Readers; namespace SharpCompress.Services; -internal sealed partial class ArchiveService +internal sealed partial class ArchiveDetector { /// /// 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 FindFactoryAsync( + public async ValueTask FindFactoryAsync( FileInfo fileInfo, ReaderOptions readerOptions, CancellationToken cancellationToken @@ -141,7 +141,7 @@ internal sealed partial class ArchiveService } [Zomp.SyncMethodGenerator.CreateSyncVersion] - private async ValueTask FindFactoryAsync( + public async ValueTask FindFactoryAsync( 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. /// [Zomp.SyncMethodGenerator.CreateSyncVersion] - private async ValueTask TryFindFactoryAsync( + public async ValueTask TryFindFactoryAsync( Stream stream, ReaderOptions readerOptions, CancellationToken cancellationToken @@ -270,7 +270,7 @@ internal sealed partial class ArchiveService } [Zomp.SyncMethodGenerator.CreateSyncVersion] - private async ValueTask TryDetectArchiveAsync( + public async ValueTask TryDetectArchiveAsync( Stream stream, ReaderOptions readerOptions, CancellationToken cancellationToken diff --git a/src/SharpCompress/Services/ArchiveService.Detection.cs b/src/SharpCompress/Services/ArchiveDetector.Probes.cs similarity index 96% rename from src/SharpCompress/Services/ArchiveService.Detection.cs rename to src/SharpCompress/Services/ArchiveDetector.Probes.cs index 649f5022..7839372e 100644 --- a/src/SharpCompress/Services/ArchiveService.Detection.cs +++ b/src/SharpCompress/Services/ArchiveDetector.Probes.cs @@ -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) { diff --git a/src/SharpCompress/Services/ArchiveDetector.cs b/src/SharpCompress/Services/ArchiveDetector.cs new file mode 100644 index 00000000..848e26b1 --- /dev/null +++ b/src/SharpCompress/Services/ArchiveDetector.cs @@ -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(string filePath) + where T : IFactory + { + filePath.NotNullOrEmpty(nameof(filePath)); + using Stream stream = File.OpenRead(filePath); + return FindFactory(stream); + } + + public T FindFactory(FileInfo finfo) + where T : IFactory + { + finfo.NotNull(nameof(finfo)); + using Stream stream = finfo.OpenRead(); + return FindFactory(stream); + } + + public T FindFactory(Stream stream) + where T : IFactory => FindFactory(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); + } +} diff --git a/src/SharpCompress/Services/ArchiveExtractor.Entry.cs b/src/SharpCompress/Services/ArchiveEntryExtractor.Files.cs similarity index 97% rename from src/SharpCompress/Services/ArchiveExtractor.Entry.cs rename to src/SharpCompress/Services/ArchiveEntryExtractor.Files.cs index 0b0989ea..8afecca5 100644 --- a/src/SharpCompress/Services/ArchiveExtractor.Entry.cs +++ b/src/SharpCompress/Services/ArchiveEntryExtractor.Files.cs @@ -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( diff --git a/src/SharpCompress/Services/ArchiveEntryExtractor.cs b/src/SharpCompress/Services/ArchiveEntryExtractor.cs new file mode 100644 index 00000000..c6395eee --- /dev/null +++ b/src/SharpCompress/Services/ArchiveEntryExtractor.cs @@ -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? 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); + } +} diff --git a/src/SharpCompress/Services/ArchiveExtractor.Archive.cs b/src/SharpCompress/Services/ArchiveExtractor.Archive.cs deleted file mode 100644 index e139b829..00000000 --- a/src/SharpCompress/Services/ArchiveExtractor.Archive.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using SharpCompress.Archives; -using SharpCompress.Common; -using SharpCompress.Readers; - -namespace SharpCompress.Services; - -internal sealed partial class ArchiveExtractor -{ - /// - /// Extract to specific directory with progress reporting - /// - /// The folder to extract into. - /// Extraction options. - /// Optional progress reporter for tracking extraction progress. - public void WriteToDirectory( - IArchive archive, - string destinationDirectory, - ExtractionOptions? options = null, - IProgress? 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? 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)); - } - } -} diff --git a/src/SharpCompress/Services/ArchiveExtractor.Archive.Async.cs b/src/SharpCompress/Services/ArchiveExtractor.Async.cs similarity index 94% rename from src/SharpCompress/Services/ArchiveExtractor.Archive.Async.cs rename to src/SharpCompress/Services/ArchiveExtractor.Async.cs index 72df0fd5..5ddae64f 100644 --- a/src/SharpCompress/Services/ArchiveExtractor.Archive.Async.cs +++ b/src/SharpCompress/Services/ArchiveExtractor.Async.cs @@ -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); diff --git a/src/SharpCompress/Services/ArchiveExtractor.cs b/src/SharpCompress/Services/ArchiveExtractor.cs index e4f1cdce..f47b7d9d 100644 --- a/src/SharpCompress/Services/ArchiveExtractor.cs +++ b/src/SharpCompress/Services/ArchiveExtractor.cs @@ -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? 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, + /// + /// Extract to specific directory with progress reporting + /// + /// The folder to extract into. + /// Extraction options. + /// Optional progress reporter for tracking extraction progress. + public void WriteToDirectory( + IArchive archive, + string destinationDirectory, + ExtractionOptions? options = null, + IProgress? 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? 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)); } } } diff --git a/src/SharpCompress/Services/ArchiveService.Information.Async.cs b/src/SharpCompress/Services/ArchiveInspector.Async.cs similarity index 93% rename from src/SharpCompress/Services/ArchiveService.Information.Async.cs rename to src/SharpCompress/Services/ArchiveInspector.Async.cs index 3d160af7..d77f4f52 100644 --- a/src/SharpCompress/Services/ArchiveService.Information.Async.cs +++ b/src/SharpCompress/Services/ArchiveInspector.Async.cs @@ -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 { /// /// 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); } } diff --git a/src/SharpCompress/Services/ArchiveService.Information.cs b/src/SharpCompress/Services/ArchiveInspector.Metadata.cs similarity index 99% rename from src/SharpCompress/Services/ArchiveService.Information.cs rename to src/SharpCompress/Services/ArchiveInspector.Metadata.cs index b60d4f7f..fb5e4af6 100644 --- a/src/SharpCompress/Services/ArchiveService.Information.cs +++ b/src/SharpCompress/Services/ArchiveInspector.Metadata.cs @@ -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]; diff --git a/src/SharpCompress/Services/ArchiveService.Async.cs b/src/SharpCompress/Services/ArchiveService.Async.cs index fd1ac7ef..d722aa7c 100644 --- a/src/SharpCompress/Services/ArchiveService.Async.cs +++ b/src/SharpCompress/Services/ArchiveService.Async.cs @@ -18,11 +18,8 @@ internal sealed partial class ArchiveService ) { readerOptions ??= ReaderOptions.ForExternalStream; - var factory = await FindFactoryAsync( - stream, - readerOptions, - cancellationToken - ) + var factory = await detector + .FindFactoryAsync(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(fileInfo, options, cancellationToken) + var factory = await detector + .FindFactoryAsync(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( - fileInfo, - options, - cancellationToken - ) + var factory = await detector + .FindFactoryAsync(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( - firstStream, - options, - cancellationToken - ) + var factory = await detector + .FindFactoryAsync(firstStream, options, cancellationToken) .ConfigureAwait(false); return await factory .OpenAsyncArchive(streamsArray, options, cancellationToken) diff --git a/src/SharpCompress/Services/ArchiveService.cs b/src/SharpCompress/Services/ArchiveService.cs index 3c8093e6..ebc1f9cd 100644 --- a/src/SharpCompress/Services/ArchiveService.cs +++ b/src/SharpCompress/Services/ArchiveService.cs @@ -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(stream, readerOptions) + return detector + .FindFactory(stream, readerOptions) .OpenArchive(stream, readerOptions); } @@ -52,7 +48,9 @@ internal sealed partial class ArchiveService( { options ??= ReaderOptions.ForFilePath; - return FindFactory(fileInfo, options).OpenArchive(fileInfo, options); + return detector + .FindFactory(fileInfo, options) + .OpenArchive(fileInfo, options); } public IArchive OpenArchive(IReadOnlyList fileInfos, ReaderOptions? options = null) @@ -73,7 +71,8 @@ internal sealed partial class ArchiveService( fileInfo.NotNull(nameof(fileInfo)); options ??= ReaderOptions.ForFilePath; - return FindFactory(fileInfo, options) + return detector + .FindFactory(fileInfo, options) .OpenArchive(filesArray, options); } @@ -94,114 +93,11 @@ internal sealed partial class ArchiveService( firstStream.NotNull(nameof(firstStream)); options ??= ReaderOptions.ForExternalStream; - return FindFactory(firstStream, options) + return detector + .FindFactory(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(string filePath) - where T : IFactory - { - filePath.NotNullOrEmpty(nameof(filePath)); - using Stream stream = File.OpenRead(filePath); - return FindFactory(stream); - } - - public T FindFactory(FileInfo finfo) - where T : IFactory - { - finfo.NotNull(nameof(finfo)); - using Stream stream = finfo.OpenRead(); - return FindFactory(stream); - } - - public T FindFactory(Stream stream) - where T : IFactory => FindFactory(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 GetFileParts(string part1) { part1.NotNullOrEmpty(nameof(part1)); diff --git a/src/SharpCompress/Services/ExtractionStreamHelper.cs b/src/SharpCompress/Services/ExtractionStreamHelper.cs new file mode 100644 index 00000000..f3f31f77 --- /dev/null +++ b/src/SharpCompress/Services/ExtractionStreamHelper.cs @@ -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? 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; + } + } +} diff --git a/src/SharpCompress/Services/ArchiveExtractor.Reader.Async.cs b/src/SharpCompress/Services/ReaderExtractor.Async.cs similarity index 93% rename from src/SharpCompress/Services/ArchiveExtractor.Reader.Async.cs rename to src/SharpCompress/Services/ReaderExtractor.Async.cs index 7c65ab4e..445fdd0b 100644 --- a/src/SharpCompress/Services/ArchiveExtractor.Reader.Async.cs +++ b/src/SharpCompress/Services/ReaderExtractor.Async.cs @@ -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 { /// /// 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); diff --git a/src/SharpCompress/Services/ArchiveExtractor.Reader.cs b/src/SharpCompress/Services/ReaderExtractor.cs similarity index 87% rename from src/SharpCompress/Services/ArchiveExtractor.Reader.cs rename to src/SharpCompress/Services/ReaderExtractor.cs index 730a6e91..4d1c5ad7 100644 --- a/src/SharpCompress/Services/ArchiveExtractor.Reader.cs +++ b/src/SharpCompress/Services/ReaderExtractor.cs @@ -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); - } } diff --git a/src/SharpCompress/Services/ServiceComposition.cs b/src/SharpCompress/Services/ServiceComposition.cs index 2042160f..b54069c5 100644 --- a/src/SharpCompress/Services/ServiceComposition.cs +++ b/src/SharpCompress/Services/ServiceComposition.cs @@ -16,20 +16,40 @@ internal partial class ServiceComposition .Bind() .As(Lifetime.Singleton) .To() + .Bind() + .As(Lifetime.Singleton) + .To() + .Bind() + .As(Lifetime.Singleton) + .To() + .Bind() + .As(Lifetime.Singleton) + .To() + .Bind() + .As(Lifetime.Singleton) + .To() .Bind() .As(Lifetime.Singleton) .To() .Bind() .As(Lifetime.Singleton) .To() - .Bind() + .Bind() .As(Lifetime.Singleton) - .To() + .To() + .Bind() + .As(Lifetime.Singleton) + .To() .Root("Archives") .Root("Readers") .Root("Writers") .Root("Extractor") - .Root("FileWriter"); + .Root("Detector") + .Root("Inspector") + .Root("ReaderExtractor") + .Root("EntryExtractor") + .Root("WriterFiles") + .Root("ArchiveFiles"); } internal static class LibraryDefaults diff --git a/src/SharpCompress/Services/ServiceContracts.cs b/src/SharpCompress/Services/ServiceContracts.cs index 98c4b9d0..2e99080f 100644 --- a/src/SharpCompress/Services/ServiceContracts.cs +++ b/src/SharpCompress/Services/ServiceContracts.cs @@ -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(FileInfo fileInfo, ReaderOptions readerOptions) + where T : SharpCompress.Factories.IFactory; + T FindFactory(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, diff --git a/src/SharpCompress/Services/ArchiveFileWriter.Archive.cs b/src/SharpCompress/Services/WritableArchiveFileOperations.cs similarity index 96% rename from src/SharpCompress/Services/ArchiveFileWriter.Archive.cs rename to src/SharpCompress/Services/WritableArchiveFileOperations.cs index 5a7bf11d..c5c3de5b 100644 --- a/src/SharpCompress/Services/ArchiveFileWriter.Archive.cs +++ b/src/SharpCompress/Services/WritableArchiveFileOperations.cs @@ -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, diff --git a/src/SharpCompress/Services/ArchiveFileWriter.cs b/src/SharpCompress/Services/WriterFileOperations.cs similarity index 97% rename from src/SharpCompress/Services/ArchiveFileWriter.cs rename to src/SharpCompress/Services/WriterFileOperations.cs index eaa31f49..05500b44 100644 --- a/src/SharpCompress/Services/ArchiveFileWriter.cs +++ b/src/SharpCompress/Services/WriterFileOperations.cs @@ -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) { diff --git a/src/SharpCompress/Writers/IWriterExtensions.cs b/src/SharpCompress/Writers/IWriterExtensions.cs index 6d81da56..4a5cd110 100644 --- a/src/SharpCompress/Writers/IWriterExtensions.cs +++ b/src/SharpCompress/Writers/IWriterExtensions.cs @@ -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? 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, diff --git a/tests/SharpCompress.Test/ServiceCompositionTests.cs b/tests/SharpCompress.Test/ServiceCompositionTests.cs index ca1da906..afaf11bd 100644 --- a/tests/SharpCompress.Test/ServiceCompositionTests.cs +++ b/tests/SharpCompress.Test/ServiceCompositionTests.cs @@ -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( + 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( + 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(() => - services.Archives.DetectArchiveAsync(stream, cancellation.Token).AsTask() + services.Detector.DetectArchiveAsync(stream, cancellation.Token).AsTask() ); Assert.Equal(0, stream.Position); } diff --git a/tests/SharpCompress.Test/ServiceWorkflowTests.cs b/tests/SharpCompress.Test/ServiceWorkflowTests.cs index 38e32abc..fd1b9f17 100644 --- a/tests/SharpCompress.Test/ServiceWorkflowTests.cs +++ b/tests/SharpCompress.Test/ServiceWorkflowTests.cs @@ -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); } }