From 1d52618137e89ddd6930fabb3dea3af70d1987dd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 11:32:17 +0000 Subject: [PATCH 1/9] Initial plan From 224989f19b6be9935a178873536d1242ab7885db Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 11:45:21 +0000 Subject: [PATCH 2/9] Remove restriction on ExtractAllEntries and add comprehensive tests Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- src/SharpCompress/Archives/AbstractArchive.cs | 6 - .../ExtractAllEntriesTests.cs | 178 ++++++++++++++++++ 2 files changed, 178 insertions(+), 6 deletions(-) create mode 100644 tests/SharpCompress.Test/ExtractAllEntriesTests.cs diff --git a/src/SharpCompress/Archives/AbstractArchive.cs b/src/SharpCompress/Archives/AbstractArchive.cs index 01ed5b90..5f5988d3 100644 --- a/src/SharpCompress/Archives/AbstractArchive.cs +++ b/src/SharpCompress/Archives/AbstractArchive.cs @@ -144,12 +144,6 @@ public abstract class AbstractArchive : IArchive, IArchiveExtra /// public IReader ExtractAllEntries() { - if (!IsSolid && Type != ArchiveType.SevenZip) - { - throw new InvalidOperationException( - "ExtractAllEntries can only be used on solid archives or 7Zip archives (which require random access)." - ); - } ((IArchiveExtractionListener)this).EnsureEntriesLoaded(); return CreateReaderForSolidExtraction(); } diff --git a/tests/SharpCompress.Test/ExtractAllEntriesTests.cs b/tests/SharpCompress.Test/ExtractAllEntriesTests.cs new file mode 100644 index 00000000..15ddf4eb --- /dev/null +++ b/tests/SharpCompress.Test/ExtractAllEntriesTests.cs @@ -0,0 +1,178 @@ +using System; +using System.IO; +using System.Linq; +using SharpCompress.Archives; +using SharpCompress.Common; +using SharpCompress.Readers; +using Xunit; + +namespace SharpCompress.Test; + +/// +/// Tests for ExtractAllEntries method which should work for all archive types +/// regardless of whether they are SOLID or not. +/// +public class ExtractAllEntriesTests : TestBase +{ + [Theory] + [InlineData("Zip.deflate.zip", false)] + [InlineData("Zip.bzip2.zip", false)] + [InlineData("Zip.lzma.zip", false)] + [InlineData("Zip.ppmd.zip", false)] + [InlineData("Rar.rar", false)] + [InlineData("Rar.solid.rar", true)] + [InlineData("7Zip.LZMA.7z", true)] + [InlineData("7Zip.PPMd.7z", true)] + [InlineData("Tar.tar", false)] + public void ExtractAllEntries_WorksForAllArchiveTypes(string archiveName, bool expectedSolid) + { + var archivePath = Path.Combine(TEST_ARCHIVES_PATH, archiveName); + + using var archive = ArchiveFactory.Open(archivePath); + + // Verify IsSolid matches expectation + Assert.Equal(expectedSolid, archive.IsSolid); + + // This should not throw for any archive type + using var reader = archive.ExtractAllEntries(); + + var entryCount = 0; + var filesExtracted = 0; + while (reader.MoveToNextEntry()) + { + entryCount++; + if (!reader.Entry.IsDirectory) + { + filesExtracted++; + // Extract to scratch path to verify it actually works + reader.WriteEntryToDirectory( + SCRATCH_FILES_PATH, + new ExtractionOptions { ExtractFullPath = true, Overwrite = true } + ); + } + } + + Assert.True(entryCount > 0, $"Archive {archiveName} should have at least one entry"); + Assert.True( + filesExtracted > 0, + $"Archive {archiveName} should have at least one file entry" + ); + } + + [Fact] + public void ExtractAllEntries_WithProgressReporting_NonSolidArchive() + { + var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip"); + + using var archive = ArchiveFactory.Open(archivePath); + + // Calculate total size like user code does + double totalSize = archive.Entries.Where(e => !e.IsDirectory).Sum(e => e.Size); + long completed = 0; + var progressReports = 0; + + using var reader = archive.ExtractAllEntries(); + while (reader.MoveToNextEntry()) + { + if (!reader.Entry.IsDirectory) + { + reader.WriteEntryToDirectory( + SCRATCH_FILES_PATH, + new ExtractionOptions { ExtractFullPath = true, Overwrite = true } + ); + + completed += reader.Entry.Size; + var progress = completed / totalSize; + progressReports++; + + Assert.True(progress >= 0 && progress <= 1.0); + } + } + + Assert.True(progressReports > 0); + } + + [Fact] + public void ExtractAllEntries_WithProgressReporting_SolidArchive() + { + var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "Rar.solid.rar"); + + using var archive = ArchiveFactory.Open(archivePath); + Assert.True(archive.IsSolid); + + // Calculate total size like user code does + double totalSize = archive.Entries.Where(e => !e.IsDirectory).Sum(e => e.Size); + long completed = 0; + var progressReports = 0; + + using var reader = archive.ExtractAllEntries(); + while (reader.MoveToNextEntry()) + { + if (!reader.Entry.IsDirectory) + { + reader.WriteEntryToDirectory( + SCRATCH_FILES_PATH, + new ExtractionOptions { ExtractFullPath = true, Overwrite = true } + ); + + completed += reader.Entry.Size; + var progress = completed / totalSize; + progressReports++; + + Assert.True(progress >= 0 && progress <= 1.0); + } + } + + Assert.True(progressReports > 0); + } + + [Fact] + public void ExtractAllEntries_UserScenario_UnknownArchiveType() + { + // Test the exact user scenario: they don't know if archive is SOLID or not + // and want to use ExtractAllEntries with progress reporting + var testArchives = new[] + { + "Zip.deflate.zip", // Non-SOLID + "Rar.rar", // Non-SOLID + "Rar.solid.rar", // SOLID + "7Zip.LZMA.7z", // SOLID (all 7z are treated as SOLID) + }; + + foreach (var archiveName in testArchives) + { + var archivePath = Path.Combine(TEST_ARCHIVES_PATH, archiveName); + + // User code pattern + var options = new ExtractionOptions { ExtractFullPath = true, Overwrite = true }; + + Directory.CreateDirectory(SCRATCH_FILES_PATH); + + using var archive = ArchiveFactory.Open(archivePath); + double totalSize = archive.Entries.Where(e => !e.IsDirectory).Sum(e => e.Size); + long completed = 0; + + // This should work regardless of archive type + using var reader = archive.ExtractAllEntries(); + while (reader.MoveToNextEntry()) + { + if (!reader.Entry.IsDirectory) + { + reader.WriteEntryToDirectory(SCRATCH_FILES_PATH, options); + + completed += reader.Entry.Size; + var progress = completed / totalSize; + // In real code, user would invoke progress callback here + Assert.True(progress >= 0 && progress <= 1.0); + } + } + + // Clean up for next iteration + if (Directory.Exists(SCRATCH_FILES_PATH)) + { + Directory.Delete(SCRATCH_FILES_PATH, true); + Directory.CreateDirectory(SCRATCH_FILES_PATH); + } + } + } +} From cfc6651fffb5564863ac7ce0c814f718d6138e28 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 19 Dec 2025 11:49:35 +0000 Subject: [PATCH 3/9] Update documentation to reflect ExtractAllEntries universal compatibility Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com> --- USAGE.md | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/USAGE.md b/USAGE.md index a06d2e83..b62a9787 100644 --- a/USAGE.md +++ b/USAGE.md @@ -87,8 +87,8 @@ memoryStream.Position = 0; ### Extract all files from a rar file to a directory using RarArchive Note: Extracting a solid rar or 7z file needs to be done in sequential order to get acceptable decompression speed. -It is explicitly recommended to use `ExtractAllEntries` when extracting an entire `IArchive` instead of iterating over all its `Entries`. -Alternatively, use `IArchive.WriteToDirectory`. +`ExtractAllEntries` provides a universal interface that works for all archive types (SOLID and non-SOLID) and is recommended when extracting an entire `IArchive` with progress reporting or when you don't know the archive type in advance. +Alternatively, use `IArchive.WriteToDirectory` for simple extraction without progress reporting. ```C# using (var archive = RarArchive.Open("Test.rar")) @@ -116,6 +116,38 @@ using (var archive = RarArchive.Open("Test.rar")) } ``` +### Extract any archive type with progress reporting + +`ExtractAllEntries` works for all archive types, making it ideal when you don't know the archive type in advance: + +```C# +using (var archive = ArchiveFactory.Open("archive.rar")) // or .zip, .7z, .tar, etc. +{ + // Calculate total size for progress reporting + double totalSize = archive.Entries.Where(e => !e.IsDirectory).Sum(e => e.Size); + long completed = 0; + + using (var reader = archive.ExtractAllEntries()) + { + while (reader.MoveToNextEntry()) + { + if (!reader.Entry.IsDirectory) + { + reader.WriteEntryToDirectory(@"D:\output", new ExtractionOptions() + { + ExtractFullPath = true, + Overwrite = true + }); + + completed += reader.Entry.Size; + double progress = completed / totalSize; + Console.WriteLine($"Progress: {progress:P}"); + } + } + } +} +``` + ### Use ReaderFactory to autodetect archive type and Open the entry stream ```C# From 87c38d6dab814c7de6d0c01002b5aecc943cfc4e Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 23 Dec 2025 09:22:38 +0000 Subject: [PATCH 4/9] fix ordering and token passing --- src/SharpCompress/Archives/AbstractArchive.cs | 2 +- .../Archives/IArchiveEntryExtensions.cs | 211 +++++++++--------- src/SharpCompress/Common/ExtractionMethods.cs | 9 +- .../Readers/IReaderExtensions.cs | 195 ++++++++-------- .../SharpCompress.Test/ProgressReportTests.cs | 4 +- 5 files changed, 205 insertions(+), 216 deletions(-) diff --git a/src/SharpCompress/Archives/AbstractArchive.cs b/src/SharpCompress/Archives/AbstractArchive.cs index 131fcd3f..e42abfc3 100644 --- a/src/SharpCompress/Archives/AbstractArchive.cs +++ b/src/SharpCompress/Archives/AbstractArchive.cs @@ -107,7 +107,7 @@ public abstract class AbstractArchive : IArchive /// public IReader ExtractAllEntries() { - ((IArchiveExtractionListener)this).EnsureEntriesLoaded(); + EnsureEntriesLoaded(); return CreateReaderForSolidExtraction(); } diff --git a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs index 1ec3bfec..af2c9be4 100644 --- a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs +++ b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs @@ -11,52 +11,49 @@ public static class IArchiveEntryExtensions { private const int BufferSize = 81920; - /// - /// Extract entry to the specified stream. - /// /// The archive entry to extract. - /// The stream to write the entry content to. - /// Optional progress reporter for tracking extraction progress. - public static void WriteTo( - this IArchiveEntry archiveEntry, - Stream streamToWriteTo, - IProgress? progress = null - ) + extension(IArchiveEntry archiveEntry) { - if (archiveEntry.IsDirectory) + /// + /// Extract entry to the specified stream. + /// + /// The stream to write the entry content to. + /// Optional progress reporter for tracking extraction progress. + public void WriteTo(Stream streamToWriteTo, IProgress? progress = null) { - throw new ExtractionException("Entry is a file directory and cannot be extracted."); + if (archiveEntry.IsDirectory) + { + throw new ExtractionException("Entry is a file directory and cannot be extracted."); + } + + using var entryStream = archiveEntry.OpenEntryStream(); + var sourceStream = WrapWithProgress(entryStream, archiveEntry, progress); + sourceStream.CopyTo(streamToWriteTo, BufferSize); } - using var entryStream = archiveEntry.OpenEntryStream(); - var sourceStream = WrapWithProgress(entryStream, archiveEntry, progress); - sourceStream.CopyTo(streamToWriteTo, BufferSize); - } - - /// - /// Extract entry to the specified stream asynchronously. - /// - /// The archive entry to extract. - /// The stream to write the entry content to. - /// Cancellation token. - /// Optional progress reporter for tracking extraction progress. - public static async Task WriteToAsync( - this IArchiveEntry archiveEntry, - Stream streamToWriteTo, - CancellationToken cancellationToken = default, - IProgress? progress = null - ) - { - if (archiveEntry.IsDirectory) + /// + /// Extract entry to the specified stream asynchronously. + /// + /// The stream to write the entry content to. + /// Cancellation token. + /// Optional progress reporter for tracking extraction progress. + public async Task WriteToAsync( + Stream streamToWriteTo, + IProgress? progress = null, + CancellationToken cancellationToken = default + ) { - throw new ExtractionException("Entry is a file directory and cannot be extracted."); - } + if (archiveEntry.IsDirectory) + { + throw new ExtractionException("Entry is a file directory and cannot be extracted."); + } - using var entryStream = archiveEntry.OpenEntryStream(); - var sourceStream = WrapWithProgress(entryStream, archiveEntry, progress); - await sourceStream - .CopyToAsync(streamToWriteTo, BufferSize, cancellationToken) - .ConfigureAwait(false); + using var entryStream = await archiveEntry.OpenEntryStreamAsync(cancellationToken); + var sourceStream = WrapWithProgress(entryStream, archiveEntry, progress); + await sourceStream + .CopyToAsync(streamToWriteTo, BufferSize, cancellationToken) + .ConfigureAwait(false); + } } private static Stream WrapWithProgress( @@ -71,7 +68,7 @@ public static class IArchiveEntryExtensions } var entryPath = entry.Key ?? string.Empty; - long? totalBytes = GetEntrySizeSafe(entry); + var totalBytes = GetEntrySizeSafe(entry); return new ProgressReportingStream( source, progress, @@ -94,77 +91,71 @@ public static class IArchiveEntryExtensions } } - /// - /// Extract to specific directory, retaining filename - /// - public static void WriteToDirectory( - this IArchiveEntry entry, - string destinationDirectory, - ExtractionOptions? options = null - ) => - ExtractionMethods.WriteEntryToDirectory( - entry, - destinationDirectory, - options, - entry.WriteToFile - ); + extension(IArchiveEntry entry) + { + /// + /// Extract to specific directory, retaining filename + /// + public void WriteToDirectory( + string destinationDirectory, + ExtractionOptions? options = null + ) => + ExtractionMethods.WriteEntryToDirectory( + entry, + destinationDirectory, + options, + entry.WriteToFile + ); - /// - /// Extract to specific directory asynchronously, retaining filename - /// - public static Task WriteToDirectoryAsync( - this IArchiveEntry entry, - string destinationDirectory, - ExtractionOptions? options = null, - CancellationToken cancellationToken = default - ) => - ExtractionMethods.WriteEntryToDirectoryAsync( - entry, - destinationDirectory, - options, - (x, opt) => entry.WriteToFileAsync(x, opt, cancellationToken), - cancellationToken - ); + /// + /// Extract to specific directory asynchronously, retaining filename + /// + public Task WriteToDirectoryAsync( + string destinationDirectory, + ExtractionOptions? options = null, + CancellationToken cancellationToken = default + ) => + ExtractionMethods.WriteEntryToDirectoryAsync( + entry, + destinationDirectory, + options, + entry.WriteToFileAsync, + cancellationToken + ); - /// - /// Extract to specific file - /// - public static void WriteToFile( - this IArchiveEntry entry, - string destinationFileName, - ExtractionOptions? options = null - ) => - ExtractionMethods.WriteEntryToFile( - entry, - destinationFileName, - options, - (x, fm) => - { - using var fs = File.Open(destinationFileName, fm); - entry.WriteTo(fs); - } - ); + /// + /// Extract to specific file + /// + public void WriteToFile(string destinationFileName, ExtractionOptions? options = null) => + ExtractionMethods.WriteEntryToFile( + entry, + destinationFileName, + options, + (x, fm) => + { + using var fs = File.Open(destinationFileName, fm); + entry.WriteTo(fs); + } + ); - /// - /// Extract to specific file asynchronously - /// - public static Task WriteToFileAsync( - this IArchiveEntry entry, - string destinationFileName, - ExtractionOptions? options = null, - CancellationToken cancellationToken = default - ) => - ExtractionMethods.WriteEntryToFileAsync( - entry, - destinationFileName, - options, - async (x, fm) => - { - using var fs = File.Open(destinationFileName, fm); - await entry - .WriteToAsync(fs, progress: null, cancellationToken: cancellationToken) - .ConfigureAwait(false); - }, - cancellationToken - ); + /// + /// Extract to specific file asynchronously + /// + public Task WriteToFileAsync( + string destinationFileName, + ExtractionOptions? options = null, + CancellationToken cancellationToken = default + ) => + ExtractionMethods.WriteEntryToFileAsync( + entry, + destinationFileName, + options, + async (x, fm, ct) => + { + using var fs = File.Open(destinationFileName, fm); + await entry.WriteToAsync(fs, null, ct).ConfigureAwait(false); + }, + cancellationToken + ); + } } diff --git a/src/SharpCompress/Common/ExtractionMethods.cs b/src/SharpCompress/Common/ExtractionMethods.cs index 485fdf4d..509524b1 100644 --- a/src/SharpCompress/Common/ExtractionMethods.cs +++ b/src/SharpCompress/Common/ExtractionMethods.cs @@ -128,7 +128,7 @@ internal static class ExtractionMethods IEntry entry, string destinationDirectory, ExtractionOptions? options, - Func writeAsync, + Func writeAsync, CancellationToken cancellationToken = default ) { @@ -189,7 +189,7 @@ internal static class ExtractionMethods "Entry is trying to write a file outside of the destination directory." ); } - await writeAsync(destinationFileName, options).ConfigureAwait(false); + await writeAsync(destinationFileName, options, cancellationToken).ConfigureAwait(false); } else if (options.ExtractFullPath && !Directory.Exists(destinationFileName)) { @@ -201,7 +201,7 @@ internal static class ExtractionMethods IEntry entry, string destinationFileName, ExtractionOptions? options, - Func openAndWriteAsync, + Func openAndWriteAsync, CancellationToken cancellationToken = default ) { @@ -225,7 +225,8 @@ internal static class ExtractionMethods fm = FileMode.CreateNew; } - await openAndWriteAsync(destinationFileName, fm).ConfigureAwait(false); + await openAndWriteAsync(destinationFileName, fm, cancellationToken) + .ConfigureAwait(false); entry.PreserveExtractionOptions(destinationFileName, options); } } diff --git a/src/SharpCompress/Readers/IReaderExtensions.cs b/src/SharpCompress/Readers/IReaderExtensions.cs index 6480df1d..65c6b1fa 100644 --- a/src/SharpCompress/Readers/IReaderExtensions.cs +++ b/src/SharpCompress/Readers/IReaderExtensions.cs @@ -7,124 +7,121 @@ namespace SharpCompress.Readers; public static class IReaderExtensions { - public static void WriteEntryTo(this IReader reader, string filePath) + extension(IReader reader) { - using Stream stream = File.Open(filePath, FileMode.Create, FileAccess.Write); - reader.WriteEntryTo(stream); - } - - public static void WriteEntryTo(this IReader reader, FileInfo filePath) - { - using Stream stream = filePath.Open(FileMode.Create); - reader.WriteEntryTo(stream); - } - - /// - /// Extract all remaining unread entries to specific directory, retaining filename - /// - public static void WriteAllToDirectory( - this IReader reader, - string destinationDirectory, - ExtractionOptions? options = null - ) - { - while (reader.MoveToNextEntry()) + public void WriteEntryTo(string filePath) { - reader.WriteEntryToDirectory(destinationDirectory, options); + using Stream stream = File.Open(filePath, FileMode.Create, FileAccess.Write); + reader.WriteEntryTo(stream); } - } - /// - /// Extract to specific directory, retaining filename - /// - public static void WriteEntryToDirectory( - this IReader reader, - string destinationDirectory, - ExtractionOptions? options = null - ) => - ExtractionMethods.WriteEntryToDirectory( - reader.Entry, - destinationDirectory, - options, - reader.WriteEntryToFile - ); + public void WriteEntryTo(FileInfo filePath) + { + using Stream stream = filePath.Open(FileMode.Create); + reader.WriteEntryTo(stream); + } - /// - /// Extract to specific file - /// - public static void WriteEntryToFile( - this IReader reader, - string destinationFileName, - ExtractionOptions? options = null - ) => - ExtractionMethods.WriteEntryToFile( - reader.Entry, - destinationFileName, - options, - (x, fm) => + /// + /// Extract all remaining unread entries to specific directory, retaining filename + /// + public void WriteAllToDirectory( + string destinationDirectory, + ExtractionOptions? options = null + ) + { + while (reader.MoveToNextEntry()) { - using var fs = File.Open(destinationFileName, fm); - reader.WriteEntryTo(fs); + reader.WriteEntryToDirectory(destinationDirectory, options); } - ); + } - /// - /// Extract to specific directory asynchronously, retaining filename - /// - public static async Task WriteEntryToDirectoryAsync( - this IReader reader, - string destinationDirectory, - ExtractionOptions? options = null, - CancellationToken cancellationToken = default - ) => - await ExtractionMethods - .WriteEntryToDirectoryAsync( + /// + /// Extract to specific directory, retaining filename + /// + public void WriteEntryToDirectory( + string destinationDirectory, + ExtractionOptions? options = null + ) => + ExtractionMethods.WriteEntryToDirectory( reader.Entry, destinationDirectory, options, - (fileName, opts) => reader.WriteEntryToFileAsync(fileName, opts, cancellationToken), - cancellationToken - ) - .ConfigureAwait(false); + reader.WriteEntryToFile + ); - /// - /// Extract to specific file asynchronously - /// - public static async Task WriteEntryToFileAsync( - this IReader reader, - string destinationFileName, - ExtractionOptions? options = null, - CancellationToken cancellationToken = default - ) => - await ExtractionMethods - .WriteEntryToFileAsync( + /// + /// Extract to specific file + /// + public void WriteEntryToFile( + string destinationFileName, + ExtractionOptions? options = null + ) => + ExtractionMethods.WriteEntryToFile( reader.Entry, destinationFileName, options, - async (x, fm) => + (x, fm) => { using var fs = File.Open(destinationFileName, fm); - await reader.WriteEntryToAsync(fs, cancellationToken).ConfigureAwait(false); - }, - cancellationToken - ) - .ConfigureAwait(false); + reader.WriteEntryTo(fs); + } + ); - /// - /// Extract all remaining unread entries to specific directory asynchronously, retaining filename - /// - public static async Task WriteAllToDirectoryAsync( - this IReader reader, - string destinationDirectory, - ExtractionOptions? options = null, - CancellationToken cancellationToken = default - ) - { - while (reader.MoveToNextEntry()) - { - await reader - .WriteEntryToDirectoryAsync(destinationDirectory, options, cancellationToken) + /// + /// Extract to specific directory asynchronously, retaining filename + /// + public async Task WriteEntryToDirectoryAsync( + string destinationDirectory, + ExtractionOptions? options = null, + CancellationToken cancellationToken = default + ) => + await ExtractionMethods + .WriteEntryToDirectoryAsync( + reader.Entry, + destinationDirectory, + options, + reader.WriteEntryToFileAsync, + cancellationToken + ) .ConfigureAwait(false); + + /// + /// Extract to specific file asynchronously + /// + public async Task WriteEntryToFileAsync( + string destinationFileName, + ExtractionOptions? options = null, + CancellationToken cancellationToken = default + ) => + await ExtractionMethods + .WriteEntryToFileAsync( + reader.Entry, + destinationFileName, + options, + async (x, fm, ct) => + { + using var fs = File.Open(destinationFileName, fm); + await reader.WriteEntryToAsync(fs, ct).ConfigureAwait(false); + }, + cancellationToken + ) + .ConfigureAwait(false); + + /// + /// Extract all remaining unread entries to specific directory asynchronously, retaining filename + /// + public async Task WriteAllToDirectoryAsync( + string destinationDirectory, + ExtractionOptions? options = null, + CancellationToken cancellationToken = default + ) + { + while (await reader.MoveToNextEntryAsync(cancellationToken)) + { + await reader + .WriteEntryToDirectoryAsync(destinationDirectory, options, cancellationToken) + .ConfigureAwait(false); + } } } } diff --git a/tests/SharpCompress.Test/ProgressReportTests.cs b/tests/SharpCompress.Test/ProgressReportTests.cs index 58ea4304..75fa2116 100644 --- a/tests/SharpCompress.Test/ProgressReportTests.cs +++ b/tests/SharpCompress.Test/ProgressReportTests.cs @@ -188,7 +188,7 @@ public class ProgressReportTests : TestBase if (!entry.IsDirectory) { using var extractedStream = new MemoryStream(); - await entry.WriteToAsync(extractedStream, CancellationToken.None, progress); + await entry.WriteToAsync(extractedStream, progress, CancellationToken.None); } } @@ -410,7 +410,7 @@ public class ProgressReportTests : TestBase if (!entry.IsDirectory) { using var extractedStream = new MemoryStream(); - await entry.WriteToAsync(extractedStream, CancellationToken.None, progress); + await entry.WriteToAsync(extractedStream, progress, CancellationToken.None); } } From e79dceb67ecf8ad1226a0287b19a32437299b6e3 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 23 Dec 2025 09:31:51 +0000 Subject: [PATCH 5/9] check should be there --- src/SharpCompress/Archives/AbstractArchive.cs | 6 + .../ExtractAllEntriesTests.cs | 123 +----------------- 2 files changed, 9 insertions(+), 120 deletions(-) diff --git a/src/SharpCompress/Archives/AbstractArchive.cs b/src/SharpCompress/Archives/AbstractArchive.cs index e42abfc3..672382ff 100644 --- a/src/SharpCompress/Archives/AbstractArchive.cs +++ b/src/SharpCompress/Archives/AbstractArchive.cs @@ -107,6 +107,12 @@ public abstract class AbstractArchive : IArchive /// public IReader ExtractAllEntries() { + if (!IsSolid && Type != ArchiveType.SevenZip) + { + throw new SharpCompressException( + "ExtractAllEntries can only be used on solid archives or 7Zip archives (which require random access)." + ); + } EnsureEntriesLoaded(); return CreateReaderForSolidExtraction(); } diff --git a/tests/SharpCompress.Test/ExtractAllEntriesTests.cs b/tests/SharpCompress.Test/ExtractAllEntriesTests.cs index 15ddf4eb..204d47bb 100644 --- a/tests/SharpCompress.Test/ExtractAllEntriesTests.cs +++ b/tests/SharpCompress.Test/ExtractAllEntriesTests.cs @@ -1,4 +1,3 @@ -using System; using System.IO; using System.Linq; using SharpCompress.Archives; @@ -14,82 +13,16 @@ namespace SharpCompress.Test; /// public class ExtractAllEntriesTests : TestBase { - [Theory] - [InlineData("Zip.deflate.zip", false)] - [InlineData("Zip.bzip2.zip", false)] - [InlineData("Zip.lzma.zip", false)] - [InlineData("Zip.ppmd.zip", false)] - [InlineData("Rar.rar", false)] - [InlineData("Rar.solid.rar", true)] - [InlineData("7Zip.LZMA.7z", true)] - [InlineData("7Zip.PPMd.7z", true)] - [InlineData("Tar.tar", false)] - public void ExtractAllEntries_WorksForAllArchiveTypes(string archiveName, bool expectedSolid) - { - var archivePath = Path.Combine(TEST_ARCHIVES_PATH, archiveName); - - using var archive = ArchiveFactory.Open(archivePath); - - // Verify IsSolid matches expectation - Assert.Equal(expectedSolid, archive.IsSolid); - - // This should not throw for any archive type - using var reader = archive.ExtractAllEntries(); - - var entryCount = 0; - var filesExtracted = 0; - while (reader.MoveToNextEntry()) - { - entryCount++; - if (!reader.Entry.IsDirectory) - { - filesExtracted++; - // Extract to scratch path to verify it actually works - reader.WriteEntryToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); - } - } - - Assert.True(entryCount > 0, $"Archive {archiveName} should have at least one entry"); - Assert.True( - filesExtracted > 0, - $"Archive {archiveName} should have at least one file entry" - ); - } - [Fact] public void ExtractAllEntries_WithProgressReporting_NonSolidArchive() { var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip"); using var archive = ArchiveFactory.Open(archivePath); - - // Calculate total size like user code does - double totalSize = archive.Entries.Where(e => !e.IsDirectory).Sum(e => e.Size); - long completed = 0; - var progressReports = 0; - - using var reader = archive.ExtractAllEntries(); - while (reader.MoveToNextEntry()) + Assert.Throws(() => { - if (!reader.Entry.IsDirectory) - { - reader.WriteEntryToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); - - completed += reader.Entry.Size; - var progress = completed / totalSize; - progressReports++; - - Assert.True(progress >= 0 && progress <= 1.0); - } - } - - Assert.True(progressReports > 0); + using var reader = archive.ExtractAllEntries(); + }); } [Fact] @@ -125,54 +58,4 @@ public class ExtractAllEntriesTests : TestBase Assert.True(progressReports > 0); } - - [Fact] - public void ExtractAllEntries_UserScenario_UnknownArchiveType() - { - // Test the exact user scenario: they don't know if archive is SOLID or not - // and want to use ExtractAllEntries with progress reporting - var testArchives = new[] - { - "Zip.deflate.zip", // Non-SOLID - "Rar.rar", // Non-SOLID - "Rar.solid.rar", // SOLID - "7Zip.LZMA.7z", // SOLID (all 7z are treated as SOLID) - }; - - foreach (var archiveName in testArchives) - { - var archivePath = Path.Combine(TEST_ARCHIVES_PATH, archiveName); - - // User code pattern - var options = new ExtractionOptions { ExtractFullPath = true, Overwrite = true }; - - Directory.CreateDirectory(SCRATCH_FILES_PATH); - - using var archive = ArchiveFactory.Open(archivePath); - double totalSize = archive.Entries.Where(e => !e.IsDirectory).Sum(e => e.Size); - long completed = 0; - - // This should work regardless of archive type - using var reader = archive.ExtractAllEntries(); - while (reader.MoveToNextEntry()) - { - if (!reader.Entry.IsDirectory) - { - reader.WriteEntryToDirectory(SCRATCH_FILES_PATH, options); - - completed += reader.Entry.Size; - var progress = completed / totalSize; - // In real code, user would invoke progress callback here - Assert.True(progress >= 0 && progress <= 1.0); - } - } - - // Clean up for next iteration - if (Directory.Exists(SCRATCH_FILES_PATH)) - { - Directory.Delete(SCRATCH_FILES_PATH, true); - Directory.CreateDirectory(SCRATCH_FILES_PATH); - } - } - } } From eb2cba09b2b489c5ea1231d915b89aef413f3f80 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 23 Dec 2025 09:34:55 +0000 Subject: [PATCH 6/9] update usage --- USAGE.md | 72 +++++++++++++++++++++++++++----------------------------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/USAGE.md b/USAGE.md index b62a9787..6d267c5e 100644 --- a/USAGE.md +++ b/USAGE.md @@ -87,20 +87,17 @@ memoryStream.Position = 0; ### Extract all files from a rar file to a directory using RarArchive Note: Extracting a solid rar or 7z file needs to be done in sequential order to get acceptable decompression speed. -`ExtractAllEntries` provides a universal interface that works for all archive types (SOLID and non-SOLID) and is recommended when extracting an entire `IArchive` with progress reporting or when you don't know the archive type in advance. -Alternatively, use `IArchive.WriteToDirectory` for simple extraction without progress reporting. +`ExtractAllEntries` is only available for solid archives (like solid Rar) or 7Zip archives. For simple extraction, use `archive.WriteToDirectory()` instead. ```C# using (var archive = RarArchive.Open("Test.rar")) { - using (var reader = archive.ExtractAllEntries()) + // Simple extraction - works for all archive types + archive.WriteToDirectory(@"D:\temp", new ExtractionOptions() { - reader.WriteAllToDirectory(@"D:\temp", new ExtractionOptions() - { - ExtractFullPath = true, - Overwrite = true - }); - } + ExtractFullPath = true, + Overwrite = true + }); } ``` @@ -116,32 +113,35 @@ using (var archive = RarArchive.Open("Test.rar")) } ``` -### Extract any archive type with progress reporting +### Extract solid Rar or 7Zip archives with manual progress reporting -`ExtractAllEntries` works for all archive types, making it ideal when you don't know the archive type in advance: +`ExtractAllEntries` only works for solid archives (Rar) or 7Zip archives. For optimal performance with these archive types, use this method: ```C# -using (var archive = ArchiveFactory.Open("archive.rar")) // or .zip, .7z, .tar, etc. +using (var archive = RarArchive.Open("archive.rar")) // Must be solid Rar or 7Zip { - // Calculate total size for progress reporting - double totalSize = archive.Entries.Where(e => !e.IsDirectory).Sum(e => e.Size); - long completed = 0; - - using (var reader = archive.ExtractAllEntries()) + if (archive.IsSolid || archive.Type == ArchiveType.SevenZip) { - while (reader.MoveToNextEntry()) - { - if (!reader.Entry.IsDirectory) - { - reader.WriteEntryToDirectory(@"D:\output", new ExtractionOptions() - { - ExtractFullPath = true, - Overwrite = true - }); + // Calculate total size for progress reporting + double totalSize = archive.Entries.Where(e => !e.IsDirectory).Sum(e => e.Size); + long completed = 0; - completed += reader.Entry.Size; - double progress = completed / totalSize; - Console.WriteLine($"Progress: {progress:P}"); + using (var reader = archive.ExtractAllEntries()) + { + while (reader.MoveToNextEntry()) + { + if (!reader.Entry.IsDirectory) + { + reader.WriteEntryToDirectory(@"D:\output", new ExtractionOptions() + { + ExtractFullPath = true, + Overwrite = true + }); + + completed += reader.Entry.Size; + double progress = completed / totalSize; + Console.WriteLine($"Progress: {progress:P}"); + } } } } @@ -330,14 +330,12 @@ using (var writer = WriterFactory.Open(stream, ArchiveType.Zip, CompressionType. ```C# using (var archive = ZipArchive.Open("archive.zip")) { - using (var reader = archive.ExtractAllEntries()) - { - await reader.WriteAllToDirectoryAsync( - @"C:\output", - new ExtractionOptions() { ExtractFullPath = true, Overwrite = true }, - cancellationToken - ); - } + // Simple async extraction - works for all archive types + await archive.WriteToDirectoryAsync( + @"C:\output", + new ExtractionOptions() { ExtractFullPath = true, Overwrite = true }, + cancellationToken + ); } ``` From ead5916eae24e4ecfdc0b4ef0dc146eb3aa4deeb Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 23 Dec 2025 15:37:28 +0000 Subject: [PATCH 7/9] Update USAGE.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- USAGE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/USAGE.md b/USAGE.md index 6d267c5e..4a1a37fc 100644 --- a/USAGE.md +++ b/USAGE.md @@ -92,7 +92,7 @@ Note: Extracting a solid rar or 7z file needs to be done in sequential order to ```C# using (var archive = RarArchive.Open("Test.rar")) { - // Simple extraction - works for all archive types + // Simple extraction with RarArchive; this WriteToDirectory pattern works for all archive types archive.WriteToDirectory(@"D:\temp", new ExtractionOptions() { ExtractFullPath = true, From 583b048046f1a9cbd03552b69603c2bebb57e93a Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 23 Dec 2025 15:38:00 +0000 Subject: [PATCH 8/9] Update USAGE.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- USAGE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/USAGE.md b/USAGE.md index 4a1a37fc..1e3a6c78 100644 --- a/USAGE.md +++ b/USAGE.md @@ -87,7 +87,7 @@ memoryStream.Position = 0; ### Extract all files from a rar file to a directory using RarArchive Note: Extracting a solid rar or 7z file needs to be done in sequential order to get acceptable decompression speed. -`ExtractAllEntries` is only available for solid archives (like solid Rar) or 7Zip archives. For simple extraction, use `archive.WriteToDirectory()` instead. +`ExtractAllEntries` is primarily intended for solid archives (like solid Rar) or 7Zip archives, where sequential extraction provides the best performance. For general/simple extraction with any supported archive type, use `archive.WriteToDirectory()` instead. ```C# using (var archive = RarArchive.Open("Test.rar")) From d9274cf7942624b4d5652ebc3702ca17dc5894c1 Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Tue, 23 Dec 2025 15:38:17 +0000 Subject: [PATCH 9/9] Update tests/SharpCompress.Test/ExtractAllEntriesTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/SharpCompress.Test/ExtractAllEntriesTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/SharpCompress.Test/ExtractAllEntriesTests.cs b/tests/SharpCompress.Test/ExtractAllEntriesTests.cs index 204d47bb..007f620f 100644 --- a/tests/SharpCompress.Test/ExtractAllEntriesTests.cs +++ b/tests/SharpCompress.Test/ExtractAllEntriesTests.cs @@ -8,8 +8,8 @@ using Xunit; namespace SharpCompress.Test; /// -/// Tests for ExtractAllEntries method which should work for all archive types -/// regardless of whether they are SOLID or not. +/// Tests for the ExtractAllEntries method behavior on both solid and non-solid +/// archives, including progress reporting and current usage restrictions. /// public class ExtractAllEntriesTests : TestBase {