diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index cdb3c8d1..97f37dcc 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "csharpier": { - "version": "1.2.5", + "version": "1.2.6", "commands": [ "csharpier" ], diff --git a/.github/workflows/performance-benchmarks.yml b/.github/workflows/performance-benchmarks.yml new file mode 100644 index 00000000..907a96e9 --- /dev/null +++ b/.github/workflows/performance-benchmarks.yml @@ -0,0 +1,50 @@ +name: Performance Benchmarks + +on: + push: + branches: + - 'master' + - 'release' + pull_request: + branches: + - 'master' + - 'release' + workflow_dispatch: + +permissions: + contents: read + +jobs: + benchmark: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + + - name: Build Performance Project + run: dotnet build tests/SharpCompress.Performance/SharpCompress.Performance.csproj --configuration Release + + - name: Run Benchmarks + run: dotnet run --project tests/SharpCompress.Performance/SharpCompress.Performance.csproj --configuration Release --no-build -- --filter "*" --exporters json markdown --artifacts benchmark-results + continue-on-error: true + + - name: Display Benchmark Results + if: always() + run: dotnet run --project build/build.csproj -- display-benchmark-results + + - name: Compare with Baseline + if: always() + run: dotnet run --project build/build.csproj -- compare-benchmark-results + + - name: Upload Benchmark Results + if: always() + uses: actions/upload-artifact@v6 + with: + name: benchmark-results + path: benchmark-results/ diff --git a/.gitignore b/.gitignore index 17b16704..66147659 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,10 @@ tests/TestArchives/*/Scratch2 tools .idea/ artifacts/ +BenchmarkDotNet.Artifacts/ +baseline-artifacts/ +profiler-snapshots/ .DS_Store *.snupkg +benchmark-results/ diff --git a/Directory.Packages.props b/Directory.Packages.props index 11d6f262..8f8510d9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,5 +1,6 @@ + diff --git a/build/Program.cs b/build/Program.cs index 1ad21f50..bfeb60b9 100644 --- a/build/Program.cs +++ b/build/Program.cs @@ -19,6 +19,9 @@ const string Publish = "publish"; const string DetermineVersion = "determine-version"; const string UpdateVersion = "update-version"; const string PushToNuGet = "push-to-nuget"; +const string DisplayBenchmarkResults = "display-benchmark-results"; +const string CompareBenchmarkResults = "compare-benchmark-results"; +const string GenerateBaseline = "generate-baseline"; Target( Clean, @@ -210,6 +213,249 @@ Target( } ); +Target( + DisplayBenchmarkResults, + () => + { + var githubStepSummary = Environment.GetEnvironmentVariable("GITHUB_STEP_SUMMARY"); + var resultsDir = "benchmark-results/results"; + + if (!Directory.Exists(resultsDir)) + { + Console.WriteLine("No benchmark results found."); + return; + } + + var markdownFiles = Directory + .GetFiles(resultsDir, "*-report-github.md") + .OrderBy(f => f) + .ToList(); + + if (markdownFiles.Count == 0) + { + Console.WriteLine("No benchmark markdown reports found."); + return; + } + + var output = new List { "## Benchmark Results", "" }; + + foreach (var file in markdownFiles) + { + Console.WriteLine($"Processing {Path.GetFileName(file)}"); + var content = File.ReadAllText(file); + output.Add(content); + output.Add(""); + } + + // Write to GitHub Step Summary if available + if (!string.IsNullOrEmpty(githubStepSummary)) + { + File.AppendAllLines(githubStepSummary, output); + Console.WriteLine($"Benchmark results written to GitHub Step Summary"); + } + else + { + // Write to console if not in GitHub Actions + foreach (var line in output) + { + Console.WriteLine(line); + } + } + } +); + +Target( + CompareBenchmarkResults, + () => + { + var githubStepSummary = Environment.GetEnvironmentVariable("GITHUB_STEP_SUMMARY"); + var baselinePath = "tests/SharpCompress.Performance/baseline-results.md"; + var resultsDir = "benchmark-results/results"; + + var output = new List { "## Comparison with Baseline", "" }; + + if (!File.Exists(baselinePath)) + { + Console.WriteLine("Baseline file not found"); + output.Add("⚠️ Baseline file not found. Run `generate-baseline` to create it."); + WriteOutput(output, githubStepSummary); + return; + } + + if (!Directory.Exists(resultsDir)) + { + Console.WriteLine("No current benchmark results found."); + output.Add("⚠️ No current benchmark results found. Showing baseline only."); + output.Add(""); + output.Add("### Baseline Results"); + output.AddRange(File.ReadAllLines(baselinePath)); + WriteOutput(output, githubStepSummary); + return; + } + + var markdownFiles = Directory + .GetFiles(resultsDir, "*-report-github.md") + .OrderBy(f => f) + .ToList(); + + if (markdownFiles.Count == 0) + { + Console.WriteLine("No current benchmark markdown reports found."); + output.Add("⚠️ No current benchmark results found. Showing baseline only."); + output.Add(""); + output.Add("### Baseline Results"); + output.AddRange(File.ReadAllLines(baselinePath)); + WriteOutput(output, githubStepSummary); + return; + } + + Console.WriteLine("Parsing baseline results..."); + var baselineMetrics = ParseBenchmarkResults(File.ReadAllText(baselinePath)); + + Console.WriteLine("Parsing current results..."); + var currentText = string.Join("\n", markdownFiles.Select(f => File.ReadAllText(f))); + var currentMetrics = ParseBenchmarkResults(currentText); + + Console.WriteLine("Comparing results..."); + output.Add("### Performance Comparison"); + output.Add(""); + output.Add( + "| Benchmark | Baseline Mean | Current Mean | Change | Baseline Memory | Current Memory | Change |" + ); + output.Add( + "|-----------|---------------|--------------|--------|-----------------|----------------|--------|" + ); + + var hasRegressions = false; + var hasImprovements = false; + + foreach (var method in currentMetrics.Keys.Union(baselineMetrics.Keys).OrderBy(k => k)) + { + var hasCurrent = currentMetrics.TryGetValue(method, out var current); + var hasBaseline = baselineMetrics.TryGetValue(method, out var baseline); + + if (!hasCurrent) + { + output.Add( + $"| {method} | {baseline!.Mean} | ❌ Missing | N/A | {baseline.Memory} | N/A | N/A |" + ); + continue; + } + + if (!hasBaseline) + { + output.Add( + $"| {method} | ❌ New | {current!.Mean} | N/A | N/A | {current.Memory} | N/A |" + ); + continue; + } + + var timeChange = CalculateChange(baseline!.MeanValue, current!.MeanValue); + var memChange = CalculateChange(baseline.MemoryValue, current.MemoryValue); + + var timeIcon = + timeChange > 25 ? "🔴" + : timeChange < -25 ? "🟢" + : "⚪"; + var memIcon = + memChange > 25 ? "🔴" + : memChange < -25 ? "🟢" + : "⚪"; + + if (timeChange > 25 || memChange > 25) + hasRegressions = true; + if (timeChange < -25 || memChange < -25) + hasImprovements = true; + + output.Add( + $"| {method} | {baseline.Mean} | {current.Mean} | {timeIcon} {timeChange:+0.0;-0.0;0}% | {baseline.Memory} | {current.Memory} | {memIcon} {memChange:+0.0;-0.0;0}% |" + ); + } + + output.Add(""); + output.Add("**Legend:**"); + output.Add("- 🔴 Regression (>25% slower/more memory)"); + output.Add("- 🟢 Improvement (>25% faster/less memory)"); + output.Add("- ⚪ No significant change"); + + if (hasRegressions) + { + output.Add(""); + output.Add( + "⚠️ **Warning**: Performance regressions detected. Review the changes carefully." + ); + } + else if (hasImprovements) + { + output.Add(""); + output.Add("✅ Performance improvements detected!"); + } + else + { + output.Add(""); + output.Add("✅ Performance is stable compared to baseline."); + } + + WriteOutput(output, githubStepSummary); + } +); + +Target( + GenerateBaseline, + () => + { + var perfProject = "tests/SharpCompress.Performance/SharpCompress.Performance.csproj"; + var baselinePath = "tests/SharpCompress.Performance/baseline-results.md"; + var artifactsDir = "baseline-artifacts"; + + Console.WriteLine("Building performance project..."); + Run("dotnet", $"build {perfProject} --configuration Release"); + + Console.WriteLine("Running benchmarks to generate baseline..."); + Run( + "dotnet", + $"run --project {perfProject} --configuration Release --no-build -- --filter \"*\" --exporters markdown --artifacts {artifactsDir}" + ); + + var resultsDir = Path.Combine(artifactsDir, "results"); + if (!Directory.Exists(resultsDir)) + { + Console.WriteLine("ERROR: No benchmark results generated."); + return; + } + + var markdownFiles = Directory + .GetFiles(resultsDir, "*-report-github.md") + .OrderBy(f => f) + .ToList(); + + if (markdownFiles.Count == 0) + { + Console.WriteLine("ERROR: No markdown reports found."); + return; + } + + Console.WriteLine($"Combining {markdownFiles.Count} benchmark reports..."); + var baselineContent = new List(); + + foreach (var file in markdownFiles) + { + var lines = File.ReadAllLines(file); + baselineContent.AddRange(lines.Select(l => l.Trim()).Where(l => l.StartsWith('|'))); + } + + File.WriteAllText(baselinePath, string.Join(Environment.NewLine, baselineContent)); + Console.WriteLine($"Baseline written to {baselinePath}"); + + // Clean up artifacts directory + if (Directory.Exists(artifactsDir)) + { + Directory.Delete(artifactsDir, true); + Console.WriteLine("Cleaned up artifacts directory."); + } + } +); + Target("default", [Publish], () => Console.WriteLine("Done!")); await RunTargetsAndExitAsync(args); @@ -302,3 +548,142 @@ static async Task GetGitOutput(string command, string args) throw new Exception($"Git command failed: git {command} {args}\n{ex.Message}", ex); } } + +static void WriteOutput(List output, string? githubStepSummary) +{ + if (!string.IsNullOrEmpty(githubStepSummary)) + { + File.AppendAllLines(githubStepSummary, output); + Console.WriteLine("Comparison written to GitHub Step Summary"); + } + else + { + foreach (var line in output) + { + Console.WriteLine(line); + } + } +} + +static Dictionary ParseBenchmarkResults(string markdown) +{ + var metrics = new Dictionary(); + var lines = markdown.Split('\n'); + + for (int i = 0; i < lines.Length; i++) + { + var line = lines[i].Trim(); + + // Look for table rows with benchmark data + if (line.StartsWith("|") && line.Contains("'") && i > 0) + { + var parts = line.Split('|', StringSplitOptions.TrimEntries); + if (parts.Length >= 5) + { + var method = parts[1].Replace("'", "'"); + var meanStr = parts[2]; + + // Find Allocated column - it's usually the last column or labeled "Allocated" + string memoryStr = "N/A"; + for (int j = parts.Length - 2; j >= 2; j--) + { + if ( + parts[j].Contains("KB") + || parts[j].Contains("MB") + || parts[j].Contains("GB") + || parts[j].Contains("B") + ) + { + memoryStr = parts[j]; + break; + } + } + + if ( + !method.Equals("Method", StringComparison.OrdinalIgnoreCase) + && !string.IsNullOrWhiteSpace(method) + ) + { + var metric = new BenchmarkMetric + { + Method = method, + Mean = meanStr, + MeanValue = ParseTimeValue(meanStr), + Memory = memoryStr, + MemoryValue = ParseMemoryValue(memoryStr), + }; + metrics[method] = metric; + } + } + } + } + + return metrics; +} + +static double ParseTimeValue(string timeStr) +{ + if (string.IsNullOrWhiteSpace(timeStr) || timeStr == "N/A" || timeStr == "NA") + return 0; + + // Remove thousands separators and parse + timeStr = timeStr.Replace(",", "").Trim(); + + var match = Regex.Match(timeStr, @"([\d.]+)\s*(\w+)"); + if (!match.Success) + return 0; + + var value = double.Parse(match.Groups[1].Value); + var unit = match.Groups[2].Value.ToLower(); + + // Convert to microseconds for comparison + return unit switch + { + "s" => value * 1_000_000, + "ms" => value * 1_000, + "μs" or "us" => value, + "ns" => value / 1_000, + _ => value, + }; +} + +static double ParseMemoryValue(string memStr) +{ + if (string.IsNullOrWhiteSpace(memStr) || memStr == "N/A" || memStr == "NA") + return 0; + + memStr = memStr.Replace(",", "").Trim(); + + var match = Regex.Match(memStr, @"([\d.]+)\s*(\w+)"); + if (!match.Success) + return 0; + + var value = double.Parse(match.Groups[1].Value); + var unit = match.Groups[2].Value.ToUpper(); + + // Convert to KB for comparison + return unit switch + { + "GB" => value * 1_024 * 1_024, + "MB" => value * 1_024, + "KB" => value, + "B" => value / 1_024, + _ => value, + }; +} + +static double CalculateChange(double baseline, double current) +{ + if (baseline == 0) + return 0; + return ((current - baseline) / baseline) * 100; +} + +record BenchmarkMetric +{ + public string Method { get; init; } = ""; + public string Mean { get; init; } = ""; + public double MeanValue { get; init; } + public string Memory { get; init; } = ""; + public double MemoryValue { get; init; } +} diff --git a/docs/API.md b/docs/API.md index eb8f007a..d2a944ad 100644 --- a/docs/API.md +++ b/docs/API.md @@ -20,14 +20,18 @@ using (var archive = RarArchive.OpenArchive("file.rar")) using (var archive = SevenZipArchive.OpenArchive("file.7z")) using (var archive = GZipArchive.OpenArchive("file.gz")) -// With options -var options = new ReaderOptions +// With fluent options (preferred) +var options = ReaderOptions.ForEncryptedArchive("password") + .WithArchiveEncoding(new ArchiveEncoding { Default = Encoding.GetEncoding(932) }); +using (var archive = ZipArchive.OpenArchive("encrypted.zip", options)) + +// Alternative: object initializer +var options2 = new ReaderOptions { Password = "password", LeaveStreamOpen = true, ArchiveEncoding = new ArchiveEncoding { Default = Encoding.GetEncoding(932) } }; -using (var archive = ZipArchive.OpenArchive("encrypted.zip", options)) ``` ### Creating Archives @@ -44,16 +48,21 @@ using (var archive = ZipArchive.CreateArchive()) using (var archive = TarArchive.CreateArchive()) using (var archive = GZipArchive.CreateArchive()) -// With options -var options = new WriterOptions(CompressionType.Deflate) -{ - CompressionLevel = 9, - LeaveStreamOpen = false -}; +// With fluent options (preferred) +var options = WriterOptions.ForZip() + .WithCompressionLevel(9) + .WithLeaveStreamOpen(false); using (var archive = ZipArchive.CreateArchive()) { archive.SaveTo("output.zip", options); } + +// Alternative: constructor with object initializer +var options2 = new WriterOptions(CompressionType.Deflate) +{ + CompressionLevel = 9, + LeaveStreamOpen = false +}; ``` --- @@ -72,16 +81,11 @@ using (var archive = ZipArchive.OpenArchive("file.zip")) var entry = archive.Entries.FirstOrDefault(e => e.Key == "file.txt"); // Extract all - archive.WriteToDirectory(@"C:\output", new ExtractionOptions - { - ExtractFullPath = true, - Overwrite = true - }); + archive.WriteToDirectory(@"C:\output"); // Extract single entry var entry = archive.Entries.First(); entry.WriteToFile(@"C:\output\file.txt"); - entry.WriteToFile(@"C:\output\file.txt", new ExtractionOptions { Overwrite = true }); // Get entry stream using (var stream = entry.OpenEntryStream()) @@ -95,7 +99,6 @@ using (var asyncArchive = await ZipArchive.OpenAsyncArchive("file.zip")) { await asyncArchive.WriteToDirectoryAsync( @"C:\output", - new ExtractionOptions { ExtractFullPath = true, Overwrite = true }, cancellationToken: cancellationToken ); } @@ -187,7 +190,6 @@ using (var reader = await ReaderFactory.OpenAsyncReader(stream)) // Async extraction of all entries await reader.WriteAllToDirectoryAsync( @"C:\output", - new ExtractionOptions { ExtractFullPath = true, Overwrite = true }, cancellationToken ); } @@ -229,43 +231,91 @@ using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Zip, Compressio ### ReaderOptions +Use factory presets and fluent helpers for common configurations: + ```csharp -var options = new ReaderOptions -{ - Password = "password", // For encrypted archives - LeaveStreamOpen = true, // Don't close wrapped stream - ArchiveEncoding = new ArchiveEncoding // Custom character encoding - { - Default = Encoding.GetEncoding(932) - } -}; +// External stream with password and custom encoding +var options = ReaderOptions.ForExternalStream() + .WithPassword("password") + .WithArchiveEncoding(new ArchiveEncoding { Default = Encoding.GetEncoding(932) }); + using (var archive = ZipArchive.OpenArchive("file.zip", options)) { // ... } + +// Common presets +var safeOptions = ReaderOptions.SafeExtract; // No overwrite +var flatOptions = ReaderOptions.FlatExtract; // No directory structure + +// Factory defaults: +// - file path / FileInfo overloads use LeaveStreamOpen = false +// - stream overloads use LeaveStreamOpen = true +``` + +Alternative: traditional object initializer: + +```csharp +var options = new ReaderOptions +{ + Password = "password", + LeaveStreamOpen = true, + ArchiveEncoding = new ArchiveEncoding { Default = Encoding.GetEncoding(932) } +}; ``` ### WriterOptions +Factory methods provide a clean, discoverable way to create writer options: + +```csharp +// Factory methods for common archive types +var zipOptions = WriterOptions.ForZip() // ZIP with Deflate + .WithCompressionLevel(9) // 0-9 for Deflate + .WithLeaveStreamOpen(false); // Close stream when done + +var tarOptions = WriterOptions.ForTar(CompressionType.GZip) // TAR with GZip + .WithLeaveStreamOpen(false); + +var gzipOptions = WriterOptions.ForGZip() // GZip file + .WithCompressionLevel(6); + +archive.SaveTo("output.zip", zipOptions); +``` + +Alternative: traditional constructor with object initializer: + ```csharp var options = new WriterOptions(CompressionType.Deflate) { - CompressionLevel = 9, // 0-9 for Deflate - LeaveStreamOpen = true, // Don't close stream + CompressionLevel = 9, + LeaveStreamOpen = true, }; archive.SaveTo("output.zip", options); ``` -### ExtractionOptions +### Extraction behavior ```csharp -var options = new ExtractionOptions +var options = new ReaderOptions { ExtractFullPath = true, // Recreate directory structure Overwrite = true, // Overwrite existing files PreserveFileTime = true // Keep original timestamps }; -archive.WriteToDirectory(@"C:\output", options); + +using (var archive = ZipArchive.OpenArchive("file.zip", options)) +{ + archive.WriteToDirectory(@"C:\output"); +} +``` + +### Options matrix + +```text +ReaderOptions: open-time behavior (password, encoding, stream ownership, extraction defaults) +WriterOptions: write-time behavior (compression type/level, encoding, stream ownership) +ZipWriterEntryOptions: per-entry ZIP overrides (compression, level, timestamps, comments, zip64) ``` --- @@ -317,13 +367,9 @@ ArchiveType.ZStandard try { using (var archive = ZipArchive.Open("archive.zip", - new ReaderOptions { Password = "password" })) + ReaderOptions.ForEncryptedArchive("password"))) { - archive.WriteToDirectory(@"C:\output", new ExtractionOptions - { - ExtractFullPath = true, - Overwrite = true - }); + archive.WriteToDirectory(@"C:\output"); } } catch (PasswordRequiredException) @@ -348,7 +394,7 @@ var progress = new Progress(report => Console.WriteLine($"Extracting {report.EntryPath}: {report.PercentComplete}%"); }); -var options = new ReaderOptions { Progress = progress }; +var options = ReaderOptions.ForOwnedFile().WithProgress(progress); using (var archive = ZipArchive.OpenArchive("archive.zip", options)) { archive.WriteToDirectory(@"C:\output"); @@ -367,7 +413,6 @@ try { await archive.WriteToDirectoryAsync( @"C:\output", - new ExtractionOptions { ExtractFullPath = true, Overwrite = true }, cancellationToken: cts.Token ); } diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 2a3bb985..9db75bbc 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -90,7 +90,7 @@ Common types, options, and enumerations used across formats. - `ArchiveType.cs` - Enum for archive formats - `CompressionType.cs` - Enum for compression methods - `ArchiveEncoding.cs` - Character encoding configuration -- `ExtractionOptions.cs` - Extraction configuration +- `IExtractionOptions.cs` - Extraction configuration exposed through `ReaderOptions` - Format-specific headers: `Zip/Headers/`, `Tar/Headers/`, `Rar/Headers/`, etc. #### `Compressors/` - Compression Algorithms @@ -215,13 +215,13 @@ using (var compressor = new DeflateStream(nonDisposingStream)) public abstract class AbstractArchive : IArchive { // Template methods - public virtual void WriteToDirectory(string destinationDirectory, ExtractionOptions options) + public virtual void WriteToDirectory(string destinationDirectory) { // Common extraction logic foreach (var entry in Entries) { // Call subclass method - entry.WriteToFile(destinationPath, options); + entry.WriteToFile(destinationPath); } } @@ -267,8 +267,7 @@ public interface IArchive : IDisposable { IEnumerable Entries { get; } - void WriteToDirectory(string destinationDirectory, - ExtractionOptions options = null); + void WriteToDirectory(string destinationDirectory); IEntry FirstOrDefault(Func predicate); @@ -287,8 +286,7 @@ public interface IReader : IDisposable bool MoveToNextEntry(); - void WriteEntryToDirectory(string destinationDirectory, - ExtractionOptions options = null); + void WriteEntryToDirectory(string destinationDirectory); Stream OpenEntryStream(); @@ -327,7 +325,7 @@ public interface IEntry DateTime? LastModifiedTime { get; } CompressionType CompressionType { get; } - void WriteToFile(string fullPath, ExtractionOptions options = null); + void WriteToFile(string fullPath); void WriteToStream(Stream destinationStream); Stream OpenEntryStream(); diff --git a/docs/ENCODING.md b/docs/ENCODING.md index 8200e756..5ae0de09 100644 --- a/docs/ENCODING.md +++ b/docs/ENCODING.md @@ -18,14 +18,9 @@ Most archive formats store filenames and metadata as bytes. SharpCompress must c using SharpCompress.Common; using SharpCompress.Readers; -// Configure encoding before opening archive -var options = new ReaderOptions -{ - ArchiveEncoding = new ArchiveEncoding - { - Default = Encoding.GetEncoding(932) // cp932 for Japanese - } -}; +// Configure encoding using fluent factory method (preferred) +var options = ReaderOptions.ForEncoding( + new ArchiveEncoding { Default = Encoding.GetEncoding(932) }); // cp932 for Japanese using (var archive = ZipArchive.OpenArchive("japanese.zip", options)) { @@ -34,6 +29,12 @@ using (var archive = ZipArchive.OpenArchive("japanese.zip", options)) Console.WriteLine(entry.Key); // Now shows correct characters } } + +// Alternative: object initializer +var options2 = new ReaderOptions +{ + ArchiveEncoding = new ArchiveEncoding { Default = Encoding.GetEncoding(932) } +}; ``` ### ArchiveEncoding Properties @@ -47,10 +48,8 @@ using (var archive = ZipArchive.OpenArchive("japanese.zip", options)) **Archive API:** ```csharp -var options = new ReaderOptions -{ - ArchiveEncoding = new ArchiveEncoding { Default = Encoding.GetEncoding(932) } -}; +var options = ReaderOptions.ForEncoding( + new ArchiveEncoding { Default = Encoding.GetEncoding(932) }); using (var archive = ZipArchive.OpenArchive("file.zip", options)) { // Use archive with correct encoding @@ -59,10 +58,8 @@ using (var archive = ZipArchive.OpenArchive("file.zip", options)) **Reader API:** ```csharp -var options = new ReaderOptions -{ - ArchiveEncoding = new ArchiveEncoding { Default = Encoding.GetEncoding(932) } -}; +var options = ReaderOptions.ForEncoding( + new ArchiveEncoding { Default = Encoding.GetEncoding(932) }); using (var stream = File.OpenRead("file.zip")) using (var reader = ReaderFactory.OpenReader(stream, options)) { @@ -390,11 +387,7 @@ var options = new ReaderOptions using (var archive = ZipArchive.OpenArchive("japanese_files.zip", options)) { - archive.WriteToDirectory(@"C:\output", new ExtractionOptions - { - ExtractFullPath = true, - Overwrite = true - }); + archive.WriteToDirectory(@"C:\output"); } // Files extracted with correct Japanese names ``` diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index 51b2f920..604fd2cf 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -213,11 +213,7 @@ using (var archive = RarArchive.OpenArchive("solid.rar")) using (var archive = RarArchive.OpenArchive("solid.rar")) { // Method 1: Use WriteToDirectory (recommended) - archive.WriteToDirectory(@"C:\output", new ExtractionOptions - { - ExtractFullPath = true, - Overwrite = true - }); + archive.WriteToDirectory(@"C:\output"); // Method 2: Use ExtractAllEntries archive.ExtractAllEntries(); @@ -337,7 +333,6 @@ using (var archive = ZipArchive.OpenArchive("archive.zip")) { await archive.WriteToDirectoryAsync( @"C:\output", - new ExtractionOptions { ExtractFullPath = true, Overwrite = true }, cancellationToken ); } @@ -355,10 +350,7 @@ Async doesn't improve performance for: // Sync extraction (simpler, same performance on fast I/O) using (var archive = ZipArchive.OpenArchive("archive.zip")) { - archive.WriteToDirectory( - @"C:\output", - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + archive.WriteToDirectory(@"C:\output"); } // Simple and fast - no async needed ``` @@ -377,7 +369,6 @@ try { await archive.WriteToDirectoryAsync( @"C:\output", - new ExtractionOptions { ExtractFullPath = true, Overwrite = true }, cts.Token ); } diff --git a/docs/USAGE.md b/docs/USAGE.md index 52d690a3..df863d32 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -6,7 +6,7 @@ SharpCompress now provides full async/await support for all I/O operations. All **Key Async Methods:** - `reader.WriteEntryToAsync(stream, cancellationToken)` - Extract entry asynchronously -- `reader.WriteAllToDirectoryAsync(path, options, cancellationToken)` - Extract all asynchronously +- `reader.WriteAllToDirectoryAsync(path, cancellationToken)` - Extract all asynchronously - `writer.WriteAsync(filename, stream, modTime, cancellationToken)` - Write entry asynchronously - `writer.WriteAllAsync(directory, pattern, searchOption, cancellationToken)` - Write directory asynchronously - `entry.OpenEntryStreamAsync(cancellationToken)` - Open entry stream asynchronously @@ -40,6 +40,10 @@ To deal with the "correct" rules as well as the expectations of users, I've deci To be explicit though, consider always using the overloads that use `ReaderOptions` or `WriterOptions` and explicitly set `LeaveStreamOpen` the way you want. +Default behavior in factory APIs: +- File path / `FileInfo` overloads set `LeaveStreamOpen = false`. +- Caller-provided `Stream` overloads set `LeaveStreamOpen = true`. + If using Compression Stream classes directly and you don't want the wrapped stream to be closed. Use the `NonDisposingStream` as a wrapper to prevent the stream being disposed. The change in 0.21 simplified a lot even though the usage is a bit more convoluted. ## Samples @@ -90,14 +94,14 @@ Note: Extracting a solid rar or 7z file needs to be done in sequential order to `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.OpenArchive("Test.rar")) +// Using fluent factory method for extraction options +using (var archive = RarArchive.OpenArchive("Test.rar", + ReaderOptions.ForOwnedFile() + .WithExtractFullPath(true) + .WithOverwrite(true))) { // Simple extraction with RarArchive; this WriteToDirectory pattern works for all archive types - archive.WriteToDirectory(@"D:\temp", new ExtractionOptions() - { - ExtractFullPath = true, - Overwrite = true - }); + archive.WriteToDirectory(@"D:\temp"); } ``` @@ -126,13 +130,13 @@ var progress = new Progress(report => Console.WriteLine($"Extracting {report.EntryPath}: {report.PercentComplete}%"); }); -using (var archive = RarArchive.OpenArchive("archive.rar", new ReaderOptions { Progress = progress })) // Must be solid Rar or 7Zip +using (var archive = RarArchive.OpenArchive("archive.rar", + ReaderOptions.ForOwnedFile() + .WithProgress(progress) + .WithExtractFullPath(true) + .WithOverwrite(true))) // Must be solid Rar or 7Zip { - archive.WriteToDirectory(@"D:\output", new ExtractionOptions() - { - ExtractFullPath = true, - Overwrite = true - }); + archive.WriteToDirectory(@"D:\output"); } ``` @@ -147,11 +151,7 @@ using (var reader = ReaderFactory.OpenReader(stream)) if (!reader.Entry.IsDirectory) { Console.WriteLine(reader.Entry.Key); - reader.WriteEntryToDirectory(@"C:\temp", new ExtractionOptions() - { - ExtractFullPath = true, - Overwrite = true - }); + reader.WriteEntryToDirectory(@"C:\temp"); } } } @@ -180,10 +180,10 @@ using (var reader = ReaderFactory.OpenReader(stream)) ```C# using (Stream stream = File.OpenWrite("C:\\temp.tgz")) -using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Tar, new WriterOptions(CompressionType.GZip) - { - LeaveOpenStream = true - })) +using (var writer = WriterFactory.OpenWriter( + stream, + ArchiveType.Tar, + WriterOptions.ForTar(CompressionType.GZip).WithLeaveStreamOpen(true))) { writer.WriteAll("D:\\temp", "*", SearchOption.AllDirectories); } @@ -192,15 +192,15 @@ using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Tar, new Writer ### Extract zip which has non-utf8 encoded filename(cp932) ```C# -var opts = new SharpCompress.Readers.ReaderOptions(); var encoding = Encoding.GetEncoding(932); -opts.ArchiveEncoding = new SharpCompress.Common.ArchiveEncoding(); -opts.ArchiveEncoding.CustomDecoder = (data, x, y) => -{ - return encoding.GetString(data); -}; -var tr = SharpCompress.Archives.Zip.ZipArchive.OpenArchive("test.zip", opts); -foreach(var entry in tr.Entries) +var opts = new ReaderOptions() + .WithArchiveEncoding(new ArchiveEncoding + { + CustomDecoder = (data, x, y) => encoding.GetString(data) + }); + +using var archive = ZipArchive.OpenArchive("test.zip", opts); +foreach(var entry in archive.Entries) { Console.WriteLine($"{entry.Key}"); } @@ -238,11 +238,6 @@ using (var reader = ReaderFactory.OpenReader(stream)) { await reader.WriteAllToDirectoryAsync( @"D:\temp", - new ExtractionOptions() - { - ExtractFullPath = true, - Overwrite = true - }, cancellationToken ); } @@ -321,7 +316,6 @@ using (var archive = ZipArchive.OpenArchive("archive.zip")) // Simple async extraction - works for all archive types await archive.WriteToDirectoryAsync( @"C:\output", - new ExtractionOptions() { ExtractFullPath = true, Overwrite = true }, cancellationToken ); } diff --git a/src/SharpCompress/Archives/AbstractArchive.Async.cs b/src/SharpCompress/Archives/AbstractArchive.Async.cs index 9ba878d6..3f66a004 100644 --- a/src/SharpCompress/Archives/AbstractArchive.Async.cs +++ b/src/SharpCompress/Archives/AbstractArchive.Async.cs @@ -21,7 +21,7 @@ public abstract partial class AbstractArchive IAsyncEnumerable volumes ) { - foreach (var item in LoadEntries(await volumes.ToListAsync())) + foreach (var item in LoadEntries(await volumes.ToListAsync().ConfigureAwait(false))) { yield return item; } @@ -47,8 +47,8 @@ public abstract partial class AbstractArchive private async ValueTask EnsureEntriesLoadedAsync() { - await _lazyEntriesAsync.EnsureFullyLoaded(); - await _lazyVolumesAsync.EnsureFullyLoaded(); + await _lazyEntriesAsync.EnsureFullyLoaded().ConfigureAwait(false); + await _lazyVolumesAsync.EnsureFullyLoaded().ConfigureAwait(false); } private async IAsyncEnumerable EntriesAsyncCast() @@ -73,29 +73,31 @@ public abstract partial class AbstractArchive public async ValueTask ExtractAllEntriesAsync() { - if (!await IsSolidAsync() && Type != ArchiveType.SevenZip) + if (!await IsSolidAsync().ConfigureAwait(false) && Type != ArchiveType.SevenZip) { throw new SharpCompressException( "ExtractAllEntries can only be used on solid archives or 7Zip archives (which require random access)." ); } - await EnsureEntriesLoadedAsync(); - return await CreateReaderForSolidExtractionAsync(); + await EnsureEntriesLoadedAsync().ConfigureAwait(false); + return await CreateReaderForSolidExtractionAsync().ConfigureAwait(false); } public virtual ValueTask IsSolidAsync() => new(false); public async ValueTask IsCompleteAsync() { - await EnsureEntriesLoadedAsync(); - return await EntriesAsync.AllAsync(x => x.IsComplete); + await EnsureEntriesLoadedAsync().ConfigureAwait(false); + return await EntriesAsync.AllAsync(x => x.IsComplete).ConfigureAwait(false); } public async ValueTask TotalSizeAsync() => - await EntriesAsync.AggregateAsync(0L, (total, cf) => total + cf.CompressedSize); + await EntriesAsync + .AggregateAsync(0L, (total, cf) => total + cf.CompressedSize) + .ConfigureAwait(false); public async ValueTask TotalUncompressedSizeAsync() => - await EntriesAsync.AggregateAsync(0L, (total, cf) => total + cf.Size); + await EntriesAsync.AggregateAsync(0L, (total, cf) => total + cf.Size).ConfigureAwait(false); public ValueTask IsEncryptedAsync() => new(IsEncrypted); diff --git a/src/SharpCompress/Archives/AbstractArchive.cs b/src/SharpCompress/Archives/AbstractArchive.cs index 989eca70..7d89067f 100644 --- a/src/SharpCompress/Archives/AbstractArchive.cs +++ b/src/SharpCompress/Archives/AbstractArchive.cs @@ -20,7 +20,7 @@ public abstract partial class AbstractArchive : IArchive, IAsyn private readonly LazyAsyncReadOnlyCollection _lazyVolumesAsync; private readonly LazyAsyncReadOnlyCollection _lazyEntriesAsync; - protected ReaderOptions ReaderOptions { get; } + public ReaderOptions ReaderOptions { get; protected set; } internal AbstractArchive(ArchiveType type, SourceStream sourceStream) { diff --git a/src/SharpCompress/Archives/AbstractWritableArchive.Async.cs b/src/SharpCompress/Archives/AbstractWritableArchive.Async.cs index 8c74004c..8b8434e7 100644 --- a/src/SharpCompress/Archives/AbstractWritableArchive.Async.cs +++ b/src/SharpCompress/Archives/AbstractWritableArchive.Async.cs @@ -6,13 +6,13 @@ using System.Threading; using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Common.Options; -using SharpCompress.Writers; namespace SharpCompress.Archives; -public abstract partial class AbstractWritableArchive +public abstract partial class AbstractWritableArchive where TEntry : IArchiveEntry where TVolume : IVolume + where TOptions : IWriterOptions { // Async property moved from main file private IAsyncEnumerable OldEntriesAsync => @@ -39,7 +39,7 @@ public abstract partial class AbstractWritableArchive if (!removedEntries.Contains(entry)) { removedEntries.Add(entry); - await RebuildModifiedCollectionAsync(); + await RebuildModifiedCollectionAsync().ConfigureAwait(false); } } @@ -86,7 +86,7 @@ public abstract partial class AbstractWritableArchive } var entry = CreateEntry(key, source, size, modified, closeStream); newEntries.Add(entry); - await RebuildModifiedCollectionAsync(); + await RebuildModifiedCollectionAsync().ConfigureAwait(false); return entry; } @@ -106,13 +106,13 @@ public abstract partial class AbstractWritableArchive } var entry = CreateDirectoryEntry(key, modified); newEntries.Add(entry); - await RebuildModifiedCollectionAsync(); + await RebuildModifiedCollectionAsync().ConfigureAwait(false); return entry; } public async ValueTask SaveToAsync( Stream stream, - IWriterOptions options, + TOptions options, CancellationToken cancellationToken = default ) { @@ -121,4 +121,12 @@ public abstract partial class AbstractWritableArchive await SaveToAsync(stream, options, OldEntriesAsync, newEntries, cancellationToken) .ConfigureAwait(false); } + + protected abstract ValueTask SaveToAsync( + Stream stream, + TOptions options, + IAsyncEnumerable oldEntries, + IEnumerable newEntries, + CancellationToken cancellationToken = default + ); } diff --git a/src/SharpCompress/Archives/AbstractWritableArchive.cs b/src/SharpCompress/Archives/AbstractWritableArchive.cs index 6943c5bd..59d55771 100644 --- a/src/SharpCompress/Archives/AbstractWritableArchive.cs +++ b/src/SharpCompress/Archives/AbstractWritableArchive.cs @@ -11,18 +11,19 @@ using SharpCompress.Writers; namespace SharpCompress.Archives; -public abstract partial class AbstractWritableArchive +public abstract partial class AbstractWritableArchive : AbstractArchive, - IWritableArchive, - IWritableAsyncArchive + IWritableArchive, + IWritableAsyncArchive where TEntry : IArchiveEntry where TVolume : IVolume + where TOptions : IWriterOptions { private class RebuildPauseDisposable : IDisposable { - private readonly AbstractWritableArchive archive; + private readonly AbstractWritableArchive archive; - public RebuildPauseDisposable(AbstractWritableArchive archive) + public RebuildPauseDisposable(AbstractWritableArchive archive) { this.archive = archive; archive.pauseRebuilding = true; @@ -151,13 +152,15 @@ public abstract partial class AbstractWritableArchive long size, DateTime? modified, CancellationToken cancellationToken - ) => await AddEntryAsync(key, source, closeStream, size, modified, cancellationToken); + ) => + await AddEntryAsync(key, source, closeStream, size, modified, cancellationToken) + .ConfigureAwait(false); async ValueTask IWritableAsyncArchive.AddDirectoryEntryAsync( string key, DateTime? modified, CancellationToken cancellationToken - ) => await AddDirectoryEntryAsync(key, modified, cancellationToken); + ) => await AddDirectoryEntryAsync(key, modified, cancellationToken).ConfigureAwait(false); public TEntry AddDirectoryEntry(string key, DateTime? modified = null) { @@ -175,7 +178,7 @@ public abstract partial class AbstractWritableArchive return entry; } - public void SaveTo(Stream stream, IWriterOptions options) + public void SaveTo(Stream stream, TOptions options) { //reset streams of new entries newEntries.Cast().ForEach(x => x.Stream.Seek(0, SeekOrigin.Begin)); @@ -211,19 +214,11 @@ public abstract partial class AbstractWritableArchive protected abstract void SaveTo( Stream stream, - IWriterOptions options, + TOptions options, IEnumerable oldEntries, IEnumerable newEntries ); - protected abstract ValueTask SaveToAsync( - Stream stream, - IWriterOptions options, - IAsyncEnumerable oldEntries, - IEnumerable newEntries, - CancellationToken cancellationToken = default - ); - public override void Dispose() { base.Dispose(); diff --git a/src/SharpCompress/Archives/ArchiveFactory.Async.cs b/src/SharpCompress/Archives/ArchiveFactory.Async.cs index 59e16c31..8c6d9e34 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.Async.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.Async.cs @@ -19,8 +19,9 @@ public static partial class ArchiveFactory CancellationToken cancellationToken = default ) { - readerOptions ??= new ReaderOptions(); - var factory = await FindFactoryAsync(stream, cancellationToken); + readerOptions ??= ReaderOptions.ForExternalStream; + var factory = await FindFactoryAsync(stream, cancellationToken) + .ConfigureAwait(false); return factory.OpenAsyncArchive(stream, readerOptions); } @@ -40,9 +41,10 @@ public static partial class ArchiveFactory CancellationToken cancellationToken = default ) { - options ??= new ReaderOptions { LeaveStreamOpen = false }; + options ??= ReaderOptions.ForOwnedFile; - var factory = await FindFactoryAsync(fileInfo, cancellationToken); + var factory = await FindFactoryAsync(fileInfo, cancellationToken) + .ConfigureAwait(false); return factory.OpenAsyncArchive(fileInfo, options); } @@ -62,14 +64,16 @@ public static partial class ArchiveFactory var fileInfo = filesArray[0]; if (filesArray.Length == 1) { - return await OpenAsyncArchive(fileInfo, options, cancellationToken); + return await OpenAsyncArchive(fileInfo, options, cancellationToken) + .ConfigureAwait(false); } fileInfo.NotNull(nameof(fileInfo)); - options ??= new ReaderOptions { LeaveStreamOpen = false }; + options ??= ReaderOptions.ForOwnedFile; - var factory = await FindFactoryAsync(fileInfo, cancellationToken); - return factory.OpenAsyncArchive(filesArray, options, cancellationToken); + var factory = await FindFactoryAsync(fileInfo, cancellationToken) + .ConfigureAwait(false); + return factory.OpenAsyncArchive(filesArray, options); } public static async ValueTask OpenAsyncArchive( @@ -89,13 +93,15 @@ public static partial class ArchiveFactory var firstStream = streamsArray[0]; if (streamsArray.Length == 1) { - return await OpenAsyncArchive(firstStream, options, cancellationToken); + return await OpenAsyncArchive(firstStream, options, cancellationToken) + .ConfigureAwait(false); } firstStream.NotNull(nameof(firstStream)); - options ??= new ReaderOptions(); + options ??= ReaderOptions.ForExternalStream; - var factory = await FindFactoryAsync(firstStream, cancellationToken); + var factory = await FindFactoryAsync(firstStream, cancellationToken) + .ConfigureAwait(false); return factory.OpenAsyncArchive(streamsArray, options); } @@ -117,7 +123,7 @@ public static partial class ArchiveFactory { finfo.NotNull(nameof(finfo)); using Stream stream = finfo.OpenRead(); - return await FindFactoryAsync(stream, cancellationToken); + return await FindFactoryAsync(stream, cancellationToken).ConfigureAwait(false); } private static async ValueTask FindFactoryAsync( @@ -140,7 +146,11 @@ public static partial class ArchiveFactory { stream.Seek(startPosition, SeekOrigin.Begin); - if (await factory.IsArchiveAsync(stream, cancellationToken: cancellationToken)) + if ( + await factory + .IsArchiveAsync(stream, cancellationToken: cancellationToken) + .ConfigureAwait(false) + ) { stream.Seek(startPosition, SeekOrigin.Begin); diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs index 33a1ecc5..4da070b5 100644 --- a/src/SharpCompress/Archives/ArchiveFactory.cs +++ b/src/SharpCompress/Archives/ArchiveFactory.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using SharpCompress.Common; +using SharpCompress.Common.Options; using SharpCompress.Factories; using SharpCompress.IO; using SharpCompress.Readers; @@ -15,22 +16,23 @@ public static partial class ArchiveFactory { public static IArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null) { - readerOptions ??= new ReaderOptions(); + readerOptions ??= ReaderOptions.ForExternalStream; return FindFactory(stream).OpenArchive(stream, readerOptions); } - public static IWritableArchive CreateArchive(ArchiveType type) + public static IWritableArchive CreateArchive() + where TOptions : IWriterOptions { var factory = Factory - .Factories.OfType() - .FirstOrDefault(item => item.KnownArchiveType == type); + .Factories.OfType>() + .FirstOrDefault(); if (factory != null) { return factory.CreateArchive(); } - throw new NotSupportedException("Cannot create Archives of type: " + type); + throw new NotSupportedException("Cannot create Archives of type: " + typeof(TOptions)); } public static IArchive OpenArchive(string filePath, ReaderOptions? options = null) @@ -41,7 +43,7 @@ public static partial class ArchiveFactory public static IArchive OpenArchive(FileInfo fileInfo, ReaderOptions? options = null) { - options ??= new ReaderOptions { LeaveStreamOpen = false }; + options ??= ReaderOptions.ForOwnedFile; return FindFactory(fileInfo).OpenArchive(fileInfo, options); } @@ -65,7 +67,7 @@ public static partial class ArchiveFactory } fileInfo.NotNull(nameof(fileInfo)); - options ??= new ReaderOptions { LeaveStreamOpen = false }; + options ??= ReaderOptions.ForOwnedFile; return FindFactory(fileInfo).OpenArchive(filesArray, options); } @@ -86,7 +88,7 @@ public static partial class ArchiveFactory } firstStream.NotNull(nameof(firstStream)); - options ??= new ReaderOptions(); + options ??= ReaderOptions.ForExternalStream; return FindFactory(firstStream).OpenArchive(streamsArray, options); } @@ -94,11 +96,11 @@ public static partial class ArchiveFactory public static void WriteToDirectory( string sourceArchive, string destinationDirectory, - ExtractionOptions? options = null + ReaderOptions? options = null ) { - using var archive = OpenArchive(sourceArchive); - archive.WriteToDirectory(destinationDirectory, options); + using var archive = OpenArchive(sourceArchive, options); + archive.WriteToDirectory(destinationDirectory); } public static T FindFactory(string path) diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs b/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs index f9ea36ae..afc64bfa 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.Async.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Common.GZip; using SharpCompress.Common.Options; -using SharpCompress.IO; using SharpCompress.Readers; using SharpCompress.Readers.GZip; using SharpCompress.Writers; @@ -25,13 +24,13 @@ public partial class GZipArchive ) { using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); - await SaveToAsync(stream, new WriterOptions(CompressionType.GZip), cancellationToken) + await SaveToAsync(stream, new GZipWriterOptions(CompressionType.GZip), cancellationToken) .ConfigureAwait(false); } protected override async ValueTask SaveToAsync( Stream stream, - IWriterOptions options, + GZipWriterOptions options, IAsyncEnumerable oldEntries, IEnumerable newEntries, CancellationToken cancellationToken = default @@ -41,7 +40,7 @@ public partial class GZipArchive { throw new InvalidFormatException("Only one entry is allowed in a GZip Archive"); } - using var writer = new GZipWriter( + await using var writer = new GZipWriter( stream, options as GZipWriterOptions ?? new GZipWriterOptions(options) ); @@ -51,7 +50,9 @@ public partial class GZipArchive { if (!entry.IsDirectory) { - using var entryStream = entry.OpenEntryStream(); + using var entryStream = await entry + .OpenEntryStreamAsync(cancellationToken) + .ConfigureAwait(false); await writer .WriteAsync( entry.Key.NotNull("Entry Key is null"), @@ -63,7 +64,9 @@ public partial class GZipArchive } foreach (var entry in newEntries.Where(x => !x.IsDirectory)) { - using var entryStream = entry.OpenEntryStream(); + using var entryStream = await entry + .OpenEntryStreamAsync(cancellationToken) + .ConfigureAwait(false); await writer .WriteAsync(entry.Key.NotNull("Entry Key is null"), entryStream, cancellationToken) .ConfigureAwait(false); @@ -81,14 +84,13 @@ public partial class GZipArchive IAsyncEnumerable volumes ) { - var stream = (await volumes.SingleAsync()).Stream; + var stream = (await volumes.SingleAsync().ConfigureAwait(false)).Stream; yield return new GZipArchiveEntry( this, - await GZipFilePart.CreateAsync( - stream, - ReaderOptions.ArchiveEncoding, - ReaderOptions.CompressionProviders - ) + await GZipFilePart + .CreateAsync(stream, ReaderOptions.ArchiveEncoding) + .ConfigureAwait(false), + ReaderOptions ); } } diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs b/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs index e793cd9b..1f90de62 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.Factory.cs @@ -5,43 +5,41 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using SharpCompress.Common; -using SharpCompress.Common.GZip; using SharpCompress.IO; using SharpCompress.Readers; -using SharpCompress.Readers.GZip; -using SharpCompress.Writers; using SharpCompress.Writers.GZip; namespace SharpCompress.Archives.GZip; public partial class GZipArchive #if NET8_0_OR_GREATER - : IWritableArchiveOpenable, - IMultiArchiveOpenable + : IWritableArchiveOpenable, + IMultiArchiveOpenable< + IWritableArchive, + IWritableAsyncArchive + > #endif { - public static IWritableAsyncArchive OpenAsyncArchive( + public static IWritableAsyncArchive OpenAsyncArchive( string path, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default + ReaderOptions? readerOptions = null ) { - cancellationToken.ThrowIfCancellationRequested(); path.NotNullOrEmpty(nameof(path)); - return (IWritableAsyncArchive)OpenArchive( - new FileInfo(path), - readerOptions ?? new ReaderOptions() - ); + return (IWritableAsyncArchive) + OpenArchive(new FileInfo(path), readerOptions ?? new ReaderOptions()); } - public static IWritableArchive OpenArchive(string filePath, ReaderOptions? readerOptions = null) + public static IWritableArchive OpenArchive( + string filePath, + ReaderOptions? readerOptions = null + ) { filePath.NotNullOrEmpty(nameof(filePath)); return OpenArchive(new FileInfo(filePath), readerOptions ?? new ReaderOptions()); } - public static IWritableArchive OpenArchive( + public static IWritableArchive OpenArchive( FileInfo fileInfo, ReaderOptions? readerOptions = null ) @@ -56,7 +54,7 @@ public partial class GZipArchive ); } - public static IWritableArchive OpenArchive( + public static IWritableArchive OpenArchive( IEnumerable fileInfos, ReaderOptions? readerOptions = null ) @@ -72,7 +70,7 @@ public partial class GZipArchive ); } - public static IWritableArchive OpenArchive( + public static IWritableArchive OpenArchive( IEnumerable streams, ReaderOptions? readerOptions = null ) @@ -88,7 +86,10 @@ public partial class GZipArchive ); } - public static IWritableArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null) + public static IWritableArchive OpenArchive( + Stream stream, + ReaderOptions? readerOptions = null + ) { stream.NotNull(nameof(stream)); @@ -102,49 +103,30 @@ public partial class GZipArchive ); } - public static IWritableAsyncArchive OpenAsyncArchive( + public static IWritableAsyncArchive OpenAsyncArchive( Stream stream, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) - { - cancellationToken.ThrowIfCancellationRequested(); - return (IWritableAsyncArchive)OpenArchive(stream, readerOptions); - } + ReaderOptions? readerOptions = null + ) => (IWritableAsyncArchive)OpenArchive(stream, readerOptions); - public static IWritableAsyncArchive OpenAsyncArchive( + public static IWritableAsyncArchive OpenAsyncArchive( FileInfo fileInfo, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) - { - cancellationToken.ThrowIfCancellationRequested(); - return (IWritableAsyncArchive)OpenArchive(fileInfo, readerOptions); - } + ReaderOptions? readerOptions = null + ) => (IWritableAsyncArchive)OpenArchive(fileInfo, readerOptions); - public static IWritableAsyncArchive OpenAsyncArchive( + public static IWritableAsyncArchive OpenAsyncArchive( IReadOnlyList streams, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) - { - cancellationToken.ThrowIfCancellationRequested(); - return (IWritableAsyncArchive)OpenArchive(streams, readerOptions); - } + ReaderOptions? readerOptions = null + ) => (IWritableAsyncArchive)OpenArchive(streams, readerOptions); - public static IWritableAsyncArchive OpenAsyncArchive( + public static IWritableAsyncArchive OpenAsyncArchive( IReadOnlyList fileInfos, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) - { - cancellationToken.ThrowIfCancellationRequested(); - return (IWritableAsyncArchive)OpenArchive(fileInfos, readerOptions); - } + ReaderOptions? readerOptions = null + ) => (IWritableAsyncArchive)OpenArchive(fileInfos, readerOptions); - public static IWritableArchive CreateArchive() => new GZipArchive(); + public static IWritableArchive CreateArchive() => new GZipArchive(); - public static IWritableAsyncArchive CreateAsyncArchive() => new GZipArchive(); + public static IWritableAsyncArchive CreateAsyncArchive() => + new GZipArchive(); public static bool IsGZipFile(string filePath) => IsGZipFile(new FileInfo(filePath)); diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.cs b/src/SharpCompress/Archives/GZip/GZipArchive.cs index d6257bfd..56f61fa5 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.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 SharpCompress.Common; using SharpCompress.Common.GZip; using SharpCompress.Common.Options; @@ -15,7 +13,8 @@ using SharpCompress.Writers.GZip; namespace SharpCompress.Archives.GZip; -public partial class GZipArchive : AbstractWritableArchive +public partial class GZipArchive + : AbstractWritableArchive { private GZipArchive(SourceStream sourceStream) : base(ArchiveType.GZip, sourceStream) { } @@ -34,7 +33,7 @@ public partial class GZipArchive : AbstractWritableArchive oldEntries, IEnumerable newEntries ) @@ -88,11 +87,8 @@ public partial class GZipArchive : AbstractWritableArchive Archive = archive; + internal GZipArchiveEntry(GZipArchive archive, GZipFilePart? part, IReaderOptions readerOptions) + : base(part, readerOptions) => Archive = archive; public virtual Stream OpenEntryStream() { diff --git a/src/SharpCompress/Archives/GZip/GZipWritableArchiveEntry.cs b/src/SharpCompress/Archives/GZip/GZipWritableArchiveEntry.cs index 729ad529..8171b872 100644 --- a/src/SharpCompress/Archives/GZip/GZipWritableArchiveEntry.cs +++ b/src/SharpCompress/Archives/GZip/GZipWritableArchiveEntry.cs @@ -19,7 +19,7 @@ internal sealed class GZipWritableArchiveEntry : GZipArchiveEntry, IWritableArch DateTime? lastModified, bool closeStream ) - : base(archive, null) + : base(archive, null, archive.ReaderOptions) { this.stream = stream; Key = path; diff --git a/src/SharpCompress/Archives/IArchive.cs b/src/SharpCompress/Archives/IArchive.cs index 6016214b..72548073 100644 --- a/src/SharpCompress/Archives/IArchive.cs +++ b/src/SharpCompress/Archives/IArchive.cs @@ -12,6 +12,11 @@ public interface IArchive : IDisposable ArchiveType Type { get; } + /// + /// The options used when opening this archive, including extraction behavior settings. + /// + ReaderOptions ReaderOptions { get; } + /// /// Use this method to extract all entries in an archive in order. /// This is primarily for SOLID Rar Archives or 7Zip Archives as they need to be diff --git a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs index be352cdb..28ef61c7 100644 --- a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs +++ b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs @@ -47,11 +47,13 @@ public static class IArchiveEntryExtensions } #if LEGACY_DOTNET - using var entryStream = await archiveEntry.OpenEntryStreamAsync(cancellationToken); + using var entryStream = await archiveEntry + .OpenEntryStreamAsync(cancellationToken) + .ConfigureAwait(false); #else - await using var entryStream = await archiveEntry.OpenEntryStreamAsync( - cancellationToken - ); + await using var entryStream = await archiveEntry + .OpenEntryStreamAsync(cancellationToken) + .ConfigureAwait(false); #endif var sourceStream = WrapWithProgress(entryStream, archiveEntry, progress); await sourceStream @@ -100,15 +102,11 @@ public static class IArchiveEntryExtensions /// /// Extract to specific directory, retaining filename /// - public void WriteToDirectory( - string destinationDirectory, - ExtractionOptions? options = null - ) => + public void WriteToDirectory(string destinationDirectory) => ExtractionMethods.WriteEntryToDirectory( entry, destinationDirectory, - options, - entry.WriteToFile + (path) => entry.WriteToFile(path) ); /// @@ -116,15 +114,14 @@ public static class IArchiveEntryExtensions /// public async ValueTask WriteToDirectoryAsync( string destinationDirectory, - ExtractionOptions? options = null, CancellationToken cancellationToken = default ) => await ExtractionMethods .WriteEntryToDirectoryAsync( entry, destinationDirectory, - options, - entry.WriteToFileAsync, + async (path, ct) => + await entry.WriteToFileAsync(path, ct).ConfigureAwait(false), cancellationToken ) .ConfigureAwait(false); @@ -132,11 +129,10 @@ public static class IArchiveEntryExtensions /// /// Extract to specific file /// - public void WriteToFile(string destinationFileName, ExtractionOptions? options = null) => + public void WriteToFile(string destinationFileName) => ExtractionMethods.WriteEntryToFile( entry, destinationFileName, - options, (x, fm) => { using var fs = File.Open(destinationFileName, fm); @@ -149,14 +145,12 @@ public static class IArchiveEntryExtensions /// public async ValueTask WriteToFileAsync( string destinationFileName, - ExtractionOptions? options = null, CancellationToken cancellationToken = default ) => await ExtractionMethods .WriteEntryToFileAsync( entry, destinationFileName, - options, async (x, fm, ct) => { using var fs = File.Open(destinationFileName, fm); diff --git a/src/SharpCompress/Archives/IArchiveExtensions.cs b/src/SharpCompress/Archives/IArchiveExtensions.cs index 80857a25..a5bfd0b7 100644 --- a/src/SharpCompress/Archives/IArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IArchiveExtensions.cs @@ -14,28 +14,25 @@ public static class IArchiveExtensions /// 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 ) { if (archive.IsSolid || archive.Type == ArchiveType.SevenZip) { using var reader = archive.ExtractAllEntries(); - reader.WriteAllToDirectory(destinationDirectory, options); + reader.WriteAllToDirectory(destinationDirectory); } else { - archive.WriteToDirectoryInternal(destinationDirectory, options, progress); + archive.WriteToDirectoryInternal(destinationDirectory, progress); } } private void WriteToDirectoryInternal( string destinationDirectory, - ExtractionOptions? options, IProgress? progress ) { @@ -61,7 +58,7 @@ public static class IArchiveExtensions continue; } - entry.WriteToDirectory(destinationDirectory, options); + entry.WriteToDirectory(destinationDirectory); bytesRead += entry.Size; progress?.Report( diff --git a/src/SharpCompress/Archives/IArchiveOpenable.cs b/src/SharpCompress/Archives/IArchiveOpenable.cs index e5ae52b3..e36ea02b 100644 --- a/src/SharpCompress/Archives/IArchiveOpenable.cs +++ b/src/SharpCompress/Archives/IArchiveOpenable.cs @@ -20,20 +20,17 @@ public interface IArchiveOpenable public static abstract TASync OpenAsyncArchive( string path, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default + ReaderOptions? readerOptions = null ); public static abstract TASync OpenAsyncArchive( Stream stream, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default + ReaderOptions? readerOptions = null ); public static abstract TASync OpenAsyncArchive( FileInfo fileInfo, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default + ReaderOptions? readerOptions = null ); } diff --git a/src/SharpCompress/Archives/IAsyncArchiveExtensions.cs b/src/SharpCompress/Archives/IAsyncArchiveExtensions.cs index 7a930bb0..bb00afcf 100644 --- a/src/SharpCompress/Archives/IAsyncArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IAsyncArchiveExtensions.cs @@ -17,44 +17,45 @@ public static class IAsyncArchiveExtensions /// /// The archive to extract. /// The folder to extract into. - /// Extraction options. /// Optional progress reporter for tracking extraction progress. /// Optional cancellation token. public async ValueTask WriteToDirectoryAsync( string destinationDirectory, - ExtractionOptions? options = null, IProgress? progress = null, CancellationToken cancellationToken = default ) { - if (await archive.IsSolidAsync() || archive.Type == ArchiveType.SevenZip) + if ( + await archive.IsSolidAsync().ConfigureAwait(false) + || archive.Type == ArchiveType.SevenZip + ) { - await using var reader = await archive.ExtractAllEntriesAsync(); - await reader.WriteAllToDirectoryAsync( - destinationDirectory, - options, - cancellationToken - ); + await using var reader = await archive + .ExtractAllEntriesAsync() + .ConfigureAwait(false); + await reader + .WriteAllToDirectoryAsync(destinationDirectory, cancellationToken) + .ConfigureAwait(false); } else { - await archive.WriteToDirectoryAsyncInternal( - destinationDirectory, - options, - progress, - cancellationToken - ); + await archive + .WriteToDirectoryAsyncInternal( + destinationDirectory, + progress, + cancellationToken + ) + .ConfigureAwait(false); } } private async ValueTask WriteToDirectoryAsyncInternal( string destinationDirectory, - ExtractionOptions? options, IProgress? progress, CancellationToken cancellationToken ) { - var totalBytes = await archive.TotalUncompressedSizeAsync(); + var totalBytes = await archive.TotalUncompressedSizeAsync().ConfigureAwait(false); var bytesRead = 0L; var seenDirectories = new HashSet(); @@ -79,7 +80,7 @@ public static class IAsyncArchiveExtensions } await entry - .WriteToDirectoryAsync(destinationDirectory, options, cancellationToken) + .WriteToDirectoryAsync(destinationDirectory, cancellationToken) .ConfigureAwait(false); bytesRead += entry.Size; diff --git a/src/SharpCompress/Archives/IMultiArchiveFactory.cs b/src/SharpCompress/Archives/IMultiArchiveFactory.cs index fc418ad4..936222e6 100644 --- a/src/SharpCompress/Archives/IMultiArchiveFactory.cs +++ b/src/SharpCompress/Archives/IMultiArchiveFactory.cs @@ -50,10 +50,8 @@ public interface IMultiArchiveFactory : IFactory /// /// /// reading options. - /// Cancellation token. IAsyncArchive OpenAsyncArchive( IReadOnlyList fileInfos, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default + ReaderOptions? readerOptions = null ); } diff --git a/src/SharpCompress/Archives/IMultiArchiveOpenable.cs b/src/SharpCompress/Archives/IMultiArchiveOpenable.cs index 53f375c0..0fed7adb 100644 --- a/src/SharpCompress/Archives/IMultiArchiveOpenable.cs +++ b/src/SharpCompress/Archives/IMultiArchiveOpenable.cs @@ -22,14 +22,12 @@ public interface IMultiArchiveOpenable public static abstract TASync OpenAsyncArchive( IReadOnlyList streams, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default + ReaderOptions? readerOptions = null ); public static abstract TASync OpenAsyncArchive( IReadOnlyList fileInfos, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default + ReaderOptions? readerOptions = null ); } #endif diff --git a/src/SharpCompress/Archives/IWritableArchive.cs b/src/SharpCompress/Archives/IWritableArchive.cs index cb5c0209..0d17a937 100644 --- a/src/SharpCompress/Archives/IWritableArchive.cs +++ b/src/SharpCompress/Archives/IWritableArchive.cs @@ -28,28 +28,23 @@ public interface IWritableArchive : IArchive, IWritableArchiveCommon IArchiveEntry AddDirectoryEntry(string key, DateTime? modified = null); - /// - /// Saves the archive to the specified stream using the given writer options. - /// - void SaveTo(Stream stream, IWriterOptions options); - /// /// Removes the specified entry from the archive. /// void RemoveEntry(IArchiveEntry entry); } -public interface IWritableAsyncArchive : IAsyncArchive, IWritableArchiveCommon +public interface IWritableArchive : IWritableArchive + where TOptions : IWriterOptions { /// - /// Asynchronously saves the archive to the specified stream using the given writer options. + /// Saves the archive to the specified stream using the given writer options. /// - ValueTask SaveToAsync( - Stream stream, - IWriterOptions options, - CancellationToken cancellationToken = default - ); + void SaveTo(Stream stream, TOptions options); +} +public interface IWritableAsyncArchive : IAsyncArchive, IWritableArchiveCommon +{ /// /// Asynchronously adds an entry to the archive with the specified key, source stream, and options. /// @@ -76,3 +71,16 @@ public interface IWritableAsyncArchive : IAsyncArchive, IWritableArchiveCommon /// ValueTask RemoveEntryAsync(IArchiveEntry entry); } + +public interface IWritableAsyncArchive : IWritableAsyncArchive + where TOptions : IWriterOptions +{ + /// + /// Asynchronously saves the archive to the specified stream using the given writer options. + /// + ValueTask SaveToAsync( + Stream stream, + TOptions options, + CancellationToken cancellationToken = default + ); +} diff --git a/src/SharpCompress/Archives/IWritableArchiveExtensions.cs b/src/SharpCompress/Archives/IWritableArchiveExtensions.cs index 8757b393..6012dda5 100644 --- a/src/SharpCompress/Archives/IWritableArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IWritableArchiveExtensions.cs @@ -58,17 +58,23 @@ public static class IWritableArchiveExtensions fileInfo.LastWriteTime ); } + } - public void SaveTo(string filePath, IWriterOptions? options = null) => - writableArchive.SaveTo( - new FileInfo(filePath), - options ?? new WriterOptions(CompressionType.Deflate) - ); + public static void SaveTo( + this IWritableArchive writableArchive, + string filePath, + TOptions options + ) + where TOptions : IWriterOptions => writableArchive.SaveTo(new FileInfo(filePath), options); - public void SaveTo(FileInfo fileInfo, IWriterOptions? options = null) - { - using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); - writableArchive.SaveTo(stream, options ?? new WriterOptions(CompressionType.Deflate)); - } + public static void SaveTo( + this IWritableArchive writableArchive, + FileInfo fileInfo, + TOptions options + ) + where TOptions : IWriterOptions + { + using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); + writableArchive.SaveTo(stream, options); } } diff --git a/src/SharpCompress/Archives/IWritableArchiveOpenable.cs b/src/SharpCompress/Archives/IWritableArchiveOpenable.cs index b0b761a2..f523e1d2 100644 --- a/src/SharpCompress/Archives/IWritableArchiveOpenable.cs +++ b/src/SharpCompress/Archives/IWritableArchiveOpenable.cs @@ -1,10 +1,13 @@ +using SharpCompress.Common.Options; + #if NET8_0_OR_GREATER namespace SharpCompress.Archives; -public interface IWritableArchiveOpenable - : IArchiveOpenable +public interface IWritableArchiveOpenable + : IArchiveOpenable, IWritableAsyncArchive> + where TOptions : IWriterOptions { - public static abstract IWritableArchive CreateArchive(); - public static abstract IWritableAsyncArchive CreateAsyncArchive(); + public static abstract IWritableArchive CreateArchive(); + public static abstract IWritableAsyncArchive CreateAsyncArchive(); } #endif diff --git a/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs b/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs index 1d065fe9..4bdc792f 100644 --- a/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs @@ -25,13 +25,15 @@ public static class IWritableAsyncArchiveExtensions ) { var fileInfo = new FileInfo(path); - await writableArchive.AddEntryAsync( - path.Substring(filePath.Length), - fileInfo.OpenRead(), - true, - fileInfo.Length, - fileInfo.LastWriteTime - ); + await writableArchive + .AddEntryAsync( + path.Substring(filePath.Length), + fileInfo.OpenRead(), + true, + fileInfo.Length, + fileInfo.LastWriteTime + ) + .ConfigureAwait(false); } } } @@ -60,32 +62,26 @@ public static class IWritableAsyncArchiveExtensions fileInfo.LastWriteTime ); } + } - public ValueTask SaveToAsync( - string filePath, - IWriterOptions? options = null, - CancellationToken cancellationToken = default - ) => - writableArchive.SaveToAsync( - new FileInfo(filePath), - options ?? new WriterOptions(CompressionType.Deflate), - cancellationToken - ); + public static ValueTask SaveToAsync( + this IWritableAsyncArchive writableArchive, + string filePath, + TOptions options, + CancellationToken cancellationToken = default + ) + where TOptions : IWriterOptions => + writableArchive.SaveToAsync(new FileInfo(filePath), options, cancellationToken); - public async ValueTask SaveToAsync( - FileInfo fileInfo, - IWriterOptions? options = null, - CancellationToken cancellationToken = default - ) - { - using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); - await writableArchive - .SaveToAsync( - stream, - options ?? new WriterOptions(CompressionType.Deflate), - cancellationToken - ) - .ConfigureAwait(false); - } + public static async ValueTask SaveToAsync( + this IWritableAsyncArchive writableArchive, + FileInfo fileInfo, + TOptions options, + CancellationToken cancellationToken = default + ) + where TOptions : IWriterOptions + { + using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); + await writableArchive.SaveToAsync(stream, options, cancellationToken).ConfigureAwait(false); } } diff --git a/src/SharpCompress/Archives/IWriteableArchiveFactory.cs b/src/SharpCompress/Archives/IWriteableArchiveFactory.cs index f1de642d..40a01fac 100644 --- a/src/SharpCompress/Archives/IWriteableArchiveFactory.cs +++ b/src/SharpCompress/Archives/IWriteableArchiveFactory.cs @@ -1,3 +1,5 @@ +using SharpCompress.Common.Options; + namespace SharpCompress.Archives; /// @@ -10,11 +12,12 @@ namespace SharpCompress.Archives; /// /// /// -public interface IWriteableArchiveFactory : Factories.IFactory +public interface IWriteableArchiveFactory : Factories.IFactory + where TOptions : IWriterOptions { /// /// Creates a new, empty archive, ready to be written. /// /// - IWritableArchive CreateArchive(); + IWritableArchive CreateArchive(); } diff --git a/src/SharpCompress/Archives/Rar/RarArchive.Async.cs b/src/SharpCompress/Archives/Rar/RarArchive.Async.cs index 563d6bcb..ce74a3c3 100644 --- a/src/SharpCompress/Archives/Rar/RarArchive.Async.cs +++ b/src/SharpCompress/Archives/Rar/RarArchive.Async.cs @@ -25,13 +25,13 @@ public partial class RarArchive } _disposed = true; - await base.DisposeAsync(); + await base.DisposeAsync().ConfigureAwait(false); } } protected override async ValueTask CreateReaderForSolidExtractionAsync() { - if (await this.IsMultipartVolumeAsync()) + if (await this.IsMultipartVolumeAsync().ConfigureAwait(false)) { var streams = await VolumesAsync .Select(volume => @@ -39,15 +39,18 @@ public partial class RarArchive volume.Stream.Position = 0; return volume.Stream; }) - .ToListAsync(); + .ToListAsync() + .ConfigureAwait(false); return (RarReader)RarReader.OpenReader(streams, ReaderOptions); } - var stream = (await VolumesAsync.FirstAsync()).Stream; + var stream = (await VolumesAsync.FirstAsync().ConfigureAwait(false)).Stream; stream.Position = 0; return (RarReader)RarReader.OpenReader(stream, ReaderOptions); } public override async ValueTask IsSolidAsync() => - await (await VolumesAsync.CastAsync().FirstAsync()).IsSolidArchiveAsync(); + await (await VolumesAsync.CastAsync().FirstAsync().ConfigureAwait(false)) + .IsSolidArchiveAsync() + .ConfigureAwait(false); } diff --git a/src/SharpCompress/Archives/Rar/RarArchive.Extensions.cs b/src/SharpCompress/Archives/Rar/RarArchive.Extensions.cs index eb7c1f3b..aaca25d1 100644 --- a/src/SharpCompress/Archives/Rar/RarArchive.Extensions.cs +++ b/src/SharpCompress/Archives/Rar/RarArchive.Extensions.cs @@ -25,12 +25,16 @@ public static class RarArchiveExtensions /// RarArchive is the first volume of a multi-part archive. If MultipartVolume is true and IsFirstVolume is false then the first volume file must be missing. /// public async ValueTask IsFirstVolumeAsync() => - (await archive.VolumesAsync.CastAsync().FirstAsync()).IsFirstVolume; + ( + await archive.VolumesAsync.CastAsync().FirstAsync().ConfigureAwait(false) + ).IsFirstVolume; /// /// RarArchive is part of a multi-part archive. /// public async ValueTask IsMultipartVolumeAsync() => - (await archive.VolumesAsync.CastAsync().FirstAsync()).IsMultiVolume; + ( + await archive.VolumesAsync.CastAsync().FirstAsync().ConfigureAwait(false) + ).IsMultiVolume; } } diff --git a/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs b/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs index fe1eb5c3..76154b7f 100644 --- a/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs +++ b/src/SharpCompress/Archives/Rar/RarArchive.Factory.cs @@ -22,11 +22,9 @@ public partial class RarArchive { public static IRarAsyncArchive OpenAsyncArchive( string path, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default + ReaderOptions? readerOptions = null ) { - cancellationToken.ThrowIfCancellationRequested(); path.NotNullOrEmpty(nameof(path)); return (IRarAsyncArchive)OpenArchive(new FileInfo(path), readerOptions); } @@ -102,41 +100,33 @@ public partial class RarArchive public static IRarAsyncArchive OpenAsyncArchive( Stream stream, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default + ReaderOptions? readerOptions = null ) { - cancellationToken.ThrowIfCancellationRequested(); return (IRarAsyncArchive)OpenArchive(stream, readerOptions); } public static IRarAsyncArchive OpenAsyncArchive( FileInfo fileInfo, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default + ReaderOptions? readerOptions = null ) { - cancellationToken.ThrowIfCancellationRequested(); return (IRarAsyncArchive)OpenArchive(fileInfo, readerOptions); } public static IRarAsyncArchive OpenAsyncArchive( IReadOnlyList streams, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default + ReaderOptions? readerOptions = null ) { - cancellationToken.ThrowIfCancellationRequested(); return (IRarAsyncArchive)OpenArchive(streams, readerOptions); } public static IRarAsyncArchive OpenAsyncArchive( IReadOnlyList fileInfos, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default + ReaderOptions? readerOptions = null ) { - cancellationToken.ThrowIfCancellationRequested(); return (IRarAsyncArchive)OpenArchive(fileInfos, readerOptions); } diff --git a/src/SharpCompress/Archives/Rar/RarArchiveEntry.Async.cs b/src/SharpCompress/Archives/Rar/RarArchiveEntry.Async.cs index dbc2cac0..471609aa 100644 --- a/src/SharpCompress/Archives/Rar/RarArchiveEntry.Async.cs +++ b/src/SharpCompress/Archives/Rar/RarArchiveEntry.Async.cs @@ -21,9 +21,9 @@ public partial class RarArchiveEntry stream = new RarStream( archive.UnpackV1.Value, FileHeader, - await MultiVolumeReadOnlyAsyncStream.Create( - Parts.ToAsyncEnumerable().CastAsync() - ) + await MultiVolumeReadOnlyAsyncStream + .Create(Parts.ToAsyncEnumerable().CastAsync()) + .ConfigureAwait(false) ); } else @@ -31,13 +31,13 @@ public partial class RarArchiveEntry stream = new RarStream( archive.UnpackV2017.Value, FileHeader, - await MultiVolumeReadOnlyAsyncStream.Create( - Parts.ToAsyncEnumerable().CastAsync() - ) + await MultiVolumeReadOnlyAsyncStream + .Create(Parts.ToAsyncEnumerable().CastAsync()) + .ConfigureAwait(false) ); } - await stream.InitializeAsync(cancellationToken); + await stream.InitializeAsync(cancellationToken).ConfigureAwait(false); return stream; } } diff --git a/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs b/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs index 3d1a74d2..d147264e 100644 --- a/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs +++ b/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs @@ -23,6 +23,7 @@ public partial class RarArchiveEntry : RarEntry, IArchiveEntry IEnumerable parts, ReaderOptions readerOptions ) + : base(readerOptions) { this.parts = parts.ToList(); this.archive = archive; diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Async.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Async.cs index f860b62c..1f44fa2e 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Async.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Async.cs @@ -21,15 +21,12 @@ public partial class SevenZipArchive { stream.Position = 0; var reader = new ArchiveReader(); - await reader.OpenAsync( - stream, - lookForHeader: ReaderOptions.LookForHeader, - cancellationToken - ); - _database = await reader.ReadDatabaseAsync( - new PasswordProvider(ReaderOptions.Password), - cancellationToken - ); + await reader + .OpenAsync(stream, lookForHeader: ReaderOptions.LookForHeader, cancellationToken) + .ConfigureAwait(false); + _database = await reader + .ReadDatabaseAsync(new PasswordProvider(ReaderOptions.Password), cancellationToken) + .ConfigureAwait(false); } } @@ -37,8 +34,8 @@ public partial class SevenZipArchive IAsyncEnumerable volumes ) { - var stream = (await volumes.SingleAsync()).Stream; - await LoadFactoryAsync(stream); + var stream = (await volumes.SingleAsync().ConfigureAwait(false)).Stream; + await LoadFactoryAsync(stream).ConfigureAwait(false); if (_database is null) { yield break; @@ -49,7 +46,8 @@ public partial class SevenZipArchive var file = _database._files[i]; entries[i] = new SevenZipArchiveEntry( this, - new SevenZipFilePart(stream, _database, i, file, ReaderOptions.ArchiveEncoding) + new SevenZipFilePart(stream, _database, i, file, ReaderOptions.ArchiveEncoding), + ReaderOptions ); } foreach (var group in entries.Where(x => !x.IsDirectory).GroupBy(x => x.FilePart.Folder)) diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs index 614c4408..76f177fc 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.Factory.cs @@ -16,13 +16,8 @@ public partial class SevenZipArchive IMultiArchiveOpenable #endif { - public static IAsyncArchive OpenAsyncArchive( - string path, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) + public static IAsyncArchive OpenAsyncArchive(string path, ReaderOptions? readerOptions = null) { - cancellationToken.ThrowIfCancellationRequested(); path.NotNullOrEmpty("path"); return (IAsyncArchive)OpenArchive(new FileInfo(path), readerOptions ?? new ReaderOptions()); } @@ -91,43 +86,32 @@ public partial class SevenZipArchive ); } - public static IAsyncArchive OpenAsyncArchive( - Stream stream, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) + public static IAsyncArchive OpenAsyncArchive(Stream stream, ReaderOptions? readerOptions = null) { - cancellationToken.ThrowIfCancellationRequested(); return (IAsyncArchive)OpenArchive(stream, readerOptions); } public static IAsyncArchive OpenAsyncArchive( FileInfo fileInfo, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default + ReaderOptions? readerOptions = null ) { - cancellationToken.ThrowIfCancellationRequested(); return (IAsyncArchive)OpenArchive(fileInfo, readerOptions); } public static IAsyncArchive OpenAsyncArchive( IReadOnlyList streams, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default + ReaderOptions? readerOptions = null ) { - cancellationToken.ThrowIfCancellationRequested(); return (IAsyncArchive)OpenArchive(streams, readerOptions); } public static IAsyncArchive OpenAsyncArchive( IReadOnlyList fileInfos, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default + ReaderOptions? readerOptions = null ) { - cancellationToken.ThrowIfCancellationRequested(); return (IAsyncArchive)OpenArchive(fileInfos, readerOptions); } @@ -163,7 +147,7 @@ public partial class SevenZipArchive cancellationToken.ThrowIfCancellationRequested(); try { - return await SignatureMatchAsync(stream, cancellationToken); + return await SignatureMatchAsync(stream, cancellationToken).ConfigureAwait(false); } catch { diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs index e9b27d6a..a3c65a85 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs @@ -55,7 +55,8 @@ public partial class SevenZipArchive : AbstractArchive Archive = archive; + internal SevenZipArchiveEntry( + SevenZipArchive archive, + SevenZipFilePart part, + IReaderOptions readerOptions + ) + : base(part, readerOptions) => Archive = archive; public Stream OpenEntryStream() => FilePart.GetCompressedStream(); public async ValueTask OpenEntryStreamAsync( CancellationToken cancellationToken = default - ) => (await FilePart.GetCompressedStreamAsync(cancellationToken)).NotNull(); + ) => + ( + await FilePart.GetCompressedStreamAsync(cancellationToken).ConfigureAwait(false) + ).NotNull(); public IArchive Archive { get; } diff --git a/src/SharpCompress/Archives/Tar/TarArchive.Async.cs b/src/SharpCompress/Archives/Tar/TarArchive.Async.cs index a0a8086a..1cd7dd91 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.Async.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.Async.cs @@ -19,7 +19,7 @@ public partial class TarArchive { protected override async ValueTask SaveToAsync( Stream stream, - IWriterOptions options, + TarWriterOptions options, IAsyncEnumerable oldEntries, IEnumerable newEntries, CancellationToken cancellationToken = default @@ -96,7 +96,7 @@ public partial class TarArchive IAsyncEnumerable volumes ) { - var stream = (await volumes.SingleAsync()).Stream; + var stream = (await volumes.SingleAsync().ConfigureAwait(false)).Stream; if (stream.CanSeek) { stream.Position = 0; @@ -127,7 +127,8 @@ public partial class TarArchive var entry = new TarArchiveEntry( this, new TarFilePart(previousHeader, stream), - CompressionType.None + CompressionType.None, + ReaderOptions ); var oldStreamPos = stream.Position; @@ -135,7 +136,7 @@ public partial class TarArchive using (var entryStream = entry.OpenEntryStream()) { using var memoryStream = new MemoryStream(); - await entryStream.CopyToAsync(memoryStream); + await entryStream.CopyToAsync(memoryStream).ConfigureAwait(false); memoryStream.Position = 0; var bytes = memoryStream.ToArray(); @@ -151,7 +152,8 @@ public partial class TarArchive yield return new TarArchiveEntry( this, new TarFilePart(header, stream), - CompressionType.None + CompressionType.None, + ReaderOptions ); } } diff --git a/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs b/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs index 959b4ab4..1290f97a 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.Factory.cs @@ -9,22 +9,29 @@ using SharpCompress.Common; using SharpCompress.Common.Tar.Headers; using SharpCompress.IO; using SharpCompress.Readers; +using SharpCompress.Writers.Tar; namespace SharpCompress.Archives.Tar; public partial class TarArchive #if NET8_0_OR_GREATER - : IWritableArchiveOpenable, - IMultiArchiveOpenable + : IWritableArchiveOpenable, + IMultiArchiveOpenable< + IWritableArchive, + IWritableAsyncArchive + > #endif { - public static IWritableArchive OpenArchive(string filePath, ReaderOptions? readerOptions = null) + public static IWritableArchive OpenArchive( + string filePath, + ReaderOptions? readerOptions = null + ) { filePath.NotNullOrEmpty(nameof(filePath)); return OpenArchive(new FileInfo(filePath), readerOptions); } - public static IWritableArchive OpenArchive( + public static IWritableArchive OpenArchive( FileInfo fileInfo, ReaderOptions? readerOptions = null ) @@ -39,7 +46,7 @@ public partial class TarArchive ); } - public static IWritableArchive OpenArchive( + public static IWritableArchive OpenArchive( IEnumerable fileInfos, ReaderOptions? readerOptions = null ) @@ -55,7 +62,7 @@ public partial class TarArchive ); } - public static IWritableArchive OpenArchive( + public static IWritableArchive OpenArchive( IEnumerable streams, ReaderOptions? readerOptions = null ) @@ -71,7 +78,10 @@ public partial class TarArchive ); } - public static IWritableArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null) + public static IWritableArchive OpenArchive( + Stream stream, + ReaderOptions? readerOptions = null + ) { stream.NotNull(nameof(stream)); @@ -85,55 +95,30 @@ public partial class TarArchive ); } - public static IWritableAsyncArchive OpenAsyncArchive( + public static IWritableAsyncArchive OpenAsyncArchive( Stream stream, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) - { - cancellationToken.ThrowIfCancellationRequested(); - return (IWritableAsyncArchive)OpenArchive(stream, readerOptions); - } + ReaderOptions? readerOptions = null + ) => (IWritableAsyncArchive)OpenArchive(stream, readerOptions); - public static IWritableAsyncArchive OpenAsyncArchive( + public static IWritableAsyncArchive OpenAsyncArchive( string path, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) - { - cancellationToken.ThrowIfCancellationRequested(); - return (IWritableAsyncArchive)OpenArchive(new FileInfo(path), readerOptions); - } + ReaderOptions? readerOptions = null + ) => (IWritableAsyncArchive)OpenArchive(new FileInfo(path), readerOptions); - public static IWritableAsyncArchive OpenAsyncArchive( + public static IWritableAsyncArchive OpenAsyncArchive( FileInfo fileInfo, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) - { - cancellationToken.ThrowIfCancellationRequested(); - return (IWritableAsyncArchive)OpenArchive(fileInfo, readerOptions); - } + ReaderOptions? readerOptions = null + ) => (IWritableAsyncArchive)OpenArchive(fileInfo, readerOptions); - public static IWritableAsyncArchive OpenAsyncArchive( + public static IWritableAsyncArchive OpenAsyncArchive( IReadOnlyList streams, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) - { - cancellationToken.ThrowIfCancellationRequested(); - return (IWritableAsyncArchive)OpenArchive(streams, readerOptions); - } + ReaderOptions? readerOptions = null + ) => (IWritableAsyncArchive)OpenArchive(streams, readerOptions); - public static IWritableAsyncArchive OpenAsyncArchive( + public static IWritableAsyncArchive OpenAsyncArchive( IReadOnlyList fileInfos, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) - { - cancellationToken.ThrowIfCancellationRequested(); - return (IWritableAsyncArchive)OpenArchive(fileInfos, readerOptions); - } + ReaderOptions? readerOptions = null + ) => (IWritableAsyncArchive)OpenArchive(fileInfos, readerOptions); public static bool IsTarFile(string filePath) => IsTarFile(new FileInfo(filePath)); @@ -181,7 +166,7 @@ public partial class TarArchive #else using var reader = new AsyncBinaryReader(stream, leaveOpen: true); #endif - var readSucceeded = await tarHeader.ReadAsync(reader); + var readSucceeded = await tarHeader.ReadAsync(reader).ConfigureAwait(false); var isEmptyArchive = tarHeader.Name?.Length == 0 && tarHeader.Size == 0 @@ -196,7 +181,7 @@ public partial class TarArchive } } - public static IWritableArchive CreateArchive() => new TarArchive(); + public static IWritableArchive CreateArchive() => new TarArchive(); - public static IWritableAsyncArchive CreateAsyncArchive() => new TarArchive(); + public static IWritableAsyncArchive CreateAsyncArchive() => new TarArchive(); } diff --git a/src/SharpCompress/Archives/Tar/TarArchive.cs b/src/SharpCompress/Archives/Tar/TarArchive.cs index 8400be09..70dc93d2 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.cs @@ -16,7 +16,8 @@ using SharpCompress.Writers.Tar; namespace SharpCompress.Archives.Tar; -public partial class TarArchive : AbstractWritableArchive +public partial class TarArchive + : AbstractWritableArchive { protected override IEnumerable LoadVolumes(SourceStream sourceStream) { @@ -59,7 +60,8 @@ public partial class TarArchive : AbstractWritableArchive oldEntries, IEnumerable newEntries ) diff --git a/src/SharpCompress/Archives/Tar/TarArchiveEntry.cs b/src/SharpCompress/Archives/Tar/TarArchiveEntry.cs index 921452e9..c09d4755 100644 --- a/src/SharpCompress/Archives/Tar/TarArchiveEntry.cs +++ b/src/SharpCompress/Archives/Tar/TarArchiveEntry.cs @@ -1,22 +1,31 @@ -using System.IO; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using SharpCompress.Common; +using SharpCompress.Common.Options; using SharpCompress.Common.Tar; namespace SharpCompress.Archives.Tar; public class TarArchiveEntry : TarEntry, IArchiveEntry { - internal TarArchiveEntry(TarArchive archive, TarFilePart? part, CompressionType compressionType) - : base(part, compressionType) => Archive = archive; + internal TarArchiveEntry( + TarArchive archive, + TarFilePart? part, + CompressionType compressionType, + IReaderOptions readerOptions + ) + : base(part, compressionType, readerOptions) => Archive = archive; public virtual Stream OpenEntryStream() => Parts.Single().GetCompressedStream().NotNull(); public async ValueTask OpenEntryStreamAsync( CancellationToken cancellationToken = default - ) => (await Parts.Single().GetCompressedStreamAsync(cancellationToken)).NotNull(); + ) => + ( + await Parts.Single().GetCompressedStreamAsync(cancellationToken).ConfigureAwait(false) + ).NotNull(); #region IArchiveEntry Members diff --git a/src/SharpCompress/Archives/Tar/TarWritableArchiveEntry.cs b/src/SharpCompress/Archives/Tar/TarWritableArchiveEntry.cs index 99254369..84f8ff44 100644 --- a/src/SharpCompress/Archives/Tar/TarWritableArchiveEntry.cs +++ b/src/SharpCompress/Archives/Tar/TarWritableArchiveEntry.cs @@ -21,7 +21,7 @@ internal sealed class TarWritableArchiveEntry : TarArchiveEntry, IWritableArchiv DateTime? lastModified, bool closeStream ) - : base(archive, null, compressionType) + : base(archive, null, compressionType, archive.ReaderOptions) { this.stream = stream; Key = path; @@ -36,7 +36,7 @@ internal sealed class TarWritableArchiveEntry : TarArchiveEntry, IWritableArchiv string directoryPath, DateTime? lastModified ) - : base(archive, null, CompressionType.None) + : base(archive, null, CompressionType.None, archive.ReaderOptions) { stream = null; Key = directoryPath; diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs b/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs index d60a9259..c8139b06 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.Async.cs @@ -21,7 +21,7 @@ public partial class ZipArchive IAsyncEnumerable volumes ) { - var vols = await volumes.ToListAsync(); + var vols = await volumes.ToListAsync().ConfigureAwait(false); var volsArray = vols.ToArray(); await foreach ( @@ -55,12 +55,8 @@ public partial class ZipArchive yield return new ZipArchiveEntry( this, - new SeekableZipFilePart( - headerFactory.NotNull(), - deh, - s, - ReaderOptions.CompressionProviders - ) + new SeekableZipFilePart(headerFactory.NotNull(), deh, s), + ReaderOptions ); } break; @@ -77,7 +73,7 @@ public partial class ZipArchive protected override async ValueTask SaveToAsync( Stream stream, - IWriterOptions options, + ZipWriterOptions options, IAsyncEnumerable oldEntries, IEnumerable newEntries, CancellationToken cancellationToken = default diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs index 4904decb..fb9778a9 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.Factory.cs @@ -9,22 +9,29 @@ using SharpCompress.Common.Zip; using SharpCompress.Common.Zip.Headers; using SharpCompress.IO; using SharpCompress.Readers; +using SharpCompress.Writers.Zip; namespace SharpCompress.Archives.Zip; public partial class ZipArchive #if NET8_0_OR_GREATER - : IWritableArchiveOpenable, - IMultiArchiveOpenable + : IWritableArchiveOpenable, + IMultiArchiveOpenable< + IWritableArchive, + IWritableAsyncArchive + > #endif { - public static IWritableArchive OpenArchive(string filePath, ReaderOptions? readerOptions = null) + public static IWritableArchive OpenArchive( + string filePath, + ReaderOptions? readerOptions = null + ) { filePath.NotNullOrEmpty(nameof(filePath)); return OpenArchive(new FileInfo(filePath), readerOptions); } - public static IWritableArchive OpenArchive( + public static IWritableArchive OpenArchive( FileInfo fileInfo, ReaderOptions? readerOptions = null ) @@ -39,7 +46,7 @@ public partial class ZipArchive ); } - public static IWritableArchive OpenArchive( + public static IWritableArchive OpenArchive( IEnumerable fileInfos, ReaderOptions? readerOptions = null ) @@ -55,7 +62,7 @@ public partial class ZipArchive ); } - public static IWritableArchive OpenArchive( + public static IWritableArchive OpenArchive( IEnumerable streams, ReaderOptions? readerOptions = null ) @@ -71,7 +78,10 @@ public partial class ZipArchive ); } - public static IWritableArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null) + public static IWritableArchive OpenArchive( + Stream stream, + ReaderOptions? readerOptions = null + ) { stream.NotNull(nameof(stream)); @@ -85,55 +95,30 @@ public partial class ZipArchive ); } - public static IWritableAsyncArchive OpenAsyncArchive( + public static IWritableAsyncArchive OpenAsyncArchive( string path, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) - { - cancellationToken.ThrowIfCancellationRequested(); - return (IWritableAsyncArchive)OpenArchive(path, readerOptions); - } + ReaderOptions? readerOptions = null + ) => (IWritableAsyncArchive)OpenArchive(path, readerOptions); - public static IWritableAsyncArchive OpenAsyncArchive( + public static IWritableAsyncArchive OpenAsyncArchive( Stream stream, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) - { - cancellationToken.ThrowIfCancellationRequested(); - return (IWritableAsyncArchive)OpenArchive(stream, readerOptions); - } + ReaderOptions? readerOptions = null + ) => (IWritableAsyncArchive)OpenArchive(stream, readerOptions); - public static IWritableAsyncArchive OpenAsyncArchive( + public static IWritableAsyncArchive OpenAsyncArchive( FileInfo fileInfo, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) - { - cancellationToken.ThrowIfCancellationRequested(); - return (IWritableAsyncArchive)OpenArchive(fileInfo, readerOptions); - } + ReaderOptions? readerOptions = null + ) => (IWritableAsyncArchive)OpenArchive(fileInfo, readerOptions); - public static IWritableAsyncArchive OpenAsyncArchive( + public static IWritableAsyncArchive OpenAsyncArchive( IReadOnlyList streams, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) - { - cancellationToken.ThrowIfCancellationRequested(); - return (IWritableAsyncArchive)OpenArchive(streams, readerOptions); - } + ReaderOptions? readerOptions = null + ) => (IWritableAsyncArchive)OpenArchive(streams, readerOptions); - public static IWritableAsyncArchive OpenAsyncArchive( + public static IWritableAsyncArchive OpenAsyncArchive( IReadOnlyList fileInfos, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) - { - cancellationToken.ThrowIfCancellationRequested(); - return (IWritableAsyncArchive)OpenArchive(fileInfos, readerOptions); - } + ReaderOptions? readerOptions = null + ) => (IWritableAsyncArchive)OpenArchive(fileInfos, readerOptions); public static bool IsZipFile(string filePath, string? password = null) => IsZipFile(new FileInfo(filePath), password); @@ -218,7 +203,8 @@ public partial class ZipArchive var header = await headerFactory .ReadStreamHeaderAsync(stream) .Where(x => x.ZipHeaderType != ZipHeaderType.Split) - .FirstOrDefaultAsync(cancellationToken); + .FirstOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); if (header is null) { return false; @@ -235,9 +221,9 @@ public partial class ZipArchive } } - public static IWritableArchive CreateArchive() => new ZipArchive(); + public static IWritableArchive CreateArchive() => new ZipArchive(); - public static IWritableAsyncArchive CreateAsyncArchive() => new ZipArchive(); + public static IWritableAsyncArchive CreateAsyncArchive() => new ZipArchive(); public static async ValueTask IsZipMultiAsync( Stream stream, @@ -261,6 +247,7 @@ public partial class ZipArchive await foreach ( var h in z.ReadSeekableHeaderAsync(stream) .WithCancellation(cancellationToken) + .ConfigureAwait(false) ) { x = h; diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.cs b/src/SharpCompress/Archives/Zip/ZipArchive.cs index 6853b636..8fc423bd 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.cs @@ -17,7 +17,8 @@ using SharpCompress.Writers.Zip; namespace SharpCompress.Archives.Zip; -public partial class ZipArchive : AbstractWritableArchive +public partial class ZipArchive + : AbstractWritableArchive { private readonly SeekableZipHeaderFactory? headerFactory; @@ -95,12 +96,8 @@ public partial class ZipArchive : AbstractWritableArchive SaveTo(stream, new WriterOptions(CompressionType.Deflate)); + public void SaveTo(Stream stream) => + SaveTo(stream, new ZipWriterOptions(CompressionType.Deflate)); protected override void SaveTo( Stream stream, - IWriterOptions options, + ZipWriterOptions options, IEnumerable oldEntries, IEnumerable newEntries ) diff --git a/src/SharpCompress/Archives/Zip/ZipArchiveEntry.Async.cs b/src/SharpCompress/Archives/Zip/ZipArchiveEntry.Async.cs index 2d76dcd7..308727a6 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchiveEntry.Async.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchiveEntry.Async.cs @@ -15,7 +15,9 @@ public partial class ZipArchiveEntry var part = Parts.Single(); if (part is SeekableZipFilePart seekablePart) { - return (await seekablePart.GetCompressedStreamAsync(cancellationToken)).NotNull(); + return ( + await seekablePart.GetCompressedStreamAsync(cancellationToken).ConfigureAwait(false) + ).NotNull(); } return OpenEntryStream(); } diff --git a/src/SharpCompress/Archives/Zip/ZipArchiveEntry.cs b/src/SharpCompress/Archives/Zip/ZipArchiveEntry.cs index bf3e67ac..b4f9e8bd 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchiveEntry.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchiveEntry.cs @@ -1,15 +1,20 @@ -using System.IO; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; +using SharpCompress.Common.Options; using SharpCompress.Common.Zip; namespace SharpCompress.Archives.Zip; public partial class ZipArchiveEntry : ZipEntry, IArchiveEntry { - internal ZipArchiveEntry(ZipArchive archive, SeekableZipFilePart? part) - : base(part) => Archive = archive; + internal ZipArchiveEntry( + ZipArchive archive, + SeekableZipFilePart? part, + IReaderOptions readerOptions + ) + : base(part, readerOptions) => Archive = archive; public virtual Stream OpenEntryStream() => Parts.Single().GetCompressedStream().NotNull(); diff --git a/src/SharpCompress/Archives/Zip/ZipWritableArchiveEntry.cs b/src/SharpCompress/Archives/Zip/ZipWritableArchiveEntry.cs index 168f3a4f..b74fea04 100644 --- a/src/SharpCompress/Archives/Zip/ZipWritableArchiveEntry.cs +++ b/src/SharpCompress/Archives/Zip/ZipWritableArchiveEntry.cs @@ -21,7 +21,7 @@ internal class ZipWritableArchiveEntry : ZipArchiveEntry, IWritableArchiveEntry DateTime? lastModified, bool closeStream ) - : base(archive, null) + : base(archive, null, archive.ReaderOptions) { this.stream = stream; Key = path; @@ -36,7 +36,7 @@ internal class ZipWritableArchiveEntry : ZipArchiveEntry, IWritableArchiveEntry string directoryPath, DateTime? lastModified ) - : base(archive, null) + : base(archive, null, archive.ReaderOptions) { stream = null; Key = directoryPath; diff --git a/src/SharpCompress/Common/Ace/AceEntry.cs b/src/SharpCompress/Common/Ace/AceEntry.cs index 5aa94fb4..419329de 100644 --- a/src/SharpCompress/Common/Ace/AceEntry.cs +++ b/src/SharpCompress/Common/Ace/AceEntry.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using SharpCompress.Common.Ace.Headers; +using SharpCompress.Common.Options; namespace SharpCompress.Common.Ace; @@ -12,7 +13,8 @@ public class AceEntry : Entry { private readonly AceFilePart _filePart; - internal AceEntry(AceFilePart filePart) + internal AceEntry(AceFilePart filePart, IReaderOptions readerOptions) + : base(readerOptions) { _filePart = filePart; } diff --git a/src/SharpCompress/Common/Ace/Headers/AceFileHeader.Async.cs b/src/SharpCompress/Common/Ace/Headers/AceFileHeader.Async.cs index 07601fdb..8f8a031e 100644 --- a/src/SharpCompress/Common/Ace/Headers/AceFileHeader.Async.cs +++ b/src/SharpCompress/Common/Ace/Headers/AceFileHeader.Async.cs @@ -18,7 +18,7 @@ public sealed partial class AceFileHeader CancellationToken cancellationToken = default ) { - var headerData = await ReadHeaderAsync(stream, cancellationToken); + var headerData = await ReadHeaderAsync(stream, cancellationToken).ConfigureAwait(false); if (headerData.Length == 0) { return null; diff --git a/src/SharpCompress/Common/Ace/Headers/AceHeader.Async.cs b/src/SharpCompress/Common/Ace/Headers/AceHeader.Async.cs index 50727a91..a427ec45 100644 --- a/src/SharpCompress/Common/Ace/Headers/AceHeader.Async.cs +++ b/src/SharpCompress/Common/Ace/Headers/AceHeader.Async.cs @@ -19,7 +19,9 @@ public abstract partial class AceHeader { // Read header CRC (2 bytes) and header size (2 bytes) var headerBytes = new byte[4]; - if (!await stream.ReadFullyAsync(headerBytes, 0, 4, cancellationToken)) + if ( + !await stream.ReadFullyAsync(headerBytes, 0, 4, cancellationToken).ConfigureAwait(false) + ) { return Array.Empty(); } @@ -33,7 +35,11 @@ public abstract partial class AceHeader // Read the header data var body = new byte[HeaderSize]; - if (!await stream.ReadFullyAsync(body, 0, HeaderSize, cancellationToken)) + if ( + !await stream + .ReadFullyAsync(body, 0, HeaderSize, cancellationToken) + .ConfigureAwait(false) + ) { return Array.Empty(); } @@ -59,7 +65,7 @@ public abstract partial class AceHeader ) { var bytes = new byte[14]; - if (!await stream.ReadFullyAsync(bytes, 0, 14, cancellationToken)) + if (!await stream.ReadFullyAsync(bytes, 0, 14, cancellationToken).ConfigureAwait(false)) { return false; } diff --git a/src/SharpCompress/Common/Ace/Headers/AceMainHeader.Async.cs b/src/SharpCompress/Common/Ace/Headers/AceMainHeader.Async.cs index 10b3f022..290cd199 100644 --- a/src/SharpCompress/Common/Ace/Headers/AceMainHeader.Async.cs +++ b/src/SharpCompress/Common/Ace/Headers/AceMainHeader.Async.cs @@ -19,7 +19,7 @@ public sealed partial class AceMainHeader CancellationToken cancellationToken = default ) { - var headerData = await ReadHeaderAsync(stream, cancellationToken); + var headerData = await ReadHeaderAsync(stream, cancellationToken).ConfigureAwait(false); if (headerData.Length == 0) { return null; diff --git a/src/SharpCompress/Common/Arc/ArcEntry.cs b/src/SharpCompress/Common/Arc/ArcEntry.cs index 0a94ae0c..cb7b262c 100644 --- a/src/SharpCompress/Common/Arc/ArcEntry.cs +++ b/src/SharpCompress/Common/Arc/ArcEntry.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using SharpCompress.Common.GZip; +using SharpCompress.Common.Options; using SharpCompress.Common.Tar; namespace SharpCompress.Common.Arc; @@ -13,7 +14,8 @@ public class ArcEntry : Entry { private readonly ArcFilePart? _filePart; - internal ArcEntry(ArcFilePart? filePart) + internal ArcEntry(ArcFilePart? filePart, IReaderOptions readerOptions) + : base(readerOptions) { _filePart = filePart; } diff --git a/src/SharpCompress/Common/Arc/ArcEntryHeader.cs b/src/SharpCompress/Common/Arc/ArcEntryHeader.cs index 3645020d..983b1b78 100644 --- a/src/SharpCompress/Common/Arc/ArcEntryHeader.cs +++ b/src/SharpCompress/Common/Arc/ArcEntryHeader.cs @@ -41,8 +41,9 @@ public class ArcEntryHeader { byte[] headerBytes = new byte[29]; if ( - await stream.ReadAsync(headerBytes, 0, headerBytes.Length, cancellationToken) - != headerBytes.Length + await stream + .ReadAsync(headerBytes, 0, headerBytes.Length, cancellationToken) + .ConfigureAwait(false) != headerBytes.Length ) { return null; diff --git a/src/SharpCompress/Common/Arc/ArcFilePart.Async.cs b/src/SharpCompress/Common/Arc/ArcFilePart.Async.cs index 0ae8004d..6b00e3ff 100644 --- a/src/SharpCompress/Common/Arc/ArcFilePart.Async.cs +++ b/src/SharpCompress/Common/Arc/ArcFilePart.Async.cs @@ -31,11 +31,9 @@ public partial class ArcFilePart compressedStream = new RunLength90Stream(_stream, (int)Header.CompressedSize); break; case CompressionType.Squeezed: - compressedStream = await SqueezeStream.CreateAsync( - _stream, - (int)Header.CompressedSize, - cancellationToken - ); + compressedStream = await SqueezeStream + .CreateAsync(_stream, (int)Header.CompressedSize, cancellationToken) + .ConfigureAwait(false); break; case CompressionType.Crunched: if (Header.OriginalSize > 128 * 1024) diff --git a/src/SharpCompress/Common/ArchiveType.cs b/src/SharpCompress/Common/ArchiveType.cs index 5952f645..ae95af76 100644 --- a/src/SharpCompress/Common/ArchiveType.cs +++ b/src/SharpCompress/Common/ArchiveType.cs @@ -10,4 +10,5 @@ public enum ArchiveType Arc, Arj, Ace, + Lzw, } diff --git a/src/SharpCompress/Common/Arj/ArjEntry.cs b/src/SharpCompress/Common/Arj/ArjEntry.cs index fe4a525b..cf5e1c9f 100644 --- a/src/SharpCompress/Common/Arj/ArjEntry.cs +++ b/src/SharpCompress/Common/Arj/ArjEntry.cs @@ -5,6 +5,7 @@ using System.Text; using System.Threading.Tasks; using SharpCompress.Common.Arc; using SharpCompress.Common.Arj.Headers; +using SharpCompress.Common.Options; namespace SharpCompress.Common.Arj; @@ -12,7 +13,8 @@ public class ArjEntry : Entry { private readonly ArjFilePart _filePart; - internal ArjEntry(ArjFilePart filePart) + internal ArjEntry(ArjFilePart filePart, IReaderOptions readerOptions) + : base(readerOptions) { _filePart = filePart; } @@ -41,9 +43,9 @@ public class ArjEntry : Entry public override DateTime? LastModifiedTime => _filePart.Header.DateTimeModified.DateTime; - public override DateTime? CreatedTime => _filePart.Header.DateTimeCreated.DateTime; + public override DateTime? CreatedTime => _filePart.Header.DateTimeCreated?.DateTime; - public override DateTime? LastAccessedTime => _filePart.Header.DateTimeAccessed.DateTime; + public override DateTime? LastAccessedTime => _filePart.Header.DateTimeAccessed?.DateTime; public override DateTime? ArchivedTime => null; diff --git a/src/SharpCompress/Common/Arj/Headers/ArjHeader.Async.cs b/src/SharpCompress/Common/Arj/Headers/ArjHeader.Async.cs index 8f455444..686d15f5 100644 --- a/src/SharpCompress/Common/Arj/Headers/ArjHeader.Async.cs +++ b/src/SharpCompress/Common/Arj/Headers/ArjHeader.Async.cs @@ -21,7 +21,7 @@ public abstract partial class ArjHeader { // check for magic bytes var magic = new byte[2]; - if (await stream.ReadAsync(magic, 0, 2, cancellationToken) != 2) + if (await stream.ReadAsync(magic, 0, 2, cancellationToken).ConfigureAwait(false) != 2) { return Array.Empty(); } @@ -33,7 +33,7 @@ public abstract partial class ArjHeader // read header_size byte[] headerBytes = new byte[2]; - await stream.ReadAsync(headerBytes, 0, 2, cancellationToken); + await stream.ReadAsync(headerBytes, 0, 2, cancellationToken).ConfigureAwait(false); var headerSize = (ushort)(headerBytes[0] | headerBytes[1] << 8); if (headerSize < 1) { @@ -41,14 +41,16 @@ public abstract partial class ArjHeader } var body = new byte[headerSize]; - var read = await stream.ReadAsync(body, 0, headerSize, cancellationToken); + var read = await stream + .ReadAsync(body, 0, headerSize, cancellationToken) + .ConfigureAwait(false); if (read < headerSize) { return Array.Empty(); } byte[] crc = new byte[4]; - read = await stream.ReadAsync(crc, 0, 4, cancellationToken); + await stream.ReadFullyAsync(crc, 0, 4, cancellationToken).ConfigureAwait(false); var checksum = Crc32Stream.Compute(body); // Compute the hash value if (checksum != BitConverter.ToUInt32(crc, 0)) @@ -68,7 +70,9 @@ public abstract partial class ArjHeader while (true) { - int bytesRead = await reader.ReadAsync(buffer, 0, 2, cancellationToken); + int bytesRead = await reader + .ReadAsync(buffer, 0, 2, cancellationToken) + .ConfigureAwait(false); if (bytesRead < 2) { throw new EndOfStreamException( @@ -83,7 +87,9 @@ public abstract partial class ArjHeader } byte[] header = new byte[extHeaderSize]; - bytesRead = await reader.ReadAsync(header, 0, extHeaderSize, cancellationToken); + bytesRead = await reader + .ReadAsync(header, 0, extHeaderSize, cancellationToken) + .ConfigureAwait(false); if (bytesRead < extHeaderSize) { throw new EndOfStreamException( @@ -92,7 +98,9 @@ public abstract partial class ArjHeader } byte[] crcextended = new byte[4]; - bytesRead = await reader.ReadAsync(crcextended, 0, 4, cancellationToken); + bytesRead = await reader + .ReadAsync(crcextended, 0, 4, cancellationToken) + .ConfigureAwait(false); if (bytesRead < 4) { throw new EndOfStreamException( @@ -122,7 +130,7 @@ public abstract partial class ArjHeader ) { var bytes = new byte[2]; - if (await stream.ReadAsync(bytes, 0, 2, cancellationToken) != 2) + if (await stream.ReadAsync(bytes, 0, 2, cancellationToken).ConfigureAwait(false) != 2) { return false; } diff --git a/src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.Async.cs b/src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.Async.cs index eda70ae1..c5558325 100644 --- a/src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.Async.cs +++ b/src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.Async.cs @@ -11,10 +11,10 @@ public partial class ArjLocalHeader CancellationToken cancellationToken = default ) { - var body = await ReadHeaderAsync(stream, cancellationToken); + var body = await ReadHeaderAsync(stream, cancellationToken).ConfigureAwait(false); if (body.Length > 0) { - await ReadExtendedHeadersAsync(stream, cancellationToken); + await ReadExtendedHeadersAsync(stream, cancellationToken).ConfigureAwait(false); var header = LoadFrom(body); header.DataStartPosition = stream.Position; return header; diff --git a/src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.cs b/src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.cs index 4ace325d..d37121a9 100644 --- a/src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.cs +++ b/src/SharpCompress/Common/Arj/Headers/ArjLocalHeader.cs @@ -27,8 +27,8 @@ public partial class ArjLocalHeader : ArjHeader public byte FirstChapter { get; set; } public byte LastChapter { get; set; } public long ExtendedFilePosition { get; set; } - public DosDateTime DateTimeAccessed { get; set; } = new DosDateTime(0); - public DosDateTime DateTimeCreated { get; set; } = new DosDateTime(0); + public DosDateTime? DateTimeAccessed { get; set; } + public DosDateTime? DateTimeCreated { get; set; } public long OriginalSizeEvenForVolumes { get; set; } public string Name { get; set; } = string.Empty; public string Comment { get; set; } = string.Empty; @@ -119,11 +119,9 @@ public partial class ArjLocalHeader : ArjHeader if (headerSize >= R9HdrSize) { rawTimestamp = ReadInt32(); - DateTimeAccessed = - rawTimestamp != 0 ? new DosDateTime(rawTimestamp) : new DosDateTime(0); + DateTimeAccessed = rawTimestamp != 0 ? new DosDateTime(rawTimestamp) : null; rawTimestamp = ReadInt32(); - DateTimeCreated = - rawTimestamp != 0 ? new DosDateTime(rawTimestamp) : new DosDateTime(0); + DateTimeCreated = rawTimestamp != 0 ? new DosDateTime(rawTimestamp) : null; OriginalSizeEvenForVolumes = ReadInt32(); } } diff --git a/src/SharpCompress/Common/Arj/Headers/ArjMainHeader.Async.cs b/src/SharpCompress/Common/Arj/Headers/ArjMainHeader.Async.cs index cc0592f8..f337b0d3 100644 --- a/src/SharpCompress/Common/Arj/Headers/ArjMainHeader.Async.cs +++ b/src/SharpCompress/Common/Arj/Headers/ArjMainHeader.Async.cs @@ -11,8 +11,8 @@ public partial class ArjMainHeader CancellationToken cancellationToken = default ) { - var body = await ReadHeaderAsync(stream, cancellationToken); - await ReadExtendedHeadersAsync(stream, cancellationToken); + var body = await ReadHeaderAsync(stream, cancellationToken).ConfigureAwait(false); + await ReadExtendedHeadersAsync(stream, cancellationToken).ConfigureAwait(false); return LoadFrom(body); } } diff --git a/src/SharpCompress/Common/Entry.cs b/src/SharpCompress/Common/Entry.cs index 6209b3de..1942ba46 100644 --- a/src/SharpCompress/Common/Entry.cs +++ b/src/SharpCompress/Common/Entry.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using SharpCompress.Common.Options; namespace SharpCompress.Common; @@ -87,4 +88,14 @@ public abstract class Entry : IEntry /// Entry file attribute. /// public virtual int? Attrib => throw new NotImplementedException(); + + /// + /// The options used when opening this entry's source (reader or archive). + /// + public IReaderOptions Options { get; protected set; } + + protected Entry(IReaderOptions readerOptions) + { + Options = readerOptions; + } } diff --git a/src/SharpCompress/Common/ExtractionMethods.Async.cs b/src/SharpCompress/Common/ExtractionMethods.Async.cs index f2c3ac13..75fede3d 100644 --- a/src/SharpCompress/Common/ExtractionMethods.Async.cs +++ b/src/SharpCompress/Common/ExtractionMethods.Async.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Threading; using System.Threading.Tasks; +using SharpCompress.Readers; namespace SharpCompress.Common; @@ -10,8 +11,7 @@ internal static partial class ExtractionMethods public static async ValueTask WriteEntryToDirectoryAsync( IEntry entry, string destinationDirectory, - ExtractionOptions? options, - Func writeAsync, + Func writeAsync, CancellationToken cancellationToken = default ) { @@ -34,11 +34,9 @@ internal static partial class ExtractionMethods ); } - options ??= new ExtractionOptions() { Overwrite = true }; - var file = Path.GetFileName(entry.Key.NotNull("Entry Key is null")).NotNull("File is null"); file = Utility.ReplaceInvalidFileNameChars(file); - if (options.ExtractFullPath) + if (entry.Options.ExtractFullPath) { var folder = Path.GetDirectoryName(entry.Key.NotNull("Entry Key is null")) .NotNull("Directory is null"); @@ -72,9 +70,9 @@ internal static partial class ExtractionMethods "Entry is trying to write a file outside of the destination directory." ); } - await writeAsync(destinationFileName, options, cancellationToken).ConfigureAwait(false); + await writeAsync(destinationFileName, cancellationToken).ConfigureAwait(false); } - else if (options.ExtractFullPath && !Directory.Exists(destinationFileName)) + else if (entry.Options.ExtractFullPath && !Directory.Exists(destinationFileName)) { Directory.CreateDirectory(destinationFileName); } @@ -83,36 +81,34 @@ internal static partial class ExtractionMethods public static async ValueTask WriteEntryToFileAsync( IEntry entry, string destinationFileName, - ExtractionOptions? options, Func openAndWriteAsync, CancellationToken cancellationToken = default ) { if (entry.LinkTarget != null) { - if (options?.SymbolicLinkHandler is not null) + if (entry.Options.SymbolicLinkHandler is not null) { - options.SymbolicLinkHandler(destinationFileName, entry.LinkTarget); + entry.Options.SymbolicLinkHandler(destinationFileName, entry.LinkTarget); } else { - ExtractionOptions.DefaultSymbolicLinkHandler(destinationFileName, entry.LinkTarget); + ReaderOptions.DefaultSymbolicLinkHandler(destinationFileName, entry.LinkTarget); } return; } else { var fm = FileMode.Create; - options ??= new ExtractionOptions() { Overwrite = true }; - if (!options.Overwrite) + if (!entry.Options.Overwrite) { fm = FileMode.CreateNew; } await openAndWriteAsync(destinationFileName, fm, cancellationToken) .ConfigureAwait(false); - entry.PreserveExtractionOptions(destinationFileName, options); + entry.PreserveExtractionOptions(destinationFileName); } } } diff --git a/src/SharpCompress/Common/ExtractionMethods.cs b/src/SharpCompress/Common/ExtractionMethods.cs index ab6e986b..6c6ced80 100644 --- a/src/SharpCompress/Common/ExtractionMethods.cs +++ b/src/SharpCompress/Common/ExtractionMethods.cs @@ -3,6 +3,7 @@ using System.IO; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; +using SharpCompress.Readers; namespace SharpCompress.Common; @@ -23,8 +24,7 @@ internal static partial class ExtractionMethods public static void WriteEntryToDirectory( IEntry entry, string destinationDirectory, - ExtractionOptions? options, - Action write + Action write ) { string destinationFileName; @@ -46,11 +46,9 @@ internal static partial class ExtractionMethods ); } - options ??= new ExtractionOptions() { Overwrite = true }; - var file = Path.GetFileName(entry.Key.NotNull("Entry Key is null")).NotNull("File is null"); file = Utility.ReplaceInvalidFileNameChars(file); - if (options.ExtractFullPath) + if (entry.Options.ExtractFullPath) { var folder = Path.GetDirectoryName(entry.Key.NotNull("Entry Key is null")) .NotNull("Directory is null"); @@ -84,9 +82,9 @@ internal static partial class ExtractionMethods "Entry is trying to write a file outside of the destination directory." ); } - write(destinationFileName, options); + write(destinationFileName); } - else if (options.ExtractFullPath && !Directory.Exists(destinationFileName)) + else if (entry.Options.ExtractFullPath && !Directory.Exists(destinationFileName)) { Directory.CreateDirectory(destinationFileName); } @@ -95,34 +93,32 @@ internal static partial class ExtractionMethods public static void WriteEntryToFile( IEntry entry, string destinationFileName, - ExtractionOptions? options, Action openAndWrite ) { if (entry.LinkTarget != null) { - if (options?.SymbolicLinkHandler is not null) + if (entry.Options.SymbolicLinkHandler is not null) { - options.SymbolicLinkHandler(destinationFileName, entry.LinkTarget); + entry.Options.SymbolicLinkHandler(destinationFileName, entry.LinkTarget); } else { - ExtractionOptions.DefaultSymbolicLinkHandler(destinationFileName, entry.LinkTarget); + ReaderOptions.DefaultSymbolicLinkHandler(destinationFileName, entry.LinkTarget); } return; } else { var fm = FileMode.Create; - options ??= new ExtractionOptions() { Overwrite = true }; - if (!options.Overwrite) + if (!entry.Options.Overwrite) { fm = FileMode.CreateNew; } openAndWrite(destinationFileName, fm); - entry.PreserveExtractionOptions(destinationFileName, options); + entry.PreserveExtractionOptions(destinationFileName); } } } diff --git a/src/SharpCompress/Common/ExtractionOptions.cs b/src/SharpCompress/Common/ExtractionOptions.cs deleted file mode 100644 index 269009cd..00000000 --- a/src/SharpCompress/Common/ExtractionOptions.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; - -namespace SharpCompress.Common; - -/// -/// Options for configuring extraction behavior when extracting archive entries. -/// -/// -/// This class is immutable. Use the with expression to create modified copies: -/// -/// var options = new ExtractionOptions { Overwrite = false }; -/// options = options with { PreserveFileTime = true }; -/// -/// -public sealed record ExtractionOptions -{ - /// - /// Overwrite target if it exists. - /// Breaking change: Default changed from false to true in version 0.40.0. - /// - public bool Overwrite { get; init; } = true; - - /// - /// Extract with internal directory structure. - /// Breaking change: Default changed from false to true in version 0.40.0. - /// - public bool ExtractFullPath { get; init; } = true; - - /// - /// Preserve file time. - /// Breaking change: Default changed from false to true in version 0.40.0. - /// - public bool PreserveFileTime { get; init; } = true; - - /// - /// Preserve windows file attributes. - /// - public bool PreserveAttributes { get; init; } - - /// - /// Delegate for writing symbolic links to disk. - /// The first parameter is the source path (where the symlink is created). - /// The second parameter is the target path (what the symlink refers to). - /// - /// - /// Breaking change: Changed from field to init-only property in version 0.40.0. - /// The default handler logs a warning message. - /// - public Action? SymbolicLinkHandler { get; init; } - - /// - /// Creates a new ExtractionOptions instance with default values. - /// - public ExtractionOptions() { } - - /// - /// Creates a new ExtractionOptions instance with the specified overwrite behavior. - /// - /// Whether to overwrite existing files. - public ExtractionOptions(bool overwrite) - { - Overwrite = overwrite; - } - - /// - /// Creates a new ExtractionOptions instance with the specified extraction path and overwrite behavior. - /// - /// Whether to preserve directory structure. - /// Whether to overwrite existing files. - public ExtractionOptions(bool extractFullPath, bool overwrite) - { - ExtractFullPath = extractFullPath; - Overwrite = overwrite; - } - - /// - /// Creates a new ExtractionOptions instance with the specified extraction path, overwrite behavior, and file time preservation. - /// - /// Whether to preserve directory structure. - /// Whether to overwrite existing files. - /// Whether to preserve file modification times. - public ExtractionOptions(bool extractFullPath, bool overwrite, bool preserveFileTime) - { - ExtractFullPath = extractFullPath; - Overwrite = overwrite; - PreserveFileTime = preserveFileTime; - } - - /// - /// Gets an ExtractionOptions instance configured for safe extraction (no overwrite). - /// - public static ExtractionOptions SafeExtract => new(overwrite: false); - - /// - /// Gets an ExtractionOptions instance configured for flat extraction (no directory structure). - /// - public static ExtractionOptions FlatExtract => new(extractFullPath: false, overwrite: true); - - /// - /// Default symbolic link handler that logs a warning message. - /// - public static void DefaultSymbolicLinkHandler(string sourcePath, string targetPath) - { - Console.WriteLine( - $"Could not write symlink {sourcePath} -> {targetPath}, for more information please see https://github.com/dotnet/runtime/issues/24271" - ); - } -} diff --git a/src/SharpCompress/Common/GZip/GZipEntry.Async.cs b/src/SharpCompress/Common/GZip/GZipEntry.Async.cs index 00e7b9e0..bdd56dd2 100644 --- a/src/SharpCompress/Common/GZip/GZipEntry.Async.cs +++ b/src/SharpCompress/Common/GZip/GZipEntry.Async.cs @@ -12,11 +12,8 @@ public partial class GZipEntry ) { yield return new GZipEntry( - await GZipFilePart.CreateAsync( - stream, - options.ArchiveEncoding, - options.CompressionProviders - ) + await GZipFilePart.CreateAsync(stream, options.ArchiveEncoding).ConfigureAwait(false), + options ); } } diff --git a/src/SharpCompress/Common/GZip/GZipEntry.cs b/src/SharpCompress/Common/GZip/GZipEntry.cs index ef145617..f3d41c45 100644 --- a/src/SharpCompress/Common/GZip/GZipEntry.cs +++ b/src/SharpCompress/Common/GZip/GZipEntry.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using SharpCompress.Common.Options; using SharpCompress.Readers; namespace SharpCompress.Common.GZip; @@ -9,7 +10,11 @@ public partial class GZipEntry : Entry { private readonly GZipFilePart? _filePart; - internal GZipEntry(GZipFilePart? filePart) => _filePart = filePart; + internal GZipEntry(GZipFilePart? filePart, IReaderOptions readerOptions) + : base(readerOptions) + { + _filePart = filePart; + } public override CompressionType CompressionType => CompressionType.GZip; @@ -41,9 +46,7 @@ public partial class GZipEntry : Entry internal static IEnumerable GetEntries(Stream stream, ReaderOptions options) { - yield return new GZipEntry( - GZipFilePart.Create(stream, options.ArchiveEncoding, options.CompressionProviders) - ); + yield return new GZipEntry(GZipFilePart.Create(stream, options.ArchiveEncoding), options); } // Async methods moved to GZipEntry.Async.cs diff --git a/src/SharpCompress/Common/GZip/GZipFilePart.Async.cs b/src/SharpCompress/Common/GZip/GZipFilePart.Async.cs index 2c5ab7b8..b65f81fa 100644 --- a/src/SharpCompress/Common/GZip/GZipFilePart.Async.cs +++ b/src/SharpCompress/Common/GZip/GZipFilePart.Async.cs @@ -30,12 +30,12 @@ internal sealed partial class GZipFilePart { var part = new GZipFilePart(stream, archiveEncoding, compressionProviders); - await part.ReadAndValidateGzipHeaderAsync(cancellationToken); + await part.ReadAndValidateGzipHeaderAsync(cancellationToken).ConfigureAwait(false); if (stream.CanSeek) { var position = stream.Position; stream.Position = stream.Length - 8; - await part.ReadTrailerAsync(cancellationToken); + await part.ReadTrailerAsync(cancellationToken).ConfigureAwait(false); stream.Position = position; part.EntryStartPosition = position; } @@ -52,7 +52,7 @@ internal sealed partial class GZipFilePart { // Read and potentially verify the GZIP trailer: CRC32 and size mod 2^32 var trailer = new byte[8]; - _ = await _stream.ReadFullyAsync(trailer, 0, 8, cancellationToken); + _ = await _stream.ReadFullyAsync(trailer, 0, 8, cancellationToken).ConfigureAwait(false); Crc = BinaryPrimitives.ReadUInt32LittleEndian(trailer); UncompressedSize = BinaryPrimitives.ReadUInt32LittleEndian(trailer.AsSpan().Slice(4)); @@ -64,7 +64,7 @@ internal sealed partial class GZipFilePart { // read the header on the first read var header = new byte[10]; - var n = await _stream.ReadAsync(header, 0, 10, cancellationToken); + var n = await _stream.ReadAsync(header, 0, 10, cancellationToken).ConfigureAwait(false); // workitem 8501: handle edge case (decompress empty stream) if (n == 0) @@ -88,28 +88,29 @@ internal sealed partial class GZipFilePart { // read and discard extra field var lengthField = new byte[2]; - _ = await _stream.ReadAsync(lengthField, 0, 2, cancellationToken); + _ = await _stream.ReadAsync(lengthField, 0, 2, cancellationToken).ConfigureAwait(false); var extraLength = (short)(lengthField[0] + (lengthField[1] * 256)); var extra = new byte[extraLength]; - if (!await _stream.ReadFullyAsync(extra, cancellationToken)) + if (!await _stream.ReadFullyAsync(extra, cancellationToken).ConfigureAwait(false)) { throw new ZlibException("Unexpected end-of-file reading GZIP header."); } } if ((header[3] & 0x08) == 0x08) { - _name = await ReadZeroTerminatedStringAsync(_stream, cancellationToken); + _name = await ReadZeroTerminatedStringAsync(_stream, cancellationToken) + .ConfigureAwait(false); } if ((header[3] & 0x10) == 0x010) { - await ReadZeroTerminatedStringAsync(_stream, cancellationToken); + await ReadZeroTerminatedStringAsync(_stream, cancellationToken).ConfigureAwait(false); } if ((header[3] & 0x02) == 0x02) { var buf = new byte[1]; - _ = await _stream.ReadAsync(buf, 0, 1, cancellationToken); // CRC16, ignore + _ = await _stream.ReadAsync(buf, 0, 1, cancellationToken).ConfigureAwait(false); // CRC16, ignore } } @@ -124,7 +125,7 @@ internal sealed partial class GZipFilePart do { // workitem 7740 - var n = await stream.ReadAsync(buf1, 0, 1, cancellationToken); + var n = await stream.ReadAsync(buf1, 0, 1, cancellationToken).ConfigureAwait(false); if (n != 1) { throw new ZlibException("Unexpected EOF reading GZIP header."); diff --git a/src/SharpCompress/Common/IEntry.Extensions.cs b/src/SharpCompress/Common/IEntry.Extensions.cs index 7e9b79a3..d73f8a68 100644 --- a/src/SharpCompress/Common/IEntry.Extensions.cs +++ b/src/SharpCompress/Common/IEntry.Extensions.cs @@ -4,13 +4,9 @@ namespace SharpCompress.Common; internal static class EntryExtensions { - internal static void PreserveExtractionOptions( - this IEntry entry, - string destinationFileName, - ExtractionOptions options - ) + internal static void PreserveExtractionOptions(this IEntry entry, string destinationFileName) { - if (options.PreserveFileTime || options.PreserveAttributes) + if (entry.Options.PreserveFileTime || entry.Options.PreserveAttributes) { var nf = new FileInfo(destinationFileName); if (!nf.Exists) @@ -19,7 +15,7 @@ internal static class EntryExtensions } // update file time to original packed time - if (options.PreserveFileTime) + if (entry.Options.PreserveFileTime) { if (entry.CreatedTime.HasValue) { @@ -37,7 +33,7 @@ internal static class EntryExtensions } } - if (options.PreserveAttributes) + if (entry.Options.PreserveAttributes) { if (entry.Attrib.HasValue) { diff --git a/src/SharpCompress/Common/IEntry.cs b/src/SharpCompress/Common/IEntry.cs index 56e1db81..741b4825 100644 --- a/src/SharpCompress/Common/IEntry.cs +++ b/src/SharpCompress/Common/IEntry.cs @@ -1,4 +1,5 @@ using System; +using SharpCompress.Common.Options; namespace SharpCompress.Common; @@ -21,4 +22,9 @@ public interface IEntry DateTime? LastModifiedTime { get; } long Size { get; } int? Attrib { get; } + + /// + /// The options used when opening this entry's source (reader or archive). + /// + IReaderOptions Options { get; } } diff --git a/src/SharpCompress/Common/Lzw/LzwEntry.Async.cs b/src/SharpCompress/Common/Lzw/LzwEntry.Async.cs new file mode 100644 index 00000000..a1982d1b --- /dev/null +++ b/src/SharpCompress/Common/Lzw/LzwEntry.Async.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using SharpCompress.Readers; + +namespace SharpCompress.Common.Lzw; + +public partial class LzwEntry +{ + internal static async IAsyncEnumerable GetEntriesAsync( + Stream stream, + ReaderOptions options, + [EnumeratorCancellation] CancellationToken cancellationToken = default + ) + { + yield return new LzwEntry( + await LzwFilePart.CreateAsync(stream, options.ArchiveEncoding, cancellationToken), + options + ); + } +} diff --git a/src/SharpCompress/Common/Lzw/LzwEntry.cs b/src/SharpCompress/Common/Lzw/LzwEntry.cs new file mode 100644 index 00000000..92490428 --- /dev/null +++ b/src/SharpCompress/Common/Lzw/LzwEntry.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.IO; +using SharpCompress.Common.Options; +using SharpCompress.Readers; + +namespace SharpCompress.Common.Lzw; + +public partial class LzwEntry : Entry +{ + private readonly LzwFilePart? _filePart; + + internal LzwEntry(LzwFilePart? filePart, IReaderOptions readerOptions) + : base(readerOptions) + { + _filePart = filePart; + } + + public override CompressionType CompressionType => CompressionType.Lzw; + + public override long Crc => 0; + + public override string? Key => _filePart?.FilePartName; + + public override string? LinkTarget => null; + + public override long CompressedSize => 0; + + public override long Size => 0; + + public override DateTime? LastModifiedTime => null; + + public override DateTime? CreatedTime => null; + + public override DateTime? LastAccessedTime => null; + + public override DateTime? ArchivedTime => null; + + public override bool IsEncrypted => false; + + public override bool IsDirectory => false; + + public override bool IsSplitAfter => false; + + internal override IEnumerable Parts => _filePart.Empty(); + + internal static IEnumerable GetEntries(Stream stream, ReaderOptions options) + { + yield return new LzwEntry(LzwFilePart.Create(stream, options.ArchiveEncoding), options); + } + + // Async methods moved to LzwEntry.Async.cs +} diff --git a/src/SharpCompress/Common/Lzw/LzwFilePart.Async.cs b/src/SharpCompress/Common/Lzw/LzwFilePart.Async.cs new file mode 100644 index 00000000..8b6a5a32 --- /dev/null +++ b/src/SharpCompress/Common/Lzw/LzwFilePart.Async.cs @@ -0,0 +1,23 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpCompress.Common.Lzw; + +internal sealed partial class LzwFilePart +{ + internal static async ValueTask CreateAsync( + Stream stream, + IArchiveEncoding archiveEncoding, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + var part = new LzwFilePart(stream, archiveEncoding); + + // For non-seekable streams, we can't track position, so use 0 since the stream will be + // read sequentially from its current position. + part.EntryStartPosition = stream.CanSeek ? stream.Position : 0; + return part; + } +} diff --git a/src/SharpCompress/Common/Lzw/LzwFilePart.cs b/src/SharpCompress/Common/Lzw/LzwFilePart.cs new file mode 100644 index 00000000..74d682ea --- /dev/null +++ b/src/SharpCompress/Common/Lzw/LzwFilePart.cs @@ -0,0 +1,60 @@ +using System.IO; +using SharpCompress.Compressors.Lzw; + +namespace SharpCompress.Common.Lzw; + +internal sealed partial class LzwFilePart : FilePart +{ + private readonly Stream _stream; + private readonly string? _name; + + internal static LzwFilePart Create(Stream stream, IArchiveEncoding archiveEncoding) + { + var part = new LzwFilePart(stream, archiveEncoding); + + // For non-seekable streams, we can't track position, so use 0 since the stream will be + // read sequentially from its current position. + part.EntryStartPosition = stream.CanSeek ? stream.Position : 0; + return part; + } + + private LzwFilePart(Stream stream, IArchiveEncoding archiveEncoding) + : base(archiveEncoding) + { + _stream = stream; + _name = DeriveFileName(stream); + } + + internal long EntryStartPosition { get; private set; } + + internal override string? FilePartName => _name; + + internal override Stream GetCompressedStream() => + new LzwStream(_stream) { IsStreamOwner = false }; + + internal override Stream GetRawStream() => _stream; + + private static string? DeriveFileName(Stream stream) + { + // Unwrap SharpCompressStream to get to the underlying FileStream + var unwrappedStream = stream; + if (stream is SharpCompress.IO.IStreamStack streamStack) + { + unwrappedStream = streamStack.BaseStream(); + } + + // Try to derive filename from FileStream + if (unwrappedStream is FileStream fileStream && !string.IsNullOrEmpty(fileStream.Name)) + { + var fileName = Path.GetFileName(fileStream.Name); + // Strip .Z extension if present + if (fileName.EndsWith(".Z", System.StringComparison.OrdinalIgnoreCase)) + { + return fileName.Substring(0, fileName.Length - 2); + } + return fileName; + } + // Default name for non-file streams + return "data"; + } +} diff --git a/src/SharpCompress/Common/Lzw/LzwVolume.cs b/src/SharpCompress/Common/Lzw/LzwVolume.cs new file mode 100644 index 00000000..7ded1d26 --- /dev/null +++ b/src/SharpCompress/Common/Lzw/LzwVolume.cs @@ -0,0 +1,17 @@ +using System.IO; +using SharpCompress.Readers; + +namespace SharpCompress.Common.Lzw; + +public class LzwVolume : Volume +{ + public LzwVolume(Stream stream, ReaderOptions? options, int index) + : base(stream, options, index) { } + + public LzwVolume(FileInfo fileInfo, ReaderOptions options) + : base(fileInfo.OpenRead(), options with { LeaveStreamOpen = false }) { } + + public override bool IsFirstVolume => true; + + public override bool IsMultiVolume => false; +} diff --git a/src/SharpCompress/Common/Options/IExtractionOptions.cs b/src/SharpCompress/Common/Options/IExtractionOptions.cs new file mode 100644 index 00000000..3e88d247 --- /dev/null +++ b/src/SharpCompress/Common/Options/IExtractionOptions.cs @@ -0,0 +1,39 @@ +using System; + +namespace SharpCompress.Common.Options; + +/// +/// Options for configuring extraction behavior when extracting archive entries to the filesystem. +/// +public interface IExtractionOptions +{ + /// + /// Overwrite target if it exists. + /// Breaking change: Default changed from false to true in version 0.40.0. + /// + bool Overwrite { get; init; } + + /// + /// Extract with internal directory structure. + /// Breaking change: Default changed from false to true in version 0.40.0. + /// + bool ExtractFullPath { get; init; } + + /// + /// Preserve file time. + /// Breaking change: Default changed from false to true in version 0.40.0. + /// + bool PreserveFileTime { get; init; } + + /// + /// Preserve windows file attributes. + /// + bool PreserveAttributes { get; init; } + + /// + /// Delegate for writing symbolic links to disk. + /// The first parameter is the source path (where the symlink is created). + /// The second parameter is the target path (what the symlink refers to). + /// + Action? SymbolicLinkHandler { get; init; } +} diff --git a/src/SharpCompress/Common/Options/IReaderOptions.cs b/src/SharpCompress/Common/Options/IReaderOptions.cs index 4d0f5acd..4ef9ab77 100644 --- a/src/SharpCompress/Common/Options/IReaderOptions.cs +++ b/src/SharpCompress/Common/Options/IReaderOptions.cs @@ -2,10 +2,11 @@ using SharpCompress.Compressors; namespace SharpCompress.Common.Options; -/// -/// Options for configuring reader behavior when opening archives. -/// -public interface IReaderOptions : IStreamOptions, IEncodingOptions, IProgressOptions +public interface IReaderOptions + : IStreamOptions, + IEncodingOptions, + IProgressOptions, + IExtractionOptions { /// /// Look for RarArchive (Check for self-extracting archives or cases where RarArchive isn't at the start of the file) diff --git a/src/SharpCompress/Common/Rar/AsyncRarCryptoBinaryReader.cs b/src/SharpCompress/Common/Rar/AsyncRarCryptoBinaryReader.cs index d71f35a5..c3f5d005 100644 --- a/src/SharpCompress/Common/Rar/AsyncRarCryptoBinaryReader.cs +++ b/src/SharpCompress/Common/Rar/AsyncRarCryptoBinaryReader.cs @@ -26,7 +26,9 @@ internal sealed class AsyncRarCryptoBinaryReader : AsyncRarCrcBinaryReader var binary = new AsyncRarCryptoBinaryReader(stream); if (salt == null) { - salt = await binary.ReadBytesAsyncBase(EncryptionConstV5.SIZE_SALT30); + salt = await binary + .ReadBytesAsyncBase(EncryptionConstV5.SIZE_SALT30) + .ConfigureAwait(false); binary._readCount += EncryptionConstV5.SIZE_SALT30; } binary._rijndael = new BlockTransformer(cryptKey.Transformer(salt)); diff --git a/src/SharpCompress/Common/Rar/Headers/ArchiveCryptHeader.Async.cs b/src/SharpCompress/Common/Rar/Headers/ArchiveCryptHeader.Async.cs index ef3d4b02..031fe6bd 100644 --- a/src/SharpCompress/Common/Rar/Headers/ArchiveCryptHeader.Async.cs +++ b/src/SharpCompress/Common/Rar/Headers/ArchiveCryptHeader.Async.cs @@ -27,6 +27,6 @@ internal sealed partial class ArchiveCryptHeader CancellationToken cancellationToken = default ) { - CryptInfo = await Rar5CryptoInfo.CreateAsync(reader, false); + CryptInfo = await Rar5CryptoInfo.CreateAsync(reader, false).ConfigureAwait(false); } } diff --git a/src/SharpCompress/Common/Rar/Headers/FileHeader.Async.cs b/src/SharpCompress/Common/Rar/Headers/FileHeader.Async.cs index a43ad55b..a80d121e 100644 --- a/src/SharpCompress/Common/Rar/Headers/FileHeader.Async.cs +++ b/src/SharpCompress/Common/Rar/Headers/FileHeader.Async.cs @@ -329,7 +329,9 @@ internal partial class FileHeader if (HasFlag(FileFlagsV4.SALT)) { - R4Salt = await reader.ReadBytesAsync(EncryptionConstV5.SIZE_SALT30, cancellationToken); + R4Salt = await reader + .ReadBytesAsync(EncryptionConstV5.SIZE_SALT30, cancellationToken) + .ConfigureAwait(false); } if (HasFlag(FileFlagsV4.EXT_TIME)) { diff --git a/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.Async.cs b/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.Async.cs index 38b88b25..d020add9 100644 --- a/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.Async.cs +++ b/src/SharpCompress/Common/Rar/Headers/RarHeaderFactory.Async.cs @@ -64,19 +64,19 @@ public partial class RarHeaderFactory if (_isRar5 && _cryptInfo != null) { - await _cryptInfo.ReadInitVAsync(new AsyncMarkingBinaryReader(stream)); + await _cryptInfo + .ReadInitVAsync(new AsyncMarkingBinaryReader(stream)) + .ConfigureAwait(false); var _headerKey = new CryptKey5(Options.Password!, _cryptInfo); - reader = await AsyncRarCryptoBinaryReader.Create( - stream, - _headerKey, - _cryptInfo.Salt - ); + reader = await AsyncRarCryptoBinaryReader + .Create(stream, _headerKey, _cryptInfo.Salt) + .ConfigureAwait(false); } else { var key = new CryptKey3(Options.Password); - reader = await AsyncRarCryptoBinaryReader.Create(stream, key); + reader = await AsyncRarCryptoBinaryReader.Create(stream, key).ConfigureAwait(false); } } diff --git a/src/SharpCompress/Common/Rar/Rar5CryptoInfo.cs b/src/SharpCompress/Common/Rar/Rar5CryptoInfo.cs index c02ac5c9..f86e4a38 100644 --- a/src/SharpCompress/Common/Rar/Rar5CryptoInfo.cs +++ b/src/SharpCompress/Common/Rar/Rar5CryptoInfo.cs @@ -57,47 +57,46 @@ internal class Rar5CryptoInfo ) { var cryptoInfo = new Rar5CryptoInfo(); - var cryptVersion = await reader.ReadRarVIntUInt32Async( - cancellationToken: CancellationToken.None - ); + var cryptVersion = await reader + .ReadRarVIntUInt32Async(cancellationToken: CancellationToken.None) + .ConfigureAwait(false); if (cryptVersion > EncryptionConstV5.VERSION) { throw new CryptographicException($"Unsupported crypto version of {cryptVersion}"); } - var encryptionFlags = await reader.ReadRarVIntUInt32Async( - cancellationToken: CancellationToken.None - ); + var encryptionFlags = await reader + .ReadRarVIntUInt32Async(cancellationToken: CancellationToken.None) + .ConfigureAwait(false); cryptoInfo.UsePswCheck = FlagUtility.HasFlag( encryptionFlags, EncryptionFlagsV5.CHFL_CRYPT_PSWCHECK ); cryptoInfo.LG2Count = (int) - await reader.ReadRarVIntUInt32Async(cancellationToken: CancellationToken.None); + await reader + .ReadRarVIntUInt32Async(cancellationToken: CancellationToken.None) + .ConfigureAwait(false); if (cryptoInfo.LG2Count > EncryptionConstV5.CRYPT5_KDF_LG2_COUNT_MAX) { throw new CryptographicException($"Unsupported LG2 count of {cryptoInfo.LG2Count}."); } - cryptoInfo.Salt = await reader.ReadBytesAsync( - EncryptionConstV5.SIZE_SALT50, - CancellationToken.None - ); + cryptoInfo.Salt = await reader + .ReadBytesAsync(EncryptionConstV5.SIZE_SALT50, CancellationToken.None) + .ConfigureAwait(false); if (readInitV) { - await cryptoInfo.ReadInitVAsync(reader); + await cryptoInfo.ReadInitVAsync(reader).ConfigureAwait(false); } if (cryptoInfo.UsePswCheck) { - cryptoInfo.PswCheck = await reader.ReadBytesAsync( - EncryptionConstV5.SIZE_PSWCHECK, - CancellationToken.None - ); - var _pswCheckCsm = await reader.ReadBytesAsync( - EncryptionConstV5.SIZE_PSWCHECK_CSUM, - CancellationToken.None - ); + cryptoInfo.PswCheck = await reader + .ReadBytesAsync(EncryptionConstV5.SIZE_PSWCHECK, CancellationToken.None) + .ConfigureAwait(false); + var _pswCheckCsm = await reader + .ReadBytesAsync(EncryptionConstV5.SIZE_PSWCHECK_CSUM, CancellationToken.None) + .ConfigureAwait(false); var sha = SHA256.Create(); cryptoInfo.UsePswCheck = sha.ComputeHash(cryptoInfo.PswCheck) @@ -111,7 +110,9 @@ internal class Rar5CryptoInfo InitV = reader.ReadBytes(EncryptionConstV5.SIZE_INITV); public async ValueTask ReadInitVAsync(AsyncMarkingBinaryReader reader) => - InitV = await reader.ReadBytesAsync(EncryptionConstV5.SIZE_INITV, CancellationToken.None); + InitV = await reader + .ReadBytesAsync(EncryptionConstV5.SIZE_INITV, CancellationToken.None) + .ConfigureAwait(false); public bool UsePswCheck = false; diff --git a/src/SharpCompress/Common/Rar/RarEntry.cs b/src/SharpCompress/Common/Rar/RarEntry.cs index c76c72b6..9c0f7ae5 100644 --- a/src/SharpCompress/Common/Rar/RarEntry.cs +++ b/src/SharpCompress/Common/Rar/RarEntry.cs @@ -1,4 +1,5 @@ using System; +using SharpCompress.Common.Options; using SharpCompress.Common.Rar.Headers; namespace SharpCompress.Common.Rar; @@ -7,6 +8,9 @@ public abstract class RarEntry : Entry { internal abstract FileHeader FileHeader { get; } + protected RarEntry(IReaderOptions readerOptions) + : base(readerOptions) { } + /// /// As the V2017 port isn't complete, add this check to use the legacy Rar code. /// diff --git a/src/SharpCompress/Common/Rar/RarVolume.cs b/src/SharpCompress/Common/Rar/RarVolume.cs index 4e4fa770..2e2286d9 100644 --- a/src/SharpCompress/Common/Rar/RarVolume.cs +++ b/src/SharpCompress/Common/Rar/RarVolume.cs @@ -118,7 +118,8 @@ public abstract class RarVolume : Volume var buffer = new byte[fh.CompressedSize]; await fh .PackedStream.NotNull() - .ReadFullyAsync(buffer, cancellationToken); + .ReadFullyAsync(buffer, cancellationToken) + .ConfigureAwait(false); Comment = Encoding.UTF8.GetString(buffer, 0, buffer.Length - 1); } } @@ -184,7 +185,7 @@ public abstract class RarVolume : Volume public async ValueTask IsSolidArchiveAsync(CancellationToken cancellationToken = default) { - await EnsureArchiveHeaderLoadedAsync(cancellationToken); + await EnsureArchiveHeaderLoadedAsync(cancellationToken).ConfigureAwait(false); return ArchiveHeader?.IsSolid ?? false; } @@ -248,7 +249,7 @@ public abstract class RarVolume : Volume } // we only want to load the archive header to avoid overhead but have to do the nasty thing and reset the stream - await GetVolumeFilePartsAsync(cancellationToken).FirstAsync(); + await GetVolumeFilePartsAsync(cancellationToken).FirstAsync().ConfigureAwait(false); Stream.Position = 0; } } diff --git a/src/SharpCompress/Common/SevenZip/ArchiveReader.Async.cs b/src/SharpCompress/Common/SevenZip/ArchiveReader.Async.cs index d065a4bc..f81c52b9 100644 --- a/src/SharpCompress/Common/SevenZip/ArchiveReader.Async.cs +++ b/src/SharpCompress/Common/SevenZip/ArchiveReader.Async.cs @@ -29,7 +29,7 @@ internal sealed partial class ArchiveReader { // TODO: Check Signature! _header = new byte[0x20]; - await stream.ReadExactAsync(_header, 0, 0x20, cancellationToken); + await stream.ReadExactAsync(_header, 0, 0x20, cancellationToken).ConfigureAwait(false); if ( !lookForHeader @@ -107,7 +107,9 @@ internal sealed partial class ArchiveReader _stream.Seek(nextHeaderOffset, SeekOrigin.Current); var header = new byte[nextHeaderSize]; - await _stream.ReadExactAsync(header, 0, header.Length, cancellationToken); + await _stream + .ReadExactAsync(header, 0, header.Length, cancellationToken) + .ConfigureAwait(false); if (Crc.Finish(Crc.Update(Crc.INIT_CRC, header, 0, header.Length)) != nextHeaderCrc) { diff --git a/src/SharpCompress/Common/SevenZip/SevenZipEntry.cs b/src/SharpCompress/Common/SevenZip/SevenZipEntry.cs index 79df43a0..fb6d23fa 100644 --- a/src/SharpCompress/Common/SevenZip/SevenZipEntry.cs +++ b/src/SharpCompress/Common/SevenZip/SevenZipEntry.cs @@ -1,11 +1,16 @@ -using System; +using System; using System.Collections.Generic; +using SharpCompress.Common.Options; namespace SharpCompress.Common.SevenZip; public class SevenZipEntry : Entry { - internal SevenZipEntry(SevenZipFilePart filePart) => FilePart = filePart; + internal SevenZipEntry(SevenZipFilePart filePart, IReaderOptions readerOptions) + : base(readerOptions) + { + FilePart = filePart; + } internal SevenZipFilePart FilePart { get; } diff --git a/src/SharpCompress/Common/SevenZip/SevenZipFilePart.cs b/src/SharpCompress/Common/SevenZip/SevenZipFilePart.cs index 79155273..65bb7503 100644 --- a/src/SharpCompress/Common/SevenZip/SevenZipFilePart.cs +++ b/src/SharpCompress/Common/SevenZip/SevenZipFilePart.cs @@ -68,12 +68,9 @@ internal class SevenZipFilePart : FilePart { return Stream.Null; } - var folderStream = await _database.GetFolderStreamAsync( - _stream, - Folder!, - _database.PasswordProvider, - cancellationToken - ); + var folderStream = await _database + .GetFolderStreamAsync(_stream, Folder!, _database.PasswordProvider, cancellationToken) + .ConfigureAwait(false); var firstFileIndex = _database._folderStartFileIndex[_database._folders.IndexOf(Folder!)]; var skipCount = Index - firstFileIndex; @@ -84,7 +81,7 @@ internal class SevenZipFilePart : FilePart } if (skipSize > 0) { - await folderStream.SkipAsync(skipSize, cancellationToken); + await folderStream.SkipAsync(skipSize, cancellationToken).ConfigureAwait(false); } return new ReadOnlySubStream(folderStream, Header.Size, leaveOpen: false); } diff --git a/src/SharpCompress/Common/Tar/Headers/TarHeader.Async.cs b/src/SharpCompress/Common/Tar/Headers/TarHeader.Async.cs index f5d796a4..75471aa9 100644 --- a/src/SharpCompress/Common/Tar/Headers/TarHeader.Async.cs +++ b/src/SharpCompress/Common/Tar/Headers/TarHeader.Async.cs @@ -20,10 +20,10 @@ internal sealed partial class TarHeader switch (WriteFormat) { case TarHeaderWriteFormat.GNU_TAR_LONG_LINK: - await WriteGnuTarLongLinkAsync(output, cancellationToken); + await WriteGnuTarLongLinkAsync(output, cancellationToken).ConfigureAwait(false); break; case TarHeaderWriteFormat.USTAR: - await WriteUstarAsync(output, cancellationToken); + await WriteUstarAsync(output, cancellationToken).ConfigureAwait(false); break; default: throw new Exception("This should be impossible..."); @@ -162,13 +162,13 @@ internal sealed partial class TarHeader if (nameByteCount > 100) { - await WriteLongFilenameHeaderAsync(output, cancellationToken); + await WriteLongFilenameHeaderAsync(output, cancellationToken).ConfigureAwait(false); Name = ArchiveEncoding.Decode( ArchiveEncoding.Encode(Name.NotNull("Name is null")), 0, 100 - ArchiveEncoding.GetEncoding().GetMaxByteCount(1) ); - await WriteGnuTarLongLinkAsync(output, cancellationToken); + await WriteGnuTarLongLinkAsync(output, cancellationToken).ConfigureAwait(false); } } @@ -203,7 +203,7 @@ internal sealed partial class TarHeader do { - buffer = await ReadBlockAsync(reader); + buffer = await ReadBlockAsync(reader).ConfigureAwait(false); if (buffer.Length == 0) { @@ -216,12 +216,12 @@ internal sealed partial class TarHeader // to apply to the header that follows them. if (entryType == EntryType.LongName) { - longName = await ReadLongNameAsync(reader, buffer); + longName = await ReadLongNameAsync(reader, buffer).ConfigureAwait(false); continue; } else if (entryType == EntryType.LongLink) { - longLinkName = await ReadLongNameAsync(reader, buffer); + longLinkName = await ReadLongNameAsync(reader, buffer).ConfigureAwait(false); continue; } @@ -282,7 +282,7 @@ internal sealed partial class TarHeader var buffer = ArrayPool.Shared.Rent(BLOCK_SIZE); try { - await reader.ReadBytesAsync(buffer, 0, BLOCK_SIZE); + await reader.ReadBytesAsync(buffer, 0, BLOCK_SIZE).ConfigureAwait(false); if (buffer.Length != 0 && buffer.Length < BLOCK_SIZE) { @@ -313,7 +313,7 @@ internal sealed partial class TarHeader var nameBytes = ArrayPool.Shared.Rent(nameLength); try { - await reader.ReadBytesAsync(nameBytes, 0, nameLength); + await reader.ReadBytesAsync(nameBytes, 0, nameLength).ConfigureAwait(false); var remainingBytesToRead = BLOCK_SIZE - (nameLength % BLOCK_SIZE); // Read the rest of the block and discard the data @@ -322,7 +322,9 @@ internal sealed partial class TarHeader var remainingBytes = ArrayPool.Shared.Rent(remainingBytesToRead); try { - await reader.ReadBytesAsync(remainingBytes, 0, remainingBytesToRead); + await reader + .ReadBytesAsync(remainingBytes, 0, remainingBytesToRead) + .ConfigureAwait(false); } finally { diff --git a/src/SharpCompress/Common/Tar/TarEntry.Async.cs b/src/SharpCompress/Common/Tar/TarEntry.Async.cs index cfa45377..c066da56 100644 --- a/src/SharpCompress/Common/Tar/TarEntry.Async.cs +++ b/src/SharpCompress/Common/Tar/TarEntry.Async.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using SharpCompress.Common.Options; using SharpCompress.IO; namespace SharpCompress.Common.Tar; @@ -10,7 +11,8 @@ public partial class TarEntry StreamingMode mode, Stream stream, CompressionType compressionType, - IArchiveEncoding archiveEncoding + IArchiveEncoding archiveEncoding, + IReaderOptions readerOptions ) { await foreach ( @@ -21,11 +23,19 @@ public partial class TarEntry { if (mode == StreamingMode.Seekable) { - yield return new TarEntry(new TarFilePart(header, stream), compressionType); + yield return new TarEntry( + new TarFilePart(header, stream), + compressionType, + readerOptions + ); } else { - yield return new TarEntry(new TarFilePart(header, null), compressionType); + yield return new TarEntry( + new TarFilePart(header, null), + compressionType, + readerOptions + ); } } else diff --git a/src/SharpCompress/Common/Tar/TarEntry.cs b/src/SharpCompress/Common/Tar/TarEntry.cs index fbe93de8..28e7d45f 100644 --- a/src/SharpCompress/Common/Tar/TarEntry.cs +++ b/src/SharpCompress/Common/Tar/TarEntry.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using SharpCompress.Common.Options; using SharpCompress.Common.Tar.Headers; using SharpCompress.IO; @@ -10,7 +11,8 @@ public partial class TarEntry : Entry { private readonly TarFilePart? _filePart; - internal TarEntry(TarFilePart? filePart, CompressionType type) + internal TarEntry(TarFilePart? filePart, CompressionType type, IReaderOptions readerOptions) + : base(readerOptions) { _filePart = filePart; CompressionType = type; @@ -54,7 +56,8 @@ public partial class TarEntry : Entry StreamingMode mode, Stream stream, CompressionType compressionType, - IArchiveEncoding archiveEncoding + IArchiveEncoding archiveEncoding, + IReaderOptions readerOptions ) { foreach (var header in TarHeaderFactory.ReadHeader(mode, stream, archiveEncoding)) @@ -63,11 +66,19 @@ public partial class TarEntry : Entry { if (mode == StreamingMode.Seekable) { - yield return new TarEntry(new TarFilePart(header, stream), compressionType); + yield return new TarEntry( + new TarFilePart(header, stream), + compressionType, + readerOptions + ); } else { - yield return new TarEntry(new TarFilePart(header, null), compressionType); + yield return new TarEntry( + new TarFilePart(header, null), + compressionType, + readerOptions + ); } } else diff --git a/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs b/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs index 59e0fc6b..b0a316f1 100644 --- a/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs +++ b/src/SharpCompress/Common/Tar/TarHeaderFactory.Async.cs @@ -25,7 +25,7 @@ internal static partial class TarHeaderFactory try { header = new TarHeader(archiveEncoding); - if (!await header.ReadAsync(reader)) + if (!await header.ReadAsync(reader).ConfigureAwait(false)) { yield break; } diff --git a/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.Async.cs b/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.Async.cs index 86e0f771..40b4ffc7 100644 --- a/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.Async.cs +++ b/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.Async.cs @@ -8,14 +8,14 @@ internal partial class DirectoryEndHeader { internal override async ValueTask Read(AsyncBinaryReader reader) { - VolumeNumber = await reader.ReadUInt16Async(); - FirstVolumeWithDirectory = await reader.ReadUInt16Async(); - TotalNumberOfEntriesInDisk = await reader.ReadUInt16Async(); - TotalNumberOfEntries = await reader.ReadUInt16Async(); - DirectorySize = await reader.ReadUInt32Async(); - DirectoryStartOffsetRelativeToDisk = await reader.ReadUInt32Async(); - CommentLength = await reader.ReadUInt16Async(); + VolumeNumber = await reader.ReadUInt16Async().ConfigureAwait(false); + FirstVolumeWithDirectory = await reader.ReadUInt16Async().ConfigureAwait(false); + TotalNumberOfEntriesInDisk = await reader.ReadUInt16Async().ConfigureAwait(false); + TotalNumberOfEntries = await reader.ReadUInt16Async().ConfigureAwait(false); + DirectorySize = await reader.ReadUInt32Async().ConfigureAwait(false); + DirectoryStartOffsetRelativeToDisk = await reader.ReadUInt32Async().ConfigureAwait(false); + CommentLength = await reader.ReadUInt16Async().ConfigureAwait(false); Comment = new byte[CommentLength]; - await reader.ReadBytesAsync(Comment, 0, CommentLength); + await reader.ReadBytesAsync(Comment, 0, CommentLength).ConfigureAwait(false); } } diff --git a/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.Async.cs b/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.Async.cs index 6cc356d6..c09cac91 100644 --- a/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.Async.cs +++ b/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.Async.cs @@ -10,28 +10,33 @@ internal partial class DirectoryEntryHeader { internal override async ValueTask Read(AsyncBinaryReader reader) { - Version = await reader.ReadUInt16Async(); - VersionNeededToExtract = await reader.ReadUInt16Async(); - Flags = (HeaderFlags)await reader.ReadUInt16Async(); - CompressionMethod = (ZipCompressionMethod)await reader.ReadUInt16Async(); - OriginalLastModifiedTime = LastModifiedTime = await reader.ReadUInt16Async(); - OriginalLastModifiedDate = LastModifiedDate = await reader.ReadUInt16Async(); - Crc = await reader.ReadUInt32Async(); - CompressedSize = await reader.ReadUInt32Async(); - UncompressedSize = await reader.ReadUInt32Async(); - var nameLength = await reader.ReadUInt16Async(); - var extraLength = await reader.ReadUInt16Async(); - var commentLength = await reader.ReadUInt16Async(); - DiskNumberStart = await reader.ReadUInt16Async(); - InternalFileAttributes = await reader.ReadUInt16Async(); - ExternalFileAttributes = await reader.ReadUInt32Async(); - RelativeOffsetOfEntryHeader = await reader.ReadUInt32Async(); + Version = await reader.ReadUInt16Async().ConfigureAwait(false); + VersionNeededToExtract = await reader.ReadUInt16Async().ConfigureAwait(false); + Flags = (HeaderFlags)await reader.ReadUInt16Async().ConfigureAwait(false); + CompressionMethod = (ZipCompressionMethod) + await reader.ReadUInt16Async().ConfigureAwait(false); + OriginalLastModifiedTime = LastModifiedTime = await reader + .ReadUInt16Async() + .ConfigureAwait(false); + OriginalLastModifiedDate = LastModifiedDate = await reader + .ReadUInt16Async() + .ConfigureAwait(false); + Crc = await reader.ReadUInt32Async().ConfigureAwait(false); + CompressedSize = await reader.ReadUInt32Async().ConfigureAwait(false); + UncompressedSize = await reader.ReadUInt32Async().ConfigureAwait(false); + var nameLength = await reader.ReadUInt16Async().ConfigureAwait(false); + var extraLength = await reader.ReadUInt16Async().ConfigureAwait(false); + var commentLength = await reader.ReadUInt16Async().ConfigureAwait(false); + DiskNumberStart = await reader.ReadUInt16Async().ConfigureAwait(false); + InternalFileAttributes = await reader.ReadUInt16Async().ConfigureAwait(false); + ExternalFileAttributes = await reader.ReadUInt32Async().ConfigureAwait(false); + RelativeOffsetOfEntryHeader = await reader.ReadUInt32Async().ConfigureAwait(false); var name = new byte[nameLength]; var extra = new byte[extraLength]; var comment = new byte[commentLength]; - await reader.ReadBytesAsync(name, 0, nameLength); - await reader.ReadBytesAsync(extra, 0, extraLength); - await reader.ReadBytesAsync(comment, 0, commentLength); + await reader.ReadBytesAsync(name, 0, nameLength).ConfigureAwait(false); + await reader.ReadBytesAsync(extra, 0, extraLength).ConfigureAwait(false); + await reader.ReadBytesAsync(comment, 0, commentLength).ConfigureAwait(false); ProcessReadData(name, extra, comment); } diff --git a/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.Async.cs b/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.Async.cs index 9a8a991e..950494df 100644 --- a/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.Async.cs +++ b/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.Async.cs @@ -9,20 +9,25 @@ internal partial class LocalEntryHeader { internal override async ValueTask Read(AsyncBinaryReader reader) { - Version = await reader.ReadUInt16Async(); - Flags = (HeaderFlags)await reader.ReadUInt16Async(); - CompressionMethod = (ZipCompressionMethod)await reader.ReadUInt16Async(); - OriginalLastModifiedTime = LastModifiedTime = await reader.ReadUInt16Async(); - OriginalLastModifiedDate = LastModifiedDate = await reader.ReadUInt16Async(); - Crc = await reader.ReadUInt32Async(); - CompressedSize = await reader.ReadUInt32Async(); - UncompressedSize = await reader.ReadUInt32Async(); - var nameLength = await reader.ReadUInt16Async(); - var extraLength = await reader.ReadUInt16Async(); + Version = await reader.ReadUInt16Async().ConfigureAwait(false); + Flags = (HeaderFlags)await reader.ReadUInt16Async().ConfigureAwait(false); + CompressionMethod = (ZipCompressionMethod) + await reader.ReadUInt16Async().ConfigureAwait(false); + OriginalLastModifiedTime = LastModifiedTime = await reader + .ReadUInt16Async() + .ConfigureAwait(false); + OriginalLastModifiedDate = LastModifiedDate = await reader + .ReadUInt16Async() + .ConfigureAwait(false); + Crc = await reader.ReadUInt32Async().ConfigureAwait(false); + CompressedSize = await reader.ReadUInt32Async().ConfigureAwait(false); + UncompressedSize = await reader.ReadUInt32Async().ConfigureAwait(false); + var nameLength = await reader.ReadUInt16Async().ConfigureAwait(false); + var extraLength = await reader.ReadUInt16Async().ConfigureAwait(false); var name = new byte[nameLength]; var extra = new byte[extraLength]; - await reader.ReadBytesAsync(name, 0, nameLength); - await reader.ReadBytesAsync(extra, 0, extraLength); + await reader.ReadBytesAsync(name, 0, nameLength).ConfigureAwait(false); + await reader.ReadBytesAsync(extra, 0, extraLength).ConfigureAwait(false); ProcessReadData(name, extra); } diff --git a/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndHeader.Async.cs b/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndHeader.Async.cs index 9bbfe4f8..9e510688 100644 --- a/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndHeader.Async.cs +++ b/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndHeader.Async.cs @@ -8,19 +8,20 @@ internal partial class Zip64DirectoryEndHeader { internal override async ValueTask Read(AsyncBinaryReader reader) { - SizeOfDirectoryEndRecord = (long)await reader.ReadUInt64Async(); - VersionMadeBy = await reader.ReadUInt16Async(); - VersionNeededToExtract = await reader.ReadUInt16Async(); - VolumeNumber = await reader.ReadUInt32Async(); - FirstVolumeWithDirectory = await reader.ReadUInt32Async(); - TotalNumberOfEntriesInDisk = (long)await reader.ReadUInt64Async(); - TotalNumberOfEntries = (long)await reader.ReadUInt64Async(); - DirectorySize = (long)await reader.ReadUInt64Async(); - DirectoryStartOffsetRelativeToDisk = (long)await reader.ReadUInt64Async(); + SizeOfDirectoryEndRecord = (long)await reader.ReadUInt64Async().ConfigureAwait(false); + VersionMadeBy = await reader.ReadUInt16Async().ConfigureAwait(false); + VersionNeededToExtract = await reader.ReadUInt16Async().ConfigureAwait(false); + VolumeNumber = await reader.ReadUInt32Async().ConfigureAwait(false); + FirstVolumeWithDirectory = await reader.ReadUInt32Async().ConfigureAwait(false); + TotalNumberOfEntriesInDisk = (long)await reader.ReadUInt64Async().ConfigureAwait(false); + TotalNumberOfEntries = (long)await reader.ReadUInt64Async().ConfigureAwait(false); + DirectorySize = (long)await reader.ReadUInt64Async().ConfigureAwait(false); + DirectoryStartOffsetRelativeToDisk = (long) + await reader.ReadUInt64Async().ConfigureAwait(false); var size = (int)( SizeOfDirectoryEndRecord - SIZE_OF_FIXED_HEADER_DATA_EXCEPT_SIGNATURE_AND_SIZE_FIELDS ); DataSector = new byte[size]; - await reader.ReadBytesAsync(DataSector, 0, size); + await reader.ReadBytesAsync(DataSector, 0, size).ConfigureAwait(false); } } diff --git a/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.Async.cs b/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.Async.cs index c4188c8b..e0095510 100644 --- a/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.Async.cs +++ b/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.Async.cs @@ -8,8 +8,9 @@ internal partial class Zip64DirectoryEndLocatorHeader { internal override async ValueTask Read(AsyncBinaryReader reader) { - FirstVolumeWithDirectory = await reader.ReadUInt32Async(); - RelativeOffsetOfTheEndOfDirectoryRecord = (long)await reader.ReadUInt64Async(); - TotalNumberOfVolumes = await reader.ReadUInt32Async(); + FirstVolumeWithDirectory = await reader.ReadUInt32Async().ConfigureAwait(false); + RelativeOffsetOfTheEndOfDirectoryRecord = (long) + await reader.ReadUInt64Async().ConfigureAwait(false); + TotalNumberOfVolumes = await reader.ReadUInt32Async().ConfigureAwait(false); } } diff --git a/src/SharpCompress/Common/Zip/SeekableZipFilePart.Async.cs b/src/SharpCompress/Common/Zip/SeekableZipFilePart.Async.cs index 92a8f7b3..8fff8436 100644 --- a/src/SharpCompress/Common/Zip/SeekableZipFilePart.Async.cs +++ b/src/SharpCompress/Common/Zip/SeekableZipFilePart.Async.cs @@ -13,12 +13,14 @@ internal partial class SeekableZipFilePart { if (!_isLocalHeaderLoaded) { - await LoadLocalHeaderAsync(cancellationToken); + await LoadLocalHeaderAsync(cancellationToken).ConfigureAwait(false); _isLocalHeaderLoaded = true; } - return await base.GetCompressedStreamAsync(cancellationToken); + return await base.GetCompressedStreamAsync(cancellationToken).ConfigureAwait(false); } private async ValueTask LoadLocalHeaderAsync(CancellationToken cancellationToken = default) => - Header = await _headerFactory.GetLocalHeaderAsync(BaseStream, (DirectoryEntryHeader)Header); + Header = await _headerFactory + .GetLocalHeaderAsync(BaseStream, (DirectoryEntryHeader)Header) + .ConfigureAwait(false); } diff --git a/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.Async.cs b/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.Async.cs index 23422ae4..d7372b00 100644 --- a/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.Async.cs +++ b/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.Async.cs @@ -18,11 +18,11 @@ internal sealed partial class SeekableZipHeaderFactory using var reader = new AsyncBinaryReader(stream, leaveOpen: true); #endif - await SeekBackToHeaderAsync(stream, reader); + await SeekBackToHeaderAsync(stream, reader).ConfigureAwait(false); var eocd_location = stream.Position; var entry = new DirectoryEndHeader(); - await entry.Read(reader); + await entry.Read(reader).ConfigureAwait(false); if (entry.IsZip64) { @@ -30,24 +30,24 @@ internal sealed partial class SeekableZipHeaderFactory // ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR should be before the EOCD stream.Seek(eocd_location - ZIP64_EOCD_LENGTH - 4, SeekOrigin.Begin); - uint zip64_locator = await reader.ReadUInt32Async(); + uint zip64_locator = await reader.ReadUInt32Async().ConfigureAwait(false); if (zip64_locator != ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR) { throw new ArchiveException("Failed to locate the Zip64 Directory Locator"); } var zip64Locator = new Zip64DirectoryEndLocatorHeader(); - await zip64Locator.Read(reader); + await zip64Locator.Read(reader).ConfigureAwait(false); stream.Seek(zip64Locator.RelativeOffsetOfTheEndOfDirectoryRecord, SeekOrigin.Begin); - var zip64Signature = await reader.ReadUInt32Async(); + var zip64Signature = await reader.ReadUInt32Async().ConfigureAwait(false); if (zip64Signature != ZIP64_END_OF_CENTRAL_DIRECTORY) { throw new ArchiveException("Failed to locate the Zip64 Header"); } var zip64Entry = new Zip64DirectoryEndHeader(); - await zip64Entry.Read(reader); + await zip64Entry.Read(reader).ConfigureAwait(false); stream.Seek(zip64Entry.DirectoryStartOffsetRelativeToDisk, SeekOrigin.Begin); } else @@ -59,8 +59,8 @@ internal sealed partial class SeekableZipHeaderFactory while (true) { stream.Position = position; - var signature = await reader.ReadUInt32Async(); - var nextHeader = await ReadHeader(signature, reader, _zip64); + var signature = await reader.ReadUInt32Async().ConfigureAwait(false); + var nextHeader = await ReadHeader(signature, reader, _zip64).ConfigureAwait(false); position = stream.Position; if (nextHeader is null) @@ -101,7 +101,7 @@ internal sealed partial class SeekableZipHeaderFactory try { - await reader.ReadBytesAsync(seek, 0, len, default); + await reader.ReadBytesAsync(seek, 0, len, default).ConfigureAwait(false); var memory = new Memory(seek, 0, len); var span = memory.Span; span.Reverse(); @@ -137,8 +137,11 @@ internal sealed partial class SeekableZipHeaderFactory #else using var reader = new AsyncBinaryReader(stream, leaveOpen: true); #endif - var signature = await reader.ReadUInt32Async(); - if (await ReadHeader(signature, reader, _zip64) is not LocalEntryHeader localEntryHeader) + var signature = await reader.ReadUInt32Async().ConfigureAwait(false); + if ( + await ReadHeader(signature, reader, _zip64).ConfigureAwait(false) + is not LocalEntryHeader localEntryHeader + ) { throw new InvalidOperationException(); } diff --git a/src/SharpCompress/Common/Zip/ZipEntry.cs b/src/SharpCompress/Common/Zip/ZipEntry.cs index 19f9961b..8c193f29 100644 --- a/src/SharpCompress/Common/Zip/ZipEntry.cs +++ b/src/SharpCompress/Common/Zip/ZipEntry.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using SharpCompress.Common.Options; using SharpCompress.Common.Zip.Headers; namespace SharpCompress.Common.Zip; @@ -9,7 +10,8 @@ public class ZipEntry : Entry { private readonly ZipFilePart? _filePart; - internal ZipEntry(ZipFilePart? filePart) + internal ZipEntry(ZipFilePart? filePart, IReaderOptions readerOptions) + : base(readerOptions) { if (filePart == null) { diff --git a/src/SharpCompress/Common/Zip/ZipFilePart.Async.cs b/src/SharpCompress/Common/Zip/ZipFilePart.Async.cs index bcd5650f..786f2226 100644 --- a/src/SharpCompress/Common/Zip/ZipFilePart.Async.cs +++ b/src/SharpCompress/Common/Zip/ZipFilePart.Async.cs @@ -148,53 +148,63 @@ internal abstract partial class ZipFilePart } case ZipCompressionMethod.Reduce1: { - return await ReduceStream.CreateAsync( - stream, - Header.CompressedSize, - Header.UncompressedSize, - 1, - cancellationToken - ); + return await ReduceStream + .CreateAsync( + stream, + Header.CompressedSize, + Header.UncompressedSize, + 1, + cancellationToken + ) + .ConfigureAwait(false); } case ZipCompressionMethod.Reduce2: { - return await ReduceStream.CreateAsync( - stream, - Header.CompressedSize, - Header.UncompressedSize, - 2, - cancellationToken - ); + return await ReduceStream + .CreateAsync( + stream, + Header.CompressedSize, + Header.UncompressedSize, + 2, + cancellationToken + ) + .ConfigureAwait(false); } case ZipCompressionMethod.Reduce3: { - return await ReduceStream.CreateAsync( - stream, - Header.CompressedSize, - Header.UncompressedSize, - 3, - cancellationToken - ); + return await ReduceStream + .CreateAsync( + stream, + Header.CompressedSize, + Header.UncompressedSize, + 3, + cancellationToken + ) + .ConfigureAwait(false); } case ZipCompressionMethod.Reduce4: { - return await ReduceStream.CreateAsync( - stream, - Header.CompressedSize, - Header.UncompressedSize, - 4, - cancellationToken - ); + return await ReduceStream + .CreateAsync( + stream, + Header.CompressedSize, + Header.UncompressedSize, + 4, + cancellationToken + ) + .ConfigureAwait(false); } case ZipCompressionMethod.Explode: { - return await ExplodeStream.CreateAsync( - stream, - Header.CompressedSize, - Header.UncompressedSize, - Header.Flags, - cancellationToken - ); + return await ExplodeStream + .CreateAsync( + stream, + Header.CompressedSize, + Header.UncompressedSize, + Header.Flags, + cancellationToken + ) + .ConfigureAwait(false); } case ZipCompressionMethod.Deflate: @@ -207,12 +217,14 @@ internal abstract partial class ZipFilePart } case ZipCompressionMethod.BZip2: { - return await BZip2Stream.CreateAsync( - stream, - CompressionMode.Decompress, - false, - cancellationToken: cancellationToken - ); + return await BZip2Stream + .CreateAsync( + stream, + CompressionMode.Decompress, + false, + cancellationToken: cancellationToken + ) + .ConfigureAwait(false); } case ZipCompressionMethod.LZMA: { @@ -228,14 +240,16 @@ internal abstract partial class ZipFilePart await stream .ReadFullyAsync(props, 0, propsSize, cancellationToken) .ConfigureAwait(false); - return await LzmaStream.CreateAsync( - props, - stream, - Header.CompressedSize > 0 ? Header.CompressedSize - 4 - props.Length : -1, - FlagUtility.HasFlag(Header.Flags, HeaderFlags.Bit1) - ? -1 - : Header.UncompressedSize - ); + return await LzmaStream + .CreateAsync( + props, + stream, + Header.CompressedSize > 0 ? Header.CompressedSize - 4 - props.Length : -1, + FlagUtility.HasFlag(Header.Flags, HeaderFlags.Bit1) + ? -1 + : Header.UncompressedSize + ) + .ConfigureAwait(false); } case ZipCompressionMethod.Xz: { @@ -284,11 +298,12 @@ internal abstract partial class ZipFilePart } return await CreateDecompressionStreamAsync( - stream, - (ZipCompressionMethod) - BinaryPrimitives.ReadUInt16LittleEndian(data.DataBytes.AsSpan(5)), - cancellationToken - ); + stream, + (ZipCompressionMethod) + BinaryPrimitives.ReadUInt16LittleEndian(data.DataBytes.AsSpan(5)), + cancellationToken + ) + .ConfigureAwait(false); } default: { diff --git a/src/SharpCompress/Common/Zip/ZipHeaderFactory.Async.cs b/src/SharpCompress/Common/Zip/ZipHeaderFactory.Async.cs index 8d585505..c31e24a6 100644 --- a/src/SharpCompress/Common/Zip/ZipHeaderFactory.Async.cs +++ b/src/SharpCompress/Common/Zip/ZipHeaderFactory.Async.cs @@ -21,7 +21,7 @@ internal partial class ZipHeaderFactory case ENTRY_HEADER_BYTES: { var entryHeader = new LocalEntryHeader(_archiveEncoding); - await entryHeader.Read(reader); + await entryHeader.Read(reader).ConfigureAwait(false); await LoadHeaderAsync(entryHeader, reader.BaseStream).ConfigureAwait(false); _lastEntryHeader = entryHeader; @@ -30,7 +30,7 @@ internal partial class ZipHeaderFactory case DIRECTORY_START_HEADER_BYTES: { var entry = new DirectoryEntryHeader(_archiveEncoding); - await entry.Read(reader); + await entry.Read(reader).ConfigureAwait(false); return entry; } case POST_DATA_DESCRIPTOR: @@ -43,17 +43,17 @@ internal partial class ZipHeaderFactory ) ) { - _lastEntryHeader.Crc = await reader.ReadUInt32Async(); + _lastEntryHeader.Crc = await reader.ReadUInt32Async().ConfigureAwait(false); _lastEntryHeader.CompressedSize = zip64 - ? (long)await reader.ReadUInt64Async() - : await reader.ReadUInt32Async(); + ? (long)await reader.ReadUInt64Async().ConfigureAwait(false) + : await reader.ReadUInt32Async().ConfigureAwait(false); _lastEntryHeader.UncompressedSize = zip64 - ? (long)await reader.ReadUInt64Async() - : await reader.ReadUInt32Async(); + ? (long)await reader.ReadUInt64Async().ConfigureAwait(false) + : await reader.ReadUInt32Async().ConfigureAwait(false); } else { - await reader.SkipAsync(zip64 ? 20 : 12); + await reader.SkipAsync(zip64 ? 20 : 12).ConfigureAwait(false); } return null; } @@ -62,7 +62,7 @@ internal partial class ZipHeaderFactory case DIRECTORY_END_HEADER_BYTES: { var entry = new DirectoryEndHeader(); - await entry.Read(reader); + await entry.Read(reader).ConfigureAwait(false); return entry; } case SPLIT_ARCHIVE_HEADER_BYTES: @@ -72,13 +72,13 @@ internal partial class ZipHeaderFactory case ZIP64_END_OF_CENTRAL_DIRECTORY: { var entry = new Zip64DirectoryEndHeader(); - await entry.Read(reader); + await entry.Read(reader).ConfigureAwait(false); return entry; } case ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR: { var entry = new Zip64DirectoryEndLocatorHeader(); - await entry.Read(reader); + await entry.Read(reader).ConfigureAwait(false); return entry; } default: diff --git a/src/SharpCompress/Compressors/ADC/ADCBase.Async.cs b/src/SharpCompress/Compressors/ADC/ADCBase.Async.cs index 5ef29c2a..7bbbacde 100644 --- a/src/SharpCompress/Compressors/ADC/ADCBase.Async.cs +++ b/src/SharpCompress/Compressors/ADC/ADCBase.Async.cs @@ -19,7 +19,9 @@ public static partial class ADCBase byte[] input, int bufferSize = 262144, CancellationToken cancellationToken = default - ) => await DecompressAsync(new MemoryStream(input), bufferSize, cancellationToken); + ) => + await DecompressAsync(new MemoryStream(input), bufferSize, cancellationToken) + .ConfigureAwait(false); /// /// Decompresses a stream asynchronously that's compressed with ADC @@ -76,12 +78,9 @@ public static partial class ADCBase break; } - var readCount = await input.ReadAsync( - buffer, - outPosition, - chunkSize, - cancellationToken - ); + var readCount = await input + .ReadAsync(buffer, outPosition, chunkSize, cancellationToken) + .ConfigureAwait(false); outPosition += readCount; position += readCount + 1; break; diff --git a/src/SharpCompress/Compressors/ADC/ADCStream.Async.cs b/src/SharpCompress/Compressors/ADC/ADCStream.Async.cs index cf12a8c7..22941be5 100644 --- a/src/SharpCompress/Compressors/ADC/ADCStream.Async.cs +++ b/src/SharpCompress/Compressors/ADC/ADCStream.Async.cs @@ -66,10 +66,9 @@ public sealed partial class ADCStream if (_outBuffer is null) { - var result = await ADCBase.DecompressAsync( - _stream, - cancellationToken: cancellationToken - ); + var result = await ADCBase + .DecompressAsync(_stream, cancellationToken: cancellationToken) + .ConfigureAwait(false); _outBuffer = result.Output; _outPosition = 0; } @@ -87,10 +86,9 @@ public sealed partial class ADCStream copied += piece; _position += piece; toCopy -= piece; - var result = await ADCBase.DecompressAsync( - _stream, - cancellationToken: cancellationToken - ); + var result = await ADCBase + .DecompressAsync(_stream, cancellationToken: cancellationToken) + .ConfigureAwait(false); _outBuffer = result.Output; _outPosition = 0; if (result.BytesRead == 0 || _outBuffer is null || _outBuffer.Length == 0) diff --git a/src/SharpCompress/Compressors/BZip2/BZip2Stream.Async.cs b/src/SharpCompress/Compressors/BZip2/BZip2Stream.Async.cs index c9b0b031..d14e2159 100644 --- a/src/SharpCompress/Compressors/BZip2/BZip2Stream.Async.cs +++ b/src/SharpCompress/Compressors/BZip2/BZip2Stream.Async.cs @@ -31,12 +31,9 @@ public sealed partial class BZip2Stream } else { - bZip2Stream.stream = await CBZip2InputStream.CreateAsync( - stream, - decompressConcatenated, - leaveOpen, - cancellationToken - ); + bZip2Stream.stream = await CBZip2InputStream + .CreateAsync(stream, decompressConcatenated, leaveOpen, cancellationToken) + .ConfigureAwait(false); } return bZip2Stream; @@ -55,7 +52,9 @@ public sealed partial class BZip2Stream { cancellationToken.ThrowIfCancellationRequested(); var buffer = new byte[2]; - var bytesRead = await stream.ReadAsync(buffer, 0, 2, cancellationToken); + var bytesRead = await stream + .ReadAsync(buffer, 0, 2, cancellationToken) + .ConfigureAwait(false); if (bytesRead < 2 || buffer[0] != 'B' || buffer[1] != 'Z') { return false; diff --git a/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.Async.cs b/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.Async.cs index 6a5992e8..8b2c45af 100644 --- a/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.Async.cs +++ b/src/SharpCompress/Compressors/BZip2/CBZip2InputStream.Async.cs @@ -800,14 +800,6 @@ internal partial class CBZip2InputStream return k; } - private async ValueTask BsSetStreamAsync(Stream f, CancellationToken cancellationToken) - { - bsStream = f; - bsLive = 0; - bsBuff = 0; - await Task.CompletedTask; - } - private async ValueTask BsRAsync(int n, CancellationToken cancellationToken) { int v; @@ -818,7 +810,7 @@ internal partial class CBZip2InputStream var b = ArrayPool.Shared.Rent(1); try { - await bsStream.ReadExactAsync(b, 0, 1, cancellationToken); + await bsStream.ReadExactAsync(b, 0, 1, cancellationToken).ConfigureAwait(false); thech = (char)b[0]; } catch (IOException) @@ -844,15 +836,15 @@ internal partial class CBZip2InputStream } private async ValueTask BsGetUCharAsync(CancellationToken cancellationToken) => - (char)await BsRAsync(8, cancellationToken); + (char)await BsRAsync(8, cancellationToken).ConfigureAwait(false); private async ValueTask BsGetIntVSAsync( int numBits, CancellationToken cancellationToken - ) => await BsRAsync(numBits, cancellationToken); + ) => await BsRAsync(numBits, cancellationToken).ConfigureAwait(false); private async ValueTask BsGetInt32Async(CancellationToken cancellationToken) => - await BsGetintAsync(cancellationToken); + await BsGetintAsync(cancellationToken).ConfigureAwait(false); public static async ValueTask CreateAsync( Stream zStream, @@ -864,10 +856,10 @@ internal partial class CBZip2InputStream var cbZip2InputStream = new CBZip2InputStream(decompressConcatenated, leaveOpen); cbZip2InputStream.ll8 = null; cbZip2InputStream.tt = null; - await cbZip2InputStream.BsSetStreamAsync(zStream, cancellationToken); - await cbZip2InputStream.InitializeAsync(true, cancellationToken); - await cbZip2InputStream.InitBlockAsync(cancellationToken); - await cbZip2InputStream.SetupBlockAsync(cancellationToken); + cbZip2InputStream.BsSetStream(zStream); + await cbZip2InputStream.InitializeAsync(true, cancellationToken).ConfigureAwait(false); + await cbZip2InputStream.InitBlockAsync(cancellationToken).ConfigureAwait(false); + await cbZip2InputStream.SetupBlockAsync(cancellationToken).ConfigureAwait(false); return cbZip2InputStream; } } diff --git a/src/SharpCompress/Compressors/LZMA/DecoderRegistry.Async.cs b/src/SharpCompress/Compressors/LZMA/DecoderRegistry.Async.cs index 5a3a3fcd..a3d3a2ea 100644 --- a/src/SharpCompress/Compressors/LZMA/DecoderRegistry.Async.cs +++ b/src/SharpCompress/Compressors/LZMA/DecoderRegistry.Async.cs @@ -60,12 +60,14 @@ internal static partial class DecoderRegistry case K_RISCV: return new BCJFilterRISCV(false, inStreams.Single()); case K_B_ZIP2: - return await BZip2Stream.CreateAsync( - inStreams.Single(), - CompressionMode.Decompress, - true, - cancellationToken: cancellationToken - ); + return await BZip2Stream + .CreateAsync( + inStreams.Single(), + CompressionMode.Decompress, + true, + cancellationToken: cancellationToken + ) + .ConfigureAwait(false); case K_PPMD: return await PpmdStream .CreateAsync( diff --git a/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.Async.cs b/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.Async.cs index ecf82e2e..011fdf2e 100644 --- a/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.Async.cs +++ b/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.Async.cs @@ -12,7 +12,7 @@ internal partial class OutWindow : IAsyncDisposable { public async ValueTask InitAsync(Stream stream) { - await ReleaseStreamAsync(); + await ReleaseStreamAsync().ConfigureAwait(false); _stream = stream; } @@ -24,7 +24,7 @@ internal partial class OutWindow : IAsyncDisposable public async ValueTask DisposeAsync() { - await ReleaseStreamAsync(); + await ReleaseStreamAsync().ConfigureAwait(false); if (_buffer is null) { return; @@ -167,7 +167,7 @@ internal partial class OutWindow : IAsyncDisposable _total = 0; _limit = size; _pos = _windowSize - size; - await CopyStreamAsync(stream, size); + await CopyStreamAsync(stream, size).ConfigureAwait(false); if (_pos == _windowSize) { _pos = 0; diff --git a/src/SharpCompress/Compressors/LZMA/LZipStream.Async.cs b/src/SharpCompress/Compressors/LZMA/LZipStream.Async.cs index f83cbff9..0a85a378 100644 --- a/src/SharpCompress/Compressors/LZMA/LZipStream.Async.cs +++ b/src/SharpCompress/Compressors/LZMA/LZipStream.Async.cs @@ -20,7 +20,7 @@ public sealed partial class LZipStream public static async ValueTask IsLZipFileAsync( Stream stream, CancellationToken cancellationToken = default - ) => await ValidateAndReadSizeAsync(stream, cancellationToken) != 0; + ) => await ValidateAndReadSizeAsync(stream, cancellationToken).ConfigureAwait(false) != 0; /// /// Asynchronously reads the 6-byte header of the stream, and returns 0 if either the header @@ -91,7 +91,7 @@ public sealed partial class LZipStream ) { cancellationToken.ThrowIfCancellationRequested(); - await _stream.WriteAsync(buffer, offset, count, cancellationToken); + await _stream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); _writeCount += count; } } diff --git a/src/SharpCompress/Compressors/LZMA/LzmaDecoder.Async.cs b/src/SharpCompress/Compressors/LZMA/LzmaDecoder.Async.cs index add73b8e..b1d061e8 100644 --- a/src/SharpCompress/Compressors/LZMA/LzmaDecoder.Async.cs +++ b/src/SharpCompress/Compressors/LZMA/LzmaDecoder.Async.cs @@ -137,7 +137,7 @@ public partial class Decoder : ICoder, ISetDecoderProperties { CreateDictionary(); } - await _outWindow.InitAsync(outStream); + await _outWindow.InitAsync(outStream).ConfigureAwait(false); if (outSize > 0) { _outWindow.SetLimit(outSize); @@ -178,29 +178,34 @@ public partial class Decoder : ICoder, ISetDecoderProperties var posState = (uint)outWindow.Total & _posStateMask; if ( await _isMatchDecoders[(_state._index << Base.K_NUM_POS_STATES_BITS_MAX) + posState] - .DecodeAsync(rangeDecoder, cancellationToken) == 0 + .DecodeAsync(rangeDecoder, cancellationToken) + .ConfigureAwait(false) == 0 ) { byte b; var prevByte = outWindow.GetByte(0); if (!_state.IsCharState()) { - b = await _literalDecoder.DecodeWithMatchByteAsync( - rangeDecoder, - (uint)outWindow.Total, - prevByte, - outWindow.GetByte((int)_rep0), - cancellationToken - ); + b = await _literalDecoder + .DecodeWithMatchByteAsync( + rangeDecoder, + (uint)outWindow.Total, + prevByte, + outWindow.GetByte((int)_rep0), + cancellationToken + ) + .ConfigureAwait(false); } else { - b = await _literalDecoder.DecodeNormalAsync( - rangeDecoder, - (uint)outWindow.Total, - prevByte, - cancellationToken - ); + b = await _literalDecoder + .DecodeNormalAsync( + rangeDecoder, + (uint)outWindow.Total, + prevByte, + cancellationToken + ) + .ConfigureAwait(false); } await outWindow.PutByteAsync(b, cancellationToken).ConfigureAwait(false); _state.UpdateChar(); @@ -209,20 +214,23 @@ public partial class Decoder : ICoder, ISetDecoderProperties { uint len; if ( - await _isRepDecoders[_state._index].DecodeAsync(rangeDecoder, cancellationToken) - == 1 + await _isRepDecoders[_state._index] + .DecodeAsync(rangeDecoder, cancellationToken) + .ConfigureAwait(false) == 1 ) { if ( await _isRepG0Decoders[_state._index] - .DecodeAsync(rangeDecoder, cancellationToken) == 0 + .DecodeAsync(rangeDecoder, cancellationToken) + .ConfigureAwait(false) == 0 ) { if ( await _isRep0LongDecoders[ (_state._index << Base.K_NUM_POS_STATES_BITS_MAX) + posState ] - .DecodeAsync(rangeDecoder, cancellationToken) == 0 + .DecodeAsync(rangeDecoder, cancellationToken) + .ConfigureAwait(false) == 0 ) { _state.UpdateShortRep(); @@ -237,7 +245,8 @@ public partial class Decoder : ICoder, ISetDecoderProperties uint distance; if ( await _isRepG1Decoders[_state._index] - .DecodeAsync(rangeDecoder, cancellationToken) == 0 + .DecodeAsync(rangeDecoder, cancellationToken) + .ConfigureAwait(false) == 0 ) { distance = _rep1; @@ -246,7 +255,8 @@ public partial class Decoder : ICoder, ISetDecoderProperties { if ( await _isRepG2Decoders[_state._index] - .DecodeAsync(rangeDecoder, cancellationToken) == 0 + .DecodeAsync(rangeDecoder, cancellationToken) + .ConfigureAwait(false) == 0 ) { distance = _rep2; @@ -339,6 +349,6 @@ public partial class Decoder : ICoder, ISetDecoderProperties { CreateDictionary(); } - await _outWindow.TrainAsync(stream); + await _outWindow.TrainAsync(stream).ConfigureAwait(false); } } diff --git a/src/SharpCompress/Compressors/LZMA/LzmaStream.Async.cs b/src/SharpCompress/Compressors/LZMA/LzmaStream.Async.cs index 0187a684..6167c8b2 100644 --- a/src/SharpCompress/Compressors/LZMA/LzmaStream.Async.cs +++ b/src/SharpCompress/Compressors/LZMA/LzmaStream.Async.cs @@ -47,16 +47,16 @@ public partial class LzmaStream { if (presetDictionary != null) { - await lzma._outWindow.TrainAsync(presetDictionary); + await lzma._outWindow.TrainAsync(presetDictionary).ConfigureAwait(false); } - await lzma._rangeDecoder.InitAsync(inputStream); + await lzma._rangeDecoder.InitAsync(inputStream).ConfigureAwait(false); } else { if (presetDictionary != null) { - await lzma._outWindow.TrainAsync(presetDictionary); + await lzma._outWindow.TrainAsync(presetDictionary).ConfigureAwait(false); lzma._needDictReset = false; } } @@ -147,7 +147,7 @@ public partial class LzmaStream _decoder.SetDecoderProperties(Properties); } - await _rangeDecoder.InitAsync(_inputStream, cancellationToken); + await _rangeDecoder.InitAsync(_inputStream, cancellationToken).ConfigureAwait(false); } else if (control > 0x02) { diff --git a/src/SharpCompress/Compressors/LZMA/Utilites/CrcBuilderStream.Async.cs b/src/SharpCompress/Compressors/LZMA/Utilites/CrcBuilderStream.Async.cs index cca7b2dd..8ba2901b 100644 --- a/src/SharpCompress/Compressors/LZMA/Utilites/CrcBuilderStream.Async.cs +++ b/src/SharpCompress/Compressors/LZMA/Utilites/CrcBuilderStream.Async.cs @@ -22,6 +22,6 @@ internal partial class CrcBuilderStream : Stream Processed += count; _mCrc = Crc.Update(_mCrc, buffer, offset, count); - await _mTarget.WriteAsync(buffer, offset, count, cancellationToken); + await _mTarget.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); } } diff --git a/src/SharpCompress/Compressors/Lzw/LzwStream.Async.cs b/src/SharpCompress/Compressors/Lzw/LzwStream.Async.cs index 063b9df9..c6103e10 100644 --- a/src/SharpCompress/Compressors/Lzw/LzwStream.Async.cs +++ b/src/SharpCompress/Compressors/Lzw/LzwStream.Async.cs @@ -24,7 +24,9 @@ public partial class LzwStream { byte[] hdr = new byte[LzwConstants.HDR_SIZE]; - int result = await stream.ReadAsync(hdr, 0, hdr.Length, cancellationToken); + int result = await stream + .ReadAsync(hdr, 0, hdr.Length, cancellationToken) + .ConfigureAwait(false); // Check the magic marker if (result < 0) diff --git a/src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyAsyncStream.Async.cs b/src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyAsyncStream.Async.cs index e0b34720..64f461d4 100644 --- a/src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyAsyncStream.Async.cs +++ b/src/SharpCompress/Compressors/Rar/MultiVolumeReadOnlyAsyncStream.Async.cs @@ -15,7 +15,7 @@ internal sealed partial class MultiVolumeReadOnlyAsyncStream : MultiVolumeReadOn ) { var stream = new MultiVolumeReadOnlyAsyncStream(parts); - await stream.filePartEnumerator.MoveNextAsync(); + await stream.filePartEnumerator.MoveNextAsync().ConfigureAwait(false); stream.InitializeNextFilePart(); return stream; } @@ -23,10 +23,10 @@ internal sealed partial class MultiVolumeReadOnlyAsyncStream : MultiVolumeReadOn #if NET8_0_OR_GREATER public override async ValueTask DisposeAsync() { - await base.DisposeAsync(); + await base.DisposeAsync().ConfigureAwait(false); if (filePartEnumerator != null) { - await filePartEnumerator.DisposeAsync(); + await filePartEnumerator.DisposeAsync().ConfigureAwait(false); } currentStream = null; } @@ -85,7 +85,7 @@ internal sealed partial class MultiVolumeReadOnlyAsyncStream : MultiVolumeReadOn } var fileName = filePartEnumerator.Current.FileHeader.FileName; - if (!await filePartEnumerator.MoveNextAsync()) + if (!await filePartEnumerator.MoveNextAsync().ConfigureAwait(false)) { throw new InvalidFormatException( "Multi-part rar file is incomplete. Entry expects a new volume: " @@ -146,7 +146,7 @@ internal sealed partial class MultiVolumeReadOnlyAsyncStream : MultiVolumeReadOn ); } var fileName = filePartEnumerator.Current.FileHeader.FileName; - if (!await filePartEnumerator.MoveNextAsync()) + if (!await filePartEnumerator.MoveNextAsync().ConfigureAwait(false)) { throw new InvalidFormatException( "Multi-part rar file is incomplete. Entry expects a new volume: " diff --git a/src/SharpCompress/Compressors/Rar/RarBLAKE2spStream.Async.cs b/src/SharpCompress/Compressors/Rar/RarBLAKE2spStream.Async.cs index 4015b420..6b44acb3 100644 --- a/src/SharpCompress/Compressors/Rar/RarBLAKE2spStream.Async.cs +++ b/src/SharpCompress/Compressors/Rar/RarBLAKE2spStream.Async.cs @@ -17,7 +17,7 @@ internal partial class RarBLAKE2spStream : RarStream ) { var stream = new RarBLAKE2spStream(unpack, fileHeader, readStream); - await stream.InitializeAsync(cancellationToken); + await stream.InitializeAsync(cancellationToken).ConfigureAwait(false); return stream; } diff --git a/src/SharpCompress/Compressors/Rar/RarCrcStream.Async.cs b/src/SharpCompress/Compressors/Rar/RarCrcStream.Async.cs index f49f63ba..670ff8b8 100644 --- a/src/SharpCompress/Compressors/Rar/RarCrcStream.Async.cs +++ b/src/SharpCompress/Compressors/Rar/RarCrcStream.Async.cs @@ -17,7 +17,7 @@ internal partial class RarCrcStream : RarStream ) { var stream = new RarCrcStream(unpack, fileHeader, readStream); - await stream.InitializeAsync(cancellationToken); + await stream.InitializeAsync(cancellationToken).ConfigureAwait(false); return stream; } diff --git a/src/SharpCompress/Compressors/Rar/RarStream.Async.cs b/src/SharpCompress/Compressors/Rar/RarStream.Async.cs index 4ea0651d..444da3e7 100644 --- a/src/SharpCompress/Compressors/Rar/RarStream.Async.cs +++ b/src/SharpCompress/Compressors/Rar/RarStream.Async.cs @@ -18,7 +18,9 @@ internal partial class RarStream public async ValueTask InitializeAsync(CancellationToken cancellationToken = default) { fetch = true; - await unpack.DoUnpackAsync(fileHeader, readStream, this, cancellationToken); + await unpack + .DoUnpackAsync(fileHeader, readStream, this, cancellationToken) + .ConfigureAwait(false); fetch = false; _position = 0; } diff --git a/src/SharpCompress/Compressors/ZStandard/ZStandardStream.Async.cs b/src/SharpCompress/Compressors/ZStandard/ZStandardStream.Async.cs index abef7d42..f5932619 100644 --- a/src/SharpCompress/Compressors/ZStandard/ZStandardStream.Async.cs +++ b/src/SharpCompress/Compressors/ZStandard/ZStandardStream.Async.cs @@ -15,7 +15,9 @@ internal partial class ZStandardStream { cancellationToken.ThrowIfCancellationRequested(); var buffer = new byte[4]; - var bytesRead = await stream.ReadAsync(buffer, 0, 4, cancellationToken); + var bytesRead = await stream + .ReadAsync(buffer, 0, 4, cancellationToken) + .ConfigureAwait(false); if (bytesRead < 4) { return false; diff --git a/src/SharpCompress/Factories/ArcFactory.cs b/src/SharpCompress/Factories/ArcFactory.cs index 75558ce7..46c12666 100644 --- a/src/SharpCompress/Factories/ArcFactory.cs +++ b/src/SharpCompress/Factories/ArcFactory.cs @@ -76,7 +76,7 @@ public class ArcFactory : Factory, IReaderFactory var buffer = ArrayPool.Shared.Rent(2); try { - await stream.ReadExactAsync(buffer, 0, 2, cancellationToken); + await stream.ReadExactAsync(buffer, 0, 2, cancellationToken).ConfigureAwait(false); return buffer[0] == 0x1A && buffer[1] < 10; //rather thin, but this is all we have } finally diff --git a/src/SharpCompress/Factories/Factory.cs b/src/SharpCompress/Factories/Factory.cs index 7bca8ecb..69254cad 100644 --- a/src/SharpCompress/Factories/Factory.cs +++ b/src/SharpCompress/Factories/Factory.cs @@ -18,6 +18,7 @@ public abstract class Factory : IFactory RegisterFactory(new RarFactory()); RegisterFactory(new TarFactory()); //put tar before most RegisterFactory(new GZipFactory()); + RegisterFactory(new LzwFactory()); RegisterFactory(new ArcFactory()); RegisterFactory(new ArjFactory()); RegisterFactory(new AceFactory()); diff --git a/src/SharpCompress/Factories/GZipFactory.cs b/src/SharpCompress/Factories/GZipFactory.cs index f68673b0..6d38d418 100644 --- a/src/SharpCompress/Factories/GZipFactory.cs +++ b/src/SharpCompress/Factories/GZipFactory.cs @@ -27,7 +27,7 @@ public class GZipFactory IMultiArchiveFactory, IReaderFactory, IWriterFactory, - IWriteableArchiveFactory + IWriteableArchiveFactory { #region IFactory @@ -95,13 +95,8 @@ public class GZipFactory /// public IAsyncArchive OpenAsyncArchive( IReadOnlyList fileInfos, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) - { - cancellationToken.ThrowIfCancellationRequested(); - return (IAsyncArchive)OpenArchive(fileInfos, readerOptions); - } + ReaderOptions? readerOptions = null + ) => (IAsyncArchive)OpenArchive(fileInfos, readerOptions); #endregion @@ -193,7 +188,7 @@ public class GZipFactory #region IWriteableArchiveFactory /// - public IWritableArchive CreateArchive() => GZipArchive.CreateArchive(); + public IWritableArchive CreateArchive() => GZipArchive.CreateArchive(); #endregion } diff --git a/src/SharpCompress/Factories/LzwFactory.cs b/src/SharpCompress/Factories/LzwFactory.cs new file mode 100644 index 00000000..078fad37 --- /dev/null +++ b/src/SharpCompress/Factories/LzwFactory.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using SharpCompress.Archives.Tar; +using SharpCompress.Common; +using SharpCompress.Compressors.Lzw; +using SharpCompress.IO; +using SharpCompress.Readers; +using SharpCompress.Readers.Lzw; +using SharpCompress.Readers.Tar; + +namespace SharpCompress.Factories; + +/// +/// Represents the foundation factory of LZW archive. +/// +public class LzwFactory : Factory, IReaderFactory +{ + #region IFactory + + /// + public override string Name => "Lzw"; + + /// + public override ArchiveType? KnownArchiveType => ArchiveType.Lzw; + + /// + public override IEnumerable GetSupportedExtensions() + { + yield return "z"; + } + + /// + public override bool IsArchive(Stream stream, string? password = null) => + LzwStream.IsLzwStream(stream); + + /// + public override ValueTask IsArchiveAsync( + Stream stream, + string? password = null, + CancellationToken cancellationToken = default + ) => LzwStream.IsLzwStreamAsync(stream, cancellationToken); + + #endregion + + #region IReaderFactory + + /// + internal override bool TryOpenReader( + SharpCompressStream sharpCompressStream, + ReaderOptions options, + out IReader? reader + ) + { + reader = null; + + if (LzwStream.IsLzwStream(sharpCompressStream)) + { + sharpCompressStream.Rewind(); + using (var testStream = new LzwStream(sharpCompressStream) { IsStreamOwner = false }) + { + if (TarArchive.IsTarFile(testStream)) + { + sharpCompressStream.StopRecording(); + reader = new TarReader(sharpCompressStream, options, CompressionType.Lzw); + return true; + } + } + sharpCompressStream.StopRecording(); + reader = OpenReader(sharpCompressStream, options); + return true; + } + sharpCompressStream.Rewind(); + return false; + } + + /// + public IReader OpenReader(Stream stream, ReaderOptions? options) => + LzwReader.OpenReader(stream, options); + + /// + public ValueTask OpenAsyncReader( + Stream stream, + ReaderOptions? options, + CancellationToken cancellationToken = default + ) + { + cancellationToken.ThrowIfCancellationRequested(); + return new(LzwReader.OpenAsyncReader(stream, options)); + } + + #endregion +} diff --git a/src/SharpCompress/Factories/RarFactory.cs b/src/SharpCompress/Factories/RarFactory.cs index 11fe6cf8..f3236d2e 100644 --- a/src/SharpCompress/Factories/RarFactory.cs +++ b/src/SharpCompress/Factories/RarFactory.cs @@ -90,11 +90,9 @@ public class RarFactory : Factory, IArchiveFactory, IMultiArchiveFactory, IReade /// public IAsyncArchive OpenAsyncArchive( IReadOnlyList fileInfos, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default + ReaderOptions? readerOptions = null ) { - cancellationToken.ThrowIfCancellationRequested(); return (IAsyncArchive)OpenArchive(fileInfos, readerOptions); } diff --git a/src/SharpCompress/Factories/SevenZipFactory.cs b/src/SharpCompress/Factories/SevenZipFactory.cs index f8f4b779..0a276223 100644 --- a/src/SharpCompress/Factories/SevenZipFactory.cs +++ b/src/SharpCompress/Factories/SevenZipFactory.cs @@ -50,7 +50,7 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory /// public IAsyncArchive OpenAsyncArchive(Stream stream, ReaderOptions? readerOptions = null) => - SevenZipArchive.OpenAsyncArchive(stream, readerOptions, CancellationToken.None); + SevenZipArchive.OpenAsyncArchive(stream, readerOptions); /// public IArchive OpenArchive(FileInfo fileInfo, ReaderOptions? readerOptions = null) => @@ -58,7 +58,7 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory /// public IAsyncArchive OpenAsyncArchive(FileInfo fileInfo, ReaderOptions? readerOptions = null) => - SevenZipArchive.OpenAsyncArchive(fileInfo, readerOptions, CancellationToken.None); + SevenZipArchive.OpenAsyncArchive(fileInfo, readerOptions); #endregion @@ -74,7 +74,7 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory public IAsyncArchive OpenAsyncArchive( IReadOnlyList streams, ReaderOptions? readerOptions = null - ) => SevenZipArchive.OpenAsyncArchive(streams, readerOptions, CancellationToken.None); + ) => SevenZipArchive.OpenAsyncArchive(streams, readerOptions); /// public IArchive OpenArchive( @@ -85,9 +85,8 @@ public class SevenZipFactory : Factory, IArchiveFactory, IMultiArchiveFactory /// public IAsyncArchive OpenAsyncArchive( IReadOnlyList fileInfos, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) => SevenZipArchive.OpenAsyncArchive(fileInfos, readerOptions, cancellationToken); + ReaderOptions? readerOptions = null + ) => SevenZipArchive.OpenAsyncArchive(fileInfos, readerOptions); #endregion diff --git a/src/SharpCompress/Factories/TarFactory.cs b/src/SharpCompress/Factories/TarFactory.cs index 3ef9f840..abc800e1 100644 --- a/src/SharpCompress/Factories/TarFactory.cs +++ b/src/SharpCompress/Factories/TarFactory.cs @@ -25,7 +25,7 @@ public class TarFactory IMultiArchiveFactory, IReaderFactory, IWriterFactory, - IWriteableArchiveFactory + IWriteableArchiveFactory { #region IFactory @@ -82,14 +82,21 @@ public class TarFactory foreach (var wrapper in TarWrapper.Wrappers) { sharpCompressStream.Rewind(); - if (await wrapper.IsMatchAsync(sharpCompressStream, cancellationToken)) + if ( + await wrapper + .IsMatchAsync(sharpCompressStream, cancellationToken) + .ConfigureAwait(false) + ) { sharpCompressStream.Rewind(); - var decompressedStream = await wrapper.CreateStreamAsync( - sharpCompressStream, - cancellationToken - ); - if (await TarArchive.IsTarFileAsync(decompressedStream, cancellationToken)) + var decompressedStream = await wrapper + .CreateStreamAsync(sharpCompressStream, cancellationToken) + .ConfigureAwait(false); + if ( + await TarArchive + .IsTarFileAsync(decompressedStream, cancellationToken) + .ConfigureAwait(false) + ) { sharpCompressStream.Rewind(); return true; @@ -145,13 +152,8 @@ public class TarFactory /// public IAsyncArchive OpenAsyncArchive( IReadOnlyList fileInfos, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) - { - cancellationToken.ThrowIfCancellationRequested(); - return (IAsyncArchive)OpenArchive(fileInfos, readerOptions); - } + ReaderOptions? readerOptions = null + ) => (IAsyncArchive)OpenArchive(fileInfos, readerOptions); #endregion @@ -194,14 +196,21 @@ public class TarFactory foreach (var wrapper in TarWrapper.Wrappers) { sharpCompressStream.Rewind(); - if (await wrapper.IsMatchAsync(sharpCompressStream, cancellationToken)) + if ( + await wrapper + .IsMatchAsync(sharpCompressStream, cancellationToken) + .ConfigureAwait(false) + ) { sharpCompressStream.Rewind(); - var decompressedStream = await wrapper.CreateStreamAsync( - sharpCompressStream, - cancellationToken - ); - if (await TarArchive.IsTarFileAsync(decompressedStream, cancellationToken)) + var decompressedStream = await wrapper + .CreateStreamAsync(sharpCompressStream, cancellationToken) + .ConfigureAwait(false); + if ( + await TarArchive + .IsTarFileAsync(decompressedStream, cancellationToken) + .ConfigureAwait(false) + ) { sharpCompressStream.Rewind(); sharpCompressStream.StopRecording(); @@ -247,7 +256,7 @@ public class TarFactory #region IWriteableArchiveFactory /// - public IWritableArchive CreateArchive() => TarArchive.CreateArchive(); + public IWritableArchive CreateArchive() => TarArchive.CreateArchive(); #endregion } diff --git a/src/SharpCompress/Factories/TarWrapper.cs b/src/SharpCompress/Factories/TarWrapper.cs index 2f3ff64a..fee9707e 100644 --- a/src/SharpCompress/Factories/TarWrapper.cs +++ b/src/SharpCompress/Factories/TarWrapper.cs @@ -54,7 +54,9 @@ public class TarWrapper( BZip2Stream.IsBZip2Async, (stream) => BZip2Stream.Create(stream, CompressionMode.Decompress, false), async (stream, _) => - await BZip2Stream.CreateAsync(stream, CompressionMode.Decompress, false), + await BZip2Stream + .CreateAsync(stream, CompressionMode.Decompress, false) + .ConfigureAwait(false), ["tar.bz2", "tb2", "tbz", "tbz2", "tz2"] ), new( diff --git a/src/SharpCompress/Factories/ZipFactory.cs b/src/SharpCompress/Factories/ZipFactory.cs index b793d010..6b7df689 100644 --- a/src/SharpCompress/Factories/ZipFactory.cs +++ b/src/SharpCompress/Factories/ZipFactory.cs @@ -24,7 +24,7 @@ public class ZipFactory IMultiArchiveFactory, IReaderFactory, IWriterFactory, - IWriteableArchiveFactory + IWriteableArchiveFactory { #region IFactory @@ -82,7 +82,11 @@ public class ZipFactory var startPosition = stream.CanSeek ? stream.Position : -1; // probe for single volume zip - if (await ZipArchive.IsZipFileAsync(stream, password, cancellationToken)) + if ( + await ZipArchive + .IsZipFileAsync(stream, password, cancellationToken) + .ConfigureAwait(false) + ) { return true; } @@ -96,7 +100,11 @@ public class ZipFactory stream.Position = startPosition; //test the zip (last) file of a multipart zip - if (await ZipArchive.IsZipMultiAsync(stream, password, cancellationToken)) + if ( + await ZipArchive + .IsZipMultiAsync(stream, password, cancellationToken) + .ConfigureAwait(false) + ) { return true; } @@ -155,13 +163,8 @@ public class ZipFactory /// public IAsyncArchive OpenAsyncArchive( IReadOnlyList fileInfos, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) - { - cancellationToken.ThrowIfCancellationRequested(); - return (IAsyncArchive)OpenArchive(fileInfos, readerOptions); - } + ReaderOptions? readerOptions = null + ) => (IAsyncArchive)OpenArchive(fileInfos, readerOptions); #endregion @@ -217,7 +220,7 @@ public class ZipFactory #region IWriteableArchiveFactory /// - public IWritableArchive CreateArchive() => ZipArchive.CreateArchive(); + public IWritableArchive CreateArchive() => ZipArchive.CreateArchive(); #endregion } diff --git a/src/SharpCompress/IO/SharpCompressStream.Async.cs b/src/SharpCompress/IO/SharpCompressStream.Async.cs index 3544a649..e18a47a1 100644 --- a/src/SharpCompress/IO/SharpCompressStream.Async.cs +++ b/src/SharpCompress/IO/SharpCompressStream.Async.cs @@ -243,9 +243,16 @@ internal partial class SharpCompressStream { byte[] buffer = new byte[bufferSize]; int bytesRead; - while ((bytesRead = await ReadAsync(buffer, 0, buffer.Length, cancellationToken)) != 0) + while ( + ( + bytesRead = await ReadAsync(buffer, 0, buffer.Length, cancellationToken) + .ConfigureAwait(false) + ) != 0 + ) { - await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken); + await destination + .WriteAsync(buffer, 0, bytesRead, cancellationToken) + .ConfigureAwait(false); } } @@ -263,7 +270,7 @@ internal partial class SharpCompressStream isDisposed = true; if (!LeaveStreamOpen) { - await stream.DisposeAsync(); + await stream.DisposeAsync().ConfigureAwait(false); } _ringBuffer?.Dispose(); _ringBuffer = null; diff --git a/src/SharpCompress/LazyAsyncReadOnlyCollection.cs b/src/SharpCompress/LazyAsyncReadOnlyCollection.cs index 114bc71a..6d152593 100644 --- a/src/SharpCompress/LazyAsyncReadOnlyCollection.cs +++ b/src/SharpCompress/LazyAsyncReadOnlyCollection.cs @@ -41,7 +41,7 @@ internal sealed class LazyAsyncReadOnlyCollection(IAsyncEnumerable source) } if ( !lazyReadOnlyCollection._fullyLoaded - && await lazyReadOnlyCollection._source.MoveNextAsync() + && await lazyReadOnlyCollection._source.MoveNextAsync().ConfigureAwait(false) ) { lazyReadOnlyCollection._backing.Add(lazyReadOnlyCollection._source.Current); @@ -76,7 +76,7 @@ internal sealed class LazyAsyncReadOnlyCollection(IAsyncEnumerable source) if (!_fullyLoaded) { var loader = new LazyLoader(this, CancellationToken.None); - while (await loader.MoveNextAsync()) + while (await loader.MoveNextAsync().ConfigureAwait(false)) { // Intentionally empty } diff --git a/src/SharpCompress/Polyfills/AsyncEnumerableExtensions.cs b/src/SharpCompress/Polyfills/AsyncEnumerableExtensions.cs index 9d0dad0a..6df982a0 100644 --- a/src/SharpCompress/Polyfills/AsyncEnumerableExtensions.cs +++ b/src/SharpCompress/Polyfills/AsyncEnumerableExtensions.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -12,7 +10,7 @@ public static class AsyncEnumerableEx public static async IAsyncEnumerable Empty() where T : notnull { - await Task.CompletedTask; + await Task.Yield(); yield break; } } @@ -21,7 +19,7 @@ public static class EnumerableExtensions { public static async IAsyncEnumerable ToAsyncEnumerable(this IEnumerable source) { - await Task.CompletedTask; + await Task.Yield(); foreach (var item in source) { yield return item; @@ -47,7 +45,7 @@ public static class AsyncEnumerableExtensions await using var e = source.GetAsyncEnumerator(cancellationToken); var count = 0; - while (await e.MoveNextAsync()) + while (await e.MoveNextAsync().ConfigureAwait(false)) { checked { @@ -117,12 +115,12 @@ public static class AsyncEnumerableExtensions enumerator = source.Where(predicate).GetAsyncEnumerator(); } - if (!await enumerator.MoveNextAsync()) + if (!await enumerator.MoveNextAsync().ConfigureAwait(false)) { throw new InvalidOperationException("The source sequence is empty."); } var value = enumerator.Current; - if (await enumerator.MoveNextAsync()) + if (await enumerator.MoveNextAsync().ConfigureAwait(false)) { throw new InvalidOperationException( "The source sequence contains more than one element." diff --git a/src/SharpCompress/Readers/AbstractReader.Async.cs b/src/SharpCompress/Readers/AbstractReader.Async.cs index 958045e0..dc73e6fc 100644 --- a/src/SharpCompress/Readers/AbstractReader.Async.cs +++ b/src/SharpCompress/Readers/AbstractReader.Async.cs @@ -17,13 +17,13 @@ public abstract partial class AbstractReader { if (_entriesForCurrentReadStreamAsync is not null) { - await _entriesForCurrentReadStreamAsync.DisposeAsync(); + await _entriesForCurrentReadStreamAsync.DisposeAsync().ConfigureAwait(false); } // If Volume implements IAsyncDisposable, use async disposal if (Volume is IAsyncDisposable asyncDisposable) { - await asyncDisposable.DisposeAsync(); + await asyncDisposable.DisposeAsync().ConfigureAwait(false); } else { @@ -43,14 +43,14 @@ public abstract partial class AbstractReader } if (_entriesForCurrentReadStreamAsync is null) { - return await LoadStreamForReadingAsync(RequestInitialStream()); + return await LoadStreamForReadingAsync(RequestInitialStream()).ConfigureAwait(false); } if (!_wroteCurrentEntry) { await SkipEntryAsync(cancellationToken).ConfigureAwait(false); } _wroteCurrentEntry = false; - if (await NextEntryForCurrentStreamAsync(cancellationToken)) + if (await NextEntryForCurrentStreamAsync(cancellationToken).ConfigureAwait(false)) { return true; } @@ -62,7 +62,7 @@ public abstract partial class AbstractReader { if (_entriesForCurrentReadStreamAsync is not null) { - await _entriesForCurrentReadStreamAsync.DisposeAsync(); + await _entriesForCurrentReadStreamAsync.DisposeAsync().ConfigureAwait(false); } if (stream is null || !stream.CanRead) { @@ -73,7 +73,7 @@ public abstract partial class AbstractReader ); } _entriesForCurrentReadStreamAsync = GetEntriesAsync(stream).GetAsyncEnumerator(); - return await _entriesForCurrentReadStreamAsync.MoveNextAsync(); + return await _entriesForCurrentReadStreamAsync.MoveNextAsync().ConfigureAwait(false); } private async ValueTask SkipEntryAsync(CancellationToken cancellationToken) @@ -202,7 +202,6 @@ public abstract partial class AbstractReader // Async iterator method protected virtual async IAsyncEnumerable GetEntriesAsync(Stream stream) { - await Task.CompletedTask; foreach (var entry in GetEntries(stream)) { yield return entry; diff --git a/src/SharpCompress/Readers/Ace/AceReader.Factory.cs b/src/SharpCompress/Readers/Ace/AceReader.Factory.cs index 8daa344e..9f873d77 100644 --- a/src/SharpCompress/Readers/Ace/AceReader.Factory.cs +++ b/src/SharpCompress/Readers/Ace/AceReader.Factory.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.IO; -using System.Threading; using SharpCompress.Common; namespace SharpCompress.Readers.Ace; @@ -34,24 +33,14 @@ public partial class AceReader return new MultiVolumeAceReader(streams, options ?? new ReaderOptions()); } - public static IAsyncReader OpenAsyncReader( - string path, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) + public static IAsyncReader OpenAsyncReader(string path, ReaderOptions? readerOptions = null) { - cancellationToken.ThrowIfCancellationRequested(); path.NotNullOrEmpty(nameof(path)); return (IAsyncReader)OpenReader(new FileInfo(path), readerOptions); } - public static IAsyncReader OpenAsyncReader( - Stream stream, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) + public static IAsyncReader OpenAsyncReader(Stream stream, ReaderOptions? readerOptions = null) { - cancellationToken.ThrowIfCancellationRequested(); return (IAsyncReader)OpenReader(stream, readerOptions); } @@ -66,11 +55,9 @@ public partial class AceReader public static IAsyncReader OpenAsyncReader( FileInfo fileInfo, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default + ReaderOptions? readerOptions = null ) { - cancellationToken.ThrowIfCancellationRequested(); return (IAsyncReader)OpenReader(fileInfo, readerOptions); } diff --git a/src/SharpCompress/Readers/Ace/AceReader.cs b/src/SharpCompress/Readers/Ace/AceReader.cs index 46f045b5..351e8566 100644 --- a/src/SharpCompress/Readers/Ace/AceReader.cs +++ b/src/SharpCompress/Readers/Ace/AceReader.cs @@ -74,7 +74,7 @@ public abstract partial class AceReader : AbstractReader break; } - yield return new AceEntry(new AceFilePart((AceFileHeader)localHeader, stream)); + yield return new AceEntry(new AceFilePart((AceFileHeader)localHeader, stream), Options); } } @@ -88,7 +88,7 @@ public abstract partial class AceReader : AbstractReader } var mainHeaderReader = new AceMainHeader(_archiveEncoding); - var mainHeader = await mainHeaderReader.ReadAsync(stream); + var mainHeader = await mainHeaderReader.ReadAsync(stream).ConfigureAwait(false); if (mainHeader == null) { yield break; @@ -102,7 +102,7 @@ public abstract partial class AceReader : AbstractReader var localHeaderReader = new AceFileHeader(_archiveEncoding); while (true) { - var localHeader = await localHeaderReader.ReadAsync(stream); + var localHeader = await localHeaderReader.ReadAsync(stream).ConfigureAwait(false); if (localHeader?.IsFileEncrypted == true) { throw new CryptographicException( @@ -114,7 +114,7 @@ public abstract partial class AceReader : AbstractReader break; } - yield return new AceEntry(new AceFilePart((AceFileHeader)localHeader, stream)); + yield return new AceEntry(new AceFilePart((AceFileHeader)localHeader, stream), Options); } } diff --git a/src/SharpCompress/Readers/Arc/ArcReader.Async.cs b/src/SharpCompress/Readers/Arc/ArcReader.Async.cs index ebb0e269..004135cd 100644 --- a/src/SharpCompress/Readers/Arc/ArcReader.Async.cs +++ b/src/SharpCompress/Readers/Arc/ArcReader.Async.cs @@ -12,10 +12,14 @@ public partial class ArcReader ArcEntryHeader headerReader = new ArcEntryHeader(Options.ArchiveEncoding); ArcEntryHeader? header; while ( - (header = await headerReader.ReadHeaderAsync(stream, CancellationToken.None)) != null + ( + header = await headerReader + .ReadHeaderAsync(stream, CancellationToken.None) + .ConfigureAwait(false) + ) != null ) { - yield return new ArcEntry(new ArcFilePart(header, stream)); + yield return new ArcEntry(new ArcFilePart(header, stream), Options); } } } diff --git a/src/SharpCompress/Readers/Arc/ArcReader.Factory.cs b/src/SharpCompress/Readers/Arc/ArcReader.Factory.cs index 22ec2666..abfad14a 100644 --- a/src/SharpCompress/Readers/Arc/ArcReader.Factory.cs +++ b/src/SharpCompress/Readers/Arc/ArcReader.Factory.cs @@ -1,40 +1,27 @@ #if NET8_0_OR_GREATER using System.IO; -using System.Threading; using SharpCompress.Common; namespace SharpCompress.Readers.Arc; public partial class ArcReader : IReaderOpenable { - public static IAsyncReader OpenAsyncReader( - string path, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) + public static IAsyncReader OpenAsyncReader(string path, ReaderOptions? readerOptions = null) { - cancellationToken.ThrowIfCancellationRequested(); path.NotNullOrEmpty(nameof(path)); return (IAsyncReader)OpenReader(new FileInfo(path), readerOptions); } - public static IAsyncReader OpenAsyncReader( - Stream stream, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) + public static IAsyncReader OpenAsyncReader(Stream stream, ReaderOptions? readerOptions = null) { - cancellationToken.ThrowIfCancellationRequested(); return (IAsyncReader)OpenReader(stream, readerOptions); } public static IAsyncReader OpenAsyncReader( FileInfo fileInfo, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default + ReaderOptions? readerOptions = null ) { - cancellationToken.ThrowIfCancellationRequested(); return (IAsyncReader)OpenReader(fileInfo, readerOptions); } diff --git a/src/SharpCompress/Readers/Arc/ArcReader.cs b/src/SharpCompress/Readers/Arc/ArcReader.cs index ee9d5fa3..d641b554 100644 --- a/src/SharpCompress/Readers/Arc/ArcReader.cs +++ b/src/SharpCompress/Readers/Arc/ArcReader.cs @@ -34,7 +34,7 @@ public partial class ArcReader : AbstractReader ArcEntryHeader? header; while ((header = headerReader.ReadHeader(stream)) != null) { - yield return new ArcEntry(new ArcFilePart(header, stream)); + yield return new ArcEntry(new ArcFilePart(header, stream), Options); } } } diff --git a/src/SharpCompress/Readers/Arj/ArjReader.Factory.cs b/src/SharpCompress/Readers/Arj/ArjReader.Factory.cs index f0f7b01b..7a84f4c2 100644 --- a/src/SharpCompress/Readers/Arj/ArjReader.Factory.cs +++ b/src/SharpCompress/Readers/Arj/ArjReader.Factory.cs @@ -1,40 +1,27 @@ #if NET8_0_OR_GREATER using System.IO; -using System.Threading; using SharpCompress.Common; namespace SharpCompress.Readers.Arj; public partial class ArjReader : IReaderOpenable { - public static IAsyncReader OpenAsyncReader( - string path, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) + public static IAsyncReader OpenAsyncReader(string path, ReaderOptions? readerOptions = null) { - cancellationToken.ThrowIfCancellationRequested(); path.NotNullOrEmpty(nameof(path)); return (IAsyncReader)OpenReader(new FileInfo(path), readerOptions); } - public static IAsyncReader OpenAsyncReader( - Stream stream, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) + public static IAsyncReader OpenAsyncReader(Stream stream, ReaderOptions? readerOptions = null) { - cancellationToken.ThrowIfCancellationRequested(); return (IAsyncReader)OpenReader(stream, readerOptions); } public static IAsyncReader OpenAsyncReader( FileInfo fileInfo, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default + ReaderOptions? readerOptions = null ) { - cancellationToken.ThrowIfCancellationRequested(); return (IAsyncReader)OpenReader(fileInfo, readerOptions); } diff --git a/src/SharpCompress/Readers/Arj/ArjReader.cs b/src/SharpCompress/Readers/Arj/ArjReader.cs index 9a589925..857a5ec9 100644 --- a/src/SharpCompress/Readers/Arj/ArjReader.cs +++ b/src/SharpCompress/Readers/Arj/ArjReader.cs @@ -90,7 +90,10 @@ public abstract partial class ArjReader : AbstractReader continue; } - yield return new ArjEntry(new ArjFilePart((ArjLocalHeader)localHeader, stream)); + yield return new ArjEntry( + new ArjFilePart((ArjLocalHeader)localHeader, stream), + Options + ); } } @@ -100,7 +103,7 @@ public abstract partial class ArjReader : AbstractReader var mainHeaderReader = new ArjMainHeader(encoding); var localHeaderReader = new ArjLocalHeader(encoding); - var mainHeader = await mainHeaderReader.ReadAsync(stream); + var mainHeader = await mainHeaderReader.ReadAsync(stream).ConfigureAwait(false); if (mainHeader?.IsVolume == true) { throw new MultiVolumeExtractionException("Multi volumes are currently not supported"); @@ -120,7 +123,7 @@ public abstract partial class ArjReader : AbstractReader while (true) { - var localHeader = await localHeaderReader.ReadAsync(stream); + var localHeader = await localHeaderReader.ReadAsync(stream).ConfigureAwait(false); if (localHeader == null) { break; @@ -135,7 +138,10 @@ public abstract partial class ArjReader : AbstractReader continue; } - yield return new ArjEntry(new ArjFilePart((ArjLocalHeader)localHeader, stream)); + yield return new ArjEntry( + new ArjFilePart((ArjLocalHeader)localHeader, stream), + Options + ); } } diff --git a/src/SharpCompress/Readers/GZip/GZipReader.Factory.cs b/src/SharpCompress/Readers/GZip/GZipReader.Factory.cs index 3132d139..55e96e73 100644 --- a/src/SharpCompress/Readers/GZip/GZipReader.Factory.cs +++ b/src/SharpCompress/Readers/GZip/GZipReader.Factory.cs @@ -1,5 +1,4 @@ using System.IO; -using System.Threading; namespace SharpCompress.Readers.GZip; @@ -8,34 +7,22 @@ public partial class GZipReader : IReaderOpenable #endif { - public static IAsyncReader OpenAsyncReader( - string path, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) + public static IAsyncReader OpenAsyncReader(string path, ReaderOptions? readerOptions = null) { - cancellationToken.ThrowIfCancellationRequested(); path.NotNullOrEmpty(nameof(path)); return (IAsyncReader)OpenReader(new FileInfo(path), readerOptions); } - public static IAsyncReader OpenAsyncReader( - Stream stream, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) + public static IAsyncReader OpenAsyncReader(Stream stream, ReaderOptions? readerOptions = null) { - cancellationToken.ThrowIfCancellationRequested(); return (IAsyncReader)OpenReader(stream, readerOptions); } public static IAsyncReader OpenAsyncReader( FileInfo fileInfo, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default + ReaderOptions? readerOptions = null ) { - cancellationToken.ThrowIfCancellationRequested(); return (IAsyncReader)OpenReader(fileInfo, readerOptions); } diff --git a/src/SharpCompress/Readers/IAsyncReaderExtensions.cs b/src/SharpCompress/Readers/IAsyncReaderExtensions.cs index be53d630..07a7db8d 100644 --- a/src/SharpCompress/Readers/IAsyncReaderExtensions.cs +++ b/src/SharpCompress/Readers/IAsyncReaderExtensions.cs @@ -14,15 +14,14 @@ public static class IAsyncReaderExtensions /// public async ValueTask WriteEntryToDirectoryAsync( string destinationDirectory, - ExtractionOptions? options = null, CancellationToken cancellationToken = default ) => await ExtractionMethods .WriteEntryToDirectoryAsync( reader.Entry, destinationDirectory, - options, - reader.WriteEntryToFileAsync, + async (path, ct) => + await reader.WriteEntryToFileAsync(path, ct).ConfigureAwait(false), cancellationToken ) .ConfigureAwait(false); @@ -32,14 +31,12 @@ public static class IAsyncReaderExtensions /// public async ValueTask 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); @@ -54,28 +51,25 @@ public static class IAsyncReaderExtensions /// public async ValueTask WriteAllToDirectoryAsync( string destinationDirectory, - ExtractionOptions? options = null, CancellationToken cancellationToken = default ) { - while (await reader.MoveToNextEntryAsync(cancellationToken)) + while (await reader.MoveToNextEntryAsync(cancellationToken).ConfigureAwait(false)) { await reader - .WriteEntryToDirectoryAsync(destinationDirectory, options, cancellationToken) + .WriteEntryToDirectoryAsync(destinationDirectory, cancellationToken) .ConfigureAwait(false); } } public async ValueTask WriteEntryToAsync( 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); @@ -87,11 +81,10 @@ public static class IAsyncReaderExtensions public async ValueTask WriteEntryToAsync( FileInfo destinationFileInfo, - ExtractionOptions? options = null, CancellationToken cancellationToken = default ) => await reader - .WriteEntryToAsync(destinationFileInfo.FullName, options, cancellationToken) + .WriteEntryToAsync(destinationFileInfo.FullName, cancellationToken) .ConfigureAwait(false); } } diff --git a/src/SharpCompress/Readers/IReaderExtensions.cs b/src/SharpCompress/Readers/IReaderExtensions.cs index cfa7c13a..436696bf 100644 --- a/src/SharpCompress/Readers/IReaderExtensions.cs +++ b/src/SharpCompress/Readers/IReaderExtensions.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using SharpCompress.Common; namespace SharpCompress.Readers; @@ -22,42 +22,31 @@ public static class IReaderExtensions /// /// Extract all remaining unread entries to specific directory, retaining filename /// - public void WriteAllToDirectory( - string destinationDirectory, - ExtractionOptions? options = null - ) + public void WriteAllToDirectory(string destinationDirectory) { while (reader.MoveToNextEntry()) { - reader.WriteEntryToDirectory(destinationDirectory, options); + reader.WriteEntryToDirectory(destinationDirectory); } } /// /// Extract to specific directory, retaining filename /// - public void WriteEntryToDirectory( - string destinationDirectory, - ExtractionOptions? options = null - ) => + public void WriteEntryToDirectory(string destinationDirectory) => ExtractionMethods.WriteEntryToDirectory( reader.Entry, destinationDirectory, - options, - reader.WriteEntryToFile + (path) => reader.WriteEntryToFile(path) ); /// /// Extract to specific file /// - public void WriteEntryToFile( - string destinationFileName, - ExtractionOptions? options = null - ) => + public void WriteEntryToFile(string destinationFileName) => ExtractionMethods.WriteEntryToFile( reader.Entry, destinationFileName, - options, (x, fm) => { using var fs = File.Open(destinationFileName, fm); diff --git a/src/SharpCompress/Readers/IReaderOpenable.cs b/src/SharpCompress/Readers/IReaderOpenable.cs index a421a49e..ea42b827 100644 --- a/src/SharpCompress/Readers/IReaderOpenable.cs +++ b/src/SharpCompress/Readers/IReaderOpenable.cs @@ -17,20 +17,17 @@ public interface IReaderOpenable public static abstract IAsyncReader OpenAsyncReader( string path, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default + ReaderOptions? readerOptions = null ); public static abstract IAsyncReader OpenAsyncReader( Stream stream, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default + ReaderOptions? readerOptions = null ); public static abstract IAsyncReader OpenAsyncReader( FileInfo fileInfo, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default + ReaderOptions? readerOptions = null ); } #endif diff --git a/src/SharpCompress/Readers/Lzw/LzwReader.Async.cs b/src/SharpCompress/Readers/Lzw/LzwReader.Async.cs new file mode 100644 index 00000000..6479c9b5 --- /dev/null +++ b/src/SharpCompress/Readers/Lzw/LzwReader.Async.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.IO; +using SharpCompress.Common; +using SharpCompress.Common.Lzw; + +namespace SharpCompress.Readers.Lzw; + +public partial class LzwReader +{ + /// + /// Returns entries asynchronously for streams that only support async reads. + /// + protected override IAsyncEnumerable GetEntriesAsync(Stream stream) => + LzwEntry.GetEntriesAsync(stream, Options); +} diff --git a/src/SharpCompress/Readers/Lzw/LzwReader.Factory.cs b/src/SharpCompress/Readers/Lzw/LzwReader.Factory.cs new file mode 100644 index 00000000..344a9572 --- /dev/null +++ b/src/SharpCompress/Readers/Lzw/LzwReader.Factory.cs @@ -0,0 +1,46 @@ +using System.IO; + +namespace SharpCompress.Readers.Lzw; + +public partial class LzwReader +#if NET8_0_OR_GREATER + : IReaderOpenable +#endif +{ + public static IAsyncReader OpenAsyncReader(string path, ReaderOptions? readerOptions = null) + { + path.NotNullOrEmpty(nameof(path)); + return (IAsyncReader)OpenReader(new FileInfo(path), readerOptions); + } + + public static IAsyncReader OpenAsyncReader(Stream stream, ReaderOptions? readerOptions = null) + { + return (IAsyncReader)OpenReader(stream, readerOptions); + } + + public static IAsyncReader OpenAsyncReader( + FileInfo fileInfo, + ReaderOptions? readerOptions = null + ) + { + return (IAsyncReader)OpenReader(fileInfo, readerOptions); + } + + public static IReader OpenReader(string filePath, ReaderOptions? readerOptions = null) + { + filePath.NotNullOrEmpty(nameof(filePath)); + return OpenReader(new FileInfo(filePath), readerOptions); + } + + public static IReader OpenReader(FileInfo fileInfo, ReaderOptions? readerOptions = null) + { + fileInfo.NotNull(nameof(fileInfo)); + return OpenReader(fileInfo.OpenRead(), readerOptions); + } + + public static IReader OpenReader(Stream stream, ReaderOptions? options = null) + { + stream.NotNull(nameof(stream)); + return new LzwReader(stream, options ?? new ReaderOptions()); + } +} diff --git a/src/SharpCompress/Readers/Lzw/LzwReader.cs b/src/SharpCompress/Readers/Lzw/LzwReader.cs new file mode 100644 index 00000000..875faf7a --- /dev/null +++ b/src/SharpCompress/Readers/Lzw/LzwReader.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.IO; +using SharpCompress.Common; +using SharpCompress.Common.Lzw; + +namespace SharpCompress.Readers.Lzw; + +public partial class LzwReader : AbstractReader +{ + private LzwReader(Stream stream, ReaderOptions options) + : base(options, ArchiveType.Lzw) => Volume = new LzwVolume(stream, options, 0); + + public override LzwVolume Volume { get; } + + protected override IEnumerable GetEntries(Stream stream) => + LzwEntry.GetEntries(stream, Options); + + // GetEntriesAsync moved to LzwReader.Async.cs +} diff --git a/src/SharpCompress/Readers/Rar/MultiVolumeRarReader.Async.cs b/src/SharpCompress/Readers/Rar/MultiVolumeRarReader.Async.cs index 7107adba..1f9d4463 100644 --- a/src/SharpCompress/Readers/Rar/MultiVolumeRarReader.Async.cs +++ b/src/SharpCompress/Readers/Rar/MultiVolumeRarReader.Async.cs @@ -57,7 +57,7 @@ internal partial class MultiVolumeRarReader : RarReader } if (tempStream != null) { - await reader.LoadStreamForReadingAsync(tempStream); + await reader.LoadStreamForReadingAsync(tempStream).ConfigureAwait(false); tempStream = null; } else if (!nextReadableStreams.MoveNext()) @@ -68,7 +68,9 @@ internal partial class MultiVolumeRarReader : RarReader } else { - await reader.LoadStreamForReadingAsync(nextReadableStreams.Current); + await reader + .LoadStreamForReadingAsync(nextReadableStreams.Current) + .ConfigureAwait(false); } Current = reader.Entry.Parts.First(); diff --git a/src/SharpCompress/Readers/Rar/RarReader.Async.cs b/src/SharpCompress/Readers/Rar/RarReader.Async.cs index 0e1943d2..9ccc5231 100644 --- a/src/SharpCompress/Readers/Rar/RarReader.Async.cs +++ b/src/SharpCompress/Readers/Rar/RarReader.Async.cs @@ -32,9 +32,9 @@ public abstract partial class RarReader throw new InvalidOperationException("no stream for redirect entry"); } - var stream = await MultiVolumeReadOnlyAsyncStream.Create( - CreateFilePartEnumerableForCurrentEntryAsync().CastAsync() - ); + var stream = await MultiVolumeReadOnlyAsyncStream + .Create(CreateFilePartEnumerableForCurrentEntryAsync().CastAsync()) + .ConfigureAwait(false); if (Entry.IsRarV3) { return CreateEntryStream( diff --git a/src/SharpCompress/Readers/Rar/RarReader.Factory.cs b/src/SharpCompress/Readers/Rar/RarReader.Factory.cs index 775b3764..5fa1cba0 100644 --- a/src/SharpCompress/Readers/Rar/RarReader.Factory.cs +++ b/src/SharpCompress/Readers/Rar/RarReader.Factory.cs @@ -1,40 +1,27 @@ #if NET8_0_OR_GREATER using System.IO; -using System.Threading; using SharpCompress.Common; namespace SharpCompress.Readers.Rar; public partial class RarReader : IReaderOpenable { - public static IAsyncReader OpenAsyncReader( - string path, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) + public static IAsyncReader OpenAsyncReader(string path, ReaderOptions? readerOptions = null) { - cancellationToken.ThrowIfCancellationRequested(); path.NotNullOrEmpty(nameof(path)); return (IAsyncReader)OpenReader(new FileInfo(path), readerOptions); } - public static IAsyncReader OpenAsyncReader( - Stream stream, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) + public static IAsyncReader OpenAsyncReader(Stream stream, ReaderOptions? readerOptions = null) { - cancellationToken.ThrowIfCancellationRequested(); return (IAsyncReader)OpenReader(stream, readerOptions); } public static IAsyncReader OpenAsyncReader( FileInfo fileInfo, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default + ReaderOptions? readerOptions = null ) { - cancellationToken.ThrowIfCancellationRequested(); return (IAsyncReader)OpenReader(fileInfo, readerOptions); } } diff --git a/src/SharpCompress/Readers/Rar/RarReader.cs b/src/SharpCompress/Readers/Rar/RarReader.cs index cd0aebc9..741f7155 100644 --- a/src/SharpCompress/Readers/Rar/RarReader.cs +++ b/src/SharpCompress/Readers/Rar/RarReader.cs @@ -93,7 +93,7 @@ public abstract partial class RarReader : AbstractReader /// -/// This class is immutable. Use the with expression to create modified copies: +/// This class is immutable. Use factory presets and fluent helpers for common configurations: /// -/// var options = new ReaderOptions { Password = "secret" }; -/// options = options with { LeaveStreamOpen = false }; +/// var options = ReaderOptions.ForExternalStream() +/// .WithPassword("secret") +/// .WithLookForHeader(true); +/// +/// Or use object initializers for simple cases: +/// +/// var options = new ReaderOptions { Password = "secret", LeaveStreamOpen = false }; /// /// public sealed record ReaderOptions : IReaderOptions @@ -112,6 +117,38 @@ public sealed record ReaderOptions : IReaderOptions public int? RewindableBufferSize { get; init; } /// + /// Overwrite target if it exists. + /// Breaking change: Default changed from false to true in version 0.40.0. + /// + public bool Overwrite { get; init; } = true; + + /// + /// Extract with internal directory structure. + /// Breaking change: Default changed from false to true in version 0.40.0. + /// + public bool ExtractFullPath { get; init; } = true; + + /// + /// Preserve file time. + /// Breaking change: Default changed from false to true in version 0.40.0. + /// + public bool PreserveFileTime { get; init; } = true; + + /// + /// Preserve windows file attributes. + /// + public bool PreserveAttributes { get; init; } + + /// + /// Delegate for writing symbolic links to disk. + /// The first parameter is the source path (where the symlink is created). + /// The second parameter is the target path (what the symlink refers to). + /// + /// + /// Breaking change: Changed from field to init-only property in version 0.40.0. + /// The default handler logs a warning message. + /// + public Action? SymbolicLinkHandler { get; init; } /// Optional registry of compression providers. /// If null, the default registry (SharpCompress internal implementations) will be used. /// Use this to provide alternative decompression implementations, such as @@ -125,65 +162,61 @@ public sealed record ReaderOptions : IReaderOptions public ReaderOptions() { } /// - /// Creates a new ReaderOptions instance with the specified password. + /// Gets ReaderOptions configured for caller-provided streams. /// - /// The password for encrypted archives. - public ReaderOptions(string? password) => Password = password; + public static ReaderOptions ForExternalStream => new() { LeaveStreamOpen = true }; /// - /// Creates a new ReaderOptions instance with the specified password and header search option. + /// Gets ReaderOptions configured for file-based overloads that open their own stream. /// - /// The password for encrypted archives. - /// Whether to search for the archive header. - public ReaderOptions(string? password, bool lookForHeader) - { - Password = password; - LookForHeader = lookForHeader; - } + public static ReaderOptions ForOwnedFile => new() { LeaveStreamOpen = false }; /// - /// Creates a new ReaderOptions instance with the specified encoding. + /// Gets a ReaderOptions instance configured for safe extraction (no overwrite). + /// + public static ReaderOptions SafeExtract => new() { Overwrite = false }; + + /// + /// Gets a ReaderOptions instance configured for flat extraction (no directory structure). + /// + public static ReaderOptions FlatExtract => new() { ExtractFullPath = false, Overwrite = true }; + + /// + /// Creates ReaderOptions for reading encrypted archives. + /// + /// The password for encrypted archives. + public static ReaderOptions ForEncryptedArchive(string? password = null) => + new ReaderOptions().WithPassword(password); + + /// + /// Creates ReaderOptions for archives with custom character encoding. /// /// The encoding for archive entry names. - public ReaderOptions(IArchiveEncoding encoding) => ArchiveEncoding = encoding; + public static ReaderOptions ForEncoding(IArchiveEncoding encoding) => + new ReaderOptions().WithArchiveEncoding(encoding); /// - /// Creates a new ReaderOptions instance with the specified password and encoding. + /// Creates ReaderOptions for self-extracting archives that require header search. /// - /// The password for encrypted archives. - /// The encoding for archive entry names. - public ReaderOptions(string? password, IArchiveEncoding encoding) - { - Password = password; - ArchiveEncoding = encoding; - } + public static ReaderOptions ForSelfExtractingArchive(string? password = null) => + new ReaderOptions() + .WithLookForHeader(true) + .WithPassword(password) + .WithRewindableBufferSize(1_048_576); // 1MB for SFX archives /// - /// Creates a new ReaderOptions instance with the specified stream open behavior. + /// Default symbolic link handler that logs a warning message. /// - /// Whether to leave the stream open after reading. - public ReaderOptions(bool leaveStreamOpen) + public static void DefaultSymbolicLinkHandler(string sourcePath, string targetPath) { - LeaveStreamOpen = leaveStreamOpen; + Console.WriteLine( + $"Could not write symlink {sourcePath} -> {targetPath}, for more information please see https://github.com/dotnet/runtime/issues/24271" + ); } - /// - /// Creates a new ReaderOptions instance with the specified stream open behavior and password. - /// - /// Whether to leave the stream open after reading. - /// The password for encrypted archives. - public ReaderOptions(bool leaveStreamOpen, string? password) - { - LeaveStreamOpen = leaveStreamOpen; - Password = password; - } - - /// - /// Creates a new ReaderOptions instance with the specified buffer size. - /// - /// The buffer size for stream operations. - public ReaderOptions(int bufferSize) - { - BufferSize = bufferSize; - } + // Note: Parameterized constructors have been removed. + // Use fluent With*() helpers or object initializers instead: + // new ReaderOptions().WithPassword("secret").WithLookForHeader(true) + // or + // new ReaderOptions { Password = "secret", LookForHeader = true } } diff --git a/src/SharpCompress/Readers/ReaderOptionsExtensions.cs b/src/SharpCompress/Readers/ReaderOptionsExtensions.cs new file mode 100644 index 00000000..26a8dfbe --- /dev/null +++ b/src/SharpCompress/Readers/ReaderOptionsExtensions.cs @@ -0,0 +1,127 @@ +using System; +using SharpCompress.Common; +using SharpCompress.Common.Options; + +namespace SharpCompress.Readers; + +/// +/// Extension methods for fluent configuration of reader options. +/// +public static class ReaderOptionsExtensions +{ + /// + /// Creates a copy with the specified LeaveStreamOpen value. + /// + public static ReaderOptions WithLeaveStreamOpen( + this ReaderOptions options, + bool leaveStreamOpen + ) => options with { LeaveStreamOpen = leaveStreamOpen }; + + /// + /// Creates a copy with the specified password. + /// + public static ReaderOptions WithPassword(this ReaderOptions options, string? password) => + options with + { + Password = password, + }; + + /// + /// Creates a copy with the specified archive encoding. + /// + public static ReaderOptions WithArchiveEncoding( + this ReaderOptions options, + IArchiveEncoding encoding + ) => options with { ArchiveEncoding = encoding }; + + /// + /// Creates a copy with the specified LookForHeader value. + /// + public static ReaderOptions WithLookForHeader(this ReaderOptions options, bool lookForHeader) => + options with + { + LookForHeader = lookForHeader, + }; + + /// + /// Creates a copy with the specified DisableCheckIncomplete value. + /// + public static ReaderOptions WithDisableCheckIncomplete( + this ReaderOptions options, + bool disableCheckIncomplete + ) => options with { DisableCheckIncomplete = disableCheckIncomplete }; + + /// + /// Creates a copy with the specified buffer size. + /// + public static ReaderOptions WithBufferSize(this ReaderOptions options, int bufferSize) => + options with + { + BufferSize = bufferSize, + }; + + /// + /// Creates a copy with the specified extension hint. + /// + public static ReaderOptions WithExtensionHint( + this ReaderOptions options, + string? extensionHint + ) => options with { ExtensionHint = extensionHint }; + + /// + /// Creates a copy with the specified progress reporter. + /// + public static ReaderOptions WithProgress( + this ReaderOptions options, + IProgress? progress + ) => options with { Progress = progress }; + + /// + /// Creates a copy with the specified rewindable buffer size. + /// + public static ReaderOptions WithRewindableBufferSize( + this ReaderOptions options, + int? rewindableBufferSize + ) => options with { RewindableBufferSize = rewindableBufferSize }; + + /// + /// Creates a copy with the specified overwrite setting. + /// + public static ReaderOptions WithOverwrite(this ReaderOptions options, bool overwrite) => + options with + { + Overwrite = overwrite, + }; + + /// + /// Creates a copy with the specified extract full path setting. + /// + public static ReaderOptions WithExtractFullPath( + this ReaderOptions options, + bool extractFullPath + ) => options with { ExtractFullPath = extractFullPath }; + + /// + /// Creates a copy with the specified preserve file time setting. + /// + public static ReaderOptions WithPreserveFileTime( + this ReaderOptions options, + bool preserveFileTime + ) => options with { PreserveFileTime = preserveFileTime }; + + /// + /// Creates a copy with the specified preserve attributes setting. + /// + public static ReaderOptions WithPreserveAttributes( + this ReaderOptions options, + bool preserveAttributes + ) => options with { PreserveAttributes = preserveAttributes }; + + /// + /// Creates a copy with the specified symbolic link handler. + /// + public static ReaderOptions WithSymbolicLinkHandler( + this ReaderOptions options, + Action? handler + ) => options with { SymbolicLinkHandler = handler }; +} diff --git a/src/SharpCompress/Readers/Tar/TarReader.Async.cs b/src/SharpCompress/Readers/Tar/TarReader.Async.cs index 60026308..6684bdab 100644 --- a/src/SharpCompress/Readers/Tar/TarReader.Async.cs +++ b/src/SharpCompress/Readers/Tar/TarReader.Async.cs @@ -26,6 +26,7 @@ public partial class TarReader StreamingMode.Streaming, stream, compressionType, - Options.ArchiveEncoding + Options.ArchiveEncoding, + Options ); } diff --git a/src/SharpCompress/Readers/Tar/TarReader.Factory.cs b/src/SharpCompress/Readers/Tar/TarReader.Factory.cs index 801440a7..b8f41e60 100644 --- a/src/SharpCompress/Readers/Tar/TarReader.Factory.cs +++ b/src/SharpCompress/Readers/Tar/TarReader.Factory.cs @@ -1,5 +1,4 @@ using System.IO; -using System.Threading; using SharpCompress.Common; namespace SharpCompress.Readers.Tar; @@ -9,34 +8,22 @@ public partial class TarReader : IReaderOpenable #endif { - public static IAsyncReader OpenAsyncReader( - string path, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) + public static IAsyncReader OpenAsyncReader(string path, ReaderOptions? readerOptions = null) { - cancellationToken.ThrowIfCancellationRequested(); path.NotNullOrEmpty(nameof(path)); return (IAsyncReader)OpenReader(new FileInfo(path), readerOptions); } - public static IAsyncReader OpenAsyncReader( - Stream stream, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) + public static IAsyncReader OpenAsyncReader(Stream stream, ReaderOptions? readerOptions = null) { - cancellationToken.ThrowIfCancellationRequested(); return (IAsyncReader)OpenReader(stream, readerOptions); } public static IAsyncReader OpenAsyncReader( FileInfo fileInfo, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default + ReaderOptions? readerOptions = null ) { - cancellationToken.ThrowIfCancellationRequested(); return (IAsyncReader)OpenReader(fileInfo, readerOptions); } diff --git a/src/SharpCompress/Readers/Tar/TarReader.cs b/src/SharpCompress/Readers/Tar/TarReader.cs index 3b8b5e6e..08fa9e52 100644 --- a/src/SharpCompress/Readers/Tar/TarReader.cs +++ b/src/SharpCompress/Readers/Tar/TarReader.cs @@ -134,7 +134,8 @@ public partial class TarReader : AbstractReader StreamingMode.Streaming, stream, compressionType, - Options.ArchiveEncoding + Options.ArchiveEncoding, + Options ); // GetEntriesAsync moved to TarReader.Async.cs diff --git a/src/SharpCompress/Readers/Zip/ZipReader.Async.cs b/src/SharpCompress/Readers/Zip/ZipReader.Async.cs index ed38eac0..5bb8a116 100644 --- a/src/SharpCompress/Readers/Zip/ZipReader.Async.cs +++ b/src/SharpCompress/Readers/Zip/ZipReader.Async.cs @@ -4,6 +4,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; using SharpCompress.Common; +using SharpCompress.Common.Options; using SharpCompress.Common.Zip; using SharpCompress.Common.Zip.Headers; using SharpCompress.Compressors; @@ -19,28 +20,25 @@ public partial class ZipReader { private readonly StreamingZipHeaderFactory _headerFactory; private readonly Stream _stream; + private readonly IReaderOptions _options; private readonly CompressionProviderRegistry? _compressionProviders; public ZipEntryAsyncEnumerable( StreamingZipHeaderFactory headerFactory, Stream stream, + IReaderOptions options CompressionProviderRegistry? compressionProviders ) { _headerFactory = headerFactory; _stream = stream; + _options = options; _compressionProviders = compressionProviders; } public IAsyncEnumerator GetAsyncEnumerator( CancellationToken cancellationToken = default - ) => - new ZipEntryAsyncEnumerator( - _headerFactory, - _stream, - _compressionProviders, - cancellationToken - ); + ) => new ZipEntryAsyncEnumerator(_headerFactory, _stream, _options, cancellationToken); } /// @@ -50,17 +48,20 @@ public partial class ZipReader { private readonly Stream _stream; private readonly IAsyncEnumerator _headerEnumerator; + private readonly IReaderOptions _options; private readonly CompressionProviderRegistry? _compressionProviders; private ZipEntry? _current; public ZipEntryAsyncEnumerator( StreamingZipHeaderFactory headerFactory, Stream stream, + IReaderOptions options, CompressionProviderRegistry? compressionProviders, CancellationToken cancellationToken ) { _stream = stream; + _options = options; _compressionProviders = compressionProviders; _headerEnumerator = headerFactory .ReadStreamHeaderAsync(stream) @@ -83,11 +84,8 @@ public partial class ZipReader { case ZipHeaderType.LocalEntry: _current = new ZipEntry( - new StreamingZipFilePart( - (LocalEntryHeader)header, - _stream, - _compressionProviders - ) + new StreamingZipFilePart((LocalEntryHeader)header, _stream), + _options ); return true; case ZipHeaderType.DirectoryEntry: diff --git a/src/SharpCompress/Readers/Zip/ZipReader.Factory.cs b/src/SharpCompress/Readers/Zip/ZipReader.Factory.cs index 289059fc..f03ea4ab 100644 --- a/src/SharpCompress/Readers/Zip/ZipReader.Factory.cs +++ b/src/SharpCompress/Readers/Zip/ZipReader.Factory.cs @@ -1,40 +1,27 @@ #if NET8_0_OR_GREATER using System.IO; -using System.Threading; using SharpCompress.Common; namespace SharpCompress.Readers.Zip; public partial class ZipReader : IReaderOpenable { - public static IAsyncReader OpenAsyncReader( - string path, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) + public static IAsyncReader OpenAsyncReader(string path, ReaderOptions? readerOptions = null) { - cancellationToken.ThrowIfCancellationRequested(); path.NotNullOrEmpty(nameof(path)); return (IAsyncReader)OpenReader(new FileInfo(path), readerOptions); } - public static IAsyncReader OpenAsyncReader( - Stream stream, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default - ) + public static IAsyncReader OpenAsyncReader(Stream stream, ReaderOptions? readerOptions = null) { - cancellationToken.ThrowIfCancellationRequested(); return (IAsyncReader)OpenReader(stream, readerOptions); } public static IAsyncReader OpenAsyncReader( FileInfo fileInfo, - ReaderOptions? readerOptions = null, - CancellationToken cancellationToken = default + ReaderOptions? readerOptions = null ) { - cancellationToken.ThrowIfCancellationRequested(); return (IAsyncReader)OpenReader(fileInfo, readerOptions); } diff --git a/src/SharpCompress/Readers/Zip/ZipReader.cs b/src/SharpCompress/Readers/Zip/ZipReader.cs index 89227a34..803dad0c 100644 --- a/src/SharpCompress/Readers/Zip/ZipReader.cs +++ b/src/SharpCompress/Readers/Zip/ZipReader.cs @@ -74,11 +74,8 @@ public partial class ZipReader : AbstractReader case ZipHeaderType.LocalEntry: { yield return new ZipEntry( - new StreamingZipFilePart( - (LocalEntryHeader)h, - stream, - Options.CompressionProviders - ) + new StreamingZipFilePart((LocalEntryHeader)h, stream), + Options ); } break; @@ -103,7 +100,7 @@ public partial class ZipReader : AbstractReader /// Returns entries asynchronously for streams that only support async reads. /// protected override IAsyncEnumerable GetEntriesAsync(Stream stream) => - new ZipEntryAsyncEnumerable(_headerFactory, stream, Options.CompressionProviders); + new ZipEntryAsyncEnumerable(_headerFactory, stream, Options); // Async nested classes moved to ZipReader.Async.cs } diff --git a/src/SharpCompress/Writers/CompressionLevelValidation.cs b/src/SharpCompress/Writers/CompressionLevelValidation.cs new file mode 100644 index 00000000..ab964191 --- /dev/null +++ b/src/SharpCompress/Writers/CompressionLevelValidation.cs @@ -0,0 +1,49 @@ +using System; +using SharpCompress.Common; + +namespace SharpCompress.Writers; + +internal static class CompressionLevelValidation +{ + public static void Validate(CompressionType compressionType, int compressionLevel) + { + switch (compressionType) + { + case CompressionType.Deflate: + case CompressionType.Deflate64: + case CompressionType.GZip: + EnsureRange(compressionLevel, 0, 9, compressionType); + break; + case CompressionType.ZStandard: + EnsureRange(compressionLevel, 1, 22, compressionType); + break; + default: + if (compressionLevel != 0) + { + throw new ArgumentOutOfRangeException( + nameof(compressionLevel), + compressionLevel, + $"Compression type {compressionType} does not support configurable compression levels. Use 0." + ); + } + break; + } + } + + private static void EnsureRange( + int compressionLevel, + int minInclusive, + int maxInclusive, + CompressionType compressionType + ) + { + if (compressionLevel < minInclusive || compressionLevel > maxInclusive) + { + throw new ArgumentOutOfRangeException( + nameof(compressionLevel), + compressionLevel, + $"Compression level for {compressionType} must be between {minInclusive} and {maxInclusive}." + ); + } + } +} diff --git a/src/SharpCompress/Writers/GZip/GZipWriter.Async.cs b/src/SharpCompress/Writers/GZip/GZipWriter.Async.cs index 81b9d825..478373aa 100644 --- a/src/SharpCompress/Writers/GZip/GZipWriter.Async.cs +++ b/src/SharpCompress/Writers/GZip/GZipWriter.Async.cs @@ -24,9 +24,9 @@ public partial class GZipWriter stream.LastModified = modificationTime; var progressStream = WrapWithProgress(source, filename); #if LEGACY_DOTNET - await progressStream.CopyToAsync(stream); + await progressStream.CopyToAsync(stream).ConfigureAwait(false); #else - await progressStream.CopyToAsync(stream, cancellationToken); + await progressStream.CopyToAsync(stream, cancellationToken).ConfigureAwait(false); #endif _wroteToStream = true; } diff --git a/src/SharpCompress/Writers/GZip/GZipWriter.Factory.cs b/src/SharpCompress/Writers/GZip/GZipWriter.Factory.cs index 7fd8aec1..715bdc22 100644 --- a/src/SharpCompress/Writers/GZip/GZipWriter.Factory.cs +++ b/src/SharpCompress/Writers/GZip/GZipWriter.Factory.cs @@ -1,6 +1,5 @@ #if NET8_0_OR_GREATER using System.IO; -using System.Threading; using SharpCompress.Common; namespace SharpCompress.Writers.GZip; @@ -25,33 +24,18 @@ public partial class GZipWriter : IWriterOpenable return new GZipWriter(stream, writerOptions); } - public static IAsyncWriter OpenAsyncWriter( - string path, - GZipWriterOptions writerOptions, - CancellationToken cancellationToken = default - ) + public static IAsyncWriter OpenAsyncWriter(string path, GZipWriterOptions writerOptions) { - cancellationToken.ThrowIfCancellationRequested(); return (IAsyncWriter)OpenWriter(path, writerOptions); } - public static IAsyncWriter OpenAsyncWriter( - Stream stream, - GZipWriterOptions writerOptions, - CancellationToken cancellationToken = default - ) + public static IAsyncWriter OpenAsyncWriter(Stream stream, GZipWriterOptions writerOptions) { - cancellationToken.ThrowIfCancellationRequested(); return (IAsyncWriter)OpenWriter(stream, writerOptions); } - public static IAsyncWriter OpenAsyncWriter( - FileInfo fileInfo, - GZipWriterOptions writerOptions, - CancellationToken cancellationToken = default - ) + public static IAsyncWriter OpenAsyncWriter(FileInfo fileInfo, GZipWriterOptions writerOptions) { - cancellationToken.ThrowIfCancellationRequested(); return (IAsyncWriter)OpenWriter(fileInfo, writerOptions); } } diff --git a/src/SharpCompress/Writers/GZip/GZipWriterOptions.cs b/src/SharpCompress/Writers/GZip/GZipWriterOptions.cs index 7303f7d1..0c795a3a 100644 --- a/src/SharpCompress/Writers/GZip/GZipWriterOptions.cs +++ b/src/SharpCompress/Writers/GZip/GZipWriterOptions.cs @@ -1,7 +1,7 @@ using System; using SharpCompress.Common; using SharpCompress.Common.Options; -using SharpCompress.Compressors; +using SharpCompress.Writers; using D = SharpCompress.Compressors.Deflate; namespace SharpCompress.Writers.GZip; @@ -10,23 +10,46 @@ namespace SharpCompress.Writers.GZip; /// Options for configuring GZip writer behavior. /// /// -/// This class is immutable. Use the with expression to create modified copies: +/// This class is immutable. Use factory methods for creation: /// -/// var options = new GZipWriterOptions { CompressionLevel = 9 }; -/// options = options with { LeaveStreamOpen = false }; +/// var options = WriterOptions.ForGZip().WithLeaveStreamOpen(false).WithCompressionLevel(9); /// /// public sealed record GZipWriterOptions : IWriterOptions { + private int _compressionLevel = (int)D.CompressionLevel.Default; + /// /// The compression type (always GZip for this writer). /// - public CompressionType CompressionType { get; init; } = CompressionType.GZip; + public CompressionType CompressionType + { + get => CompressionType.GZip; + init + { + if (value != CompressionType.GZip) + { + throw new ArgumentOutOfRangeException( + nameof(CompressionType), + value, + "GZipWriterOptions only supports CompressionType.GZip." + ); + } + } + } /// /// The compression level to be used (0-9 for Deflate). /// - public int CompressionLevel { get; init; } = (int)D.CompressionLevel.Default; + public int CompressionLevel + { + get => _compressionLevel; + init + { + CompressionLevelValidation.Validate(CompressionType.GZip, value); + _compressionLevel = value; + } + } /// /// SharpCompress will keep the supplied streams open. Default is true. @@ -74,14 +97,11 @@ public sealed record GZipWriterOptions : IWriterOptions CompressionLevel = (int)compressionLevel; } - /// - /// Creates a new GZipWriterOptions instance with the specified stream open behavior. - /// - /// Whether to leave the stream open after writing. - public GZipWriterOptions(bool leaveStreamOpen) - { - LeaveStreamOpen = leaveStreamOpen; - } + // Note: Constructor with boolean leaveStreamOpen parameter removed. + // Use the fluent WithLeaveStreamOpen() helper or object initializer instead: + // new GZipWriterOptions() { LeaveStreamOpen = false } + // or + // WriterOptions.ForGZip().WithLeaveStreamOpen(false) /// /// Creates a new GZipWriterOptions instance from an existing WriterOptions instance. diff --git a/src/SharpCompress/Writers/IWriterOpenable.cs b/src/SharpCompress/Writers/IWriterOpenable.cs index fe40cda7..828b0b7c 100644 --- a/src/SharpCompress/Writers/IWriterOpenable.cs +++ b/src/SharpCompress/Writers/IWriterOpenable.cs @@ -19,24 +19,20 @@ public interface IWriterOpenable /// The stream to write to. /// The archive type. /// Writer options. - /// Cancellation token. /// A task that returns an IWriter. public static abstract IAsyncWriter OpenAsyncWriter( Stream stream, - TWriterOptions writerOptions, - CancellationToken cancellationToken = default + TWriterOptions writerOptions ); public static abstract IAsyncWriter OpenAsyncWriter( string filePath, - TWriterOptions writerOptions, - CancellationToken cancellationToken = default + TWriterOptions writerOptions ); public static abstract IAsyncWriter OpenAsyncWriter( FileInfo fileInfo, - TWriterOptions writerOptions, - CancellationToken cancellationToken = default + TWriterOptions writerOptions ); } #endif diff --git a/src/SharpCompress/Writers/Tar/TarWriter.Async.cs b/src/SharpCompress/Writers/Tar/TarWriter.Async.cs index 5cc6c622..db5e00d4 100644 --- a/src/SharpCompress/Writers/Tar/TarWriter.Async.cs +++ b/src/SharpCompress/Writers/Tar/TarWriter.Async.cs @@ -40,7 +40,9 @@ public partial class TarWriter Stream source, DateTime? modificationTime, CancellationToken cancellationToken = default - ) => await WriteAsync(filename, source, modificationTime, null, cancellationToken); + ) => + await WriteAsync(filename, source, modificationTime, null, cancellationToken) + .ConfigureAwait(false); /// /// Asynchronously writes a file entry with optional size specification. diff --git a/src/SharpCompress/Writers/Tar/TarWriter.Factory.cs b/src/SharpCompress/Writers/Tar/TarWriter.Factory.cs index c5f9c846..d7077478 100644 --- a/src/SharpCompress/Writers/Tar/TarWriter.Factory.cs +++ b/src/SharpCompress/Writers/Tar/TarWriter.Factory.cs @@ -1,6 +1,5 @@ #if NET8_0_OR_GREATER using System.IO; -using System.Threading; using SharpCompress.Common; namespace SharpCompress.Writers.Tar; @@ -25,33 +24,18 @@ public partial class TarWriter : IWriterOpenable return new TarWriter(stream, writerOptions); } - public static IAsyncWriter OpenAsyncWriter( - string path, - TarWriterOptions writerOptions, - CancellationToken cancellationToken = default - ) + public static IAsyncWriter OpenAsyncWriter(string path, TarWriterOptions writerOptions) { - cancellationToken.ThrowIfCancellationRequested(); return (IAsyncWriter)OpenWriter(path, writerOptions); } - public static IAsyncWriter OpenAsyncWriter( - Stream stream, - TarWriterOptions writerOptions, - CancellationToken cancellationToken = default - ) + public static IAsyncWriter OpenAsyncWriter(Stream stream, TarWriterOptions writerOptions) { - cancellationToken.ThrowIfCancellationRequested(); return (IAsyncWriter)OpenWriter(stream, writerOptions); } - public static IAsyncWriter OpenAsyncWriter( - FileInfo fileInfo, - TarWriterOptions writerOptions, - CancellationToken cancellationToken = default - ) + public static IAsyncWriter OpenAsyncWriter(FileInfo fileInfo, TarWriterOptions writerOptions) { - cancellationToken.ThrowIfCancellationRequested(); return (IAsyncWriter)OpenWriter(fileInfo, writerOptions); } } diff --git a/src/SharpCompress/Writers/WriterOptions.cs b/src/SharpCompress/Writers/WriterOptions.cs index 2904ae77..b0d19b36 100644 --- a/src/SharpCompress/Writers/WriterOptions.cs +++ b/src/SharpCompress/Writers/WriterOptions.cs @@ -10,18 +10,24 @@ namespace SharpCompress.Writers; /// Options for configuring writer behavior when creating archives. /// /// -/// This class is immutable. Use the with expression to create modified copies: +/// This class is immutable. Use factory methods for creation: /// -/// var options = new WriterOptions(CompressionType.Zip); -/// options = options with { LeaveStreamOpen = false }; +/// var options = WriterOptions.ForZip().WithLeaveStreamOpen(false).WithCompressionLevel(9); /// /// public sealed record WriterOptions : IWriterOptions { + private CompressionType _compressionType; + private int _compressionLevel; + /// /// The compression type to use for the archive. /// - public CompressionType CompressionType { get; init; } + public CompressionType CompressionType + { + get => _compressionType; + init => _compressionType = value; + } /// /// The compression level to be used when the compression type supports variable levels. @@ -31,7 +37,15 @@ public sealed record WriterOptions : IWriterOptions /// Note: BZip2 and LZMA do not support compression levels in this implementation. /// Defaults are set automatically based on compression type in the constructor. /// - public int CompressionLevel { get; init; } + public int CompressionLevel + { + get => _compressionLevel; + init + { + CompressionLevelValidation.Validate(CompressionType, value); + _compressionLevel = value; + } + } /// /// SharpCompress will keep the supplied streams open. Default is true. @@ -86,32 +100,11 @@ public sealed record WriterOptions : IWriterOptions CompressionLevel = compressionLevel; } - /// - /// Creates a new WriterOptions instance with the specified compression type and stream open behavior. - /// - /// The compression type for the archive. - /// Whether to leave the stream open after writing. - public WriterOptions(CompressionType compressionType, bool leaveStreamOpen) - : this(compressionType) - { - LeaveStreamOpen = leaveStreamOpen; - } - - /// - /// Creates a new WriterOptions instance with the specified compression type, level, and stream open behavior. - /// - /// The compression type for the archive. - /// The compression level (algorithm-specific). - /// Whether to leave the stream open after writing. - public WriterOptions( - CompressionType compressionType, - int compressionLevel, - bool leaveStreamOpen - ) - : this(compressionType, compressionLevel) - { - LeaveStreamOpen = leaveStreamOpen; - } + // Note: Constructors with boolean leaveStreamOpen parameter removed. + // Use the fluent WithLeaveStreamOpen() helper or object initializer instead: + // new WriterOptions(type) { LeaveStreamOpen = false } + // or + // WriterOptions.ForZip().WithLeaveStreamOpen(false) /// /// Implicit conversion from CompressionType to WriterOptions. @@ -119,4 +112,23 @@ public sealed record WriterOptions : IWriterOptions /// The compression type. public static implicit operator WriterOptions(CompressionType compressionType) => new(compressionType); + + /// + /// Creates a new ZipWriterOptions for writing ZIP archives. + /// + /// The compression type for the archive. Defaults to Deflate. + public static WriterOptions ForZip(CompressionType compressionType = CompressionType.Deflate) => + new(compressionType); + + /// + /// Creates a new WriterOptions for writing TAR archives. + /// + /// The compression type for the archive. Defaults to None. + public static WriterOptions ForTar(CompressionType compressionType = CompressionType.None) => + new(compressionType); + + /// + /// Creates a new WriterOptions for writing GZip compressed files. + /// + public static WriterOptions ForGZip() => new(CompressionType.GZip); } diff --git a/src/SharpCompress/Writers/WriterOptionsExtensions.cs b/src/SharpCompress/Writers/WriterOptionsExtensions.cs new file mode 100644 index 00000000..d86b5406 --- /dev/null +++ b/src/SharpCompress/Writers/WriterOptionsExtensions.cs @@ -0,0 +1,55 @@ +using System; +using SharpCompress.Common; +using SharpCompress.Common.Options; + +namespace SharpCompress.Writers; + +/// +/// Extension methods for fluent configuration of writer options. +/// +public static class WriterOptionsExtensions +{ + /// + /// Creates a copy with the specified LeaveStreamOpen value. + /// + /// The source options. + /// Whether to leave the stream open. + /// A new options instance with the specified LeaveStreamOpen value. + public static WriterOptions WithLeaveStreamOpen( + this WriterOptions options, + bool leaveStreamOpen + ) => options with { LeaveStreamOpen = leaveStreamOpen }; + + /// + /// Creates a copy with the specified compression level. + /// + /// The source options. + /// The compression level (algorithm-specific). + /// A new options instance with the specified compression level. + public static WriterOptions WithCompressionLevel( + this WriterOptions options, + int compressionLevel + ) => options with { CompressionLevel = compressionLevel }; + + /// + /// Creates a copy with the specified archive encoding. + /// + /// The source options. + /// The archive encoding to use. + /// A new options instance with the specified archive encoding. + public static WriterOptions WithArchiveEncoding( + this WriterOptions options, + IArchiveEncoding archiveEncoding + ) => options with { ArchiveEncoding = archiveEncoding }; + + /// + /// Creates a copy with the specified progress reporter. + /// + /// The source options. + /// The progress reporter. + /// A new options instance with the specified progress reporter. + public static WriterOptions WithProgress( + this WriterOptions options, + IProgress progress + ) => options with { Progress = progress }; +} diff --git a/src/SharpCompress/Writers/Zip/ZipWriter.Factory.cs b/src/SharpCompress/Writers/Zip/ZipWriter.Factory.cs index 0df1a81c..c4083aea 100644 --- a/src/SharpCompress/Writers/Zip/ZipWriter.Factory.cs +++ b/src/SharpCompress/Writers/Zip/ZipWriter.Factory.cs @@ -1,6 +1,5 @@ #if NET8_0_OR_GREATER using System.IO; -using System.Threading; using SharpCompress.Common; namespace SharpCompress.Writers.Zip; @@ -25,33 +24,18 @@ public partial class ZipWriter : IWriterOpenable return new ZipWriter(stream, writerOptions); } - public static IAsyncWriter OpenAsyncWriter( - string path, - ZipWriterOptions writerOptions, - CancellationToken cancellationToken = default - ) + public static IAsyncWriter OpenAsyncWriter(string path, ZipWriterOptions writerOptions) { - cancellationToken.ThrowIfCancellationRequested(); return (IAsyncWriter)OpenWriter(path, writerOptions); } - public static IAsyncWriter OpenAsyncWriter( - Stream stream, - ZipWriterOptions writerOptions, - CancellationToken cancellationToken = default - ) + public static IAsyncWriter OpenAsyncWriter(Stream stream, ZipWriterOptions writerOptions) { - cancellationToken.ThrowIfCancellationRequested(); return (IAsyncWriter)OpenWriter(stream, writerOptions); } - public static IAsyncWriter OpenAsyncWriter( - FileInfo fileInfo, - ZipWriterOptions writerOptions, - CancellationToken cancellationToken = default - ) + public static IAsyncWriter OpenAsyncWriter(FileInfo fileInfo, ZipWriterOptions writerOptions) { - cancellationToken.ThrowIfCancellationRequested(); return (IAsyncWriter)OpenWriter(fileInfo, writerOptions); } } diff --git a/src/SharpCompress/Writers/Zip/ZipWriter.cs b/src/SharpCompress/Writers/Zip/ZipWriter.cs index a34a78a7..3473a42c 100644 --- a/src/SharpCompress/Writers/Zip/ZipWriter.cs +++ b/src/SharpCompress/Writers/Zip/ZipWriter.cs @@ -93,6 +93,7 @@ public partial class ZipWriter : AbstractWriter public Stream WriteToStream(string entryPath, ZipWriterEntryOptions options) { + options.ValidateWithFallback(compressionType, compressionLevel); var compression = ToZipCompressionMethod(options.CompressionType ?? compressionType); entryPath = NormalizeFilename(entryPath); diff --git a/src/SharpCompress/Writers/Zip/ZipWriterEntryOptions.cs b/src/SharpCompress/Writers/Zip/ZipWriterEntryOptions.cs index dcadb21c..d51f2bd1 100644 --- a/src/SharpCompress/Writers/Zip/ZipWriterEntryOptions.cs +++ b/src/SharpCompress/Writers/Zip/ZipWriterEntryOptions.cs @@ -1,12 +1,27 @@ using System; using SharpCompress.Common; using SharpCompress.Compressors.Deflate; +using SharpCompress.Writers; namespace SharpCompress.Writers.Zip; public class ZipWriterEntryOptions { - public CompressionType? CompressionType { get; set; } + private CompressionType? compressionType; + private int? compressionLevel; + + public CompressionType? CompressionType + { + get => compressionType; + set + { + if (value.HasValue && compressionLevel.HasValue) + { + CompressionLevelValidation.Validate(value.Value, compressionLevel.Value); + } + compressionType = value; + } + } /// /// The compression level to be used when the compression type supports variable levels. @@ -16,7 +31,18 @@ public class ZipWriterEntryOptions /// When null, uses the archive's default compression level for the specified compression type. /// Note: BZip2 and LZMA do not support compression levels in this implementation. /// - public int? CompressionLevel { get; set; } + public int? CompressionLevel + { + get => compressionLevel; + set + { + if (value.HasValue && compressionType.HasValue) + { + CompressionLevelValidation.Validate(compressionType.Value, value.Value); + } + compressionLevel = value; + } + } /// /// When CompressionType.Deflate is used, this property is referenced. @@ -49,4 +75,12 @@ public class ZipWriterEntryOptions /// This option is not supported with non-seekable streams. /// public bool? EnableZip64 { get; set; } + + internal void ValidateWithFallback(CompressionType fallbackCompressionType, int fallbackLevel) + { + CompressionLevelValidation.Validate( + CompressionType ?? fallbackCompressionType, + CompressionLevel ?? fallbackLevel + ); + } } diff --git a/src/SharpCompress/Writers/Zip/ZipWriterOptions.cs b/src/SharpCompress/Writers/Zip/ZipWriterOptions.cs index c67f91d5..2e9cf8fd 100644 --- a/src/SharpCompress/Writers/Zip/ZipWriterOptions.cs +++ b/src/SharpCompress/Writers/Zip/ZipWriterOptions.cs @@ -3,6 +3,7 @@ using SharpCompress.Common; using SharpCompress.Common.Options; using SharpCompress.Compressors; using SharpCompress.Compressors.Deflate; +using SharpCompress.Writers; using D = SharpCompress.Compressors.Deflate; namespace SharpCompress.Writers.Zip; @@ -19,15 +20,30 @@ namespace SharpCompress.Writers.Zip; /// public sealed record ZipWriterOptions : IWriterOptions { + private CompressionType _compressionType; + private int _compressionLevel; + /// /// The compression type to use for the archive. /// - public CompressionType CompressionType { get; init; } + public CompressionType CompressionType + { + get => _compressionType; + init => _compressionType = value; + } /// /// The compression level to be used when the compression type supports variable levels. /// - public int CompressionLevel { get; init; } + public int CompressionLevel + { + get => _compressionLevel; + init + { + CompressionLevelValidation.Validate(CompressionType, value); + _compressionLevel = value; + } + } /// /// SharpCompress will keep the supplied streams open. Default is true. diff --git a/tests/SharpCompress.Performance/Benchmarks/ArchiveBenchmarkBase.cs b/tests/SharpCompress.Performance/Benchmarks/ArchiveBenchmarkBase.cs new file mode 100644 index 00000000..21013236 --- /dev/null +++ b/tests/SharpCompress.Performance/Benchmarks/ArchiveBenchmarkBase.cs @@ -0,0 +1,39 @@ +using System; +using System.IO; + +namespace SharpCompress.Performance.Benchmarks; + +public abstract class ArchiveBenchmarkBase +{ + protected static readonly string TEST_ARCHIVES_PATH; + + static ArchiveBenchmarkBase() + { + var baseDirectory = AppDomain.CurrentDomain.BaseDirectory; + var index = baseDirectory.IndexOf( + "SharpCompress.Performance", + StringComparison.OrdinalIgnoreCase + ); + + if (index == -1) + { + throw new InvalidOperationException( + "Could not find SharpCompress.Performance in the base directory path" + ); + } + + var path = baseDirectory.Substring(0, index); + var solutionBasePath = Path.GetDirectoryName(path) ?? throw new InvalidOperationException(); + TEST_ARCHIVES_PATH = Path.Combine(solutionBasePath, "TestArchives", "Archives"); + + if (!Directory.Exists(TEST_ARCHIVES_PATH)) + { + throw new InvalidOperationException( + $"Test archives directory not found: {TEST_ARCHIVES_PATH}" + ); + } + } + + protected static string GetArchivePath(string fileName) => + Path.Combine(TEST_ARCHIVES_PATH, fileName); +} diff --git a/tests/SharpCompress.Performance/Benchmarks/GZipBenchmarks.cs b/tests/SharpCompress.Performance/Benchmarks/GZipBenchmarks.cs new file mode 100644 index 00000000..2167e034 --- /dev/null +++ b/tests/SharpCompress.Performance/Benchmarks/GZipBenchmarks.cs @@ -0,0 +1,46 @@ +using System; +using System.IO; +using BenchmarkDotNet.Attributes; +using SharpCompress.Compressors; +using SharpCompress.Compressors.Deflate; + +namespace SharpCompress.Performance.Benchmarks; + +[MemoryDiagnoser] +public class GZipBenchmarks +{ + private byte[] _sourceData = null!; + private byte[] _compressedData = null!; + + [GlobalSetup] + public void Setup() + { + // Create 100KB of test data + _sourceData = new byte[100 * 1024]; + new Random(42).NextBytes(_sourceData); + + // Pre-compress for decompression benchmark + using var compressStream = new MemoryStream(); + using (var gzipStream = new GZipStream(compressStream, CompressionMode.Compress)) + { + gzipStream.Write(_sourceData, 0, _sourceData.Length); + } + _compressedData = compressStream.ToArray(); + } + + [Benchmark(Description = "GZip: Compress 100KB")] + public void GZipCompress() + { + using var outputStream = new MemoryStream(); + using var gzipStream = new GZipStream(outputStream, CompressionMode.Compress); + gzipStream.Write(_sourceData, 0, _sourceData.Length); + } + + [Benchmark(Description = "GZip: Decompress 100KB")] + public void GZipDecompress() + { + using var inputStream = new MemoryStream(_compressedData); + using var gzipStream = new GZipStream(inputStream, CompressionMode.Decompress); + gzipStream.CopyTo(Stream.Null); + } +} diff --git a/tests/SharpCompress.Performance/Benchmarks/RarBenchmarks.cs b/tests/SharpCompress.Performance/Benchmarks/RarBenchmarks.cs new file mode 100644 index 00000000..7a2d6709 --- /dev/null +++ b/tests/SharpCompress.Performance/Benchmarks/RarBenchmarks.cs @@ -0,0 +1,46 @@ +using System; +using System.IO; +using System.Linq; +using BenchmarkDotNet.Attributes; +using SharpCompress.Archives.Rar; +using SharpCompress.Readers; + +namespace SharpCompress.Performance.Benchmarks; + +[MemoryDiagnoser] +public class RarBenchmarks : ArchiveBenchmarkBase +{ + private byte[] _rarBytes = null!; + + [GlobalSetup] + public void Setup() + { + _rarBytes = File.ReadAllBytes(GetArchivePath("Rar.rar")); + } + + [Benchmark(Description = "Rar: Extract all entries (Archive API)")] + public void RarExtractArchiveApi() + { + using var stream = new MemoryStream(_rarBytes); + using var archive = RarArchive.OpenArchive(stream); + foreach (var entry in archive.Entries.Where(e => !e.IsDirectory)) + { + using var entryStream = entry.OpenEntryStream(); + entryStream.CopyTo(Stream.Null); + } + } + + [Benchmark(Description = "Rar: Extract all entries (Reader API)")] + public void RarExtractReaderApi() + { + using var stream = new MemoryStream(_rarBytes); + using var reader = ReaderFactory.OpenReader(stream); + while (reader.MoveToNextEntry()) + { + if (!reader.Entry.IsDirectory) + { + reader.WriteEntryTo(Stream.Null); + } + } + } +} diff --git a/tests/SharpCompress.Performance/Benchmarks/SevenZipBenchmarks.cs b/tests/SharpCompress.Performance/Benchmarks/SevenZipBenchmarks.cs new file mode 100644 index 00000000..74fdc584 --- /dev/null +++ b/tests/SharpCompress.Performance/Benchmarks/SevenZipBenchmarks.cs @@ -0,0 +1,45 @@ +using System; +using System.IO; +using System.Linq; +using BenchmarkDotNet.Attributes; +using SharpCompress.Archives.SevenZip; + +namespace SharpCompress.Performance.Benchmarks; + +[MemoryDiagnoser] +public class SevenZipBenchmarks : ArchiveBenchmarkBase +{ + private byte[] _lzmaBytes = null!; + private byte[] _lzma2Bytes = null!; + + [GlobalSetup] + public void Setup() + { + _lzmaBytes = File.ReadAllBytes(GetArchivePath("7Zip.LZMA.7z")); + _lzma2Bytes = File.ReadAllBytes(GetArchivePath("7Zip.LZMA2.7z")); + } + + [Benchmark(Description = "7Zip LZMA: Extract all entries")] + public void SevenZipLzmaExtract() + { + using var stream = new MemoryStream(_lzmaBytes); + using var archive = SevenZipArchive.OpenArchive(stream); + foreach (var entry in archive.Entries.Where(e => !e.IsDirectory)) + { + using var entryStream = entry.OpenEntryStream(); + entryStream.CopyTo(Stream.Null); + } + } + + [Benchmark(Description = "7Zip LZMA2: Extract all entries")] + public void SevenZipLzma2Extract() + { + using var stream = new MemoryStream(_lzma2Bytes); + using var archive = SevenZipArchive.OpenArchive(stream); + foreach (var entry in archive.Entries.Where(e => !e.IsDirectory)) + { + using var entryStream = entry.OpenEntryStream(); + entryStream.CopyTo(Stream.Null); + } + } +} diff --git a/tests/SharpCompress.Performance/Benchmarks/TarBenchmarks.cs b/tests/SharpCompress.Performance/Benchmarks/TarBenchmarks.cs new file mode 100644 index 00000000..39d1d97d --- /dev/null +++ b/tests/SharpCompress.Performance/Benchmarks/TarBenchmarks.cs @@ -0,0 +1,81 @@ +using System; +using System.IO; +using System.Linq; +using BenchmarkDotNet.Attributes; +using SharpCompress.Archives.Tar; +using SharpCompress.Common; +using SharpCompress.Readers; +using SharpCompress.Writers; + +namespace SharpCompress.Performance.Benchmarks; + +[MemoryDiagnoser] +public class TarBenchmarks : ArchiveBenchmarkBase +{ + private byte[] _tarBytes = null!; + private byte[] _tarGzBytes = null!; + + [GlobalSetup] + public void Setup() + { + _tarBytes = File.ReadAllBytes(GetArchivePath("Tar.tar")); + _tarGzBytes = File.ReadAllBytes(GetArchivePath("Tar.tar.gz")); + } + + [Benchmark(Description = "Tar: Extract all entries (Archive API)")] + public void TarExtractArchiveApi() + { + using var stream = new MemoryStream(_tarBytes); + using var archive = TarArchive.OpenArchive(stream); + foreach (var entry in archive.Entries.Where(e => !e.IsDirectory)) + { + using var entryStream = entry.OpenEntryStream(); + entryStream.CopyTo(Stream.Null); + } + } + + [Benchmark(Description = "Tar: Extract all entries (Reader API)")] + public void TarExtractReaderApi() + { + using var stream = new MemoryStream(_tarBytes); + using var reader = ReaderFactory.OpenReader(stream); + while (reader.MoveToNextEntry()) + { + if (!reader.Entry.IsDirectory) + { + reader.WriteEntryTo(Stream.Null); + } + } + } + + [Benchmark(Description = "Tar.GZip: Extract all entries")] + public void TarGzipExtract() + { + using var stream = new MemoryStream(_tarGzBytes); + using var archive = TarArchive.OpenArchive(stream); + foreach (var entry in archive.Entries.Where(e => !e.IsDirectory)) + { + using var entryStream = entry.OpenEntryStream(); + entryStream.CopyTo(Stream.Null); + } + } + + [Benchmark(Description = "Tar: Create archive with small files")] + public void TarCreateSmallFiles() + { + using var outputStream = new MemoryStream(); + using var writer = WriterFactory.OpenWriter( + outputStream, + ArchiveType.Tar, + new WriterOptions(CompressionType.None) { LeaveStreamOpen = true } + ); + + // Create 10 small files + for (int i = 0; i < 10; i++) + { + var data = new byte[1024]; // 1KB each + using var entryStream = new MemoryStream(data); + writer.Write($"file{i}.txt", entryStream); + } + } +} diff --git a/tests/SharpCompress.Performance/Benchmarks/ZipBenchmarks.cs b/tests/SharpCompress.Performance/Benchmarks/ZipBenchmarks.cs new file mode 100644 index 00000000..00e58a0e --- /dev/null +++ b/tests/SharpCompress.Performance/Benchmarks/ZipBenchmarks.cs @@ -0,0 +1,69 @@ +using System; +using System.IO; +using System.Linq; +using BenchmarkDotNet.Attributes; +using SharpCompress.Archives.Zip; +using SharpCompress.Common; +using SharpCompress.Readers; +using SharpCompress.Writers; + +namespace SharpCompress.Performance.Benchmarks; + +[MemoryDiagnoser] +public class ZipBenchmarks : ArchiveBenchmarkBase +{ + private string _archivePath = null!; + private byte[] _archiveBytes = null!; + + [GlobalSetup] + public void Setup() + { + _archivePath = GetArchivePath("Zip.deflate.zip"); + _archiveBytes = File.ReadAllBytes(_archivePath); + } + + [Benchmark(Description = "Zip: Extract all entries (Archive API)")] + public void ZipExtractArchiveApi() + { + using var stream = new MemoryStream(_archiveBytes); + using var archive = ZipArchive.OpenArchive(stream); + foreach (var entry in archive.Entries.Where(e => !e.IsDirectory)) + { + using var entryStream = entry.OpenEntryStream(); + entryStream.CopyTo(Stream.Null); + } + } + + [Benchmark(Description = "Zip: Extract all entries (Reader API)")] + public void ZipExtractReaderApi() + { + using var stream = new MemoryStream(_archiveBytes); + using var reader = ReaderFactory.OpenReader(stream); + while (reader.MoveToNextEntry()) + { + if (!reader.Entry.IsDirectory) + { + reader.WriteEntryTo(Stream.Null); + } + } + } + + [Benchmark(Description = "Zip: Create archive with small files")] + public void ZipCreateSmallFiles() + { + using var outputStream = new MemoryStream(); + using var writer = WriterFactory.OpenWriter( + outputStream, + ArchiveType.Zip, + new WriterOptions(CompressionType.Deflate) { LeaveStreamOpen = true } + ); + + // Create 10 small files + for (int i = 0; i < 10; i++) + { + var data = new byte[1024]; // 1KB each + using var entryStream = new MemoryStream(data); + writer.Write($"file{i}.txt", entryStream); + } + } +} diff --git a/tests/SharpCompress.Performance/Program.cs b/tests/SharpCompress.Performance/Program.cs index c6d29ec6..75d4b16b 100644 --- a/tests/SharpCompress.Performance/Program.cs +++ b/tests/SharpCompress.Performance/Program.cs @@ -1,54 +1,112 @@ using System; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using SharpCompress.Archives; -using SharpCompress.Performance; -using SharpCompress.Readers; -using SharpCompress.Test; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Running; +using BenchmarkDotNet.Toolchains.InProcess.Emit; -var index = AppDomain.CurrentDomain.BaseDirectory.IndexOf( - "SharpCompress.Performance", - StringComparison.OrdinalIgnoreCase -); -var path = AppDomain.CurrentDomain.BaseDirectory.Substring(0, index); -var SOLUTION_BASE_PATH = Path.GetDirectoryName(path) ?? throw new ArgumentNullException(); +namespace SharpCompress.Performance; -var TEST_ARCHIVES_PATH = Path.Combine(SOLUTION_BASE_PATH, "TestArchives", "Archives"); - -//using var _ = JetbrainsProfiler.Memory($"/Users/adam/temp/"); -using (var __ = JetbrainsProfiler.Cpu($"/Users/adam/temp/")) +public class Program { - var testArchives = new[] + public static void Main(string[] args) { - "Rar.Audio_program.rar", - - //"64bitstream.zip.7z", - //"TarWithSymlink.tar.gz" - }; - var arcs = testArchives.Select(a => Path.Combine(TEST_ARCHIVES_PATH, a)).ToArray(); - - for (int i = 0; i < 50; i++) - { - using var found = ArchiveFactory.OpenArchive(arcs[0]); - foreach (var entry in found.Entries.Where(entry => !entry.IsDirectory)) + // Check if profiling mode is requested + if (args.Length > 0 && args[0].Equals("--profile", StringComparison.OrdinalIgnoreCase)) { - Console.WriteLine($"Extracting {entry.Key}"); - using var entryStream = entry.OpenEntryStream(); - entryStream.CopyTo(Stream.Null); + RunWithProfiler(args); + return; } - /*using var found = ReaderFactory.OpenReader(arcs[0]); - while (found.MoveToNextEntry()) - { - var entry = found.Entry; - if (entry.IsDirectory) - continue; - Console.WriteLine($"Extracting {entry.Key}"); - found.WriteEntryTo(Stream.Null); - }*/ + // Default: Run BenchmarkDotNet + var config = DefaultConfig.Instance.AddJob( + Job.Default.WithToolchain(InProcessEmitToolchain.Instance) + .WithWarmupCount(3) // Minimal warmup iterations for CI + .WithIterationCount(10) // Minimal measurement iterations for CI + .WithInvocationCount(10) + .WithUnrollFactor(1) + ); + + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config); } - Console.WriteLine("Still running..."); + private static void RunWithProfiler(string[] args) + { + var profileType = "cpu"; // Default to CPU profiling + var outputPath = "./profiler-snapshots"; + + // Parse arguments + for (int i = 1; i < args.Length; i++) + { + if (args[i].Equals("--type", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length) + { + profileType = args[++i].ToLowerInvariant(); + } + else if ( + args[i].Equals("--output", StringComparison.OrdinalIgnoreCase) + && i + 1 < args.Length + ) + { + outputPath = args[++i]; + } + } + + Console.WriteLine($"Running with JetBrains Profiler ({profileType} mode)"); + Console.WriteLine($"Output path: {outputPath}"); + Console.WriteLine(); + Console.WriteLine( + "Usage: dotnet run --project SharpCompress.Performance.csproj -c Release -- --profile [--type cpu|memory] [--output ]" + ); + Console.WriteLine(); + + // Run a sample benchmark with profiling + RunSampleBenchmarkWithProfiler(profileType, outputPath); + } + + private static void RunSampleBenchmarkWithProfiler(string profileType, string outputPath) + { + Console.WriteLine("Running sample benchmark with profiler..."); + Console.WriteLine("Note: JetBrains profiler requires the profiler tools to be installed."); + Console.WriteLine("Install from: https://www.jetbrains.com/profiler/"); + Console.WriteLine(); + + try + { + IDisposable? profiler = null; + + if (profileType == "cpu") + { + profiler = Test.JetbrainsProfiler.Cpu(outputPath); + } + else if (profileType == "memory") + { + profiler = Test.JetbrainsProfiler.Memory(outputPath); + } + + using (profiler) + { + // Run a simple benchmark iteration + var zipBenchmark = new Benchmarks.ZipBenchmarks(); + zipBenchmark.Setup(); + + Console.WriteLine("Running benchmark iterations..."); + for (int i = 0; i < 10; i++) + { + zipBenchmark.ZipExtractArchiveApi(); + if (i % 3 == 0) + { + Console.Write("."); + } + } + Console.WriteLine(); + Console.WriteLine("Benchmark iterations completed."); + } + + Console.WriteLine($"Profiler snapshot saved to: {outputPath}"); + } + catch (Exception ex) + { + Console.WriteLine($"Error running profiler: {ex.Message}"); + Console.WriteLine("Make sure JetBrains profiler tools are installed and accessible."); + } + } } -await Task.Delay(500); diff --git a/tests/SharpCompress.Performance/README.md b/tests/SharpCompress.Performance/README.md new file mode 100644 index 00000000..91ebfb33 --- /dev/null +++ b/tests/SharpCompress.Performance/README.md @@ -0,0 +1,143 @@ +# SharpCompress Performance Benchmarks + +This project contains performance benchmarks for SharpCompress using [BenchmarkDotNet](https://benchmarkdotnet.org/). + +## Overview + +The benchmarks test all major archive formats supported by SharpCompress: +- **Zip**: Read (Archive & Reader API) and Write operations +- **Tar**: Read (Archive & Reader API) and Write operations, including Tar.GZip +- **Rar**: Read operations (Archive & Reader API) +- **7Zip**: Read operations for LZMA and LZMA2 compression +- **GZip**: Compression and decompression + +## Running Benchmarks + +### Run all benchmarks +```bash +dotnet run --project tests/SharpCompress.Performance/SharpCompress.Performance.csproj --configuration Release +``` + +### Run specific benchmark class +```bash +dotnet run --project tests/SharpCompress.Performance/SharpCompress.Performance.csproj --configuration Release -- --filter "*ZipBenchmarks*" +``` + +### Run with specific job configuration +```bash +# Quick run for testing (1 warmup, 1 iteration) +dotnet run --project tests/SharpCompress.Performance/SharpCompress.Performance.csproj --configuration Release -- --job Dry + +# Short run (3 warmup, 3 iterations) +dotnet run --project tests/SharpCompress.Performance/SharpCompress.Performance.csproj --configuration Release -- --job Short + +# Medium run (default) +dotnet run --project tests/SharpCompress.Performance/SharpCompress.Performance.csproj --configuration Release -- --job Medium +``` + +### Export results +```bash +# Export to JSON +dotnet run --project tests/SharpCompress.Performance/SharpCompress.Performance.csproj --configuration Release -- --exporters json + +# Export to multiple formats +dotnet run --project tests/SharpCompress.Performance/SharpCompress.Performance.csproj --configuration Release -- --exporters json markdown html +``` + +### List available benchmarks +```bash +dotnet run --project tests/SharpCompress.Performance/SharpCompress.Performance.csproj --configuration Release -- --list flat +``` + +## Baseline Results + +The baseline results are stored in `baseline-results.md` and represent the expected performance characteristics of the library. These results are used in CI to detect significant performance regressions. + +### Generate Baseline (Automated) + +Use the build target to generate baseline results: +```bash +dotnet run --project build/build.csproj -- generate-baseline +``` + +This will: +1. Build the performance project +2. Run all benchmarks +3. Combine the markdown reports into `baseline-results.md` +4. Clean up temporary artifacts + +### Generate Baseline (Manual) + +To manually update the baseline: +1. Run the benchmarks: `dotnet run --project tests/SharpCompress.Performance/SharpCompress.Performance.csproj --configuration Release -- --exporters markdown --artifacts baseline-output` +2. Combine the results: `cat baseline-output/results/*-report-github.md > baseline-results.md` +3. Review the changes and commit if appropriate + +## JetBrains Profiler Integration + +The performance project supports JetBrains profiler for detailed CPU and memory profiling during local development. + +### Prerequisites + +Install JetBrains profiler tools from: https://www.jetbrains.com/profiler/ + +### Run with CPU Profiling +```bash +dotnet run --project tests/SharpCompress.Performance/SharpCompress.Performance.csproj --configuration Release -- --profile --type cpu --output ./my-cpu-snapshots +``` + +### Run with Memory Profiling +```bash +dotnet run --project tests/SharpCompress.Performance/SharpCompress.Performance.csproj --configuration Release -- --profile --type memory --output ./my-memory-snapshots +``` + +### Profiler Options +- `--profile`: Enable profiler mode +- `--type cpu|memory`: Choose profiling type (default: cpu) +- `--output `: Specify snapshot output directory (default: ./profiler-snapshots) + +The profiler will run a sample benchmark and save snapshots that can be opened in JetBrains profiler tools for detailed analysis. + +## CI Integration + +The performance benchmarks run automatically in GitHub Actions on: +- Push to `master` or `release` branches +- Pull requests to `master` or `release` branches +- Manual workflow dispatch + +Results are displayed in the GitHub Actions summary and uploaded as artifacts. + +## Benchmark Configuration + +The benchmarks are configured with minimal iterations for CI efficiency: +- **Warmup Count**: 1 iteration +- **Iteration Count**: 3 iterations +- **Invocation Count**: 1 +- **Unroll Factor**: 1 +- **Toolchain**: InProcessEmitToolchain (for fast execution) + +These settings provide a good balance between speed and accuracy for CI purposes. For more accurate results, use the `Short`, `Medium`, or `Long` job configurations. + +## Memory Diagnostics + +All benchmarks include memory diagnostics using `[MemoryDiagnoser]`, which provides: +- Total allocated memory per operation +- Gen 0/1/2 collection counts + +## Understanding Results + +Key metrics in the benchmark results: +- **Mean**: Average execution time +- **Error**: Half of 99.9% confidence interval +- **StdDev**: Standard deviation +- **Allocated**: Total managed memory allocated per operation + +## Contributing + +When adding new benchmarks: +1. Create a new class in the `Benchmarks/` directory +2. Inherit from `ArchiveBenchmarkBase` for archive-related benchmarks +3. Add `[MemoryDiagnoser]` attribute to the class +4. Use `[Benchmark(Description = "...")]` for each benchmark method +5. Add `[GlobalSetup]` for one-time initialization +6. Update this README if needed diff --git a/tests/SharpCompress.Performance/SharpCompress.Performance.csproj b/tests/SharpCompress.Performance/SharpCompress.Performance.csproj index cab757e4..d4ad46d2 100644 --- a/tests/SharpCompress.Performance/SharpCompress.Performance.csproj +++ b/tests/SharpCompress.Performance/SharpCompress.Performance.csproj @@ -4,6 +4,7 @@ net10.0 + diff --git a/tests/SharpCompress.Performance/baseline-results.md b/tests/SharpCompress.Performance/baseline-results.md new file mode 100644 index 00000000..479a7deb --- /dev/null +++ b/tests/SharpCompress.Performance/baseline-results.md @@ -0,0 +1,23 @@ +| Method | Mean | Error | StdDev | Allocated | +|------------------------- |-----------:|---------:|---------:|----------:| +| 'GZip: Compress 100KB' | 3,268.7 μs | 28.50 μs | 16.96 μs | 519.2 KB | +| 'GZip: Decompress 100KB' | 436.6 μs | 3.23 μs | 1.69 μs | 34.18 KB | +| Method | Mean | Error | StdDev | Allocated | +|----------------------------------------- |---------:|----------:|----------:|----------:| +| 'Rar: Extract all entries (Archive API)' | 2.054 ms | 0.3927 ms | 0.2598 ms | 91.09 KB | +| 'Rar: Extract all entries (Reader API)' | 2.235 ms | 0.0253 ms | 0.0132 ms | 149.48 KB | +| Method | Mean | Error | StdDev | Allocated | +|---------------------------------- |---------:|----------:|----------:|----------:| +| '7Zip LZMA: Extract all entries' | 9.124 ms | 2.1930 ms | 1.4505 ms | 272.8 KB | +| '7Zip LZMA2: Extract all entries' | 7.810 ms | 0.1323 ms | 0.0788 ms | 272.58 KB | +| Method | Mean | Error | StdDev | Allocated | +|----------------------------------------- |----------:|---------:|---------:|----------:| +| 'Tar: Extract all entries (Archive API)' | 56.36 μs | 3.312 μs | 1.971 μs | 16.65 KB | +| 'Tar: Extract all entries (Reader API)' | 175.34 μs | 2.616 μs | 1.557 μs | 213.36 KB | +| 'Tar.GZip: Extract all entries' | NA | NA | NA | NA | +| 'Tar: Create archive with small files' | 51.38 μs | 2.349 μs | 1.398 μs | 68.7 KB | +| Method | Mean | Error | StdDev | Gen0 | Allocated | +|----------------------------------------- |-----------:|---------:|---------:|---------:|-----------:| +| 'Zip: Extract all entries (Archive API)' | 1,188.4 μs | 28.62 μs | 14.97 μs | - | 181.66 KB | +| 'Zip: Extract all entries (Reader API)' | 1,137.0 μs | 5.58 μs | 2.92 μs | - | 123.19 KB | +| 'Zip: Create archive with small files' | 258.2 μs | 8.98 μs | 4.70 μs | 100.0000 | 2806.93 KB | \ No newline at end of file diff --git a/tests/SharpCompress.Performance/packages.lock.json b/tests/SharpCompress.Performance/packages.lock.json index d3684a0d..939ede42 100644 --- a/tests/SharpCompress.Performance/packages.lock.json +++ b/tests/SharpCompress.Performance/packages.lock.json @@ -2,6 +2,24 @@ "version": 2, "dependencies": { "net10.0": { + "BenchmarkDotNet": { + "type": "Direct", + "requested": "[0.15.8, )", + "resolved": "0.15.8", + "contentHash": "paCfrWxSeHqn3rUZc0spYXVFnHCF0nzRhG0nOLnyTjZYs8spsimBaaNmb3vwqvALKIplbYq/TF393vYiYSnh/Q==", + "dependencies": { + "BenchmarkDotNet.Annotations": "0.15.8", + "CommandLineParser": "2.9.1", + "Gee.External.Capstone": "2.3.0", + "Iced": "1.21.0", + "Microsoft.CodeAnalysis.CSharp": "4.14.0", + "Microsoft.Diagnostics.Runtime": "3.1.512801", + "Microsoft.Diagnostics.Tracing.TraceEvent": "3.1.21", + "Microsoft.DotNet.PlatformAbstractions": "3.1.6", + "Perfolizer": "[0.6.1]", + "System.Management": "9.0.5" + } + }, "JetBrains.Profiler.SelfApi": { "type": "Direct", "requested": "[2.5.16, )", @@ -37,6 +55,26 @@ "resolved": "17.14.15", "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw==" }, + "BenchmarkDotNet.Annotations": { + "type": "Transitive", + "resolved": "0.15.8", + "contentHash": "hfucY0ycAsB0SsoaZcaAp9oq5wlWBJcylvEJb9pmvdYUx6PD6S4mDiYnZWjdjAlLhIpe/xtGCwzORfzAzPqvzA==" + }, + "CommandLineParser": { + "type": "Transitive", + "resolved": "2.9.1", + "contentHash": "OE0sl1/sQ37bjVsPKKtwQlWDgqaxWgtme3xZz7JssWUzg5JpMIyHgCTY9MVMxOg48fJ1AgGT3tgdH5m/kQ5xhA==" + }, + "Gee.External.Capstone": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "2ap/rYmjtzCOT8hxrnEW/QeiOt+paD8iRrIcdKX0cxVwWLFa1e+JDBNeECakmccXrSFeBQuu5AV8SNkipFMMMw==" + }, + "Iced": { + "type": "Transitive", + "resolved": "1.21.0", + "contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg==" + }, "JetBrains.FormatRipper": { "type": "Transitive", "resolved": "2.4.0", @@ -63,6 +101,101 @@ "resolved": "10.0.102", "contentHash": "0i81LYX31U6UiXz4NOLbvc++u+/mVDmOt+PskrM/MygpDxkv9THKQyRUmavBpLK6iBV0abNWnn+CQgSRz//Pwg==" }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.11.0", + "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "4.14.0", + "contentHash": "PC3tuwZYnC+idaPuoC/AZpEdwrtX7qFpmnrfQkgobGIWiYmGi5MCRtl5mx6QrfMGQpK78X2lfIEoZDLg/qnuHg==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "4.14.0", + "contentHash": "568a6wcTivauIhbeWcCwfWwIn7UV7MeHEBvFB2uzGIpM2OhJ4eM/FZ8KS0yhPoNxnSpjGzz7x7CIjTxhslojQA==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[4.14.0]" + } + }, + "Microsoft.Diagnostics.NETCore.Client": { + "type": "Transitive", + "resolved": "0.2.510501", + "contentHash": "juoqJYMDs+lRrrZyOkXXMImJHneCF23cuvO4waFRd2Ds7j+ZuGIPbJm0Y/zz34BdeaGiiwGWraMUlln05W1PCQ==", + "dependencies": { + "Microsoft.Extensions.Logging": "6.0.0" + } + }, + "Microsoft.Diagnostics.Runtime": { + "type": "Transitive", + "resolved": "3.1.512801", + "contentHash": "0lMUDr2oxNZa28D6NH5BuSQEe5T9tZziIkvkD44YkkCGQXPJqvFjLq5ZQq1hYLl3RjQJrY+hR0jFgap+EWPDTw==", + "dependencies": { + "Microsoft.Diagnostics.NETCore.Client": "0.2.410101" + } + }, + "Microsoft.Diagnostics.Tracing.TraceEvent": { + "type": "Transitive", + "resolved": "3.1.21", + "contentHash": "/OrJFKaojSR6TkUKtwh8/qA9XWNtxLrXMqvEb89dBSKCWjaGVTbKMYodIUgF5deCEtmd6GXuRerciXGl5bhZ7Q==", + "dependencies": { + "Microsoft.Diagnostics.NETCore.Client": "0.2.510501", + "System.Reflection.TypeExtensions": "4.7.0" + } + }, + "Microsoft.DotNet.PlatformAbstractions": { + "type": "Transitive", + "resolved": "3.1.6", + "contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg==" + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "k6PWQMuoBDGGHOQTtyois2u4AwyVcIwL2LaSLlTZQm2CYcJ1pxbt6jfAnpWmzENA/wfrYRI/X9DTLoUkE4AsLw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "xlzi2IYREJH3/m6+lUrQlujzX8wDitm4QGnUu6kUXTQAWPuZY8i+ticFJbzfqaetLA6KR/rO6Ew/HuYD+bxifg==" + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "eIbyj40QDg1NDz0HBW0S5f3wrLVnKWnDJ/JtZ+yJDFnDj90VoPuoPmFkeaXrtu+0cKm5GRAwoDf+dBWXK0TUdg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "6.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/HggWBbTwy8TgebGSX5DBZ24ndhzi93sHUBDvP1IxbZD7FDokYzdAr6+vbWGjw2XAfR2EJ1sfKUotpjHnFWPxA==" + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "dzXN0+V1AyjOe2xcJ86Qbo233KHuLEY0njf/P2Kw8SfJU+d45HNS2ctJdnEnrWbM9Ye2eFgaC5Mj9otRMU6IsQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "9+PnzmQFfEFNR9J2aDTfJGGupShHjOuGw4VUv+JB044biSHrnmCIMD+mJHmb2H7YryrfBEXDurxQ47gJZdCKNQ==" + }, "Microsoft.NETFramework.ReferenceAssemblies.net461": { "type": "Transitive", "resolved": "1.0.3", @@ -73,6 +206,37 @@ "resolved": "10.0.102", "contentHash": "Mk1IMb9q5tahC2NltxYXFkLBtuBvfBoCQ3pIxYQWfzbCE9o1OB9SsHe0hnNGo7lWgTA/ePbFAJLWu6nLL9K17A==" }, + "Perfolizer": { + "type": "Transitive", + "resolved": "0.6.1", + "contentHash": "CR1QmWg4XYBd1Pb7WseP+sDmV8nGPwvmowKynExTqr3OuckIGVMhvmN4LC5PGzfXqDlR295+hz/T7syA1CxEqA==", + "dependencies": { + "Pragmastat": "3.2.4" + } + }, + "Pragmastat": { + "type": "Transitive", + "resolved": "3.2.4", + "contentHash": "I5qFifWw/gaTQT52MhzjZpkm/JPlfjSeO/DTZJjO7+hTKI+0aGRgOgZ3NN6D96dDuuqbIAZSeA5RimtHjqrA2A==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "9.0.5", + "contentHash": "cuzLM2MWutf9ZBEMPYYfd0DXwYdvntp7VCT6a/wvbKCa2ZuvGmW74xi+YBa2mrfEieAXqM4TNKlMmSnfAfpUoQ==" + }, + "System.Management": { + "type": "Transitive", + "resolved": "9.0.5", + "contentHash": "n6o9PZm9p25+zAzC3/48K0oHnaPKTInRrxqFq1fi/5TPbMLjuoCm/h//mS3cUmSy+9AO1Z+qsC/Ilt/ZFatv5Q==", + "dependencies": { + "System.CodeDom": "9.0.5" + } + }, + "System.Reflection.TypeExtensions": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "VybpaOQQhqE6siHppMktjfGBw1GCwvCqiufqmP8F1nj7fTUNtW35LOEt3UZTEsECfo+ELAl/9o9nJx3U91i7vA==" + }, "sharpcompress": { "type": "Project" } diff --git a/tests/SharpCompress.Test/Ace/AceReaderAsyncTests.cs b/tests/SharpCompress.Test/Ace/AceReaderAsyncTests.cs index 4dde6780..62d10265 100644 --- a/tests/SharpCompress.Test/Ace/AceReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Ace/AceReaderAsyncTests.cs @@ -80,10 +80,7 @@ public class AceReaderAsyncTests : ReaderTests if (!reader.Entry.IsDirectory) { Assert.Equal(expectedCompression, reader.Entry.CompressionType); - await reader.WriteEntryToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH); } } VerifyFiles(); @@ -105,10 +102,7 @@ public class AceReaderAsyncTests : ReaderTests if (!reader.Entry.IsDirectory) { Assert.Equal(expectedCompression, reader.Entry.CompressionType); - await reader.WriteEntryToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH); } } CompareFilesByPath( @@ -130,10 +124,7 @@ public class AceReaderAsyncTests : ReaderTests { if (!reader.Entry.IsDirectory) { - await reader.WriteEntryToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH); } } } diff --git a/tests/SharpCompress.Test/ArchiveTests.cs b/tests/SharpCompress.Test/ArchiveTests.cs index 9ef5837a..95e10214 100644 --- a/tests/SharpCompress.Test/ArchiveTests.cs +++ b/tests/SharpCompress.Test/ArchiveTests.cs @@ -62,10 +62,7 @@ public class ArchiveTests : ReaderTests } foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) { - entry.WriteToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + entry.WriteToDirectory(SCRATCH_FILES_PATH); } stream.ThrowOnDispose = false; } @@ -151,10 +148,7 @@ public class ArchiveTests : ReaderTests { foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) { - entry.WriteToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + entry.WriteToDirectory(SCRATCH_FILES_PATH); } } catch (IndexOutOfRangeException) @@ -192,10 +186,7 @@ public class ArchiveTests : ReaderTests { foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) { - entry.WriteToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + entry.WriteToDirectory(SCRATCH_FILES_PATH); } } VerifyFiles(); @@ -224,10 +215,7 @@ public class ArchiveTests : ReaderTests { foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) { - entry.WriteToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + entry.WriteToDirectory(SCRATCH_FILES_PATH); } } VerifyFiles(); @@ -299,10 +287,7 @@ public class ArchiveTests : ReaderTests { foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) { - entry.WriteToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + entry.WriteToDirectory(SCRATCH_FILES_PATH); } } VerifyFiles(); @@ -346,16 +331,7 @@ public class ArchiveTests : ReaderTests { foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) { - entry.WriteToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions - { - ExtractFullPath = true, - Overwrite = true, - PreserveAttributes = true, - PreserveFileTime = true, - } - ); + entry.WriteToDirectory(SCRATCH_FILES_PATH); } } VerifyFilesEx(); @@ -414,7 +390,7 @@ public class ArchiveTests : ReaderTests ) { var writerOptions = compressionLevel.HasValue - ? new WriterOptions(compressionType, compressionLevel.Value, leaveStreamOpen: true) + ? new WriterOptions(compressionType, compressionLevel.Value) { LeaveStreamOpen = true } : new WriterOptions(compressionType) { LeaveStreamOpen = true }; return WriterFactory.OpenAsyncWriter( new AsyncOnlyStream(stream), @@ -650,10 +626,7 @@ public class ArchiveTests : ReaderTests var entry in archive.EntriesAsync.Where(entry => !entry.IsDirectory) ) { - await entry.WriteToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await entry.WriteToDirectoryAsync(SCRATCH_FILES_PATH); } } catch (IndexOutOfRangeException) diff --git a/tests/SharpCompress.Test/Arj/ArjReaderAsyncTests.cs b/tests/SharpCompress.Test/Arj/ArjReaderAsyncTests.cs index 6a32cdeb..17dfa9cf 100644 --- a/tests/SharpCompress.Test/Arj/ArjReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Arj/ArjReaderAsyncTests.cs @@ -104,10 +104,7 @@ public class ArjReaderAsyncTests : ReaderTests { Assert.Equal(expectedCompression.Value, reader.Entry.CompressionType); } - await reader.WriteEntryToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH); } } VerifyFiles(); @@ -129,10 +126,7 @@ public class ArjReaderAsyncTests : ReaderTests if (!reader.Entry.IsDirectory) { Assert.Equal(expectedCompression, reader.Entry.CompressionType); - await reader.WriteEntryToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH); } } CompareFilesByPath( @@ -155,10 +149,7 @@ public class ArjReaderAsyncTests : ReaderTests { if (!reader.Entry.IsDirectory) { - await reader.WriteEntryToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH); } } } diff --git a/tests/SharpCompress.Test/ExtractAll.cs b/tests/SharpCompress.Test/ExtractAll.cs index 1e047bc0..8b71edf3 100644 --- a/tests/SharpCompress.Test/ExtractAll.cs +++ b/tests/SharpCompress.Test/ExtractAll.cs @@ -21,10 +21,9 @@ public class ExtractAllTests : TestBase public async ValueTask ExtractAllEntriesAsync(string archivePath) { var testArchive = Path.Combine(TEST_ARCHIVES_PATH, archivePath); - var options = new ExtractionOptions() { ExtractFullPath = true, Overwrite = true }; await using var archive = await ArchiveFactory.OpenAsyncArchive(testArchive); - await archive.WriteToDirectoryAsync(SCRATCH_FILES_PATH, options); + await archive.WriteToDirectoryAsync(SCRATCH_FILES_PATH); } [Theory] @@ -38,9 +37,8 @@ public class ExtractAllTests : TestBase public void ExtractAllEntriesSync(string archivePath) { var testArchive = Path.Combine(TEST_ARCHIVES_PATH, archivePath); - var options = new ExtractionOptions() { ExtractFullPath = true, Overwrite = true }; using var archive = ArchiveFactory.OpenArchive(testArchive); - archive.WriteToDirectory(SCRATCH_FILES_PATH, options); + archive.WriteToDirectory(SCRATCH_FILES_PATH); } } diff --git a/tests/SharpCompress.Test/ExtractAllEntriesTests.cs b/tests/SharpCompress.Test/ExtractAllEntriesTests.cs index 57bbcc50..cc8a32bd 100644 --- a/tests/SharpCompress.Test/ExtractAllEntriesTests.cs +++ b/tests/SharpCompress.Test/ExtractAllEntriesTests.cs @@ -43,10 +43,7 @@ public class ExtractAllEntriesTests : TestBase { if (!reader.Entry.IsDirectory) { - reader.WriteEntryToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + reader.WriteEntryToDirectory(SCRATCH_FILES_PATH); completed += reader.Entry.Size; var progress = completed / totalSize; diff --git a/tests/SharpCompress.Test/ExtractionTests.cs b/tests/SharpCompress.Test/ExtractionTests.cs index 24ed6bf4..cf233042 100644 --- a/tests/SharpCompress.Test/ExtractionTests.cs +++ b/tests/SharpCompress.Test/ExtractionTests.cs @@ -47,12 +47,7 @@ public class ExtractionTests : TestBase // This should not throw an exception even if Path.GetFullPath returns // a path with different casing than the actual directory - var exception = Record.Exception(() => - reader.WriteAllToDirectory( - extractPath, - new ExtractionOptions { ExtractFullPath = false, Overwrite = true } - ) - ); + var exception = Record.Exception(() => reader.WriteAllToDirectory(extractPath)); Assert.Null(exception); } @@ -95,10 +90,7 @@ public class ExtractionTests : TestBase using var reader = ReaderFactory.OpenReader(stream); var exception = Assert.Throws(() => - reader.WriteAllToDirectory( - extractPath, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ) + reader.WriteAllToDirectory(extractPath) ); Assert.Contains("outside of the destination", exception.Message); diff --git a/tests/SharpCompress.Test/GZip/AsyncTests.cs b/tests/SharpCompress.Test/GZip/AsyncTests.cs index ebc3c920..b525a15b 100644 --- a/tests/SharpCompress.Test/GZip/AsyncTests.cs +++ b/tests/SharpCompress.Test/GZip/AsyncTests.cs @@ -29,10 +29,7 @@ public class AsyncTests : TestBase #endif await using var reader = await ReaderFactory.OpenAsyncReader(new AsyncOnlyStream(stream)); - await reader.WriteAllToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await reader.WriteAllToDirectoryAsync(SCRATCH_FILES_PATH); // Just verify some files were extracted var extractedFiles = Directory.GetFiles( @@ -147,11 +144,7 @@ public class AsyncTests : TestBase cancellationToken: cts.Token ); - await reader.WriteAllToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true }, - cts.Token - ); + await reader.WriteAllToDirectoryAsync(SCRATCH_FILES_PATH, cts.Token); // Just verify some files were extracted var extractedFiles = Directory.GetFiles( diff --git a/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs b/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs index 87f0b203..3bcfb910 100644 --- a/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs @@ -7,6 +7,7 @@ using SharpCompress.Archives.GZip; using SharpCompress.Archives.Tar; using SharpCompress.Common; using SharpCompress.Test.Mocks; +using SharpCompress.Writers.GZip; using Xunit; namespace SharpCompress.Test.GZip; @@ -83,7 +84,10 @@ public class GZipArchiveAsyncTests : ArchiveTests await Assert.ThrowsAsync(async () => await archive.AddEntryAsync("jpg\\test.jpg", File.OpenRead(jpg), closeStream: true) ); - await archive.SaveToAsync(Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz")); + await archive.SaveToAsync( + Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"), + new GZipWriterOptions() + ); } } diff --git a/tests/SharpCompress.Test/GZip/GZipArchiveTests.cs b/tests/SharpCompress.Test/GZip/GZipArchiveTests.cs index b6ee03d3..a33256a6 100644 --- a/tests/SharpCompress.Test/GZip/GZipArchiveTests.cs +++ b/tests/SharpCompress.Test/GZip/GZipArchiveTests.cs @@ -5,6 +5,7 @@ using SharpCompress.Archives; using SharpCompress.Archives.GZip; using SharpCompress.Archives.Tar; using SharpCompress.Common; +using SharpCompress.Writers.GZip; using Xunit; namespace SharpCompress.Test.GZip; @@ -64,7 +65,7 @@ public class GZipArchiveTests : ArchiveTests using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")); using var archive = GZipArchive.OpenArchive(stream); Assert.Throws(() => archive.AddEntry("jpg\\test.jpg", jpg)); - archive.SaveTo(Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz")); + archive.SaveTo(Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"), new GZipWriterOptions()); } [Fact] diff --git a/tests/SharpCompress.Test/Lzw/LzwReaderAsyncTests.cs b/tests/SharpCompress.Test/Lzw/LzwReaderAsyncTests.cs new file mode 100644 index 00000000..6ecc6047 --- /dev/null +++ b/tests/SharpCompress.Test/Lzw/LzwReaderAsyncTests.cs @@ -0,0 +1,43 @@ +using System.IO; +using SharpCompress.Common; +using SharpCompress.Readers; +using SharpCompress.Readers.Lzw; +using Xunit; + +namespace SharpCompress.Test.Lzw; + +public class LzwReaderAsyncTests : ReaderTests +{ + public LzwReaderAsyncTests() => UseExtensionInsteadOfNameToVerify = true; + + [Fact] + public async System.Threading.Tasks.Task Lzw_Reader_Async() + { + await ReadAsync("Tar.tar.Z", CompressionType.Lzw); + } + + [Fact] + public async System.Threading.Tasks.Task Lzw_Reader_Plain_Z_File_Async() + { + // Test async reading of a plain .Z file (not tar-wrapped) using LzwReader directly + using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "large_test.txt.Z")); + using var reader = LzwReader.OpenReader(stream); + + Assert.Equal(ArchiveType.Lzw, reader.ArchiveType); + Assert.True(reader.MoveToNextEntry()); + + var entry = reader.Entry; + Assert.NotNull(entry); + Assert.Equal(CompressionType.Lzw, entry.CompressionType); + + // When opened as FileStream, key should be derived from filename + Assert.Equal("large_test.txt", entry.Key); + + // Decompress asynchronously + using var entryStream = reader.OpenEntryStream(); + using var ms = new MemoryStream(); + await entryStream.CopyToAsync(ms); + + Assert.Equal(22300, ms.Length); + } +} diff --git a/tests/SharpCompress.Test/Lzw/LzwReaderTests.cs b/tests/SharpCompress.Test/Lzw/LzwReaderTests.cs new file mode 100644 index 00000000..f75fd666 --- /dev/null +++ b/tests/SharpCompress.Test/Lzw/LzwReaderTests.cs @@ -0,0 +1,91 @@ +using System.IO; +using SharpCompress.Common; +using SharpCompress.IO; +using SharpCompress.Readers; +using SharpCompress.Readers.Lzw; +using Xunit; + +namespace SharpCompress.Test.Lzw; + +public class LzwReaderTests : ReaderTests +{ + public LzwReaderTests() => UseExtensionInsteadOfNameToVerify = true; + + [Fact] + public void Lzw_Reader_Generic() => Read("Tar.tar.Z", CompressionType.Lzw); + + [Fact] + public void Lzw_Reader_Generic2() + { + //read only as Lzw item + using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.Z")); + using var reader = LzwReader.OpenReader(SharpCompressStream.CreateNonDisposing(stream)); + while (reader.MoveToNextEntry()) + { + // LZW doesn't have CRC or Size in header like GZip, so we just check the entry exists + Assert.NotNull(reader.Entry); + } + } + + [Fact] + public void Lzw_Reader_Factory_Detects_Tar_Wrapper() + { + // Note: Testing with Tar.tar.Z because: + // 1. LzwStream only supports decompression, not compression + // 2. This tests the important tar wrapper detection code path in LzwFactory.TryOpenReader + // 3. Verifies that tar.Z files correctly return TarReader with CompressionType.Lzw + using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.Z")); + using var reader = ReaderFactory.OpenReader( + stream, + new ReaderOptions { LeaveStreamOpen = false } + ); + + // Should detect as Tar archive with Lzw compression + Assert.Equal(ArchiveType.Tar, reader.ArchiveType); + Assert.True(reader.MoveToNextEntry()); + Assert.NotNull(reader.Entry); + Assert.Equal(CompressionType.Lzw, reader.Entry.CompressionType); + } + + [Fact] + public void Lzw_Reader_Plain_Z_File() + { + // Test with a plain .Z file (not tar-wrapped) using LzwReader directly + using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "large_test.txt.Z")); + using var reader = LzwReader.OpenReader(stream); + + Assert.True(reader.MoveToNextEntry()); + var entry = reader.Entry; + Assert.NotNull(entry); + Assert.Equal(CompressionType.Lzw, entry.CompressionType); + + // Entry key should be "large_test.txt" (stripped .Z extension) when opened via FileStream + Assert.Equal("large_test.txt", entry.Key); + + // Decompress and verify content + using var entryStream = reader.OpenEntryStream(); + using var ms = new MemoryStream(); + entryStream.CopyTo(ms); + var decompressed = System.Text.Encoding.UTF8.GetString(ms.ToArray()); + + Assert.Equal(22300, ms.Length); + Assert.Contains("This is a test file for LZW compression testing", decompressed); + } + + [Fact] + public void Lzw_Reader_Factory_Detects_Plain_Z_File() + { + // Test that ReaderFactory correctly identifies a plain .Z file (not tar-wrapped) + using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "large_test.txt.Z")); + using var reader = ReaderFactory.OpenReader(stream); + + // Should detect as Lzw archive (not Tar) + Assert.Equal(ArchiveType.Lzw, reader.ArchiveType); + Assert.True(reader.MoveToNextEntry()); + Assert.NotNull(reader.Entry); + Assert.Equal(CompressionType.Lzw, reader.Entry.CompressionType); + + // When opened via ReaderFactory with a non-FileStream, key defaults to "data" + Assert.NotNull(reader.Entry.Key); + } +} diff --git a/tests/SharpCompress.Test/OptionsUsabilityTests.cs b/tests/SharpCompress.Test/OptionsUsabilityTests.cs new file mode 100644 index 00000000..603007c9 --- /dev/null +++ b/tests/SharpCompress.Test/OptionsUsabilityTests.cs @@ -0,0 +1,248 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using SharpCompress.Archives; +using SharpCompress.Common; +using SharpCompress.Readers; +using SharpCompress.Test.Mocks; +using SharpCompress.Writers; +using SharpCompress.Writers.GZip; +using SharpCompress.Writers.Zip; +using Xunit; + +namespace SharpCompress.Test; + +public class OptionsUsabilityTests : TestBase +{ + [Fact] + public void ReaderFactory_Stream_Default_Leaves_Stream_Open() + { + using var file = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip")); + using var testStream = new TestStream(file); + + using (var reader = ReaderFactory.OpenReader(testStream)) + { + reader.MoveToNextEntry(); + } + + Assert.False(testStream.IsDisposed); + } + + [Fact] + public void ArchiveFactory_Stream_Default_Leaves_Stream_Open() + { + using var file = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip")); + using var testStream = new TestStream(file); + + using (var archive = ArchiveFactory.OpenArchive(testStream)) + { + _ = archive.Entries; + } + + Assert.False(testStream.IsDisposed); + } + + [Fact] + public async Task ReaderFactory_Stream_Default_Leaves_Stream_Open_Async() + { + using var file = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip")); + using var testStream = new TestStream(file); + + await using ( + var reader = await ReaderFactory.OpenAsyncReader(new AsyncOnlyStream(testStream)) + ) + { + await reader.MoveToNextEntryAsync(); + } + + Assert.False(testStream.IsDisposed); + } + + [Fact] + public void WriterOptions_Invalid_CompressionLevels_Throw() + { + Assert.Throws(() => + new WriterOptions(CompressionType.Deflate, 10) + ); + Assert.Throws(() => + new WriterOptions(CompressionType.ZStandard, 0) + ); + Assert.Throws(() => + new WriterOptions(CompressionType.BZip2, 1) + ); + } + + [Fact] + public void ZipWriterOptions_Invalid_CompressionLevels_Throw() + { + Assert.Throws(() => + new ZipWriterOptions(CompressionType.Deflate, 10) + ); + Assert.Throws(() => + new ZipWriterOptions(CompressionType.ZStandard, 23) + ); + } + + [Fact] + public void GZipWriterOptions_Invalid_Settings_Throw() + { + Assert.Throws(() => new GZipWriterOptions(10)); + Assert.Throws(() => + new GZipWriterOptions { CompressionType = CompressionType.Deflate } + ); + } + + [Fact] + public void ZipWriterEntryOptions_Invalid_CompressionLevel_Throws() + { + using var destination = new MemoryStream(); + using var source = new MemoryStream(new byte[] { 1, 2, 3 }); + using var writer = new ZipWriter( + destination, + new ZipWriterOptions(CompressionType.Deflate) + ); + + var options = new ZipWriterEntryOptions { CompressionLevel = 11 }; + + Assert.Throws(() => + writer.Write("entry.bin", source, options) + ); + } + + [Fact] + public void WriterOptions_Factory_Methods_Create_Valid_Options() + { + // ForZip + var zipOptions = WriterOptions.ForZip(); + Assert.Equal(CompressionType.Deflate, zipOptions.CompressionType); + Assert.True(zipOptions.LeaveStreamOpen); + + // ForTar + var tarOptions = WriterOptions.ForTar(); + Assert.Equal(CompressionType.None, tarOptions.CompressionType); + + // ForGZip + var gzipOptions = WriterOptions.ForGZip(); + Assert.Equal(CompressionType.GZip, gzipOptions.CompressionType); + } + + [Fact] + public void WriterOptions_Fluent_Methods_Modify_Correctly() + { + var options = WriterOptions.ForZip().WithLeaveStreamOpen(false).WithCompressionLevel(9); + + Assert.Equal(CompressionType.Deflate, options.CompressionType); + Assert.Equal(9, options.CompressionLevel); + Assert.False(options.LeaveStreamOpen); + } + + [Fact] + public void WriterOptions_Factory_And_Fluent_Equivalent_To_Constructor() + { + // Factory + fluent approach + var factoryApproach = WriterOptions + .ForZip() + .WithLeaveStreamOpen(false) + .WithCompressionLevel(9); + + // Traditional constructor approach + var constructorApproach = new WriterOptions(CompressionType.Deflate) + { + CompressionLevel = 9, + LeaveStreamOpen = false, + }; + + Assert.Equal(factoryApproach.CompressionType, constructorApproach.CompressionType); + Assert.Equal(factoryApproach.CompressionLevel, constructorApproach.CompressionLevel); + Assert.Equal(factoryApproach.LeaveStreamOpen, constructorApproach.LeaveStreamOpen); + } + + [Fact] + public void ReaderOptions_Fluent_Methods_Modify_Correctly() + { + var options = new ReaderOptions() + .WithLeaveStreamOpen(false) + .WithPassword("secret") + .WithLookForHeader(true) + .WithBufferSize(65536); + + Assert.False(options.LeaveStreamOpen); + Assert.Equal("secret", options.Password); + Assert.True(options.LookForHeader); + Assert.Equal(65536, options.BufferSize); + } + + [Fact] + public void ReaderOptions_Fluent_And_Initializer_Equivalent() + { + // Fluent approach + var fluentApproach = new ReaderOptions() + .WithLeaveStreamOpen(false) + .WithPassword("secret") + .WithLookForHeader(true) + .WithOverwrite(false); + + // Object initializer approach + var initializerApproach = new ReaderOptions + { + LeaveStreamOpen = false, + Password = "secret", + LookForHeader = true, + Overwrite = false, + }; + + Assert.Equal(fluentApproach.LeaveStreamOpen, initializerApproach.LeaveStreamOpen); + Assert.Equal(fluentApproach.Password, initializerApproach.Password); + Assert.Equal(fluentApproach.LookForHeader, initializerApproach.LookForHeader); + Assert.Equal(fluentApproach.Overwrite, initializerApproach.Overwrite); + } + + [Fact] + public void ReaderOptions_Presets_Have_Correct_Defaults() + { + var external = ReaderOptions.ForExternalStream; + Assert.True(external.LeaveStreamOpen); + + var owned = ReaderOptions.ForOwnedFile; + Assert.False(owned.LeaveStreamOpen); + + var safe = ReaderOptions.SafeExtract; + Assert.False(safe.Overwrite); + + var flat = ReaderOptions.FlatExtract; + Assert.False(flat.ExtractFullPath); + Assert.True(flat.Overwrite); + } + + [Fact] + public void ReaderOptions_Factory_ForEncryptedArchive_Sets_Password() + { + var options = ReaderOptions.ForEncryptedArchive("myPassword"); + Assert.Equal("myPassword", options.Password); + + var noPassword = ReaderOptions.ForEncryptedArchive(); + Assert.Null(noPassword.Password); + } + + [Fact] + public void ReaderOptions_Factory_ForEncoding_Sets_Encoding() + { + var encoding = new ArchiveEncoding { Default = System.Text.Encoding.UTF8 }; + var options = ReaderOptions.ForEncoding(encoding); + Assert.Equal(encoding, options.ArchiveEncoding); + } + + [Fact] + public void ReaderOptions_Factory_ForSelfExtractingArchive_Configures_Correctly() + { + var options = ReaderOptions.ForSelfExtractingArchive("password"); + Assert.True(options.LookForHeader); + Assert.Equal("password", options.Password); + Assert.Equal(1_048_576, options.RewindableBufferSize); + + var noPassword = ReaderOptions.ForSelfExtractingArchive(); + Assert.True(noPassword.LookForHeader); + Assert.Null(noPassword.Password); + Assert.Equal(1_048_576, noPassword.RewindableBufferSize); + } +} diff --git a/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs index 7b2fbe7e..95fef9a8 100644 --- a/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Rar/RarArchiveAsyncTests.cs @@ -80,10 +80,7 @@ public class RarArchiveAsyncTests : ArchiveTests if (!entry.IsDirectory) { Assert.Equal(CompressionType.Rar, entry.CompressionType); - await entry.WriteToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await entry.WriteToDirectoryAsync(SCRATCH_FILES_PATH); } } } @@ -107,10 +104,7 @@ public class RarArchiveAsyncTests : ArchiveTests { foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) { - await entry.WriteToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await entry.WriteToDirectoryAsync(SCRATCH_FILES_PATH); } } VerifyFiles(); @@ -141,10 +135,7 @@ public class RarArchiveAsyncTests : ArchiveTests using var archive = ArchiveFactory.OpenArchive(stream); foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) { - await entry.WriteToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await entry.WriteToDirectoryAsync(SCRATCH_FILES_PATH); } } @@ -158,10 +149,7 @@ public class RarArchiveAsyncTests : ArchiveTests { foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) { - await entry.WriteToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await entry.WriteToDirectoryAsync(SCRATCH_FILES_PATH); } } VerifyFiles(); @@ -183,10 +171,7 @@ public class RarArchiveAsyncTests : ArchiveTests Assert.False(archive.IsSolid); foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) { - await entry.WriteToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await entry.WriteToDirectoryAsync(SCRATCH_FILES_PATH); } } VerifyFiles(); @@ -273,10 +258,7 @@ public class RarArchiveAsyncTests : ArchiveTests Assert.Equal(archive.IsSolid, isSolid); foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) { - await entry.WriteToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await entry.WriteToDirectoryAsync(SCRATCH_FILES_PATH); } } @@ -337,10 +319,7 @@ public class RarArchiveAsyncTests : ArchiveTests { foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) { - await entry.WriteToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await entry.WriteToDirectoryAsync(SCRATCH_FILES_PATH); } } VerifyFiles(); @@ -639,10 +618,7 @@ public class RarArchiveAsyncTests : ArchiveTests using var archive = ArchiveFactory.OpenArchive(stream); foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) { - await entry.WriteToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await entry.WriteToDirectoryAsync(SCRATCH_FILES_PATH); } VerifyFiles(); } @@ -665,10 +641,7 @@ public class RarArchiveAsyncTests : ArchiveTests if (!reader.Entry.IsDirectory) { Assert.Equal(compression, reader.Entry.CompressionType); - await reader.WriteEntryToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH); } } } @@ -676,10 +649,7 @@ public class RarArchiveAsyncTests : ArchiveTests await foreach (var entry in archive.EntriesAsync.Where(entry => !entry.IsDirectory)) { - await entry.WriteToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await entry.WriteToDirectoryAsync(SCRATCH_FILES_PATH); } VerifyFiles(); } @@ -690,10 +660,7 @@ public class RarArchiveAsyncTests : ArchiveTests using var archive = ArchiveFactory.OpenArchive(testArchive); foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) { - await entry.WriteToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await entry.WriteToDirectoryAsync(SCRATCH_FILES_PATH); } VerifyFiles(); } @@ -710,14 +677,52 @@ public class RarArchiveAsyncTests : ArchiveTests ); foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) { - await entry.WriteToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await entry.WriteToDirectoryAsync(SCRATCH_FILES_PATH); } VerifyFiles(); } + /// + /// Tests for Issue #1050 - RAR extraction with WriteToDirectoryAsync creates folders + /// but places all files at the top level instead of in their subdirectories. + /// + [Fact] + public async ValueTask Rar_Issue1050_WriteToDirectoryAsync_ExtractsToSubdirectories() + { + var testFile = "Rar.issue1050.rar"; + using var fileStream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, testFile)); + await using var archive = RarArchive.OpenAsyncArchive(fileStream); + + // Extract using archive.WriteToDirectoryAsync without explicit options + await archive.WriteToDirectoryAsync(SCRATCH_FILES_PATH); + + // Verify files are in their subdirectories, not at the root + Assert.True( + File.Exists(Path.Combine(SCRATCH_FILES_PATH, "PhysicsBraid", "263825.tr11dtp")), + "File should be in PhysicsBraid subdirectory" + ); + Assert.True( + File.Exists(Path.Combine(SCRATCH_FILES_PATH, "Animations", "15441.tr11anim")), + "File should be in Animations subdirectory" + ); + Assert.True( + File.Exists(Path.Combine(SCRATCH_FILES_PATH, "Braid", "766728.tr11dtp")), + "File should be in Braid subdirectory" + ); + Assert.True( + File.Exists(Path.Combine(SCRATCH_FILES_PATH, "Braid", "766832.tr11dtp")), + "File should be in Braid subdirectory" + ); + Assert.True( + File.Exists(Path.Combine(SCRATCH_FILES_PATH, "HeadBraid", "321353.tr11modeldata")), + "File should be in HeadBraid subdirectory" + ); + + // NOTE: The file size check is omitted because there's a separate pre-existing bug + // in the async RAR stream implementation that causes incorrect file sizes. + // This test only verifies the directory structure fix. + } + private async ValueTask ArchiveOpenStreamReadAsync( ReaderOptions? readerOptions, params string[] testArchives @@ -730,10 +735,7 @@ public class RarArchiveAsyncTests : ArchiveTests ); foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) { - await entry.WriteToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await entry.WriteToDirectoryAsync(SCRATCH_FILES_PATH); } VerifyFiles(); } diff --git a/tests/SharpCompress.Test/Rar/RarArchiveTests.cs b/tests/SharpCompress.Test/Rar/RarArchiveTests.cs index c33511d4..72029d56 100644 --- a/tests/SharpCompress.Test/Rar/RarArchiveTests.cs +++ b/tests/SharpCompress.Test/Rar/RarArchiveTests.cs @@ -79,10 +79,7 @@ public class RarArchiveTests : ArchiveTests if (!entry.IsDirectory) { Assert.Equal(CompressionType.Rar, entry.CompressionType); - entry.WriteToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + entry.WriteToDirectory(SCRATCH_FILES_PATH); } } } @@ -106,10 +103,7 @@ public class RarArchiveTests : ArchiveTests { foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) { - entry.WriteToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + entry.WriteToDirectory(SCRATCH_FILES_PATH); } } VerifyFiles(); @@ -137,10 +131,7 @@ public class RarArchiveTests : ArchiveTests using var archive = ArchiveFactory.OpenArchive(stream); foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) { - entry.WriteToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + entry.WriteToDirectory(SCRATCH_FILES_PATH); } } @@ -154,10 +145,7 @@ public class RarArchiveTests : ArchiveTests { foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) { - entry.WriteToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + entry.WriteToDirectory(SCRATCH_FILES_PATH); } } VerifyFiles(); @@ -177,10 +165,7 @@ public class RarArchiveTests : ArchiveTests Assert.False(archive.IsSolid); foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) { - entry.WriteToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + entry.WriteToDirectory(SCRATCH_FILES_PATH); } } VerifyFiles(); @@ -266,10 +251,7 @@ public class RarArchiveTests : ArchiveTests Assert.Equal(archive.IsSolid, isSolid); foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) { - entry.WriteToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + entry.WriteToDirectory(SCRATCH_FILES_PATH); } } @@ -327,10 +309,7 @@ public class RarArchiveTests : ArchiveTests { foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) { - entry.WriteToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + entry.WriteToDirectory(SCRATCH_FILES_PATH); } } VerifyFiles(); @@ -722,6 +701,47 @@ public class RarArchiveTests : ArchiveTests Assert.Contains("unpacked file size does not match header", exception.Message); } + /// + /// Tests for Issue #1050 - RAR extraction with WriteToDirectory creates folders + /// but places all files at the top level instead of in their subdirectories. + /// + [Fact] + public void Rar_Issue1050_WriteToDirectory_ExtractsToSubdirectories() + { + var testFile = "Rar.issue1050.rar"; + using var fileStream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, testFile)); + using var archive = RarArchive.OpenArchive(fileStream); + + // Extract using archive.WriteToDirectory without explicit options + archive.WriteToDirectory(SCRATCH_FILES_PATH); + + // Verify files are in their subdirectories, not at the root + Assert.True( + File.Exists(Path.Combine(SCRATCH_FILES_PATH, "PhysicsBraid", "263825.tr11dtp")), + "File should be in PhysicsBraid subdirectory" + ); + Assert.True( + File.Exists(Path.Combine(SCRATCH_FILES_PATH, "Animations", "15441.tr11anim")), + "File should be in Animations subdirectory" + ); + Assert.True( + File.Exists(Path.Combine(SCRATCH_FILES_PATH, "Braid", "766728.tr11dtp")), + "File should be in Braid subdirectory" + ); + Assert.True( + File.Exists(Path.Combine(SCRATCH_FILES_PATH, "Braid", "766832.tr11dtp")), + "File should be in Braid subdirectory" + ); + Assert.True( + File.Exists(Path.Combine(SCRATCH_FILES_PATH, "HeadBraid", "321353.tr11modeldata")), + "File should be in HeadBraid subdirectory" + ); + + // Verify the exact file size of 766832.tr11dtp matches the archive entry size + var fileInfo = new FileInfo(Path.Combine(SCRATCH_FILES_PATH, "Braid", "766832.tr11dtp")); + Assert.Equal(4867620, fileInfo.Length); // Expected: 4,867,620 bytes + } + /// /// Test case for malformed RAR archives that previously caused infinite loops. /// This test verifies that attempting to read entries from a potentially malformed diff --git a/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs b/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs index d81507a9..eb25b76c 100644 --- a/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Rar/RarReaderAsyncTests.cs @@ -49,10 +49,7 @@ public class RarReaderAsyncTests : ReaderTests IAsyncReader reader = (IAsyncReader)baseReader; while (await reader.MoveToNextEntryAsync()) { - await reader.WriteEntryToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH); } } VerifyFiles(); @@ -83,10 +80,7 @@ public class RarReaderAsyncTests : ReaderTests IAsyncReader reader = (IAsyncReader)baseReader; while (await reader.MoveToNextEntryAsync()) { - await reader.WriteEntryToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH); } } VerifyFiles(); @@ -132,10 +126,7 @@ public class RarReaderAsyncTests : ReaderTests IAsyncReader reader = (IAsyncReader)baseReader; while (await reader.MoveToNextEntryAsync()) { - await reader.WriteEntryToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH); } } foreach (var stream in streams) @@ -262,10 +253,7 @@ public class RarReaderAsyncTests : ReaderTests while (await reader.MoveToNextEntryAsync()) { Assert.Equal(CompressionType.Rar, reader.Entry.CompressionType); - await reader.WriteEntryToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH); } } CompareFilesByPath( @@ -289,10 +277,7 @@ public class RarReaderAsyncTests : ReaderTests while (await reader.MoveToNextEntryAsync()) { Assert.Equal(CompressionType.Rar, reader.Entry.CompressionType); - await reader.WriteEntryToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH); } } VerifyFiles(); @@ -334,10 +319,7 @@ public class RarReaderAsyncTests : ReaderTests if (reader.Entry.Key.NotNull().Contains("jpg")) { Assert.Equal(CompressionType.Rar, reader.Entry.CompressionType); - await reader.WriteEntryToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH); } } } @@ -360,10 +342,7 @@ public class RarReaderAsyncTests : ReaderTests if (reader.Entry.Key.NotNull().Contains("jpg")) { Assert.Equal(CompressionType.Rar, reader.Entry.CompressionType); - await reader.WriteEntryToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH); } } } @@ -385,10 +364,7 @@ public class RarReaderAsyncTests : ReaderTests if (!reader.Entry.IsDirectory) { Assert.Equal(expectedCompression, reader.Entry.CompressionType); - await reader.WriteEntryToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH); } } VerifyFiles(); diff --git a/tests/SharpCompress.Test/Rar/RarReaderTests.cs b/tests/SharpCompress.Test/Rar/RarReaderTests.cs index fd0be593..beb3ad0e 100644 --- a/tests/SharpCompress.Test/Rar/RarReaderTests.cs +++ b/tests/SharpCompress.Test/Rar/RarReaderTests.cs @@ -46,10 +46,7 @@ public class RarReaderTests : ReaderTests { while (reader.MoveToNextEntry()) { - reader.WriteEntryToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + reader.WriteEntryToDirectory(SCRATCH_FILES_PATH); } } VerifyFiles(); @@ -80,10 +77,7 @@ public class RarReaderTests : ReaderTests { while (reader.MoveToNextEntry()) { - reader.WriteEntryToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + reader.WriteEntryToDirectory(SCRATCH_FILES_PATH); } } VerifyFiles(); @@ -128,10 +122,7 @@ public class RarReaderTests : ReaderTests { while (reader.MoveToNextEntry()) { - reader.WriteEntryToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + reader.WriteEntryToDirectory(SCRATCH_FILES_PATH); } } foreach (var stream in streams) @@ -236,10 +227,7 @@ public class RarReaderTests : ReaderTests while (reader.MoveToNextEntry()) { Assert.Equal(CompressionType.Rar, reader.Entry.CompressionType); - reader.WriteEntryToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + reader.WriteEntryToDirectory(SCRATCH_FILES_PATH); } } CompareFilesByPath( @@ -259,10 +247,7 @@ public class RarReaderTests : ReaderTests while (reader.MoveToNextEntry()) { Assert.Equal(CompressionType.Rar, reader.Entry.CompressionType); - reader.WriteEntryToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + reader.WriteEntryToDirectory(SCRATCH_FILES_PATH); } } VerifyFiles(); @@ -298,10 +283,7 @@ public class RarReaderTests : ReaderTests if (reader.Entry.Key.NotNull().Contains("jpg")) { Assert.Equal(CompressionType.Rar, reader.Entry.CompressionType); - reader.WriteEntryToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + reader.WriteEntryToDirectory(SCRATCH_FILES_PATH); } } } @@ -324,10 +306,7 @@ public class RarReaderTests : ReaderTests if (reader.Entry.Key.NotNull().Contains("jpg")) { Assert.Equal(CompressionType.Rar, reader.Entry.CompressionType); - reader.WriteEntryToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + reader.WriteEntryToDirectory(SCRATCH_FILES_PATH); } } } diff --git a/tests/SharpCompress.Test/ReaderTests.cs b/tests/SharpCompress.Test/ReaderTests.cs index 1aa35a74..2cc7d86c 100644 --- a/tests/SharpCompress.Test/ReaderTests.cs +++ b/tests/SharpCompress.Test/ReaderTests.cs @@ -89,10 +89,7 @@ public abstract class ReaderTests : TestBase if (!reader.Entry.IsDirectory) { Assert.Equal(expectedCompression, reader.Entry.CompressionType); - reader.WriteEntryToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + reader.WriteEntryToDirectory(SCRATCH_FILES_PATH); } } } @@ -103,10 +100,7 @@ public abstract class ReaderTests : TestBase { if (!reader.Entry.IsDirectory) { - reader.WriteEntryToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + reader.WriteEntryToDirectory(SCRATCH_FILES_PATH); } } } @@ -184,8 +178,7 @@ public abstract class ReaderTests : TestBase await using ( var reader = await ReaderFactory.OpenAsyncReader( new AsyncOnlyStream(testStream), - options, - cancellationToken + options ) ) { @@ -213,11 +206,7 @@ public abstract class ReaderTests : TestBase Assert.Equal(expectedCompression, reader.Entry.CompressionType); } - await reader.WriteEntryToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true }, - cancellationToken - ); + await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH, cancellationToken); } } } @@ -234,10 +223,7 @@ public abstract class ReaderTests : TestBase { Assert.Equal(compressionType, reader.Entry.CompressionType); - reader.WriteEntryToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + reader.WriteEntryToDirectory(SCRATCH_FILES_PATH); } CompareFilesByPath( @@ -281,10 +267,7 @@ public abstract class ReaderTests : TestBase while (reader.MoveToNextEntry()) { - reader.WriteEntryToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + reader.WriteEntryToDirectory(SCRATCH_FILES_PATH); } VerifyFiles(); diff --git a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs index bcaf32b3..ea9316e5 100644 --- a/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs +++ b/tests/SharpCompress.Test/SevenZip/SevenZipArchiveTests.cs @@ -268,10 +268,7 @@ public class SevenZipArchiveTests : ArchiveTests { if (!reader.Entry.IsDirectory) { - reader.WriteEntryToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + reader.WriteEntryToDirectory(SCRATCH_FILES_PATH); } } diff --git a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs index bbae8d00..39df3cc7 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs @@ -168,7 +168,10 @@ public class TarArchiveAsyncTests : ArchiveTests await using (var archive = TarArchive.OpenAsyncArchive(unmodified)) { await archive.AddEntryAsync("jpg\\test.jpg", jpg); - await archive.SaveToAsync(scratchPath, new WriterOptions(CompressionType.None)); + await archive.SaveToAsync( + scratchPath, + new TarWriterOptions(CompressionType.None, true) + ); } CompareArchivesByPath(modified, scratchPath); } @@ -186,7 +189,10 @@ public class TarArchiveAsyncTests : ArchiveTests x.Key.NotNull().EndsWith("jpg", StringComparison.OrdinalIgnoreCase) ); await archive.RemoveEntryAsync(entry); - await archive.SaveToAsync(scratchPath, new WriterOptions(CompressionType.None)); + await archive.SaveToAsync( + scratchPath, + new TarWriterOptions(CompressionType.None, true) + ); } CompareArchivesByPath(modified, scratchPath); } diff --git a/tests/SharpCompress.Test/Tar/TarArchiveDirectoryTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveDirectoryTests.cs index 79ec51c4..08120d13 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveDirectoryTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveDirectoryTests.cs @@ -3,7 +3,7 @@ using System.IO; using System.Linq; using SharpCompress.Archives.Tar; using SharpCompress.Common; -using SharpCompress.Writers; +using SharpCompress.Writers.Tar; using Xunit; namespace SharpCompress.Test.Tar; @@ -81,7 +81,7 @@ public class TarArchiveDirectoryTests : TestBase using (var fileStream = File.Create(scratchPath)) { - archive.SaveTo(fileStream, new WriterOptions(CompressionType.None)); + archive.SaveTo(fileStream, new TarWriterOptions(CompressionType.None, true)); } } diff --git a/tests/SharpCompress.Test/Tar/TarArchiveTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveTests.cs index a1edc20f..058806d1 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveTests.cs @@ -194,7 +194,7 @@ public class TarArchiveTests : ArchiveTests using (var archive = TarArchive.OpenArchive(unmodified)) { archive.AddEntry("jpg\\test.jpg", jpg); - archive.SaveTo(scratchPath, new WriterOptions(CompressionType.None)); + archive.SaveTo(scratchPath, new TarWriterOptions(CompressionType.None, true)); } CompareArchivesByPath(modified, scratchPath); } @@ -212,7 +212,7 @@ public class TarArchiveTests : ArchiveTests x.Key.NotNull().EndsWith("jpg", StringComparison.OrdinalIgnoreCase) ); archive.RemoveEntry(entry); - archive.SaveTo(scratchPath, new WriterOptions(CompressionType.None)); + archive.SaveTo(scratchPath, new TarWriterOptions(CompressionType.None, true)); } CompareArchivesByPath(modified, scratchPath); } diff --git a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs index e2a25254..87196754 100644 --- a/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs @@ -33,10 +33,7 @@ public class TarReaderAsyncTests : ReaderTests x++; if (x % 2 == 0) { - await reader.WriteEntryToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH); } } } diff --git a/tests/SharpCompress.Test/Tar/TarReaderTests.cs b/tests/SharpCompress.Test/Tar/TarReaderTests.cs index e8d7f150..d76792cf 100644 --- a/tests/SharpCompress.Test/Tar/TarReaderTests.cs +++ b/tests/SharpCompress.Test/Tar/TarReaderTests.cs @@ -31,10 +31,7 @@ public class TarReaderTests : ReaderTests x++; if (x % 2 == 0) { - reader.WriteEntryToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + reader.WriteEntryToDirectory(SCRATCH_FILES_PATH); } } } diff --git a/tests/SharpCompress.Test/WriterTests.cs b/tests/SharpCompress.Test/WriterTests.cs index f02b65d7..786cc41d 100644 --- a/tests/SharpCompress.Test/WriterTests.cs +++ b/tests/SharpCompress.Test/WriterTests.cs @@ -47,10 +47,7 @@ public class WriterTests : TestBase SharpCompressStream.CreateNonDisposing(stream), readerOptions ); - reader.WriteAllToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true } - ); + reader.WriteAllToDirectory(SCRATCH_FILES_PATH); } VerifyFiles(); } @@ -94,14 +91,9 @@ public class WriterTests : TestBase await using var reader = await ReaderFactory.OpenAsyncReader( new AsyncOnlyStream(SharpCompressStream.CreateNonDisposing(stream)), - readerOptions, - cancellationToken - ); - await reader.WriteAllToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true }, - cancellationToken + readerOptions ); + await reader.WriteAllToDirectoryAsync(SCRATCH_FILES_PATH, cancellationToken); } VerifyFiles(); } diff --git a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs index fe0224d7..5894d065 100644 --- a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs @@ -196,10 +196,7 @@ public class ZipArchiveAsyncTests : ArchiveTests { await foreach (var entry in archive.EntriesAsync.Where(entry => !entry.IsDirectory)) { - await entry.WriteToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await entry.WriteToDirectoryAsync(SCRATCH_FILES_PATH); } } finally @@ -218,10 +215,7 @@ public class ZipArchiveAsyncTests : ArchiveTests IAsyncArchive archive = ZipArchive.OpenAsyncArchive(new AsyncOnlyStream(stream)); try { - await archive.WriteToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await archive.WriteToDirectoryAsync(SCRATCH_FILES_PATH); } finally { @@ -248,11 +242,7 @@ public class ZipArchiveAsyncTests : ArchiveTests await using IAsyncArchive archive = ZipArchive.OpenAsyncArchive( new AsyncOnlyStream(stream) ); - await archive.WriteToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true }, - progress - ); + await archive.WriteToDirectoryAsync(SCRATCH_FILES_PATH, progress); } await Task.Delay(1000); diff --git a/tests/SharpCompress.Test/Zip/ZipArchiveDirectoryTests.cs b/tests/SharpCompress.Test/Zip/ZipArchiveDirectoryTests.cs index e4b37c69..7b6dc981 100644 --- a/tests/SharpCompress.Test/Zip/ZipArchiveDirectoryTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipArchiveDirectoryTests.cs @@ -3,7 +3,7 @@ using System.IO; using System.Linq; using SharpCompress.Archives.Zip; using SharpCompress.Common; -using SharpCompress.Writers; +using SharpCompress.Writers.Zip; using Xunit; namespace SharpCompress.Test.Zip; @@ -81,7 +81,7 @@ public class ZipArchiveDirectoryTests : TestBase using (var fileStream = File.Create(scratchPath)) { - archive.SaveTo(fileStream, new WriterOptions(CompressionType.Deflate)); + archive.SaveTo(fileStream, new ZipWriterOptions(CompressionType.Deflate)); } } diff --git a/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs b/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs index 6dc96186..9040bf14 100644 --- a/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipArchiveTests.cs @@ -285,8 +285,8 @@ public class ZipArchiveTests : ArchiveTests var str = "test.txt"; var source = new MemoryStream(Encoding.UTF8.GetBytes(str)); arc.AddEntry("test.txt", source, true, source.Length); - arc.SaveTo(scratchPath1, new WriterOptions(CompressionType.Deflate)); - arc.SaveTo(scratchPath2, new WriterOptions(CompressionType.Deflate)); + arc.SaveTo(scratchPath1, new ZipWriterOptions(CompressionType.Deflate)); + arc.SaveTo(scratchPath2, new ZipWriterOptions(CompressionType.Deflate)); } Assert.Equal(new FileInfo(scratchPath1).Length, new FileInfo(scratchPath2).Length); @@ -334,8 +334,8 @@ public class ZipArchiveTests : ArchiveTests { arc.AddEntry("1.txt", stream, false, stream.Length); arc.AddEntry("2.txt", stream, false, stream.Length); - arc.SaveTo(scratchPath1, new WriterOptions(CompressionType.Deflate)); - arc.SaveTo(scratchPath2, new WriterOptions(CompressionType.Deflate)); + arc.SaveTo(scratchPath1, new ZipWriterOptions(CompressionType.Deflate)); + arc.SaveTo(scratchPath2, new ZipWriterOptions(CompressionType.Deflate)); } } @@ -399,7 +399,7 @@ public class ZipArchiveTests : ArchiveTests var archiveStream = new MemoryStream(); - archive.SaveTo(archiveStream, new WriterOptions(CompressionType.LZMA)); + archive.SaveTo(archiveStream, new ZipWriterOptions(CompressionType.LZMA)); archiveStream.Position = 0; @@ -469,10 +469,7 @@ public class ZipArchiveTests : ArchiveTests { foreach (var entry in reader.Entries.Where(x => !x.IsDirectory)) { - entry.WriteToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + entry.WriteToDirectory(SCRATCH_FILES_PATH); } } VerifyFiles(); @@ -521,10 +518,7 @@ public class ZipArchiveTests : ArchiveTests { foreach (var entry in reader.Entries.Where(x => !x.IsDirectory)) { - entry.WriteToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + entry.WriteToDirectory(SCRATCH_FILES_PATH); } } VerifyFiles(); @@ -605,10 +599,7 @@ public class ZipArchiveTests : ArchiveTests using var archive = ZipArchive.OpenArchive(zipFile); foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) { - entry.WriteToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + entry.WriteToDirectory(SCRATCH_FILES_PATH); } }); } @@ -628,7 +619,7 @@ public class ZipArchiveTests : ArchiveTests var zipWriter = WriterFactory.OpenWriter( stream, ArchiveType.Zip, - new WriterOptions(CompressionType.Deflate) + new ZipWriterOptions(CompressionType.Deflate) ) ) { diff --git a/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs index ece913bf..fabbce67 100644 --- a/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs @@ -75,10 +75,7 @@ public class ZipReaderAsyncTests : ReaderTests x++; if (x % 2 == 0) { - await reader.WriteEntryToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH); } } } @@ -99,10 +96,7 @@ public class ZipReaderAsyncTests : ReaderTests x++; if (x % 2 == 0) { - await reader.WriteEntryToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH); } } } @@ -159,10 +153,7 @@ public class ZipReaderAsyncTests : ReaderTests if (!reader.Entry.IsDirectory) { Assert.Equal(CompressionType.BZip2, reader.Entry.CompressionType); - await reader.WriteEntryToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH); } } } @@ -175,16 +166,18 @@ public class ZipReaderAsyncTests : ReaderTests using var stream = new TestStream( File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip")) ); - await using (var reader = await ReaderFactory.OpenAsyncReader(new AsyncOnlyStream(stream))) + await using ( + var reader = await ReaderFactory.OpenAsyncReader( + new AsyncOnlyStream(stream), + new ReaderOptions().WithLeaveStreamOpen(false) + ) + ) { while (await reader.MoveToNextEntryAsync()) { if (!reader.Entry.IsDirectory) { - await reader.WriteEntryToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH); } } } @@ -204,10 +197,7 @@ public class ZipReaderAsyncTests : ReaderTests { if (!reader.Entry.IsDirectory) { - await reader.WriteEntryToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH); } } Assert.False(stream.IsDisposed); @@ -235,10 +225,7 @@ public class ZipReaderAsyncTests : ReaderTests if (!reader.Entry.IsDirectory) { Assert.Equal(CompressionType.Unknown, reader.Entry.CompressionType); - await reader.WriteEntryToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH); } } } @@ -266,10 +253,7 @@ public class ZipReaderAsyncTests : ReaderTests if (!reader.Entry.IsDirectory) { Assert.Equal(CompressionType.Unknown, reader.Entry.CompressionType); - await reader.WriteEntryToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH); } } } @@ -297,10 +281,7 @@ public class ZipReaderAsyncTests : ReaderTests if (!reader.Entry.IsDirectory) { Assert.Equal(CompressionType.None, reader.Entry.CompressionType); - await reader.WriteEntryToDirectoryAsync( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + await reader.WriteEntryToDirectoryAsync(SCRATCH_FILES_PATH); count++; } } diff --git a/tests/SharpCompress.Test/Zip/ZipReaderTests.cs b/tests/SharpCompress.Test/Zip/ZipReaderTests.cs index 34c0d37a..e4e6ee7a 100644 --- a/tests/SharpCompress.Test/Zip/ZipReaderTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipReaderTests.cs @@ -71,10 +71,7 @@ public class ZipReaderTests : ReaderTests x++; if (x % 2 == 0) { - reader.WriteEntryToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + reader.WriteEntryToDirectory(SCRATCH_FILES_PATH); } } } @@ -95,10 +92,7 @@ public class ZipReaderTests : ReaderTests x++; if (x % 2 == 0) { - reader.WriteEntryToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + reader.WriteEntryToDirectory(SCRATCH_FILES_PATH); } } } @@ -142,10 +136,7 @@ public class ZipReaderTests : ReaderTests if (!reader.Entry.IsDirectory) { Assert.Equal(CompressionType.BZip2, reader.Entry.CompressionType); - reader.WriteEntryToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + reader.WriteEntryToDirectory(SCRATCH_FILES_PATH); } } } @@ -158,16 +149,18 @@ public class ZipReaderTests : ReaderTests using var stream = new TestStream( File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip")) ); - using (var reader = ReaderFactory.OpenReader(stream)) + using ( + var reader = ReaderFactory.OpenReader( + stream, + new ReaderOptions().WithLeaveStreamOpen(false) + ) + ) { while (reader.MoveToNextEntry()) { if (!reader.Entry.IsDirectory) { - reader.WriteEntryToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + reader.WriteEntryToDirectory(SCRATCH_FILES_PATH); } } } @@ -185,10 +178,7 @@ public class ZipReaderTests : ReaderTests { if (!reader.Entry.IsDirectory) { - reader.WriteEntryToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + reader.WriteEntryToDirectory(SCRATCH_FILES_PATH); } } Assert.False(stream.IsDisposed); @@ -212,10 +202,7 @@ public class ZipReaderTests : ReaderTests if (!reader.Entry.IsDirectory) { Assert.Equal(CompressionType.Unknown, reader.Entry.CompressionType); - reader.WriteEntryToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + reader.WriteEntryToDirectory(SCRATCH_FILES_PATH); } } } @@ -237,10 +224,7 @@ public class ZipReaderTests : ReaderTests if (!reader.Entry.IsDirectory) { Assert.Equal(CompressionType.Unknown, reader.Entry.CompressionType); - reader.WriteEntryToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + reader.WriteEntryToDirectory(SCRATCH_FILES_PATH); } } } @@ -259,10 +243,7 @@ public class ZipReaderTests : ReaderTests if (!reader.Entry.IsDirectory) { Assert.Equal(CompressionType.None, reader.Entry.CompressionType); - reader.WriteEntryToDirectory( - SCRATCH_FILES_PATH, - new ExtractionOptions { ExtractFullPath = true, Overwrite = true } - ); + reader.WriteEntryToDirectory(SCRATCH_FILES_PATH); count++; } } diff --git a/tests/TestArchives/Archives/Rar.issue1050.rar b/tests/TestArchives/Archives/Rar.issue1050.rar new file mode 100644 index 00000000..704a0188 Binary files /dev/null and b/tests/TestArchives/Archives/Rar.issue1050.rar differ diff --git a/tests/TestArchives/Archives/large_test.txt.Z b/tests/TestArchives/Archives/large_test.txt.Z new file mode 100644 index 00000000..64ae2af1 Binary files /dev/null and b/tests/TestArchives/Archives/large_test.txt.Z differ