diff --git a/AGENTS.md b/AGENTS.md
index c7b4a5a63..ae36265aa 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -32,12 +32,34 @@ SharpCompress is a pure C# compression library supporting multiple archive forma
- Use CSharpier for code formatting to ensure consistent style across the project
- CSharpier is configured as a local tool in `.config/dotnet-tools.json`
-- **To format code, run the task: `format` task (which runs `dotnet csharpier .` from project root)**
-- Restore tools with: `dotnet tool restore`
-- Format files from the project root with: `dotnet csharpier .`
-- Configure your IDE to format on save using CSharpier for the best experience
+
+### Commands
+
+1. **Restore tools** (first time only):
+ ```bash
+ dotnet tool restore
+ ```
+
+2. **Check if files are formatted correctly** (doesn't modify files):
+ ```bash
+ dotnet csharpier check .
+ ```
+ - Exit code 0: All files are properly formatted
+ - Exit code 1: Some files need formatting (will show which files and differences)
+
+3. **Format files** (modifies files):
+ ```bash
+ dotnet csharpier format .
+ ```
+ - Formats all files in the project to match CSharpier style
+ - Run from project root directory
+
+4. **Configure your IDE** to format on save using CSharpier for the best experience
+
+### Additional Notes
- The project also uses `.editorconfig` for editor settings (indentation, encoding, etc.)
- Let CSharpier handle code style while `.editorconfig` handles editor behavior
+- Always run `dotnet csharpier check .` before committing to verify formatting
## Project Setup and Structure
diff --git a/src/SharpCompress/Archives/IArchiveExtensions.cs b/src/SharpCompress/Archives/IArchiveExtensions.cs
index d9701da30..0d39c6e2c 100644
--- a/src/SharpCompress/Archives/IArchiveExtensions.cs
+++ b/src/SharpCompress/Archives/IArchiveExtensions.cs
@@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
-using System.Linq;
using System.Threading;
+using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Readers;
@@ -10,76 +10,159 @@ namespace SharpCompress.Archives;
public static class IArchiveExtensions
{
- ///
- /// Extract to specific directory, retaining filename
- ///
- public static void WriteToDirectory(
- this IArchive archive,
- string destinationDirectory,
- ExtractionOptions? options = null
- )
- {
- using var reader = archive.ExtractAllEntries();
- reader.WriteAllToDirectory(destinationDirectory, options);
- }
-
- ///
- /// Extracts the archive to the destination directory. Directories will be created as needed.
- ///
/// The archive to extract.
- /// The folder to extract into.
- /// Optional progress report callback.
- /// Optional cancellation token.
- public static void ExtractToDirectory(
- this IArchive archive,
- string destination,
- Action? progressReport = null,
- CancellationToken cancellationToken = default
- )
+ extension(IArchive archive)
{
- // Prepare for progress reporting
- var totalBytes = archive.TotalUncompressSize;
- var bytesRead = 0L;
-
- // Tracking for created directories.
- var seenDirectories = new HashSet();
+ ///
+ /// Extract to specific directory with progress reporting
+ ///
+ /// The folder to extract into.
+ /// Extraction options.
+ /// Optional progress reporter for tracking extraction progress.
+ public void WriteToDirectory(
+ string destinationDirectory,
+ ExtractionOptions? options = null,
+ IProgress? progress = null
+ )
+ {
+ // For solid archives (Rar, 7Zip), use the optimized reader-based approach
+ if (archive.IsSolid || archive.Type == ArchiveType.SevenZip)
+ {
+ using var reader = archive.ExtractAllEntries();
+ reader.WriteAllToDirectory(destinationDirectory, options);
+ }
+ else
+ {
+ // For non-solid archives, extract entries directly
+ archive.WriteToDirectoryInternal(destinationDirectory, options, progress);
+ }
+ }
- // Extract
- foreach (var entry in archive.Entries)
+ private void WriteToDirectoryInternal(
+ string destinationDirectory,
+ ExtractionOptions? options,
+ IProgress? progress
+ )
{
- cancellationToken.ThrowIfCancellationRequested();
+ // Prepare for progress reporting
+ var totalBytes = archive.TotalUncompressSize;
+ var bytesRead = 0L;
+
+ // Tracking for created directories.
+ var seenDirectories = new HashSet();
- if (entry.IsDirectory)
+ // Extract
+ foreach (var entry in archive.Entries)
{
- var dirPath = Path.Combine(destination, entry.Key.NotNull("Entry Key is null"));
- if (
- Path.GetDirectoryName(dirPath + "/") is { } emptyDirectory
- && seenDirectories.Add(dirPath)
- )
+ if (entry.IsDirectory)
{
- Directory.CreateDirectory(emptyDirectory);
+ var dirPath = Path.Combine(
+ destinationDirectory,
+ entry.Key.NotNull("Entry Key is null")
+ );
+ if (
+ Path.GetDirectoryName(dirPath + "/") is { } parentDirectory
+ && seenDirectories.Add(dirPath)
+ )
+ {
+ Directory.CreateDirectory(parentDirectory);
+ }
+ continue;
}
- continue;
+
+ // Use the entry's WriteToDirectory method which respects ExtractionOptions
+ entry.WriteToDirectory(destinationDirectory, options);
+
+ // Update progress
+ bytesRead += entry.Size;
+ progress?.Report(
+ new ProgressReport(entry.Key ?? string.Empty, bytesRead, totalBytes)
+ );
}
+ }
- // Create each directory if not already created
- var path = Path.Combine(destination, entry.Key.NotNull("Entry Key is null"));
- if (Path.GetDirectoryName(path) is { } directory)
+ ///
+ /// Extract to specific directory asynchronously with progress reporting and cancellation support
+ ///
+ /// The folder to extract into.
+ /// Extraction options.
+ /// Optional progress reporter for tracking extraction progress.
+ /// Optional cancellation token.
+ public async Task WriteToDirectoryAsync(
+ string destinationDirectory,
+ ExtractionOptions? options = null,
+ IProgress? progress = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ // For solid archives (Rar, 7Zip), use the optimized reader-based approach
+ if (archive.IsSolid || archive.Type == ArchiveType.SevenZip)
+ {
+ using var reader = archive.ExtractAllEntries();
+ await reader.WriteAllToDirectoryAsync(
+ destinationDirectory,
+ options,
+ cancellationToken
+ );
+ }
+ else
{
- if (!Directory.Exists(directory) && !seenDirectories.Contains(directory))
+ // For non-solid archives, extract entries directly
+ await archive.WriteToDirectoryAsyncInternal(
+ destinationDirectory,
+ options,
+ progress,
+ cancellationToken
+ );
+ }
+ }
+
+ private async Task WriteToDirectoryAsyncInternal(
+ string destinationDirectory,
+ ExtractionOptions? options,
+ IProgress? progress,
+ CancellationToken cancellationToken
+ )
+ {
+ // Prepare for progress reporting
+ var totalBytes = archive.TotalUncompressSize;
+ var bytesRead = 0L;
+
+ // Tracking for created directories.
+ var seenDirectories = new HashSet();
+
+ // Extract
+ foreach (var entry in archive.Entries)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (entry.IsDirectory)
{
- Directory.CreateDirectory(directory);
- seenDirectories.Add(directory);
+ var dirPath = Path.Combine(
+ destinationDirectory,
+ entry.Key.NotNull("Entry Key is null")
+ );
+ if (
+ Path.GetDirectoryName(dirPath + "/") is { } parentDirectory
+ && seenDirectories.Add(dirPath)
+ )
+ {
+ Directory.CreateDirectory(parentDirectory);
+ }
+ continue;
}
- }
- // Write file
- using var fs = File.OpenWrite(path);
- entry.WriteTo(fs);
+ // Use the entry's WriteToDirectoryAsync method which respects ExtractionOptions
+ await entry
+ .WriteToDirectoryAsync(destinationDirectory, options, cancellationToken)
+ .ConfigureAwait(false);
- // Update progress
- bytesRead += entry.Size;
- progressReport?.Invoke(bytesRead / (double)totalBytes);
+ // Update progress
+ bytesRead += entry.Size;
+ progress?.Report(
+ new ProgressReport(entry.Key ?? string.Empty, bytesRead, totalBytes)
+ );
+ }
}
}
}
diff --git a/tests/SharpCompress.Test/ArchiveTests.cs b/tests/SharpCompress.Test/ArchiveTests.cs
index 2f47e1215..916c9e2c0 100644
--- a/tests/SharpCompress.Test/ArchiveTests.cs
+++ b/tests/SharpCompress.Test/ArchiveTests.cs
@@ -261,7 +261,7 @@ protected void ArchiveExtractToDirectory(
testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive);
using (var archive = ArchiveFactory.Open(new FileInfo(testArchive), readerOptions))
{
- archive.ExtractToDirectory(SCRATCH_FILES_PATH);
+ archive.WriteToDirectory(SCRATCH_FILES_PATH);
}
VerifyFiles();
}
diff --git a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs
index 01fe9ca83..cd93a3c18 100644
--- a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs
+++ b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs
@@ -194,4 +194,38 @@ await entry.WriteToDirectoryAsync(
}
VerifyFiles();
}
+
+ [Fact]
+ public async Task Zip_Deflate_Archive_WriteToDirectoryAsync()
+ {
+ using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip")))
+ using (var archive = ZipArchive.Open(stream))
+ {
+ await archive.WriteToDirectoryAsync(
+ SCRATCH_FILES_PATH,
+ new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
+ );
+ }
+ VerifyFiles();
+ }
+
+ [Fact]
+ public async Task Zip_Deflate_Archive_WriteToDirectoryAsync_WithProgress()
+ {
+ var progressReports = new System.Collections.Generic.List();
+ var progress = new Progress(report => progressReports.Add(report));
+
+ using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip")))
+ using (var archive = ZipArchive.Open(stream))
+ {
+ await archive.WriteToDirectoryAsync(
+ SCRATCH_FILES_PATH,
+ new ExtractionOptions { ExtractFullPath = true, Overwrite = true },
+ progress
+ );
+ }
+
+ VerifyFiles();
+ Assert.True(progressReports.Count > 0, "Progress reports should be generated");
+ }
}