From 6125654b2e86ef49452ecedd450d1261c11d1708 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 5 Jan 2026 17:21:29 +0000
Subject: [PATCH 1/7] Initial plan
From aa3a40d968d605f57398be54f0a191cd3883c579 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 5 Jan 2026 17:27:50 +0000
Subject: [PATCH 2/7] Add BenchmarkDotNet integration with Archive and Reader
benchmarks
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
---
.gitignore | 4 +
Directory.Packages.props | 1 +
.../ArchiveReadBenchmarks.cs | 86 +++++++++
.../BenchmarkBase.cs | 27 +++
tests/SharpCompress.Performance/Program.cs | 57 +-----
.../ReaderBenchmarks.cs | 88 +++++++++
.../SharpCompress.Performance.csproj | 1 +
.../packages.lock.json | 175 ++++++++++++++++++
8 files changed, 392 insertions(+), 47 deletions(-)
create mode 100644 tests/SharpCompress.Performance/ArchiveReadBenchmarks.cs
create mode 100644 tests/SharpCompress.Performance/BenchmarkBase.cs
create mode 100644 tests/SharpCompress.Performance/ReaderBenchmarks.cs
diff --git a/.gitignore b/.gitignore
index 6c6a863dc..a657c03f2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,3 +20,7 @@ artifacts/
.DS_Store
*.snupkg
+
+# BenchmarkDotNet artifacts
+BenchmarkDotNet.Artifacts/
+**/BenchmarkDotNet.Artifacts/
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 703b2803c..74e6a08e1 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,5 +1,6 @@
+
diff --git a/tests/SharpCompress.Performance/ArchiveReadBenchmarks.cs b/tests/SharpCompress.Performance/ArchiveReadBenchmarks.cs
new file mode 100644
index 000000000..1f9d6cfd1
--- /dev/null
+++ b/tests/SharpCompress.Performance/ArchiveReadBenchmarks.cs
@@ -0,0 +1,86 @@
+using System.IO;
+using System.Linq;
+using BenchmarkDotNet.Attributes;
+using SharpCompress.Archives;
+
+namespace SharpCompress.Performance;
+
+///
+/// Benchmarks for Archive API operations across different formats.
+/// Archive API is used for random access to entries with seekable streams.
+///
+[MemoryDiagnoser]
+public class ArchiveReadBenchmarks : BenchmarkBase
+{
+ [Benchmark]
+ public void ZipArchiveRead()
+ {
+ var path = GetTestArchivePath("Zip.deflate.zip");
+ using var archive = ArchiveFactory.Open(path);
+ foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
+ {
+ using var stream = entry.OpenEntryStream();
+ stream.CopyTo(Stream.Null);
+ }
+ }
+
+ [Benchmark]
+ public void TarArchiveRead()
+ {
+ var path = GetTestArchivePath("Tar.tar");
+ using var archive = ArchiveFactory.Open(path);
+ foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
+ {
+ using var stream = entry.OpenEntryStream();
+ stream.CopyTo(Stream.Null);
+ }
+ }
+
+ [Benchmark]
+ public void TarGzArchiveRead()
+ {
+ var path = GetTestArchivePath("Tar.tar.gz");
+ using var archive = ArchiveFactory.Open(path);
+ foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
+ {
+ using var stream = entry.OpenEntryStream();
+ stream.CopyTo(Stream.Null);
+ }
+ }
+
+ [Benchmark]
+ public void TarBz2ArchiveRead()
+ {
+ var path = GetTestArchivePath("Tar.tar.bz2");
+ using var archive = ArchiveFactory.Open(path);
+ foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
+ {
+ using var stream = entry.OpenEntryStream();
+ stream.CopyTo(Stream.Null);
+ }
+ }
+
+ [Benchmark]
+ public void SevenZipArchiveRead()
+ {
+ var path = GetTestArchivePath("7Zip.LZMA2.7z");
+ using var archive = ArchiveFactory.Open(path);
+ foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
+ {
+ using var stream = entry.OpenEntryStream();
+ stream.CopyTo(Stream.Null);
+ }
+ }
+
+ [Benchmark]
+ public void RarArchiveRead()
+ {
+ var path = GetTestArchivePath("Rar.rar");
+ using var archive = ArchiveFactory.Open(path);
+ foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
+ {
+ using var stream = entry.OpenEntryStream();
+ stream.CopyTo(Stream.Null);
+ }
+ }
+}
diff --git a/tests/SharpCompress.Performance/BenchmarkBase.cs b/tests/SharpCompress.Performance/BenchmarkBase.cs
new file mode 100644
index 000000000..ae4a5b368
--- /dev/null
+++ b/tests/SharpCompress.Performance/BenchmarkBase.cs
@@ -0,0 +1,27 @@
+using System;
+using System.IO;
+
+namespace SharpCompress.Performance;
+
+///
+/// Base class for all benchmarks providing common setup for test archives path
+///
+public class BenchmarkBase
+{
+ protected readonly string TEST_ARCHIVES_PATH;
+
+ public BenchmarkBase()
+ {
+ 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();
+
+ TEST_ARCHIVES_PATH = Path.Combine(SOLUTION_BASE_PATH, "TestArchives", "Archives");
+ }
+
+ protected string GetTestArchivePath(string filename) =>
+ Path.Combine(TEST_ARCHIVES_PATH, filename);
+}
diff --git a/tests/SharpCompress.Performance/Program.cs b/tests/SharpCompress.Performance/Program.cs
index d445208af..dac63d955 100644
--- a/tests/SharpCompress.Performance/Program.cs
+++ b/tests/SharpCompress.Performance/Program.cs
@@ -1,54 +1,17 @@
-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.Running;
-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/"))
+internal class Program
{
- var testArchives = new[]
- {
- "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++)
+ static void Main(string[] args)
{
- using var found = ArchiveFactory.Open(arcs[0]);
- foreach (var entry in found.Entries.Where(entry => !entry.IsDirectory))
- {
- Console.WriteLine($"Extracting {entry.Key}");
- using var entryStream = entry.OpenEntryStream();
- entryStream.CopyTo(Stream.Null);
- }
- /*using var found = ReaderFactory.Open(arcs[0]);
- while (found.MoveToNextEntry())
- {
- var entry = found.Entry;
- if (entry.IsDirectory)
- continue;
+ // Run all benchmarks in the assembly
+ var config = DefaultConfig.Instance;
- Console.WriteLine($"Extracting {entry.Key}");
- found.WriteEntryTo(Stream.Null);
- }*/
+ // BenchmarkRunner will find all classes with [Benchmark] attributes
+ BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config);
}
-
- Console.WriteLine("Still running...");
}
-await Task.Delay(500);
+
diff --git a/tests/SharpCompress.Performance/ReaderBenchmarks.cs b/tests/SharpCompress.Performance/ReaderBenchmarks.cs
new file mode 100644
index 000000000..639f785b2
--- /dev/null
+++ b/tests/SharpCompress.Performance/ReaderBenchmarks.cs
@@ -0,0 +1,88 @@
+using System.IO;
+using BenchmarkDotNet.Attributes;
+using SharpCompress.Readers;
+
+namespace SharpCompress.Performance;
+
+///
+/// Benchmarks for Reader API operations across different formats.
+/// Reader API is used for forward-only streaming with non-seekable streams.
+///
+[MemoryDiagnoser]
+public class ReaderBenchmarks : BenchmarkBase
+{
+ [Benchmark]
+ public void ZipReaderRead()
+ {
+ var path = GetTestArchivePath("Zip.deflate.zip");
+ using var stream = File.OpenRead(path);
+ using var reader = ReaderFactory.Open(stream);
+ while (reader.MoveToNextEntry())
+ {
+ if (!reader.Entry.IsDirectory)
+ {
+ reader.WriteEntryTo(Stream.Null);
+ }
+ }
+ }
+
+ [Benchmark]
+ public void TarReaderRead()
+ {
+ var path = GetTestArchivePath("Tar.tar");
+ using var stream = File.OpenRead(path);
+ using var reader = ReaderFactory.Open(stream);
+ while (reader.MoveToNextEntry())
+ {
+ if (!reader.Entry.IsDirectory)
+ {
+ reader.WriteEntryTo(Stream.Null);
+ }
+ }
+ }
+
+ [Benchmark]
+ public void TarGzReaderRead()
+ {
+ var path = GetTestArchivePath("Tar.tar.gz");
+ using var stream = File.OpenRead(path);
+ using var reader = ReaderFactory.Open(stream);
+ while (reader.MoveToNextEntry())
+ {
+ if (!reader.Entry.IsDirectory)
+ {
+ reader.WriteEntryTo(Stream.Null);
+ }
+ }
+ }
+
+ [Benchmark]
+ public void TarBz2ReaderRead()
+ {
+ var path = GetTestArchivePath("Tar.tar.bz2");
+ using var stream = File.OpenRead(path);
+ using var reader = ReaderFactory.Open(stream);
+ while (reader.MoveToNextEntry())
+ {
+ if (!reader.Entry.IsDirectory)
+ {
+ reader.WriteEntryTo(Stream.Null);
+ }
+ }
+ }
+
+ [Benchmark]
+ public void RarReaderRead()
+ {
+ var path = GetTestArchivePath("Rar.rar");
+ using var stream = File.OpenRead(path);
+ using var reader = ReaderFactory.Open(stream);
+ while (reader.MoveToNextEntry())
+ {
+ if (!reader.Entry.IsDirectory)
+ {
+ reader.WriteEntryTo(Stream.Null);
+ }
+ }
+ }
+}
diff --git a/tests/SharpCompress.Performance/SharpCompress.Performance.csproj b/tests/SharpCompress.Performance/SharpCompress.Performance.csproj
index cab757e47..d4ad46d2f 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/packages.lock.json b/tests/SharpCompress.Performance/packages.lock.json
index 255570ad3..07abfe93f 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.14.0, )",
+ "resolved": "0.14.0",
+ "contentHash": "eIPSDKi3oni734M1rt/XJAwGQQOIf9gLjRRKKJ0HuVy3vYd7gnmAIX1bTjzI9ZbAY/nPddgqqgM/TeBYitMCIg==",
+ "dependencies": {
+ "BenchmarkDotNet.Annotations": "0.14.0",
+ "CommandLineParser": "2.9.1",
+ "Gee.External.Capstone": "2.3.0",
+ "Iced": "1.17.0",
+ "Microsoft.CodeAnalysis.CSharp": "4.1.0",
+ "Microsoft.Diagnostics.Runtime": "2.2.332302",
+ "Microsoft.Diagnostics.Tracing.TraceEvent": "3.1.8",
+ "Microsoft.DotNet.PlatformAbstractions": "3.1.6",
+ "Perfolizer": "[0.3.17]",
+ "System.Management": "5.0.0"
+ }
+ },
"JetBrains.Profiler.SelfApi": {
"type": "Direct",
"requested": "[2.5.15, )",
@@ -12,6 +30,26 @@
"JetBrains.Profiler.Api": "1.4.10"
}
},
+ "BenchmarkDotNet.Annotations": {
+ "type": "Transitive",
+ "resolved": "0.14.0",
+ "contentHash": "CUDCg6bgHrDzhjnA+IOBl5gAo8Y5hZ2YSs7MBXrYMlMKpBZqrD5ez0537uDveOkcf+YWAoK+S4sMcuWPbIz8bw=="
+ },
+ "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.17.0",
+ "contentHash": "8x+HCVTl/HHTGpscH3vMBhV8sknN/muZFw9s3TsI8SA6+c43cOTCi2+jE4KsU8pNLbJ++iF2ZFcpcXHXtDglnw=="
+ },
"JetBrains.FormatRipper": {
"type": "Transitive",
"resolved": "2.4.0",
@@ -33,8 +71,145 @@
"JetBrains.HabitatDetector": "1.4.5"
}
},
+ "Microsoft.CodeAnalysis.Analyzers": {
+ "type": "Transitive",
+ "resolved": "3.3.3",
+ "contentHash": "j/rOZtLMVJjrfLRlAMckJLPW/1rze9MT1yfWqSIbUPGRu1m1P0fuo9PmqapwsmePfGB5PJrudQLvmUOAMF0DqQ=="
+ },
+ "Microsoft.CodeAnalysis.Common": {
+ "type": "Transitive",
+ "resolved": "4.1.0",
+ "contentHash": "bNzTyxP3iD5FPFHfVDl15Y6/wSoI7e3MeV0lOaj9igbIKTjgrmuw6LoVJ06jUNFA7+KaDC/OIsStWl/FQJz6sQ==",
+ "dependencies": {
+ "Microsoft.CodeAnalysis.Analyzers": "3.3.3"
+ }
+ },
+ "Microsoft.CodeAnalysis.CSharp": {
+ "type": "Transitive",
+ "resolved": "4.1.0",
+ "contentHash": "sbu6kDGzo9bfQxuqWpeEE7I9P30bSuZEnpDz9/qz20OU6pm79Z63+/BsAzO2e/R/Q97kBrpj647wokZnEVr97w==",
+ "dependencies": {
+ "Microsoft.CodeAnalysis.Common": "[4.1.0]"
+ }
+ },
+ "Microsoft.Diagnostics.NETCore.Client": {
+ "type": "Transitive",
+ "resolved": "0.2.251802",
+ "contentHash": "bqnYl6AdSeboeN4v25hSukK6Odm6/54E3Y2B8rBvgqvAW0mF8fo7XNRVE2DMOG7Rk0fiuA079QIH28+V+W1Zdg==",
+ "dependencies": {
+ "Microsoft.Bcl.AsyncInterfaces": "1.1.0",
+ "Microsoft.Extensions.Logging": "2.1.1"
+ }
+ },
+ "Microsoft.Diagnostics.Runtime": {
+ "type": "Transitive",
+ "resolved": "2.2.332302",
+ "contentHash": "Hp84ivxSKIMTBzYSATxmUsm3YSXHWivcwiRRbsydGmqujMUK8BAueLN0ssAVEOkOBmh0vjUBhrq7YcroT7VCug==",
+ "dependencies": {
+ "Microsoft.Diagnostics.NETCore.Client": "0.2.251802"
+ }
+ },
+ "Microsoft.Diagnostics.Tracing.TraceEvent": {
+ "type": "Transitive",
+ "resolved": "3.1.8",
+ "contentHash": "kl3UMrZKSeSEYZ8rt/GjLUQToREjgQABqfg6PzQBmSlYHTZOKE9ePEOS2xptROQ9SVvngg3QGX51TIT11iZ0wA=="
+ },
+ "Microsoft.DotNet.PlatformAbstractions": {
+ "type": "Transitive",
+ "resolved": "3.1.6",
+ "contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg=="
+ },
+ "Microsoft.Extensions.Configuration": {
+ "type": "Transitive",
+ "resolved": "2.1.1",
+ "contentHash": "LjVKO6P2y52c5ZhTLX/w8zc5H4Y3J/LJsgqTBj49TtFq/hAtVNue/WA0F6/7GMY90xhD7K0MDZ4qpOeWXbLvzg==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "2.1.1"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Abstractions": {
+ "type": "Transitive",
+ "resolved": "2.1.1",
+ "contentHash": "VfuZJNa0WUshZ/+8BFZAhwFKiKuu/qOUCFntfdLpHj7vcRnsGHqd3G2Hse78DM+pgozczGM63lGPRLmy+uhUOA==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "2.1.1"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Binder": {
+ "type": "Transitive",
+ "resolved": "2.1.1",
+ "contentHash": "fcLCTS03poWE4v9tSNBr3pWn0QwGgAn1vzqHXlXgvqZeOc7LvQNzaWcKRQZTdEc3+YhQKwMsOtm3VKSA2aWQ8w==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration": "2.1.1"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection.Abstractions": {
+ "type": "Transitive",
+ "resolved": "2.1.1",
+ "contentHash": "MgYpU5cwZohUMKKg3sbPhvGG+eAZ/59E9UwPwlrUkyXU+PGzqwZg9yyQNjhxuAWmoNoFReoemeCku50prYSGzA=="
+ },
+ "Microsoft.Extensions.Logging": {
+ "type": "Transitive",
+ "resolved": "2.1.1",
+ "contentHash": "hh+mkOAQDTp6XH80xJt3+wwYVzkbwYQl9XZRCz4Um0JjP/o7N9vHM3rZ6wwwtr+BBe/L6iBO2sz0px6OWBzqZQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Binder": "2.1.1",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "2.1.1",
+ "Microsoft.Extensions.Logging.Abstractions": "2.1.1",
+ "Microsoft.Extensions.Options": "2.1.1"
+ }
+ },
+ "Microsoft.Extensions.Logging.Abstractions": {
+ "type": "Transitive",
+ "resolved": "2.1.1",
+ "contentHash": "XRzK7ZF+O6FzdfWrlFTi1Rgj2080ZDsd46vzOjadHUB0Cz5kOvDG8vI7caa5YFrsHQpcfn0DxtjS4E46N4FZsA=="
+ },
+ "Microsoft.Extensions.Options": {
+ "type": "Transitive",
+ "resolved": "2.1.1",
+ "contentHash": "V7lXCU78lAbzaulCGFKojcCyG8RTJicEbiBkPJjFqiqXwndEBBIehdXRMWEVU3UtzQ1yDvphiWUL9th6/4gJ7w==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "2.1.1",
+ "Microsoft.Extensions.Primitives": "2.1.1"
+ }
+ },
+ "Microsoft.Extensions.Primitives": {
+ "type": "Transitive",
+ "resolved": "2.1.1",
+ "contentHash": "scJ1GZNIxMmjpENh0UZ8XCQ6vzr/LzeF9WvEA51Ix2OQGAs9WPgPu8ABVUdvpKPLuor/t05gm6menJK3PwqOXg=="
+ },
+ "Microsoft.NETCore.Platforms": {
+ "type": "Transitive",
+ "resolved": "5.0.0",
+ "contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ=="
+ },
+ "Perfolizer": {
+ "type": "Transitive",
+ "resolved": "0.3.17",
+ "contentHash": "FQgtCoF2HFwvzKWulAwBS5BGLlh8pgbrJtOp47jyBwh2CW16juVtacN1azOA2BqdrJXkXTNLNRMo7ZlHHiuAnA=="
+ },
+ "System.CodeDom": {
+ "type": "Transitive",
+ "resolved": "5.0.0",
+ "contentHash": "JPJArwA1kdj8qDAkY2XGjSWoYnqiM7q/3yRNkt6n28Mnn95MuEGkZXUbPBf7qc3IjwrGY5ttQon7yqHZyQJmOQ=="
+ },
+ "System.Management": {
+ "type": "Transitive",
+ "resolved": "5.0.0",
+ "contentHash": "MF1CHaRcC+MLFdnDthv4/bKWBZnlnSpkGqa87pKukQefgEdwtb9zFW6zs0GjPp73qtpYYg4q6PEKbzJbxCpKfw==",
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "5.0.0",
+ "System.CodeDom": "5.0.0"
+ }
+ },
"sharpcompress": {
"type": "Project"
+ },
+ "Microsoft.Bcl.AsyncInterfaces": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.0, )",
+ "resolved": "1.1.0",
+ "contentHash": "1Am6l4Vpn3/K32daEqZI+FFr96OlZkgwK2LcT3pZ2zWubR5zTPW3/FkO1Rat9kb7oQOa4rxgl9LJHc5tspCWfg=="
}
}
}
From 5b1d11bc1d8f6db3e423aea11709df5a85a8a970 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 5 Jan 2026 17:31:32 +0000
Subject: [PATCH 3/7] Add WriteBenchmarks, BaselineComparisonBenchmarks, and
comprehensive documentation
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
---
.../BaselineComparisonBenchmarks.cs | 47 +++++++
tests/SharpCompress.Performance/README.md | 131 ++++++++++++++++++
.../WriteBenchmarks.cs | 105 ++++++++++++++
3 files changed, 283 insertions(+)
create mode 100644 tests/SharpCompress.Performance/BaselineComparisonBenchmarks.cs
create mode 100644 tests/SharpCompress.Performance/README.md
create mode 100644 tests/SharpCompress.Performance/WriteBenchmarks.cs
diff --git a/tests/SharpCompress.Performance/BaselineComparisonBenchmarks.cs b/tests/SharpCompress.Performance/BaselineComparisonBenchmarks.cs
new file mode 100644
index 000000000..3907858fc
--- /dev/null
+++ b/tests/SharpCompress.Performance/BaselineComparisonBenchmarks.cs
@@ -0,0 +1,47 @@
+using System.IO;
+using System.Linq;
+using BenchmarkDotNet.Attributes;
+using SharpCompress.Archives;
+
+namespace SharpCompress.Performance;
+
+///
+/// Benchmarks comparing current code against a baseline.
+/// Use [Baseline] attribute to mark the reference benchmark.
+///
+[MemoryDiagnoser]
+[RankColumn]
+public class BaselineComparisonBenchmarks : BenchmarkBase
+{
+ ///
+ /// Baseline benchmark for Zip archive reading.
+ /// This serves as the reference point for comparison.
+ ///
+ [Benchmark(Baseline = true)]
+ public void ZipArchiveRead_Baseline()
+ {
+ var path = GetTestArchivePath("Zip.deflate.zip");
+ using var archive = ArchiveFactory.Open(path);
+ foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
+ {
+ using var stream = entry.OpenEntryStream();
+ stream.CopyTo(Stream.Null);
+ }
+ }
+
+ ///
+ /// Current implementation benchmark for Zip archive reading.
+ /// BenchmarkDotNet will compare this against the baseline.
+ ///
+ [Benchmark]
+ public void ZipArchiveRead_Current()
+ {
+ var path = GetTestArchivePath("Zip.deflate.zip");
+ using var archive = ArchiveFactory.Open(path);
+ foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
+ {
+ using var stream = entry.OpenEntryStream();
+ stream.CopyTo(Stream.Null);
+ }
+ }
+}
diff --git a/tests/SharpCompress.Performance/README.md b/tests/SharpCompress.Performance/README.md
new file mode 100644
index 000000000..9dfc12dca
--- /dev/null
+++ b/tests/SharpCompress.Performance/README.md
@@ -0,0 +1,131 @@
+# SharpCompress Performance Benchmarks
+
+This project uses [BenchmarkDotNet](https://benchmarkdotnet.org/) to measure and track performance of SharpCompress archive operations.
+
+## Running Benchmarks
+
+### Run All Benchmarks
+```bash
+cd tests/SharpCompress.Performance
+dotnet run -c Release
+```
+
+### Run Specific Benchmark Classes
+```bash
+# Run only Archive API benchmarks
+dotnet run -c Release -- --filter "*ArchiveReadBenchmarks*"
+
+# Run only Reader API benchmarks
+dotnet run -c Release -- --filter "*ReaderBenchmarks*"
+```
+
+### Run Specific Benchmark Methods
+```bash
+# Run only Zip benchmarks
+dotnet run -c Release -- --filter "*Zip*"
+
+# Run a specific method
+dotnet run -c Release -- --filter "ArchiveReadBenchmarks.ZipArchiveRead"
+```
+
+### Quick Dry Run (for testing)
+```bash
+dotnet run -c Release -- --job dry
+```
+
+## Benchmark Categories
+
+### ArchiveReadBenchmarks
+Tests the **Archive API** which provides random access to entries with seekable streams. Covers:
+- Zip (deflate compression)
+- Tar (uncompressed)
+- Tar.gz (gzip compression)
+- Tar.bz2 (bzip2 compression)
+- 7Zip (LZMA2 compression)
+- Rar
+
+### ReaderBenchmarks
+Tests the **Reader API** which provides forward-only streaming for non-seekable streams. Covers:
+- Zip
+- Tar
+- Tar.gz
+- Tar.bz2
+- Rar
+
+### WriteBenchmarks
+Tests the **Writer API** for creating archives using forward-only writing. Covers:
+- Zip (deflate compression)
+- Tar (uncompressed)
+- Tar.gz (gzip compression)
+
+### BaselineComparisonBenchmarks
+Example benchmark showing how to compare implementations using the `[Baseline]` attribute. The baseline benchmark serves as a reference point, and BenchmarkDotNet calculates the ratio of performance between baseline and other methods.
+
+## Comparing Against Previous Versions
+
+### Using Baseline Attribute
+Mark one benchmark with `[Baseline = true]` and BenchmarkDotNet will show relative performance:
+
+```csharp
+[Benchmark(Baseline = true)]
+public void MethodA() { /* ... */ }
+
+[Benchmark]
+public void MethodB() { /* ... */ }
+```
+
+Results will show ratios like "1.5x slower" or "0.8x faster" compared to the baseline.
+
+### Using BenchmarkDotNet.Artifacts for Historical Comparison
+BenchmarkDotNet saves results to `BenchmarkDotNet.Artifacts/results/`. You can:
+
+1. Run benchmarks and save the results
+2. Keep a snapshot of the results file
+3. Compare new runs against saved results
+
+### Using Different NuGet Versions (Advanced)
+To compare against a published NuGet package:
+
+1. Create a separate benchmark project referencing the NuGet package
+2. Use BenchmarkDotNet's `[SimpleJob]` attribute with different runtimes
+3. Reference both the local project and NuGet package in different jobs
+
+## Interpreting Results
+
+BenchmarkDotNet provides:
+- **Mean**: Average execution time
+- **Error**: Half of 99.9% confidence interval
+- **StdDev**: Standard deviation of measurements
+- **Allocated**: Memory allocated per operation
+- **Rank**: Relative ranking (when using `[RankColumn]`)
+- **Ratio**: Relative performance vs baseline (when using `[Baseline]`)
+
+## Output Artifacts
+
+Results are saved to `BenchmarkDotNet.Artifacts/results/`:
+- `*.csv`: Raw data for further analysis
+- `*-report.html`: HTML report with charts
+- `*-report-github.md`: Markdown report for GitHub
+- `*.log`: Detailed execution log
+
+## Best Practices
+
+1. **Always run in Release mode**: Debug builds have significant overhead
+2. **Close other applications**: Minimize system noise during benchmarks
+3. **Run multiple times**: Look for consistency across runs
+4. **Use appropriate workload**: Ensure benchmarks run for at least 100ms
+5. **Track trends**: Compare results over time to detect regressions
+6. **Archive results**: Keep snapshots of benchmark results for historical comparison
+
+## CI/CD Integration
+
+Consider adding benchmarks to CI/CD to:
+- Detect performance regressions automatically
+- Track performance trends over time
+- Compare PR performance against main branch
+
+## Additional Resources
+
+- [BenchmarkDotNet Documentation](https://benchmarkdotnet.org/articles/overview.html)
+- [BenchmarkDotNet Configuration](https://benchmarkdotnet.org/articles/configs/configs.html)
+- [BenchmarkDotNet Baseline](https://benchmarkdotnet.org/articles/features/baselines.html)
diff --git a/tests/SharpCompress.Performance/WriteBenchmarks.cs b/tests/SharpCompress.Performance/WriteBenchmarks.cs
new file mode 100644
index 000000000..83d22cdd3
--- /dev/null
+++ b/tests/SharpCompress.Performance/WriteBenchmarks.cs
@@ -0,0 +1,105 @@
+using System.IO;
+using System.Linq;
+using BenchmarkDotNet.Attributes;
+using SharpCompress.Common;
+using SharpCompress.Writers;
+
+namespace SharpCompress.Performance;
+
+///
+/// Benchmarks for Writer operations.
+/// Tests creating archives with different compression formats using forward-only Writer API.
+///
+[MemoryDiagnoser]
+public class WriteBenchmarks : BenchmarkBase
+{
+ private string _tempOutputPath = null!;
+ private readonly string[] _testFiles = null!;
+
+ public WriteBenchmarks()
+ {
+ // Get some test files to compress
+ var originalPath = Path.Combine(
+ Path.GetDirectoryName(TEST_ARCHIVES_PATH)!,
+ "Original"
+ );
+ if (Directory.Exists(originalPath))
+ {
+ _testFiles = Directory.GetFiles(originalPath).Take(5).ToArray();
+ }
+ }
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ _tempOutputPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+ Directory.CreateDirectory(_tempOutputPath);
+ }
+
+ [GlobalCleanup]
+ public void Cleanup()
+ {
+ if (Directory.Exists(_tempOutputPath))
+ {
+ Directory.Delete(_tempOutputPath, true);
+ }
+ }
+
+ [Benchmark]
+ public void ZipWriterWrite()
+ {
+ if (_testFiles == null || _testFiles.Length == 0)
+ return;
+
+ var outputFile = Path.Combine(_tempOutputPath, "test.zip");
+ using var stream = File.Create(outputFile);
+ using var writer = WriterFactory.Open(
+ stream,
+ ArchiveType.Zip,
+ new WriterOptions(CompressionType.Deflate)
+ );
+ foreach (var file in _testFiles)
+ {
+ writer.Write(Path.GetFileName(file), file);
+ }
+ }
+
+ [Benchmark]
+ public void TarWriterWrite()
+ {
+ if (_testFiles == null || _testFiles.Length == 0)
+ return;
+
+ var outputFile = Path.Combine(_tempOutputPath, "test.tar");
+ using var stream = File.Create(outputFile);
+ using var writer = WriterFactory.Open(
+ stream,
+ ArchiveType.Tar,
+ new WriterOptions(CompressionType.None)
+ );
+ foreach (var file in _testFiles)
+ {
+ writer.Write(Path.GetFileName(file), file);
+ }
+ }
+
+ [Benchmark]
+ public void TarGzWriterWrite()
+ {
+ if (_testFiles == null || _testFiles.Length == 0)
+ return;
+
+ var outputFile = Path.Combine(_tempOutputPath, "test.tar.gz");
+ using var stream = File.Create(outputFile);
+ using var writer = WriterFactory.Open(
+ stream,
+ ArchiveType.Tar,
+ new WriterOptions(CompressionType.GZip)
+ );
+ foreach (var file in _testFiles)
+ {
+ writer.Write(Path.GetFileName(file), file);
+ }
+ }
+}
+
From 49f22712534f6f30172f27f5949817639df39880 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 5 Jan 2026 17:33:06 +0000
Subject: [PATCH 4/7] Format code with CSharpier
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
---
tests/SharpCompress.Performance/Program.cs | 1 -
tests/SharpCompress.Performance/WriteBenchmarks.cs | 6 +-----
2 files changed, 1 insertion(+), 6 deletions(-)
diff --git a/tests/SharpCompress.Performance/Program.cs b/tests/SharpCompress.Performance/Program.cs
index dac63d955..1d695b01c 100644
--- a/tests/SharpCompress.Performance/Program.cs
+++ b/tests/SharpCompress.Performance/Program.cs
@@ -14,4 +14,3 @@ static void Main(string[] args)
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config);
}
}
-
diff --git a/tests/SharpCompress.Performance/WriteBenchmarks.cs b/tests/SharpCompress.Performance/WriteBenchmarks.cs
index 83d22cdd3..51e80e143 100644
--- a/tests/SharpCompress.Performance/WriteBenchmarks.cs
+++ b/tests/SharpCompress.Performance/WriteBenchmarks.cs
@@ -19,10 +19,7 @@ public class WriteBenchmarks : BenchmarkBase
public WriteBenchmarks()
{
// Get some test files to compress
- var originalPath = Path.Combine(
- Path.GetDirectoryName(TEST_ARCHIVES_PATH)!,
- "Original"
- );
+ var originalPath = Path.Combine(Path.GetDirectoryName(TEST_ARCHIVES_PATH)!, "Original");
if (Directory.Exists(originalPath))
{
_testFiles = Directory.GetFiles(originalPath).Take(5).ToArray();
@@ -102,4 +99,3 @@ public void TarGzWriterWrite()
}
}
}
-
From e519f61f0fabc4cea1c82695cbbe20fbd0b91c3c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 5 Jan 2026 17:36:08 +0000
Subject: [PATCH 5/7] Address code review feedback: fix exception handling and
initialization order
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
---
.../BenchmarkBase.cs | 6 ++--
.../WriteBenchmarks.cs | 28 +++++++------------
2 files changed, 14 insertions(+), 20 deletions(-)
diff --git a/tests/SharpCompress.Performance/BenchmarkBase.cs b/tests/SharpCompress.Performance/BenchmarkBase.cs
index ae4a5b368..55b66f5ac 100644
--- a/tests/SharpCompress.Performance/BenchmarkBase.cs
+++ b/tests/SharpCompress.Performance/BenchmarkBase.cs
@@ -17,9 +17,11 @@ public BenchmarkBase()
StringComparison.OrdinalIgnoreCase
);
var path = AppDomain.CurrentDomain.BaseDirectory.Substring(0, index);
- var SOLUTION_BASE_PATH = Path.GetDirectoryName(path) ?? throw new ArgumentNullException();
+ var solutionBasePath =
+ Path.GetDirectoryName(path)
+ ?? throw new InvalidOperationException("Could not determine solution base path");
- TEST_ARCHIVES_PATH = Path.Combine(SOLUTION_BASE_PATH, "TestArchives", "Archives");
+ TEST_ARCHIVES_PATH = Path.Combine(solutionBasePath, "TestArchives", "Archives");
}
protected string GetTestArchivePath(string filename) =>
diff --git a/tests/SharpCompress.Performance/WriteBenchmarks.cs b/tests/SharpCompress.Performance/WriteBenchmarks.cs
index 51e80e143..95c31cdbb 100644
--- a/tests/SharpCompress.Performance/WriteBenchmarks.cs
+++ b/tests/SharpCompress.Performance/WriteBenchmarks.cs
@@ -14,23 +14,24 @@ namespace SharpCompress.Performance;
public class WriteBenchmarks : BenchmarkBase
{
private string _tempOutputPath = null!;
- private readonly string[] _testFiles = null!;
+ private string[] _testFiles = null!;
- public WriteBenchmarks()
+ [GlobalSetup]
+ public void Setup()
{
+ _tempOutputPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+ Directory.CreateDirectory(_tempOutputPath);
+
// Get some test files to compress
var originalPath = Path.Combine(Path.GetDirectoryName(TEST_ARCHIVES_PATH)!, "Original");
if (Directory.Exists(originalPath))
{
_testFiles = Directory.GetFiles(originalPath).Take(5).ToArray();
}
- }
-
- [GlobalSetup]
- public void Setup()
- {
- _tempOutputPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
- Directory.CreateDirectory(_tempOutputPath);
+ else
+ {
+ _testFiles = [];
+ }
}
[GlobalCleanup]
@@ -45,9 +46,6 @@ public void Cleanup()
[Benchmark]
public void ZipWriterWrite()
{
- if (_testFiles == null || _testFiles.Length == 0)
- return;
-
var outputFile = Path.Combine(_tempOutputPath, "test.zip");
using var stream = File.Create(outputFile);
using var writer = WriterFactory.Open(
@@ -64,9 +62,6 @@ public void ZipWriterWrite()
[Benchmark]
public void TarWriterWrite()
{
- if (_testFiles == null || _testFiles.Length == 0)
- return;
-
var outputFile = Path.Combine(_tempOutputPath, "test.tar");
using var stream = File.Create(outputFile);
using var writer = WriterFactory.Open(
@@ -83,9 +78,6 @@ public void TarWriterWrite()
[Benchmark]
public void TarGzWriterWrite()
{
- if (_testFiles == null || _testFiles.Length == 0)
- return;
-
var outputFile = Path.Combine(_tempOutputPath, "test.tar.gz");
using var stream = File.Create(outputFile);
using var writer = WriterFactory.Open(
From a92ce90252be88fc9f7a79986853bfc147b87d77 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 5 Jan 2026 17:38:17 +0000
Subject: [PATCH 6/7] Fix path validation and add iteration cleanup to prevent
file reuse
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
---
tests/SharpCompress.Performance/BenchmarkBase.cs | 8 ++++++++
tests/SharpCompress.Performance/WriteBenchmarks.cs | 13 +++++++++++++
2 files changed, 21 insertions(+)
diff --git a/tests/SharpCompress.Performance/BenchmarkBase.cs b/tests/SharpCompress.Performance/BenchmarkBase.cs
index 55b66f5ac..6d9c3388c 100644
--- a/tests/SharpCompress.Performance/BenchmarkBase.cs
+++ b/tests/SharpCompress.Performance/BenchmarkBase.cs
@@ -16,6 +16,14 @@ public BenchmarkBase()
"SharpCompress.Performance",
StringComparison.OrdinalIgnoreCase
);
+
+ if (index == -1)
+ {
+ throw new InvalidOperationException(
+ "Could not locate SharpCompress.Performance in the base directory path"
+ );
+ }
+
var path = AppDomain.CurrentDomain.BaseDirectory.Substring(0, index);
var solutionBasePath =
Path.GetDirectoryName(path)
diff --git a/tests/SharpCompress.Performance/WriteBenchmarks.cs b/tests/SharpCompress.Performance/WriteBenchmarks.cs
index 95c31cdbb..02c69a360 100644
--- a/tests/SharpCompress.Performance/WriteBenchmarks.cs
+++ b/tests/SharpCompress.Performance/WriteBenchmarks.cs
@@ -34,6 +34,19 @@ public void Setup()
}
}
+ [IterationCleanup]
+ public void IterationCleanup()
+ {
+ // Clean up created archives after each iteration to avoid file reuse affecting measurements
+ if (Directory.Exists(_tempOutputPath))
+ {
+ foreach (var file in Directory.GetFiles(_tempOutputPath))
+ {
+ File.Delete(file);
+ }
+ }
+ }
+
[GlobalCleanup]
public void Cleanup()
{
From 80ac10a5fecf2d2331959d3ac3142f9074c43d43 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 12 Jan 2026 14:11:57 +0000
Subject: [PATCH 7/7] Merge latest master branch and resolve conflicts
Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
---
.editorconfig | 3 +
AGENTS.md | 3 +-
Directory.Packages.props | 9 +-
README.md | 230 +-----
build/packages.lock.json | 40 ++
docs/API.md | 489 +++++++++++++
docs/ARCHITECTURE.md | 659 ++++++++++++++++++
docs/ENCODING.md | 610 ++++++++++++++++
FORMATS.md => docs/FORMATS.md | 0
docs/OLD_CHANGELOG.md | 142 ++++
docs/PERFORMANCE.md | 474 +++++++++++++
USAGE.md => docs/USAGE.md | 44 +-
src/SharpCompress/Archives/AbstractArchive.cs | 103 ++-
.../Archives/AbstractWritableArchive.cs | 4 +-
src/SharpCompress/Archives/ArchiveFactory.cs | 174 +++++
.../Archives/AutoArchiveFactory.cs | 23 +-
.../Archives/GZip/GZipArchive.cs | 102 ++-
.../Archives/GZip/GZipArchiveEntry.cs | 6 +-
src/SharpCompress/Archives/IArchiveEntry.cs | 2 +-
.../Archives/IArchiveEntryExtensions.cs | 46 +-
.../Archives/IArchiveExtensions.cs | 86 ---
src/SharpCompress/Archives/IArchiveFactory.cs | 26 +
src/SharpCompress/Archives/IAsyncArchive.cs | 43 ++
.../Archives/IAsyncArchiveExtensions.cs | 93 +++
.../Archives/IMultiArchiveFactory.cs | 26 +
.../Archives/IWritableArchive.cs | 2 +-
.../Archives/IWritableArchiveExtensions.cs | 4 +-
src/SharpCompress/Archives/Rar/RarArchive.cs | 74 +-
.../Archives/Rar/RarArchiveEntry.cs | 4 +-
.../Archives/SevenZip/SevenZipArchive.cs | 67 ++
.../Archives/SevenZip/SevenZipArchiveEntry.cs | 5 +-
src/SharpCompress/Archives/Tar/TarArchive.cs | 73 +-
.../Archives/Tar/TarArchiveEntry.cs | 4 +-
src/SharpCompress/Archives/Zip/ZipArchive.cs | 220 +++++-
.../Archives/Zip/ZipArchiveEntry.cs | 12 +-
.../Common/Ace/Headers/AceFileHeader.cs | 2 +-
.../Common/Ace/Headers/AceHeader.cs | 4 +-
.../Common/Ace/Headers/AceMainHeader.cs | 2 +-
.../Common/Arc/ArcEntryHeader.cs | 4 +-
src/SharpCompress/Common/ArchiveEncoding.cs | 54 +-
.../Common/ArchiveEncodingExtensions.cs | 87 +++
src/SharpCompress/Common/AsyncBinaryReader.cs | 95 +++
src/SharpCompress/Common/EntryStream.cs | 2 +-
src/SharpCompress/Common/ExtractionMethods.cs | 8 +-
src/SharpCompress/Common/FilePart.cs | 10 +-
src/SharpCompress/Common/GZip/GZipFilePart.cs | 2 +-
src/SharpCompress/Common/IArchiveEncoding.cs | 36 +
src/SharpCompress/Common/OptionsBase.cs | 2 +-
.../Common/Rar/Headers/RarHeader.cs | 6 +-
.../Common/SevenZip/SevenZipFilePart.cs | 4 +-
.../Common/Tar/Headers/TarHeader.cs | 4 +-
src/SharpCompress/Common/Tar/TarEntry.cs | 2 +-
.../Common/Tar/TarHeaderFactory.cs | 2 +-
.../Common/Zip/Headers/DirectoryEndHeader.cs | 13 +
.../Zip/Headers/DirectoryEntryHeader.cs | 39 +-
.../Common/Zip/Headers/IgnoreHeader.cs | 3 +
.../Common/Zip/Headers/LocalEntryHeader.cs | 33 +-
.../Common/Zip/Headers/SplitHeader.cs | 4 +
.../Zip/Headers/Zip64DirectoryEndHeader.cs | 20 +
.../Headers/Zip64DirectoryEndLocatorHeader.cs | 13 +-
.../Common/Zip/Headers/ZipFileEntry.cs | 34 +-
.../Common/Zip/Headers/ZipHeader.cs | 14 +-
.../Zip/PkwareTraditionalEncryptionData.cs | 6 +-
.../Common/Zip/SeekableZipFilePart.cs | 17 +
.../Common/Zip/SeekableZipHeaderFactory.cs | 205 +++++-
.../Common/Zip/StreamingZipFilePart.cs | 24 +
.../Common/Zip/StreamingZipHeaderFactory.cs | 332 ++++++++-
.../Common/Zip/WinzipAesEncryptionData.cs | 2 +-
src/SharpCompress/Common/Zip/ZipFilePart.cs | 242 +++++++
.../Common/Zip/ZipHeaderFactory.cs | 160 ++++-
src/SharpCompress/Compressors/ADC/ADCBase.cs | 4 +-
.../Compressors/Deflate/ZlibBaseStream.cs | 10 +-
.../Compressors/LZMA/LZ/LzOutWindow.cs | 34 +-
.../Compressors/LZMA/LZipStream.cs | 6 +
.../Compressors/LZMA/LzmaDecoder.cs | 3 +-
.../Compressors/LZMA/LzmaStream.cs | 125 +++-
.../Compressors/LZMA/Utilites/Utils.cs | 35 -
.../Compressors/Rar/RarStream.cs | 2 +-
.../Compressors/Xz/MultiByteIntegers.cs | 37 +-
src/SharpCompress/Compressors/Xz/XZBlock.cs | 10 +-
src/SharpCompress/Compressors/Xz/XZFooter.cs | 2 +-
src/SharpCompress/Compressors/Xz/XZHeader.cs | 4 +-
src/SharpCompress/Compressors/Xz/XZIndex.cs | 10 +-
src/SharpCompress/Compressors/Xz/XZStream.cs | 8 +-
.../ZStandard/CompressionStream.cs | 14 +-
.../ZStandard/DecompressionStream.cs | 4 +-
src/SharpCompress/Factories/AceFactory.cs | 18 +-
src/SharpCompress/Factories/ArcFactory.cs | 13 +
src/SharpCompress/Factories/ArjFactory.cs | 13 +
src/SharpCompress/Factories/Factory.cs | 50 ++
src/SharpCompress/Factories/GZipFactory.cs | 66 ++
src/SharpCompress/Factories/IFactory.cs | 16 +
src/SharpCompress/Factories/RarFactory.cs | 47 ++
.../Factories/SevenZipFactory.cs | 36 +
src/SharpCompress/Factories/TarFactory.cs | 58 ++
.../Factories/ZStandardFactory.cs | 6 +
src/SharpCompress/Factories/ZipFactory.cs | 101 +++
src/SharpCompress/IO/BufferedSubStream.cs | 74 ++
src/SharpCompress/IO/ReadOnlySubStream.cs | 8 +-
src/SharpCompress/IO/SharpCompressStream.cs | 2 -
.../LazyAsyncReadOnlyCollection.cs | 103 +++
.../Polyfills/AsyncEnumerableExtensions.cs | 101 +++
.../Polyfills/BinaryReaderExtensions.cs | 65 ++
.../Polyfills/StreamExtensions.cs | 85 ++-
src/SharpCompress/Readers/AbstractReader.cs | 117 +++-
src/SharpCompress/Readers/Ace/AceReader.cs | 2 +-
src/SharpCompress/Readers/IAsyncReader.cs | 41 ++
.../Readers/IAsyncReaderExtensions.cs | 69 ++
src/SharpCompress/Readers/IReader.cs | 23 -
.../Readers/IReaderExtensions.cs | 59 --
src/SharpCompress/Readers/IReaderFactory.cs | 7 +
src/SharpCompress/Readers/ReaderFactory.cs | 109 +++
src/SharpCompress/Readers/Zip/ZipReader.cs | 101 +++
src/SharpCompress/SharpCompress.csproj | 17 +-
src/SharpCompress/Utility.cs | 402 +++++------
src/SharpCompress/Writers/AbstractWriter.cs | 4 +-
src/SharpCompress/Writers/IWriter.cs | 4 +-
.../Writers/IWriterExtensions.cs | 12 +-
src/SharpCompress/Writers/IWriterFactory.cs | 8 +
src/SharpCompress/Writers/Tar/TarWriter.cs | 6 +-
src/SharpCompress/Writers/WriterFactory.cs | 31 +
.../Writers/Zip/ZipCentralDirectoryEntry.cs | 4 +-
src/SharpCompress/Writers/Zip/ZipWriter.cs | 4 +-
src/SharpCompress/packages.lock.json | 168 ++++-
.../packages.lock.json | 40 ++
tests/SharpCompress.Test/AdcAsyncTest.cs | 6 +-
.../Arc/ArcReaderAsyncTests.cs | 24 +
.../SharpCompress.Test/Arc/ArcReaderTests.cs | 7 -
tests/SharpCompress.Test/ArchiveTests.cs | 12 +-
.../BZip2/BZip2StreamAsyncTests.cs | 8 +-
tests/SharpCompress.Test/ExtractAll.cs | 4 +-
tests/SharpCompress.Test/GZip/AsyncTests.cs | 80 ++-
.../GZip/GZipArchiveAsyncTests.cs | 59 +-
.../GZip/GZipReaderAsyncTests.cs | 18 +-
.../GZip/GZipWriterAsyncTests.cs | 6 +-
.../Mocks/AsyncOnlyStream.cs | 70 ++
tests/SharpCompress.Test/Mocks/TestStream.cs | 19 +-
.../SharpCompress.Test/ProgressReportTests.cs | 19 +-
.../Rar/RarArchiveAsyncTests.cs | 136 ++--
.../Rar/RarReaderAsyncTests.cs | 109 +--
tests/SharpCompress.Test/ReaderTests.cs | 26 +-
.../SevenZip/SevenZipArchiveAsyncTests.cs | 139 ++++
.../SharpCompress.Test.csproj | 1 -
.../Streams/DisposalTests.cs | 189 +++++
.../Streams/LzmaStreamAsyncTests.cs | 8 +-
.../Streams/RewindableStreamAsyncTest.cs | 4 +-
.../Streams/SharpCompressStreamAsyncTests.cs | 8 +-
.../Streams/ZLibBaseStreamAsyncTests.cs | 10 +-
.../Tar/TarArchiveAsyncTests.cs | 22 +-
.../Tar/TarReaderAsyncTests.cs | 63 +-
.../Tar/TarWriterAsyncTests.cs | 10 +-
tests/SharpCompress.Test/TestBase.cs | 52 +-
tests/SharpCompress.Test/UtilityTests.cs | 92 +++
tests/SharpCompress.Test/WriterTests.cs | 19 +-
.../Xz/XZBlockAsyncTests.cs | 16 +-
.../Xz/XZHeaderAsyncTests.cs | 12 +-
.../Xz/XZIndexAsyncTests.cs | 12 +-
.../Xz/XZStreamAsyncTests.cs | 6 +-
.../SharpCompress.Test/Zip/Zip64AsyncTests.cs | 34 +-
tests/SharpCompress.Test/Zip/Zip64Tests.cs | 12 +-
.../Zip/ZipArchiveAsyncTests.cs | 59 +-
.../SharpCompress.Test/Zip/ZipArchiveTests.cs | 2 -
.../Zip/ZipMemoryArchiveWithCrcAsyncTests.cs | 14 +-
.../Zip/ZipReaderAsyncTests.cs | 72 +-
.../Zip/ZipWriterAsyncTests.cs | 12 +-
tests/SharpCompress.Test/packages.lock.json | 52 ++
166 files changed, 7882 insertions(+), 1428 deletions(-)
create mode 100644 docs/API.md
create mode 100644 docs/ARCHITECTURE.md
create mode 100644 docs/ENCODING.md
rename FORMATS.md => docs/FORMATS.md (100%)
create mode 100644 docs/OLD_CHANGELOG.md
create mode 100644 docs/PERFORMANCE.md
rename USAGE.md => docs/USAGE.md (90%)
create mode 100644 src/SharpCompress/Archives/IAsyncArchive.cs
create mode 100644 src/SharpCompress/Archives/IAsyncArchiveExtensions.cs
create mode 100644 src/SharpCompress/Common/ArchiveEncodingExtensions.cs
create mode 100644 src/SharpCompress/Common/AsyncBinaryReader.cs
create mode 100644 src/SharpCompress/Common/IArchiveEncoding.cs
create mode 100644 src/SharpCompress/LazyAsyncReadOnlyCollection.cs
create mode 100644 src/SharpCompress/Polyfills/AsyncEnumerableExtensions.cs
create mode 100644 src/SharpCompress/Polyfills/BinaryReaderExtensions.cs
create mode 100644 src/SharpCompress/Readers/IAsyncReader.cs
create mode 100644 src/SharpCompress/Readers/IAsyncReaderExtensions.cs
create mode 100644 tests/SharpCompress.Test/Arc/ArcReaderAsyncTests.cs
create mode 100644 tests/SharpCompress.Test/Mocks/AsyncOnlyStream.cs
create mode 100644 tests/SharpCompress.Test/SevenZip/SevenZipArchiveAsyncTests.cs
create mode 100644 tests/SharpCompress.Test/Streams/DisposalTests.cs
diff --git a/.editorconfig b/.editorconfig
index eab3d4286..96f2a953b 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -368,6 +368,9 @@ dotnet_diagnostic.NX0001.severity = error
dotnet_diagnostic.NX0002.severity = silent
dotnet_diagnostic.NX0003.severity = silent
+dotnet_diagnostic.VSTHRD110.severity = error
+dotnet_diagnostic.VSTHRD107.severity = error
+
##########################################
# Styles
##########################################
diff --git a/AGENTS.md b/AGENTS.md
index ae36265aa..38ae56925 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -14,6 +14,7 @@ SharpCompress is a pure C# compression library supporting multiple archive forma
- Follow the existing code style and patterns in the codebase.
## General Instructions
+- **Agents should NEVER commit to git** - Agents should stage files and leave committing to the user. Only create commits when the user explicitly requests them.
- Make only high confidence suggestions when reviewing code changes.
- Write code with good maintainability practices, including comments on why certain design decisions were made.
- Handle edge cases and write clear exception handling.
@@ -110,7 +111,7 @@ SharpCompress supports multiple archive and compression formats:
- **Archive Formats**: Zip, Tar, 7Zip, Rar (read-only)
- **Compression**: DEFLATE, BZip2, LZMA/LZMA2, PPMd, ZStandard (decompress only), Deflate64 (decompress only)
- **Combined Formats**: Tar.GZip, Tar.BZip2, Tar.LZip, Tar.XZ, Tar.ZStandard
-- See FORMATS.md for complete format support matrix
+- See [docs/FORMATS.md](docs/FORMATS.md) for complete format support matrix
### Stream Handling Rules
- **Disposal**: As of version 0.21, SharpCompress closes wrapped streams by default
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 74e6a08e1..248507972 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -14,8 +14,11 @@
-
-
-
+
+
+
diff --git a/README.md b/README.md
index 172003c17..ecaa15e4e 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@ SharpCompress is a compression library in pure C# for .NET Framework 4.8, .NET 8
The major feature is support for non-seekable streams so large files can be processed on the fly (i.e. download stream).
-**NEW:** All I/O operations now support async/await for improved performance and scalability. See the [Async Usage](#async-usage) section below.
+**NEW:** All I/O operations now support async/await for improved performance and scalability. See the [USAGE.md](docs/USAGE.md#async-examples) for examples.
GitHub Actions Build -
[](https://github.com/adamhathcock/sharpcompress/actions/workflows/dotnetcore.yml)
@@ -14,7 +14,7 @@ GitHub Actions Build -
Post Issues on Github!
-Check the [Supported Formats](FORMATS.md) and [Basic Usage.](USAGE.md)
+Check the [Supported Formats](docs/FORMATS.md) and [Basic Usage.](docs/USAGE.md)
## Recommended Formats
@@ -34,235 +34,11 @@ Hi everyone. I hope you're using SharpCompress and finding it useful. Please giv
Please do not email me directly to ask for help. If you think there is a real issue, please report it here.
-## Async Usage
-
-SharpCompress now provides full async/await support for all I/O operations, allowing for better performance and scalability in modern applications.
-
-### Async Reading Examples
-
-Extract entries asynchronously:
-```csharp
-using (Stream stream = File.OpenRead("archive.zip"))
-using (var reader = ReaderFactory.Open(stream))
-{
- while (reader.MoveToNextEntry())
- {
- if (!reader.Entry.IsDirectory)
- {
- // Async extraction
- await reader.WriteEntryToDirectoryAsync(
- @"C:\temp",
- new ExtractionOptions() { ExtractFullPath = true, Overwrite = true },
- cancellationToken
- );
- }
- }
-}
-```
-
-Extract all entries to directory asynchronously:
-```csharp
-using (Stream stream = File.OpenRead("archive.tar.gz"))
-using (var reader = ReaderFactory.Open(stream))
-{
- await reader.WriteAllToDirectoryAsync(
- @"C:\temp",
- new ExtractionOptions() { ExtractFullPath = true, Overwrite = true },
- cancellationToken
- );
-}
-```
-
-Open entry stream asynchronously:
-```csharp
-using (var archive = ZipArchive.Open("archive.zip"))
-{
- foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
- {
- using (var entryStream = await entry.OpenEntryStreamAsync(cancellationToken))
- {
- // Process stream asynchronously
- await entryStream.CopyToAsync(outputStream, cancellationToken);
- }
- }
-}
-```
-
-### Async Writing Examples
-
-Write files asynchronously:
-```csharp
-using (Stream stream = File.OpenWrite("output.zip"))
-using (var writer = WriterFactory.Open(stream, ArchiveType.Zip, CompressionType.Deflate))
-{
- await writer.WriteAsync("file1.txt", fileStream, DateTime.Now, cancellationToken);
-}
-```
-
-Write all files from directory asynchronously:
-```csharp
-using (Stream stream = File.OpenWrite("output.tar.gz"))
-using (var writer = WriterFactory.Open(stream, ArchiveType.Tar, new WriterOptions(CompressionType.GZip)))
-{
- await writer.WriteAllAsync(@"D:\files", "*", SearchOption.AllDirectories, cancellationToken);
-}
-```
-
-All async methods support `CancellationToken` for graceful cancellation of long-running operations.
-
## Want to contribute?
I'm always looking for help or ideas. Please submit code or email with ideas. Unfortunately, just letting me know you'd like to help is not enough because I really have no overall plan of what needs to be done. I'll definitely accept code submissions and add you as a member of the project!
-## TODOs (always lots)
-
-* RAR 5 decryption crc check support
-* 7Zip writing
-* Zip64 (Need writing and extend Reading)
-* Multi-volume Zip support.
-* ZStandard writing
-
-## Version Log
-
-* [Releases](https://github.com/adamhathcock/sharpcompress/releases)
-
-### Version 0.18
-
-* [Now on Github releases](https://github.com/adamhathcock/sharpcompress/releases/tag/0.18)
-
-### Version 0.17.1
-
-* Fix - [Bug Fix for .NET Core on Windows](https://github.com/adamhathcock/sharpcompress/pull/257)
-
-### Version 0.17.0
-
-* New - Full LZip support! Can read and write LZip files and Tars inside LZip files. [Make LZip a first class citizen. #241](https://github.com/adamhathcock/sharpcompress/issues/241)
-* New - XZ read support! Can read XZ files and Tars inside XZ files. [XZ in SharpCompress #91](https://github.com/adamhathcock/sharpcompress/issues/94)
-* Fix - [Regression - zip file writing on seekable streams always assumed stream start was 0. Introduced with Zip64 writing.](https://github.com/adamhathcock/sharpcompress/issues/244)
-* Fix - [Zip files with post-data descriptors can be properly skipped via decompression](https://github.com/adamhathcock/sharpcompress/issues/162)
-
-### Version 0.16.2
-
-* Fix [.NET 3.5 should support files and cryptography (was a regression from 0.16.0)](https://github.com/adamhathcock/sharpcompress/pull/251)
-* Fix [Zip per entry compression customization wrote the wrong method into the zip archive](https://github.com/adamhathcock/sharpcompress/pull/249)
-
-### Version 0.16.1
-
-* Fix [Preserve compression method when getting a compressed stream](https://github.com/adamhathcock/sharpcompress/pull/235)
-* Fix [RAR entry key normalization fix](https://github.com/adamhathcock/sharpcompress/issues/201)
-
-### Version 0.16.0
-
-* Breaking - [Progress Event Tracking rethink](https://github.com/adamhathcock/sharpcompress/pull/226)
-* Update to VS2017 - [VS2017](https://github.com/adamhathcock/sharpcompress/pull/231) - Framework targets have been changed.
-* New - [Add Zip64 writing](https://github.com/adamhathcock/sharpcompress/pull/211)
-* [Fix invalid/mismatching Zip version flags.](https://github.com/adamhathcock/sharpcompress/issues/164) - This allows nuget/System.IO.Packaging to read zip files generated by SharpCompress
-* [Fix 7Zip directory hiding](https://github.com/adamhathcock/sharpcompress/pull/215/files)
-* [Verify RAR CRC headers](https://github.com/adamhathcock/sharpcompress/pull/220)
-
-### Version 0.15.2
-
-* [Fix invalid headers](https://github.com/adamhathcock/sharpcompress/pull/210) - fixes an issue creating large-ish zip archives that was introduced with zip64 reading.
-
-### Version 0.15.1
-
-* [Zip64 extending information and ZipReader](https://github.com/adamhathcock/sharpcompress/pull/206)
-
-### Version 0.15.0
-
-* [Add zip64 support for ZipArchive extraction](https://github.com/adamhathcock/sharpcompress/pull/205)
-
-### Version 0.14.1
-
-* [.NET Assemblies aren't strong named](https://github.com/adamhathcock/sharpcompress/issues/158)
-* [Pkware encryption for Zip files didn't allow for multiple reads of an entry](https://github.com/adamhathcock/sharpcompress/issues/197)
-* [GZip Entry couldn't be read multiple times](https://github.com/adamhathcock/sharpcompress/issues/198)
-
-### Version 0.14.0
-
-* [Support for LZip reading in for Tars](https://github.com/adamhathcock/sharpcompress/pull/191)
-
-### Version 0.13.1
-
-* [Fix null password on ReaderFactory. Fix null options on SevenZipArchive](https://github.com/adamhathcock/sharpcompress/pull/188)
-* [Make PpmdProperties lazy to avoid unnecessary allocations.](https://github.com/adamhathcock/sharpcompress/pull/185)
-
-### Version 0.13.0
-
-* Breaking change: Big refactor of Options on API.
-* 7Zip supports Deflate
-
-### Version 0.12.4
-
-* Forward only zip issue fix https://github.com/adamhathcock/sharpcompress/issues/160
-* Try to fix frameworks again by copying targets from JSON.NET
-
-### Version 0.12.3
-
-* 7Zip fixes https://github.com/adamhathcock/sharpcompress/issues/73
-* Maybe all profiles will work with project.json now
-
-### Version 0.12.2
-
-* Support Profile 259 again
-
-### Version 0.12.1
-
-* Support Silverlight 5
-
-### Version 0.12.0
-
-* .NET Core RTM!
-* Bug fix for Tar long paths
-
-### Version 0.11.6
-
-* Bug fix for global header in Tar
-* Writers now have a leaveOpen `bool` overload. They won't close streams if not-requested to.
-
-### Version 0.11.5
-
-* Bug fix in Skip method
-
-### Version 0.11.4
-
-* SharpCompress is now endian neutral (matters for Mono platforms)
-* Fix for Inflate (need to change implementation)
-* Fixes for RAR detection
-
-### Version 0.11.1
-
-* Added Cancel on IReader
-* Removed .NET 2.0 support and LinqBridge dependency
-
-### Version 0.11
-
-* Been over a year, contains mainly fixes from contributors!
-* Possible breaking change: ArchiveEncoding is UTF8 by default now.
-* TAR supports writing long names using longlink
-* RAR Protect Header added
-
-### Version 0.10.3
-
-* Finally fixed Disposal issue when creating a new archive with the Archive API
-
-### Version 0.10.2
-
-* Fixed Rar Header reading for invalid extended time headers.
-* Windows Store assembly is now strong named
-* Known issues with Long Tar names being worked on
-* Updated to VS2013
-* Portable targets SL5 and Windows Phone 8 (up from SL4 and WP7)
-
-### Version 0.10.1
-
-* Fixed 7Zip extraction performance problem
-
-### Version 0.10:
-
-* Added support for RAR Decryption (thanks to https://github.com/hrasyid)
-* Embedded some BouncyCastle crypto classes to allow RAR Decryption and Winzip AES Decryption in Portable and Windows Store DLLs
-* Built in Release (I think)
+## Notes
XZ implementation based on: https://github.com/sambott/XZ.NET by @sambott
diff --git a/build/packages.lock.json b/build/packages.lock.json
index 9a72d50dc..ddbe3e974 100644
--- a/build/packages.lock.json
+++ b/build/packages.lock.json
@@ -14,11 +14,51 @@
"resolved": "1.1.9",
"contentHash": "AfK5+ECWYTP7G3AAdnU8IfVj+QpGjrh9GC2mpdcJzCvtQ4pnerAGwHsxJ9D4/RnhDUz2DSzd951O/lQjQby2Sw=="
},
+ "Microsoft.NETFramework.ReferenceAssemblies": {
+ "type": "Direct",
+ "requested": "[1.0.3, )",
+ "resolved": "1.0.3",
+ "contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
+ "dependencies": {
+ "Microsoft.NETFramework.ReferenceAssemblies.net461": "1.0.3"
+ }
+ },
+ "Microsoft.SourceLink.GitHub": {
+ "type": "Direct",
+ "requested": "[8.0.0, )",
+ "resolved": "8.0.0",
+ "contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
+ "dependencies": {
+ "Microsoft.Build.Tasks.Git": "8.0.0",
+ "Microsoft.SourceLink.Common": "8.0.0"
+ }
+ },
+ "Microsoft.VisualStudio.Threading.Analyzers": {
+ "type": "Direct",
+ "requested": "[17.14.15, )",
+ "resolved": "17.14.15",
+ "contentHash": "mXQPJsbuUD2ydq4/ffd8h8tSOFCXec+2xJOVNCvXjuMOq/+5EKHq3D2m2MC2+nUaXeFMSt66VS/J4HdKBixgcw=="
+ },
"SimpleExec": {
"type": "Direct",
"requested": "[13.0.0, )",
"resolved": "13.0.0",
"contentHash": "zcCR1pupa1wI1VqBULRiQKeHKKZOuJhi/K+4V5oO+rHJZlaOD53ViFo1c3PavDoMAfSn/FAXGAWpPoF57rwhYg=="
+ },
+ "Microsoft.Build.Tasks.Git": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
+ },
+ "Microsoft.NETFramework.ReferenceAssemblies.net461": {
+ "type": "Transitive",
+ "resolved": "1.0.3",
+ "contentHash": "AmOJZwCqnOCNp6PPcf9joyogScWLtwy0M1WkqfEQ0M9nYwyDD7EX9ZjscKS5iYnyvteX7kzSKFCKt9I9dXA6mA=="
+ },
+ "Microsoft.SourceLink.Common": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
}
}
}
diff --git a/docs/API.md b/docs/API.md
new file mode 100644
index 000000000..88dbf2bb1
--- /dev/null
+++ b/docs/API.md
@@ -0,0 +1,489 @@
+# API Quick Reference
+
+Quick reference for commonly used SharpCompress APIs.
+
+## Factory Methods
+
+### Opening Archives
+
+```csharp
+// Auto-detect format
+using (var reader = ReaderFactory.Open(stream))
+{
+ // Works with Zip, Tar, GZip, Rar, 7Zip, etc.
+}
+
+// Specific format - Archive API
+using (var archive = ZipArchive.Open("file.zip"))
+using (var archive = TarArchive.Open("file.tar"))
+using (var archive = RarArchive.Open("file.rar"))
+using (var archive = SevenZipArchive.Open("file.7z"))
+using (var archive = GZipArchive.Open("file.gz"))
+
+// With options
+var options = new ReaderOptions
+{
+ Password = "password",
+ LeaveStreamOpen = true,
+ ArchiveEncoding = new ArchiveEncoding { Default = Encoding.GetEncoding(932) }
+};
+using (var archive = ZipArchive.Open("encrypted.zip", options))
+```
+
+### Creating Archives
+
+```csharp
+// Writer Factory
+using (var writer = WriterFactory.Open(stream, ArchiveType.Zip, CompressionType.Deflate))
+{
+ // Write entries
+}
+
+// Specific writer
+using (var archive = ZipArchive.Create())
+using (var archive = TarArchive.Create())
+using (var archive = GZipArchive.Create())
+
+// With options
+var options = new WriterOptions(CompressionType.Deflate)
+{
+ CompressionLevel = 9,
+ LeaveStreamOpen = false
+};
+using (var archive = ZipArchive.Create())
+{
+ archive.SaveTo("output.zip", options);
+}
+```
+
+---
+
+## Archive API Methods
+
+### Reading/Extracting
+
+```csharp
+using (var archive = ZipArchive.Open("file.zip"))
+{
+ // Get all entries
+ IEnumerable entries = archive.Entries;
+
+ // Find specific entry
+ var entry = archive.Entries.FirstOrDefault(e => e.Key == "file.txt");
+
+ // Extract all
+ archive.WriteToDirectory(@"C:\output", new ExtractionOptions
+ {
+ ExtractFullPath = true,
+ Overwrite = true
+ });
+
+ // 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())
+ {
+ stream.CopyTo(outputStream);
+ }
+}
+
+// Async variants
+await archive.WriteToDirectoryAsync(@"C:\output", options, cancellationToken);
+using (var stream = await entry.OpenEntryStreamAsync(cancellationToken))
+{
+ // ...
+}
+```
+
+### Entry Properties
+
+```csharp
+foreach (var entry in archive.Entries)
+{
+ string name = entry.Key; // Entry name/path
+ long size = entry.Size; // Uncompressed size
+ long compressedSize = entry.CompressedSize;
+ bool isDir = entry.IsDirectory;
+ DateTime? modTime = entry.LastModifiedTime;
+ CompressionType compression = entry.CompressionType;
+}
+```
+
+### Creating Archives
+
+```csharp
+using (var archive = ZipArchive.Create())
+{
+ // Add file
+ archive.AddEntry("file.txt", "C:\\source\\file.txt");
+
+ // Add multiple files
+ archive.AddAllFromDirectory("C:\\source");
+ archive.AddAllFromDirectory("C:\\source", "*.txt"); // Pattern
+
+ // Save to file
+ archive.SaveTo("output.zip", CompressionType.Deflate);
+
+ // Save to stream
+ archive.SaveTo(outputStream, new WriterOptions(CompressionType.Deflate)
+ {
+ CompressionLevel = 9,
+ LeaveStreamOpen = true
+ });
+}
+```
+
+---
+
+## Reader API Methods
+
+### Forward-Only Reading
+
+```csharp
+using (var stream = File.OpenRead("file.zip"))
+using (var reader = ReaderFactory.Open(stream))
+{
+ while (reader.MoveToNextEntry())
+ {
+ IEntry entry = reader.Entry;
+
+ if (!entry.IsDirectory)
+ {
+ // Extract entry
+ reader.WriteEntryToDirectory(@"C:\output");
+ reader.WriteEntryToFile(@"C:\output\file.txt");
+
+ // Or get stream
+ using (var entryStream = reader.OpenEntryStream())
+ {
+ entryStream.CopyTo(outputStream);
+ }
+ }
+ }
+}
+
+// Async variants
+while (await reader.MoveToNextEntryAsync())
+{
+ await reader.WriteEntryToFileAsync(@"C:\output\" + reader.Entry.Key, cancellationToken);
+}
+
+// Async extraction
+await reader.WriteAllToDirectoryAsync(@"C:\output",
+ new ExtractionOptions { ExtractFullPath = true, Overwrite = true },
+ cancellationToken);
+```
+
+---
+
+## Writer API Methods
+
+### Creating Archives (Streaming)
+
+```csharp
+using (var stream = File.Create("output.zip"))
+using (var writer = WriterFactory.Open(stream, ArchiveType.Zip, CompressionType.Deflate))
+{
+ // Write single file
+ using (var fileStream = File.OpenRead("source.txt"))
+ {
+ writer.Write("entry.txt", fileStream, DateTime.Now);
+ }
+
+ // Write directory
+ writer.WriteAll("C:\\source", "*", SearchOption.AllDirectories);
+ writer.WriteAll("C:\\source", "*.txt", SearchOption.TopDirectoryOnly);
+
+ // Async variants
+ using (var fileStream = File.OpenRead("source.txt"))
+ {
+ await writer.WriteAsync("entry.txt", fileStream, DateTime.Now, cancellationToken);
+ }
+
+ await writer.WriteAllAsync("C:\\source", "*", SearchOption.AllDirectories, cancellationToken);
+}
+```
+
+---
+
+## Common Options
+
+### ReaderOptions
+
+```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)
+ }
+};
+using (var archive = ZipArchive.Open("file.zip", options))
+{
+ // ...
+}
+```
+
+### WriterOptions
+
+```csharp
+var options = new WriterOptions(CompressionType.Deflate)
+{
+ CompressionLevel = 9, // 0-9 for Deflate
+ LeaveStreamOpen = true, // Don't close stream
+};
+archive.SaveTo("output.zip", options);
+```
+
+### ExtractionOptions
+
+```csharp
+var options = new ExtractionOptions
+{
+ ExtractFullPath = true, // Recreate directory structure
+ Overwrite = true, // Overwrite existing files
+ PreserveFileTime = true // Keep original timestamps
+};
+archive.WriteToDirectory(@"C:\output", options);
+```
+
+---
+
+## Compression Types
+
+### Available Compressions
+
+```csharp
+// For creating archives
+CompressionType.None // No compression (store)
+CompressionType.Deflate // DEFLATE (default for ZIP/GZip)
+CompressionType.BZip2 // BZip2
+CompressionType.LZMA // LZMA (for 7Zip, LZip, XZ)
+CompressionType.PPMd // PPMd (for ZIP)
+CompressionType.Rar // RAR compression (read-only)
+
+// For Tar archives
+// Use CompressionType in TarWriter constructor
+using (var writer = TarWriter(stream, CompressionType.GZip)) // Tar.GZip
+using (var writer = TarWriter(stream, CompressionType.BZip2)) // Tar.BZip2
+```
+
+### Archive Types
+
+```csharp
+ArchiveType.Zip
+ArchiveType.Tar
+ArchiveType.GZip
+ArchiveType.BZip2
+ArchiveType.Rar
+ArchiveType.SevenZip
+ArchiveType.XZ
+ArchiveType.ZStandard
+```
+
+---
+
+## Patterns & Examples
+
+### Extract with Error Handling
+
+```csharp
+try
+{
+ using (var archive = ZipArchive.Open("archive.zip",
+ new ReaderOptions { Password = "password" }))
+ {
+ archive.WriteToDirectory(@"C:\output", new ExtractionOptions
+ {
+ ExtractFullPath = true,
+ Overwrite = true
+ });
+ }
+}
+catch (PasswordRequiredException)
+{
+ Console.WriteLine("Password required");
+}
+catch (InvalidArchiveException)
+{
+ Console.WriteLine("Archive is invalid");
+}
+catch (SharpCompressException ex)
+{
+ Console.WriteLine($"Error: {ex.Message}");
+}
+```
+
+### Extract with Progress
+
+```csharp
+var progress = new Progress(report =>
+{
+ Console.WriteLine($"Extracting {report.EntryPath}: {report.PercentComplete}%");
+});
+
+var options = new ReaderOptions { Progress = progress };
+using (var archive = ZipArchive.Open("archive.zip", options))
+{
+ archive.WriteToDirectory(@"C:\output");
+}
+```
+
+### Async Extract with Cancellation
+
+```csharp
+var cts = new CancellationTokenSource();
+cts.CancelAfter(TimeSpan.FromMinutes(5));
+
+try
+{
+ using (var archive = ZipArchive.Open("archive.zip"))
+ {
+ await archive.WriteToDirectoryAsync(@"C:\output",
+ new ExtractionOptions { ExtractFullPath = true, Overwrite = true },
+ cts.Token);
+ }
+}
+catch (OperationCanceledException)
+{
+ Console.WriteLine("Extraction cancelled");
+}
+```
+
+### Create with Custom Compression
+
+```csharp
+using (var archive = ZipArchive.Create())
+{
+ archive.AddAllFromDirectory(@"D:\source");
+
+ // Fastest
+ archive.SaveTo("fast.zip", new WriterOptions(CompressionType.Deflate)
+ {
+ CompressionLevel = 1
+ });
+
+ // Balanced (default)
+ archive.SaveTo("normal.zip", CompressionType.Deflate);
+
+ // Best compression
+ archive.SaveTo("best.zip", new WriterOptions(CompressionType.Deflate)
+ {
+ CompressionLevel = 9
+ });
+}
+```
+
+### Stream Processing (No File I/O)
+
+```csharp
+using (var outputStream = new MemoryStream())
+using (var archive = ZipArchive.Create())
+{
+ // Add content from memory
+ using (var contentStream = new MemoryStream(Encoding.UTF8.GetBytes("Hello")))
+ {
+ archive.AddEntry("file.txt", contentStream);
+ }
+
+ // Save to memory
+ archive.SaveTo(outputStream, CompressionType.Deflate);
+
+ // Get bytes
+ byte[] archiveBytes = outputStream.ToArray();
+}
+```
+
+### Extract Specific Files
+
+```csharp
+using (var archive = ZipArchive.Open("archive.zip"))
+{
+ var filesToExtract = new[] { "file1.txt", "file2.txt" };
+
+ foreach (var entry in archive.Entries.Where(e => filesToExtract.Contains(e.Key)))
+ {
+ entry.WriteToFile(@"C:\output\" + entry.Key);
+ }
+}
+```
+
+### List Archive Contents
+
+```csharp
+using (var archive = ZipArchive.Open("archive.zip"))
+{
+ foreach (var entry in archive.Entries)
+ {
+ if (entry.IsDirectory)
+ Console.WriteLine($"[DIR] {entry.Key}");
+ else
+ Console.WriteLine($"[FILE] {entry.Key} ({entry.Size} bytes)");
+ }
+}
+```
+
+---
+
+## Common Mistakes
+
+### ✗ Wrong - Stream not disposed
+
+```csharp
+var stream = File.OpenRead("archive.zip");
+var archive = ZipArchive.Open(stream);
+archive.WriteToDirectory(@"C:\output");
+// stream not disposed - leaked resource
+```
+
+### ✓ Correct - Using blocks
+
+```csharp
+using (var stream = File.OpenRead("archive.zip"))
+using (var archive = ZipArchive.Open(stream))
+{
+ archive.WriteToDirectory(@"C:\output");
+}
+// Both properly disposed
+```
+
+### ✗ Wrong - Mixing API styles
+
+```csharp
+// Loading entire archive then iterating
+using (var archive = ZipArchive.Open("large.zip"))
+{
+ var entries = archive.Entries.ToList(); // Loads all in memory
+ foreach (var e in entries)
+ {
+ e.WriteToFile(...); // Then extracts each
+ }
+}
+```
+
+### ✓ Correct - Use Reader for large files
+
+```csharp
+// Streaming iteration
+using (var stream = File.OpenRead("large.zip"))
+using (var reader = ReaderFactory.Open(stream))
+{
+ while (reader.MoveToNextEntry())
+ {
+ reader.WriteEntryToDirectory(@"C:\output");
+ }
+}
+```
+
+---
+
+## Related Documentation
+
+- [USAGE.md](USAGE.md) - Complete code examples
+- [FORMATS.md](FORMATS.md) - Supported formats
+- [PERFORMANCE.md](PERFORMANCE.md) - API selection guide
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
new file mode 100644
index 000000000..0ba6f5819
--- /dev/null
+++ b/docs/ARCHITECTURE.md
@@ -0,0 +1,659 @@
+# SharpCompress Architecture Guide
+
+This guide explains the internal architecture and design patterns of SharpCompress for contributors.
+
+## Overview
+
+SharpCompress is organized into three main layers:
+
+```
+┌─────────────────────────────────────────┐
+│ User-Facing APIs (Top Layer) │
+│ Archive, Reader, Writer Factories │
+├─────────────────────────────────────────┤
+│ Format-Specific Implementations │
+│ ZipArchive, TarReader, GZipWriter, │
+│ RarArchive, SevenZipArchive, etc. │
+├─────────────────────────────────────────┤
+│ Compression & Crypto (Bottom Layer) │
+│ Deflate, LZMA, BZip2, AES, CRC32 │
+└─────────────────────────────────────────┘
+```
+
+---
+
+## Directory Structure
+
+### `src/SharpCompress/`
+
+#### `Archives/` - Archive Implementations
+Contains `IArchive` implementations for seekable, random-access APIs.
+
+**Key Files:**
+- `AbstractArchive.cs` - Base class for all archives
+- `IArchive.cs` - Archive interface definition
+- `ArchiveFactory.cs` - Factory for opening archives
+- Format-specific: `ZipArchive.cs`, `TarArchive.cs`, `RarArchive.cs`, `SevenZipArchive.cs`, `GZipArchive.cs`
+
+**Use Archive API when:**
+- Stream is seekable (file, memory)
+- Need random access to entries
+- Archive fits in memory
+- Simplicity is important
+
+#### `Readers/` - Reader Implementations
+Contains `IReader` implementations for forward-only, non-seekable APIs.
+
+**Key Files:**
+- `AbstractReader.cs` - Base reader class
+- `IReader.cs` - Reader interface
+- `ReaderFactory.cs` - Auto-detection factory
+- `ReaderOptions.cs` - Configuration for readers
+- Format-specific: `ZipReader.cs`, `TarReader.cs`, `GZipReader.cs`, `RarReader.cs`, etc.
+
+**Use Reader API when:**
+- Stream is non-seekable (network, pipe, compressed)
+- Processing large files
+- Memory is limited
+- Forward-only processing is acceptable
+
+#### `Writers/` - Writer Implementations
+Contains `IWriter` implementations for forward-only writing.
+
+**Key Files:**
+- `AbstractWriter.cs` - Base writer class
+- `IWriter.cs` - Writer interface
+- `WriterFactory.cs` - Factory for creating writers
+- `WriterOptions.cs` - Configuration for writers
+- Format-specific: `ZipWriter.cs`, `TarWriter.cs`, `GZipWriter.cs`
+
+#### `Factories/` - Format Detection
+Factory classes for auto-detecting archive format and creating appropriate readers/writers.
+
+**Key Files:**
+- `Factory.cs` - Base factory class
+- `IFactory.cs` - Factory interface
+- Format-specific: `ZipFactory.cs`, `TarFactory.cs`, `RarFactory.cs`, etc.
+
+**How It Works:**
+1. `ReaderFactory.Open(stream)` probes stream signatures
+2. Identifies format by magic bytes
+3. Creates appropriate reader instance
+4. Returns generic `IReader` interface
+
+#### `Common/` - Shared Types
+Common types, options, and enumerations used across formats.
+
+**Key Files:**
+- `IEntry.cs` - Entry interface (file within archive)
+- `Entry.cs` - Entry implementation
+- `ArchiveType.cs` - Enum for archive formats
+- `CompressionType.cs` - Enum for compression methods
+- `ArchiveEncoding.cs` - Character encoding configuration
+- `ExtractionOptions.cs` - Extraction configuration
+- Format-specific headers: `Zip/Headers/`, `Tar/Headers/`, `Rar/Headers/`, etc.
+
+#### `Compressors/` - Compression Algorithms
+Low-level compression streams implementing specific algorithms.
+
+**Algorithms:**
+- `Deflate/` - DEFLATE compression (Zip default)
+- `BZip2/` - BZip2 compression
+- `LZMA/` - LZMA compression (7Zip, XZ, LZip)
+- `PPMd/` - Prediction by Partial Matching (Zip, 7Zip)
+- `ZStandard/` - ZStandard compression (decompression only)
+- `Xz/` - XZ format (decompression only)
+- `Rar/` - RAR-specific unpacking
+- `Arj/`, `Arc/`, `Ace/` - Legacy format decompression
+- `Filters/` - BCJ/BCJ2 filters for executable compression
+
+**Each Compressor:**
+- Implements a `Stream` subclass
+- Provides both compression and decompression
+- Some are read-only (decompression only)
+
+#### `Crypto/` - Encryption & Hashing
+Cryptographic functions and stream wrappers.
+
+**Key Files:**
+- `Crc32Stream.cs` - CRC32 calculation wrapper
+- `BlockTransformer.cs` - Block cipher transformations
+- AES, PKWare, WinZip encryption implementations
+
+#### `IO/` - Stream Utilities
+Stream wrappers and utilities.
+
+**Key Classes:**
+- `SharpCompressStream` - Base stream class
+- `ProgressReportingStream` - Progress tracking wrapper
+- `MarkingBinaryReader` - Binary reader with position marks
+- `BufferedSubStream` - Buffered read-only substream
+- `ReadOnlySubStream` - Read-only view of parent stream
+- `NonDisposingStream` - Prevents wrapped stream disposal
+
+---
+
+## Design Patterns
+
+### 1. Factory Pattern
+
+**Purpose:** Auto-detect format and create appropriate reader/writer.
+
+**Example:**
+```csharp
+// User calls factory
+using (var reader = ReaderFactory.Open(stream)) // Returns IReader
+{
+ while (reader.MoveToNextEntry())
+ {
+ // Process entry
+ }
+}
+
+// Behind the scenes:
+// 1. Factory.Open() probes stream signatures
+// 2. Detects format (Zip, Tar, Rar, etc.)
+// 3. Creates appropriate reader (ZipReader, TarReader, etc.)
+// 4. Returns as generic IReader interface
+```
+
+**Files:**
+- `src/SharpCompress/Factories/ReaderFactory.cs`
+- `src/SharpCompress/Factories/WriterFactory.cs`
+- `src/SharpCompress/Factories/ArchiveFactory.cs`
+
+### 2. Strategy Pattern
+
+**Purpose:** Encapsulate compression algorithms as swappable strategies.
+
+**Example:**
+```csharp
+// Different compression strategies
+CompressionType.Deflate // DEFLATE
+CompressionType.BZip2 // BZip2
+CompressionType.LZMA // LZMA
+CompressionType.PPMd // PPMd
+
+// Writer uses strategy pattern
+var archive = ZipArchive.Create();
+archive.SaveTo("output.zip", CompressionType.Deflate); // Use Deflate
+archive.SaveTo("output.bz2", CompressionType.BZip2); // Use BZip2
+```
+
+**Files:**
+- `src/SharpCompress/Compressors/` - Strategy implementations
+
+### 3. Decorator Pattern
+
+**Purpose:** Wrap streams with additional functionality.
+
+**Example:**
+```csharp
+// Progress reporting decorator
+var progressStream = new ProgressReportingStream(baseStream, progressReporter);
+progressStream.Read(buffer, 0, buffer.Length); // Reports progress
+
+// Non-disposing decorator
+var nonDisposingStream = new NonDisposingStream(baseStream);
+using (var compressor = new DeflateStream(nonDisposingStream))
+{
+ // baseStream won't be disposed when compressor is disposed
+}
+```
+
+**Files:**
+- `src/SharpCompress/IO/ProgressReportingStream.cs`
+- `src/SharpCompress/IO/NonDisposingStream.cs`
+
+### 4. Template Method Pattern
+
+**Purpose:** Define algorithm skeleton in base class, let subclasses fill details.
+
+**Example:**
+```csharp
+// AbstractArchive defines common archive operations
+public abstract class AbstractArchive : IArchive
+{
+ // Template methods
+ public virtual void WriteToDirectory(string destinationDirectory, ExtractionOptions options)
+ {
+ // Common extraction logic
+ foreach (var entry in Entries)
+ {
+ // Call subclass method
+ entry.WriteToFile(destinationPath, options);
+ }
+ }
+
+ // Subclasses override format-specific details
+ protected abstract Entry CreateEntry(EntryData data);
+}
+```
+
+**Files:**
+- `src/SharpCompress/Archives/AbstractArchive.cs`
+- `src/SharpCompress/Readers/AbstractReader.cs`
+
+### 5. Iterator Pattern
+
+**Purpose:** Provide sequential access to entries.
+
+**Example:**
+```csharp
+// Archive API - provides collection
+IEnumerable entries = archive.Entries;
+foreach (var entry in entries)
+{
+ // Random access - entries already in memory
+}
+
+// Reader API - provides iterator
+IReader reader = ReaderFactory.Open(stream);
+while (reader.MoveToNextEntry())
+{
+ // Forward-only iteration - one entry at a time
+ var entry = reader.Entry;
+}
+```
+
+---
+
+## Key Interfaces
+
+### IArchive - Random Access API
+
+```csharp
+public interface IArchive : IDisposable
+{
+ IEnumerable Entries { get; }
+
+ void WriteToDirectory(string destinationDirectory,
+ ExtractionOptions options = null);
+
+ IEntry FirstOrDefault(Func predicate);
+
+ // ... format-specific methods
+}
+```
+
+**Implementations:** `ZipArchive`, `TarArchive`, `RarArchive`, `SevenZipArchive`, `GZipArchive`
+
+### IReader - Forward-Only API
+
+```csharp
+public interface IReader : IDisposable
+{
+ IEntry Entry { get; }
+
+ bool MoveToNextEntry();
+
+ void WriteEntryToDirectory(string destinationDirectory,
+ ExtractionOptions options = null);
+
+ Stream OpenEntryStream();
+
+ // ... async variants
+}
+```
+
+**Implementations:** `ZipReader`, `TarReader`, `RarReader`, `GZipReader`, etc.
+
+### IWriter - Writing API
+
+```csharp
+public interface IWriter : IDisposable
+{
+ void Write(string entryPath, Stream source,
+ DateTime? modificationTime = null);
+
+ void WriteAll(string sourceDirectory, string searchPattern,
+ SearchOption searchOption);
+
+ // ... async variants
+}
+```
+
+**Implementations:** `ZipWriter`, `TarWriter`, `GZipWriter`
+
+### IEntry - Archive Entry
+
+```csharp
+public interface IEntry
+{
+ string Key { get; }
+ uint Size { get; }
+ uint CompressedSize { get; }
+ bool IsDirectory { get; }
+ DateTime? LastModifiedTime { get; }
+ CompressionType CompressionType { get; }
+
+ void WriteToFile(string fullPath, ExtractionOptions options = null);
+ void WriteToStream(Stream destinationStream);
+ Stream OpenEntryStream();
+
+ // ... async variants
+}
+```
+
+---
+
+## Adding Support for a New Format
+
+### Step 1: Understand the Format
+- Research format specification
+- Understand compression/encryption used
+- Study existing similar formats in codebase
+
+### Step 2: Create Format Structure Classes
+
+**Create:** `src/SharpCompress/Common/NewFormat/`
+
+```csharp
+// Headers and data structures
+public class NewFormatHeader
+{
+ public uint Magic { get; set; }
+ public ushort Version { get; set; }
+ // ... other fields
+
+ public static NewFormatHeader Read(BinaryReader reader)
+ {
+ // Deserialize from binary
+ }
+}
+
+public class NewFormatEntry
+{
+ public string FileName { get; set; }
+ public uint CompressedSize { get; set; }
+ public uint UncompressedSize { get; set; }
+ // ... other fields
+}
+```
+
+### Step 3: Create Archive Implementation
+
+**Create:** `src/SharpCompress/Archives/NewFormat/NewFormatArchive.cs`
+
+```csharp
+public class NewFormatArchive : AbstractArchive
+{
+ private NewFormatHeader _header;
+ private List _entries;
+
+ public static NewFormatArchive Open(Stream stream)
+ {
+ var archive = new NewFormatArchive();
+ archive._header = NewFormatHeader.Read(stream);
+ archive.LoadEntries(stream);
+ return archive;
+ }
+
+ public override IEnumerable Entries => _entries.Select(e => new Entry(e));
+
+ protected override Stream OpenEntryStream(Entry entry)
+ {
+ // Return decompressed stream for entry
+ }
+
+ // ... other abstract method implementations
+}
+```
+
+### Step 4: Create Reader Implementation
+
+**Create:** `src/SharpCompress/Readers/NewFormat/NewFormatReader.cs`
+
+```csharp
+public class NewFormatReader : AbstractReader
+{
+ private NewFormatHeader _header;
+ private BinaryReader _reader;
+
+ public NewFormatReader(Stream stream)
+ {
+ _reader = new BinaryReader(stream);
+ _header = NewFormatHeader.Read(_reader);
+ }
+
+ public override bool MoveToNextEntry()
+ {
+ // Read next entry header
+ if (!_reader.BaseStream.CanRead) return false;
+
+ var entryData = NewFormatEntry.Read(_reader);
+ // ... set this.Entry
+ return entryData != null;
+ }
+
+ // ... other abstract method implementations
+}
+```
+
+### Step 5: Create Factory
+
+**Create:** `src/SharpCompress/Factories/NewFormatFactory.cs`
+
+```csharp
+public class NewFormatFactory : Factory, IArchiveFactory, IReaderFactory
+{
+ // Archive format magic bytes (signature)
+ private static readonly byte[] NewFormatSignature = new byte[] { 0x4E, 0x46 }; // "NF"
+
+ public static NewFormatFactory Instance { get; } = new();
+
+ public IArchive CreateArchive(Stream stream)
+ => NewFormatArchive.Open(stream);
+
+ public IReader CreateReader(Stream stream, ReaderOptions options)
+ => new NewFormatReader(stream) { Options = options };
+
+ public bool Matches(Stream stream, ReadOnlySpan signature)
+ => signature.StartsWith(NewFormatSignature);
+}
+```
+
+### Step 6: Register Factory
+
+**Update:** `src/SharpCompress/Factories/ArchiveFactory.cs`
+
+```csharp
+private static readonly IFactory[] Factories =
+{
+ ZipFactory.Instance,
+ TarFactory.Instance,
+ RarFactory.Instance,
+ SevenZipFactory.Instance,
+ GZipFactory.Instance,
+ NewFormatFactory.Instance, // Add here
+ // ... other factories
+};
+```
+
+### Step 7: Add Tests
+
+**Create:** `tests/SharpCompress.Test/NewFormat/NewFormatTests.cs`
+
+```csharp
+public class NewFormatTests : TestBase
+{
+ [Fact]
+ public void NewFormat_Extracts_Successfully()
+ {
+ var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "archive.newformat");
+ using (var archive = NewFormatArchive.Open(archivePath))
+ {
+ archive.WriteToDirectory(SCRATCH_FILES_PATH);
+ // Assert extraction
+ }
+ }
+
+ [Fact]
+ public void NewFormat_Reader_Works()
+ {
+ var archivePath = Path.Combine(TEST_ARCHIVES_PATH, "archive.newformat");
+ using (var stream = File.OpenRead(archivePath))
+ using (var reader = new NewFormatReader(stream))
+ {
+ Assert.True(reader.MoveToNextEntry());
+ Assert.NotNull(reader.Entry);
+ }
+ }
+}
+```
+
+### Step 8: Add Test Archives
+
+Place test files in `tests/TestArchives/Archives/NewFormat/` directory.
+
+### Step 9: Document
+
+Update `docs/FORMATS.md` with format support information.
+
+---
+
+## Compression Algorithm Implementation
+
+### Creating a New Compression Stream
+
+**Example:** Creating `CustomStream` for a custom compression algorithm
+
+```csharp
+public class CustomStream : Stream
+{
+ private readonly Stream _baseStream;
+ private readonly bool _leaveOpen;
+
+ public CustomStream(Stream baseStream, bool leaveOpen = false)
+ {
+ _baseStream = baseStream;
+ _leaveOpen = leaveOpen;
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ // Decompress data from _baseStream into buffer
+ // Return number of decompressed bytes
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ // Compress data from buffer into _baseStream
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && !_leaveOpen)
+ {
+ _baseStream?.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+}
+```
+
+---
+
+## Stream Handling Best Practices
+
+### Disposal Pattern
+
+```csharp
+// Correct: Nested using blocks
+using (var fileStream = File.OpenRead("archive.zip"))
+using (var archive = ZipArchive.Open(fileStream))
+{
+ archive.WriteToDirectory(@"C:\output");
+}
+// Both archive and fileStream properly disposed
+
+// Correct: Using with options
+var options = new ReaderOptions { LeaveStreamOpen = true };
+var stream = File.OpenRead("archive.zip");
+using (var archive = ZipArchive.Open(stream, options))
+{
+ archive.WriteToDirectory(@"C:\output");
+}
+stream.Dispose(); // Manually dispose if LeaveStreamOpen = true
+```
+
+### NonDisposingStream Wrapper
+
+```csharp
+// Prevent unwanted stream closure
+var baseStream = File.OpenRead("data.bin");
+var nonDisposing = new NonDisposingStream(baseStream);
+
+using (var compressor = new DeflateStream(nonDisposing))
+{
+ // Compressor won't close baseStream when disposed
+}
+
+// baseStream still usable
+baseStream.Position = 0; // Works
+baseStream.Dispose(); // Manual disposal
+```
+
+---
+
+## Performance Considerations
+
+### Memory Efficiency
+
+1. **Avoid loading entire archive in memory** - Use Reader API for large files
+2. **Process entries sequentially** - Especially for solid archives
+3. **Use appropriate buffer sizes** - Larger buffers for network I/O
+4. **Dispose streams promptly** - Free resources when done
+
+### Algorithm Selection
+
+1. **Archive API** - Fast for small archives with random access
+2. **Reader API** - Efficient for large files or streaming
+3. **Solid archives** - Sequential extraction much faster
+4. **Compression levels** - Trade-off between speed and size
+
+---
+
+## Testing Guidelines
+
+### Test Coverage
+
+1. **Happy path** - Normal extraction works
+2. **Edge cases** - Empty archives, single file, many files
+3. **Corrupted data** - Handle gracefully
+4. **Error cases** - Missing passwords, unsupported compression
+5. **Async operations** - Both sync and async code paths
+
+### Test Archives
+
+- Use `tests/TestArchives/` for test data
+- Create format-specific subdirectories
+- Include encrypted, corrupted, and edge case archives
+- Don't recreate existing archives
+
+### Test Patterns
+
+```csharp
+[Fact]
+public void Archive_Extraction_Works()
+{
+ // Arrange
+ var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "test.zip");
+
+ // Act
+ using (var archive = ZipArchive.Open(testArchive))
+ {
+ archive.WriteToDirectory(SCRATCH_FILES_PATH);
+ }
+
+ // Assert
+ Assert.True(File.Exists(Path.Combine(SCRATCH_FILES_PATH, "file.txt")));
+}
+```
+
+---
+
+## Related Documentation
+
+- [AGENTS.md](../AGENTS.md) - Development guidelines
+- [FORMATS.md](FORMATS.md) - Supported formats
diff --git a/docs/ENCODING.md b/docs/ENCODING.md
new file mode 100644
index 000000000..1737ace93
--- /dev/null
+++ b/docs/ENCODING.md
@@ -0,0 +1,610 @@
+# SharpCompress Character Encoding Guide
+
+This guide explains how SharpCompress handles character encoding for archive entries (filenames, comments, etc.).
+
+## Overview
+
+Most archive formats store filenames and metadata as bytes. SharpCompress must convert these bytes to strings using the appropriate character encoding.
+
+**Common Problem:** Archives created on systems with non-UTF8 encodings (especially Japanese, Chinese systems) appear with corrupted filenames when extracted on systems that assume UTF8.
+
+---
+
+## ArchiveEncoding Class
+
+### Basic Usage
+
+```csharp
+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
+ }
+};
+
+using (var archive = ZipArchive.Open("japanese.zip", options))
+{
+ foreach (var entry in archive.Entries)
+ {
+ Console.WriteLine(entry.Key); // Now shows correct characters
+ }
+}
+```
+
+### ArchiveEncoding Properties
+
+| Property | Purpose |
+|----------|---------|
+| `Default` | Default encoding for filenames (fallback) |
+| `CustomDecoder` | Custom decoding function for special cases |
+
+### Setting for Different APIs
+
+**Archive API:**
+```csharp
+var options = new ReaderOptions
+{
+ ArchiveEncoding = new ArchiveEncoding { Default = Encoding.GetEncoding(932) }
+};
+using (var archive = ZipArchive.Open("file.zip", options))
+{
+ // Use archive with correct encoding
+}
+```
+
+**Reader API:**
+```csharp
+var options = new ReaderOptions
+{
+ ArchiveEncoding = new ArchiveEncoding { Default = Encoding.GetEncoding(932) }
+};
+using (var stream = File.OpenRead("file.zip"))
+using (var reader = ReaderFactory.Open(stream, options))
+{
+ while (reader.MoveToNextEntry())
+ {
+ // Filenames decoded correctly
+ }
+}
+```
+
+---
+
+## Common Encodings
+
+### Asian Encodings
+
+#### cp932 (Japanese)
+```csharp
+// Windows-31J, Shift-JIS variant used on Japanese Windows
+var options = new ReaderOptions
+{
+ ArchiveEncoding = new ArchiveEncoding
+ {
+ Default = Encoding.GetEncoding(932)
+ }
+};
+using (var archive = ZipArchive.Open("japanese.zip", options))
+{
+ // Correctly decodes Japanese filenames
+}
+```
+
+**When to use:**
+- Archives from Japanese Windows systems
+- Files with Japanese characters in names
+
+#### gb2312 (Simplified Chinese)
+```csharp
+// Simplified Chinese
+var options = new ReaderOptions
+{
+ ArchiveEncoding = new ArchiveEncoding
+ {
+ Default = Encoding.GetEncoding("gb2312")
+ }
+};
+```
+
+#### gbk (Extended Simplified Chinese)
+```csharp
+// Extended Simplified Chinese (more characters than gb2312)
+var options = new ReaderOptions
+{
+ ArchiveEncoding = new ArchiveEncoding
+ {
+ Default = Encoding.GetEncoding("gbk")
+ }
+};
+```
+
+#### big5 (Traditional Chinese)
+```csharp
+// Traditional Chinese (Taiwan, Hong Kong)
+var options = new ReaderOptions
+{
+ ArchiveEncoding = new ArchiveEncoding
+ {
+ Default = Encoding.GetEncoding("big5")
+ }
+};
+```
+
+#### euc-jp (Japanese, Unix)
+```csharp
+// Extended Unix Code for Japanese
+var options = new ReaderOptions
+{
+ ArchiveEncoding = new ArchiveEncoding
+ {
+ Default = Encoding.GetEncoding("eucjp")
+ }
+};
+```
+
+#### euc-kr (Korean)
+```csharp
+// Extended Unix Code for Korean
+var options = new ReaderOptions
+{
+ ArchiveEncoding = new ArchiveEncoding
+ {
+ Default = Encoding.GetEncoding("euc-kr")
+ }
+};
+```
+
+### Western European Encodings
+
+#### iso-8859-1 (Latin-1)
+```csharp
+// Western European (includes accented characters)
+var options = new ReaderOptions
+{
+ ArchiveEncoding = new ArchiveEncoding
+ {
+ Default = Encoding.GetEncoding("iso-8859-1")
+ }
+};
+```
+
+**When to use:**
+- Archives from French, German, Spanish systems
+- Files with accented characters (é, ñ, ü, etc.)
+
+#### cp1252 (Windows-1252)
+```csharp
+// Windows Western European
+// Very similar to iso-8859-1 but with additional printable characters
+var options = new ReaderOptions
+{
+ ArchiveEncoding = new ArchiveEncoding
+ {
+ Default = Encoding.GetEncoding("cp1252")
+ }
+};
+```
+
+**When to use:**
+- Archives from older Western European Windows systems
+- Files with smart quotes and other Windows-specific characters
+
+#### iso-8859-15 (Latin-9)
+```csharp
+// Western European with Euro symbol support
+var options = new ReaderOptions
+{
+ ArchiveEncoding = new ArchiveEncoding
+ {
+ Default = Encoding.GetEncoding("iso-8859-15")
+ }
+};
+```
+
+### Cyrillic Encodings
+
+#### cp1251 (Windows Cyrillic)
+```csharp
+// Russian, Serbian, Bulgarian, etc.
+var options = new ReaderOptions
+{
+ ArchiveEncoding = new ArchiveEncoding
+ {
+ Default = Encoding.GetEncoding("cp1251")
+ }
+};
+```
+
+#### koi8-r (KOI8 Russian)
+```csharp
+// Russian (Unix standard)
+var options = new ReaderOptions
+{
+ ArchiveEncoding = new ArchiveEncoding
+ {
+ Default = Encoding.GetEncoding("koi8-r")
+ }
+};
+```
+
+### UTF Encodings (Modern)
+
+#### UTF-8 (Default)
+```csharp
+// Modern standard - usually correct for new archives
+var options = new ReaderOptions
+{
+ ArchiveEncoding = new ArchiveEncoding
+ {
+ Default = Encoding.UTF8
+ }
+};
+```
+
+#### UTF-16
+```csharp
+// Unicode - rarely used in archives
+var options = new ReaderOptions
+{
+ ArchiveEncoding = new ArchiveEncoding
+ {
+ Default = Encoding.Unicode
+ }
+};
+```
+
+---
+
+## Encoding Auto-Detection
+
+SharpCompress attempts to auto-detect encoding, but this isn't always reliable:
+
+```csharp
+// Auto-detection (default)
+using (var archive = ZipArchive.Open("file.zip")) // Uses UTF8 by default
+{
+ // May show corrupted characters if archive uses different encoding
+}
+
+// Explicit encoding (more reliable)
+var options = new ReaderOptions
+{
+ ArchiveEncoding = new ArchiveEncoding { Default = Encoding.GetEncoding(932) }
+};
+using (var archive = ZipArchive.Open("file.zip", options))
+{
+ // Correct characters displayed
+}
+```
+
+### When Manual Override is Needed
+
+| Situation | Solution |
+|-----------|----------|
+| Archive shows corrupted characters | Specify the encoding explicitly |
+| Archives from specific region | Use that region's encoding |
+| Mixed encodings in archive | Use CustomDecoder |
+| Testing with international files | Try different encodings |
+
+---
+
+## Custom Decoder
+
+For complex scenarios where a single encoding isn't sufficient:
+
+### Basic Custom Decoder
+
+```csharp
+var options = new ReaderOptions
+{
+ ArchiveEncoding = new ArchiveEncoding
+ {
+ CustomDecoder = (data, offset, length) =>
+ {
+ // Custom decoding logic
+ var bytes = new byte[length];
+ Array.Copy(data, offset, bytes, 0, length);
+
+ // Try UTF8 first
+ try
+ {
+ return Encoding.UTF8.GetString(bytes);
+ }
+ catch
+ {
+ // Fallback to cp932 if UTF8 fails
+ return Encoding.GetEncoding(932).GetString(bytes);
+ }
+ }
+ }
+};
+
+using (var archive = ZipArchive.Open("mixed.zip", options))
+{
+ foreach (var entry in archive.Entries)
+ {
+ Console.WriteLine(entry.Key); // Uses custom decoder
+ }
+}
+```
+
+### Advanced: Detect Encoding by Content
+
+```csharp
+var options = new ReaderOptions
+{
+ ArchiveEncoding = new ArchiveEncoding
+ {
+ CustomDecoder = DetectAndDecode
+ }
+};
+
+private static string DetectAndDecode(byte[] data, int offset, int length)
+{
+ var bytes = new byte[length];
+ Array.Copy(data, offset, bytes, 0, length);
+
+ // Try UTF8 (most modern archives)
+ try
+ {
+ var str = Encoding.UTF8.GetString(bytes);
+ // Verify it decoded correctly (no replacement characters)
+ if (!str.Contains('\uFFFD'))
+ return str;
+ }
+ catch { }
+
+ // Try cp932 (Japanese)
+ try
+ {
+ var str = Encoding.GetEncoding(932).GetString(bytes);
+ if (!str.Contains('\uFFFD'))
+ return str;
+ }
+ catch { }
+
+ // Fallback to iso-8859-1 (always succeeds)
+ return Encoding.GetEncoding("iso-8859-1").GetString(bytes);
+}
+```
+
+---
+
+## Code Examples
+
+### Extract Archive with Japanese Filenames
+
+```csharp
+var options = new ReaderOptions
+{
+ ArchiveEncoding = new ArchiveEncoding
+ {
+ Default = Encoding.GetEncoding(932) // cp932
+ }
+};
+
+using (var archive = ZipArchive.Open("japanese_files.zip", options))
+{
+ archive.WriteToDirectory(@"C:\output", new ExtractionOptions
+ {
+ ExtractFullPath = true,
+ Overwrite = true
+ });
+}
+// Files extracted with correct Japanese names
+```
+
+### Extract Archive with Western European Filenames
+
+```csharp
+var options = new ReaderOptions
+{
+ ArchiveEncoding = new ArchiveEncoding
+ {
+ Default = Encoding.GetEncoding("iso-8859-1")
+ }
+};
+
+using (var archive = ZipArchive.Open("french_files.zip", options))
+{
+ archive.WriteToDirectory(@"C:\output");
+}
+// Accented characters (é, è, ê, etc.) display correctly
+```
+
+### Extract Archive with Chinese Filenames
+
+```csharp
+var options = new ReaderOptions
+{
+ ArchiveEncoding = new ArchiveEncoding
+ {
+ Default = Encoding.GetEncoding("gbk") // Simplified Chinese
+ }
+};
+
+using (var archive = ZipArchive.Open("chinese_files.zip", options))
+{
+ archive.WriteToDirectory(@"C:\output");
+}
+```
+
+### Extract Archive with Russian Filenames
+
+```csharp
+var options = new ReaderOptions
+{
+ ArchiveEncoding = new ArchiveEncoding
+ {
+ Default = Encoding.GetEncoding("cp1251") // Windows Cyrillic
+ }
+};
+
+using (var archive = ZipArchive.Open("russian_files.zip", options))
+{
+ archive.WriteToDirectory(@"C:\output");
+}
+```
+
+### Reader API with Encoding
+
+```csharp
+var options = new ReaderOptions
+{
+ ArchiveEncoding = new ArchiveEncoding
+ {
+ Default = Encoding.GetEncoding(932)
+ }
+};
+
+using (var stream = File.OpenRead("japanese.zip"))
+using (var reader = ReaderFactory.Open(stream, options))
+{
+ while (reader.MoveToNextEntry())
+ {
+ if (!reader.Entry.IsDirectory)
+ {
+ Console.WriteLine(reader.Entry.Key); // Correct characters
+ reader.WriteEntryToDirectory(@"C:\output");
+ }
+ }
+}
+```
+
+---
+
+## Creating Archives with Correct Encoding
+
+When creating archives, SharpCompress uses UTF8 by default (recommended):
+
+```csharp
+// Create with UTF8 (default, recommended)
+using (var archive = ZipArchive.Create())
+{
+ archive.AddAllFromDirectory(@"D:\my_files");
+ archive.SaveTo("output.zip", CompressionType.Deflate);
+ // Archives created with UTF8 encoding
+}
+```
+
+If you need to create archives for systems that expect specific encodings:
+
+```csharp
+// Note: SharpCompress Writer API uses UTF8 for encoding
+// To create archives with other encodings, consider:
+// 1. Let users on those systems create archives
+// 2. Use system tools (7-Zip, WinRAR) with desired encoding
+// 3. Post-process archives if absolutely necessary
+
+// For now, recommend modern UTF8-based archives
+```
+
+---
+
+## Troubleshooting Encoding Issues
+
+### Filenames Show Question Marks (?)
+
+```
+✗ Wrong encoding detected
+test文件.txt → test???.txt
+```
+
+**Solution:** Specify correct encoding explicitly
+
+```csharp
+var options = new ReaderOptions
+{
+ ArchiveEncoding = new ArchiveEncoding
+ {
+ Default = Encoding.GetEncoding("gbk") // Try different encodings
+ }
+};
+```
+
+### Filenames Show Replacement Character ()
+
+```
+✗ Invalid bytes for selected encoding
+café.txt → caf.txt
+```
+
+**Solution:**
+1. Try a different encoding (see Common Encodings table)
+2. Use CustomDecoder with fallback encoding
+3. Archive might be corrupted
+
+### Mixed Encodings in Single Archive
+
+```csharp
+// Use CustomDecoder to handle mixed encodings
+var options = new ReaderOptions
+{
+ ArchiveEncoding = new ArchiveEncoding
+ {
+ CustomDecoder = (data, offset, length) =>
+ {
+ // Try multiple encodings in priority order
+ var bytes = new byte[length];
+ Array.Copy(data, offset, bytes, 0, length);
+
+ foreach (var encoding in new[]
+ {
+ Encoding.UTF8,
+ Encoding.GetEncoding(932),
+ Encoding.GetEncoding("iso-8859-1")
+ })
+ {
+ try
+ {
+ var str = encoding.GetString(bytes);
+ if (!str.Contains('\uFFFD'))
+ return str;
+ }
+ catch { }
+ }
+
+ // Final fallback
+ return Encoding.GetEncoding("iso-8859-1").GetString(bytes);
+ }
+ }
+};
+```
+
+---
+
+## Encoding Reference Table
+
+| Encoding | Code | Use Case |
+|----------|------|----------|
+| UTF-8 | (default) | Modern archives, recommended |
+| cp932 | 932 | Japanese Windows |
+| gb2312 | "gb2312" | Simplified Chinese |
+| gbk | "gbk" | Extended Simplified Chinese |
+| big5 | "big5" | Traditional Chinese |
+| iso-8859-1 | "iso-8859-1" | Western European |
+| cp1252 | "cp1252" | Windows Western European |
+| cp1251 | "cp1251" | Russian/Cyrillic |
+| euc-jp | "euc-jp" | Japanese Unix |
+| euc-kr | "euc-kr" | Korean |
+
+---
+
+## Best Practices
+
+1. **Use UTF-8 for new archives** - Most modern systems support it
+2. **Ask the archive creator** - When receiving archives with corrupted names
+3. **Provide encoding options** - If your app handles user archives
+4. **Document your assumption** - Tell users what encoding you're using
+5. **Test with international files** - Before releasing production code
+
+---
+
+## Related Documentation
+
+- [USAGE.md](USAGE.md#extract-zip-which-has-non-utf8-encoded-filenamycp932) - Usage examples
diff --git a/FORMATS.md b/docs/FORMATS.md
similarity index 100%
rename from FORMATS.md
rename to docs/FORMATS.md
diff --git a/docs/OLD_CHANGELOG.md b/docs/OLD_CHANGELOG.md
new file mode 100644
index 000000000..fbbdd7aed
--- /dev/null
+++ b/docs/OLD_CHANGELOG.md
@@ -0,0 +1,142 @@
+
+# Version Log
+
+* [Releases](https://github.com/adamhathcock/sharpcompress/releases)
+
+## Version 0.18
+
+* [Now on Github releases](https://github.com/adamhathcock/sharpcompress/releases/tag/0.18)
+
+## Version 0.17.1
+
+* Fix - [Bug Fix for .NET Core on Windows](https://github.com/adamhathcock/sharpcompress/pull/257)
+
+## Version 0.17.0
+
+* New - Full LZip support! Can read and write LZip files and Tars inside LZip files. [Make LZip a first class citizen. #241](https://github.com/adamhathcock/sharpcompress/issues/241)
+* New - XZ read support! Can read XZ files and Tars inside XZ files. [XZ in SharpCompress #91](https://github.com/adamhathcock/sharpcompress/issues/94)
+* Fix - [Regression - zip file writing on seekable streams always assumed stream start was 0. Introduced with Zip64 writing.](https://github.com/adamhathcock/sharpcompress/issues/244)
+* Fix - [Zip files with post-data descriptors can be properly skipped via decompression](https://github.com/adamhathcock/sharpcompress/issues/162)
+
+## Version 0.16.2
+
+* Fix [.NET 3.5 should support files and cryptography (was a regression from 0.16.0)](https://github.com/adamhathcock/sharpcompress/pull/251)
+* Fix [Zip per entry compression customization wrote the wrong method into the zip archive](https://github.com/adamhathcock/sharpcompress/pull/249)
+
+## Version 0.16.1
+
+* Fix [Preserve compression method when getting a compressed stream](https://github.com/adamhathcock/sharpcompress/pull/235)
+* Fix [RAR entry key normalization fix](https://github.com/adamhathcock/sharpcompress/issues/201)
+
+## Version 0.16.0
+
+* Breaking - [Progress Event Tracking rethink](https://github.com/adamhathcock/sharpcompress/pull/226)
+* Update to VS2017 - [VS2017](https://github.com/adamhathcock/sharpcompress/pull/231) - Framework targets have been changed.
+* New - [Add Zip64 writing](https://github.com/adamhathcock/sharpcompress/pull/211)
+* [Fix invalid/mismatching Zip version flags.](https://github.com/adamhathcock/sharpcompress/issues/164) - This allows nuget/System.IO.Packaging to read zip files generated by SharpCompress
+* [Fix 7Zip directory hiding](https://github.com/adamhathcock/sharpcompress/pull/215/files)
+* [Verify RAR CRC headers](https://github.com/adamhathcock/sharpcompress/pull/220)
+
+## Version 0.15.2
+
+* [Fix invalid headers](https://github.com/adamhathcock/sharpcompress/pull/210) - fixes an issue creating large-ish zip archives that was introduced with zip64 reading.
+
+## Version 0.15.1
+
+* [Zip64 extending information and ZipReader](https://github.com/adamhathcock/sharpcompress/pull/206)
+
+## Version 0.15.0
+
+* [Add zip64 support for ZipArchive extraction](https://github.com/adamhathcock/sharpcompress/pull/205)
+
+## Version 0.14.1
+
+* [.NET Assemblies aren't strong named](https://github.com/adamhathcock/sharpcompress/issues/158)
+* [Pkware encryption for Zip files didn't allow for multiple reads of an entry](https://github.com/adamhathcock/sharpcompress/issues/197)
+* [GZip Entry couldn't be read multiple times](https://github.com/adamhathcock/sharpcompress/issues/198)
+
+## Version 0.14.0
+
+* [Support for LZip reading in for Tars](https://github.com/adamhathcock/sharpcompress/pull/191)
+
+## Version 0.13.1
+
+* [Fix null password on ReaderFactory. Fix null options on SevenZipArchive](https://github.com/adamhathcock/sharpcompress/pull/188)
+* [Make PpmdProperties lazy to avoid unnecessary allocations.](https://github.com/adamhathcock/sharpcompress/pull/185)
+
+## Version 0.13.0
+
+* Breaking change: Big refactor of Options on API.
+* 7Zip supports Deflate
+
+## Version 0.12.4
+
+* Forward only zip issue fix https://github.com/adamhathcock/sharpcompress/issues/160
+* Try to fix frameworks again by copying targets from JSON.NET
+
+## Version 0.12.3
+
+* 7Zip fixes https://github.com/adamhathcock/sharpcompress/issues/73
+* Maybe all profiles will work with project.json now
+
+## Version 0.12.2
+
+* Support Profile 259 again
+
+## Version 0.12.1
+
+* Support Silverlight 5
+
+## Version 0.12.0
+
+* .NET Core RTM!
+* Bug fix for Tar long paths
+
+## Version 0.11.6
+
+* Bug fix for global header in Tar
+* Writers now have a leaveOpen `bool` overload. They won't close streams if not-requested to.
+
+## Version 0.11.5
+
+* Bug fix in Skip method
+
+## Version 0.11.4
+
+* SharpCompress is now endian neutral (matters for Mono platforms)
+* Fix for Inflate (need to change implementation)
+* Fixes for RAR detection
+
+## Version 0.11.1
+
+* Added Cancel on IReader
+* Removed .NET 2.0 support and LinqBridge dependency
+
+## Version 0.11
+
+* Been over a year, contains mainly fixes from contributors!
+* Possible breaking change: ArchiveEncoding is UTF8 by default now.
+* TAR supports writing long names using longlink
+* RAR Protect Header added
+
+## Version 0.10.3
+
+* Finally fixed Disposal issue when creating a new archive with the Archive API
+
+## Version 0.10.2
+
+* Fixed Rar Header reading for invalid extended time headers.
+* Windows Store assembly is now strong named
+* Known issues with Long Tar names being worked on
+* Updated to VS2013
+* Portable targets SL5 and Windows Phone 8 (up from SL4 and WP7)
+
+## Version 0.10.1
+
+* Fixed 7Zip extraction performance problem
+
+## Version 0.10:
+
+* Added support for RAR Decryption (thanks to https://github.com/hrasyid)
+* Embedded some BouncyCastle crypto classes to allow RAR Decryption and Winzip AES Decryption in Portable and Windows Store DLLs
+* Built in Release (I think)
diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md
new file mode 100644
index 000000000..03c12553b
--- /dev/null
+++ b/docs/PERFORMANCE.md
@@ -0,0 +1,474 @@
+# SharpCompress Performance Guide
+
+This guide helps you optimize SharpCompress for performance in various scenarios.
+
+## API Selection Guide
+
+### Archive API vs Reader API
+
+Choose the right API based on your use case:
+
+| Aspect | Archive API | Reader API |
+|--------|------------|-----------|
+| **Stream Type** | Seekable only | Non-seekable OK |
+| **Memory Usage** | All entries in memory | One entry at a time |
+| **Random Access** | ✓ Yes | ✗ No |
+| **Best For** | Small-to-medium archives | Large or streaming data |
+| **Performance** | Fast for random access | Better for large files |
+
+### Archive API (Fast for Random Access)
+
+```csharp
+// Use when:
+// - Archive fits in memory
+// - You need random access to entries
+// - Stream is seekable (file, MemoryStream)
+
+using (var archive = ZipArchive.Open("archive.zip"))
+{
+ // Random access - all entries available
+ var specific = archive.Entries.FirstOrDefault(e => e.Key == "file.txt");
+ if (specific != null)
+ {
+ specific.WriteToFile(@"C:\output\file.txt");
+ }
+}
+```
+
+**Performance Characteristics:**
+- ✓ Instant entry lookup
+- ✓ Parallel extraction possible
+- ✗ Entire archive in memory
+- ✗ Can't process while downloading
+
+### Reader API (Best for Large Files)
+
+```csharp
+// Use when:
+// - Processing large archives (>100 MB)
+// - Streaming from network/pipe
+// - Memory is constrained
+// - Forward-only processing is acceptable
+
+using (var stream = File.OpenRead("large.zip"))
+using (var reader = ReaderFactory.Open(stream))
+{
+ while (reader.MoveToNextEntry())
+ {
+ // Process one entry at a time
+ reader.WriteEntryToDirectory(@"C:\output");
+ }
+}
+```
+
+**Performance Characteristics:**
+- ✓ Minimal memory footprint
+- ✓ Works with non-seekable streams
+- ✓ Can process while downloading
+- ✗ Forward-only (no random access)
+- ✗ Entry lookup requires iteration
+
+---
+
+## Buffer Sizing
+
+### Understanding Buffers
+
+SharpCompress uses internal buffers for reading compressed data. Buffer size affects:
+- **Speed:** Larger buffers = fewer I/O operations = faster
+- **Memory:** Larger buffers = higher memory usage
+
+### Recommended Buffer Sizes
+
+| Scenario | Size | Notes |
+|----------|------|-------|
+| Embedded/IoT devices | 4-8 KB | Minimal memory usage |
+| Memory-constrained | 16-32 KB | Conservative default |
+| Standard use (default) | 64 KB | Recommended default |
+| Large file streaming | 256 KB | Better throughput |
+| High-speed SSD | 512 KB - 1 MB | Maximum throughput |
+
+### How Buffer Size Affects Performance
+
+```csharp
+// SharpCompress manages buffers internally
+// You can't directly set buffer size, but you can:
+
+// 1. Use Stream.CopyTo with explicit buffer size
+using (var entryStream = reader.OpenEntryStream())
+using (var fileStream = File.Create(@"C:\output\file.txt"))
+{
+ // 64 KB buffer (default)
+ entryStream.CopyTo(fileStream);
+
+ // Or specify larger buffer for faster copy
+ entryStream.CopyTo(fileStream, bufferSize: 262144); // 256 KB
+}
+
+// 2. Use custom buffer for writing
+using (var entryStream = reader.OpenEntryStream())
+using (var fileStream = File.Create(@"C:\output\file.txt"))
+{
+ byte[] buffer = new byte[262144]; // 256 KB
+ int bytesRead;
+ while ((bytesRead = entryStream.Read(buffer, 0, buffer.Length)) > 0)
+ {
+ fileStream.Write(buffer, 0, bytesRead);
+ }
+}
+```
+
+---
+
+## Streaming Large Files
+
+### Non-Seekable Stream Patterns
+
+For processing archives from downloads or pipes:
+
+```csharp
+// Download stream (non-seekable)
+using (var httpStream = await httpClient.GetStreamAsync(url))
+using (var reader = ReaderFactory.Open(httpStream))
+{
+ // Process entries as they arrive
+ while (reader.MoveToNextEntry())
+ {
+ if (!reader.Entry.IsDirectory)
+ {
+ reader.WriteEntryToDirectory(@"C:\output");
+ }
+ }
+}
+```
+
+**Performance Tips:**
+- Don't try to buffer the entire stream
+- Process entries immediately
+- Use async APIs for better responsiveness
+
+### Download-Then-Extract vs Streaming
+
+Choose based on your constraints:
+
+| Approach | When to Use |
+|----------|------------|
+| **Download then extract** | Moderate size, need random access |
+| **Stream during download** | Large files, bandwidth limited, memory constrained |
+
+```csharp
+// Download then extract (requires disk space)
+var archivePath = await DownloadFile(url, @"C:\temp\archive.zip");
+using (var archive = ZipArchive.Open(archivePath))
+{
+ archive.WriteToDirectory(@"C:\output");
+}
+
+// Stream during download (on-the-fly extraction)
+using (var httpStream = await httpClient.GetStreamAsync(url))
+using (var reader = ReaderFactory.Open(httpStream))
+{
+ while (reader.MoveToNextEntry())
+ {
+ reader.WriteEntryToDirectory(@"C:\output");
+ }
+}
+```
+
+---
+
+## Solid Archive Optimization
+
+### Why Solid Archives Are Slow
+
+Solid archives (Rar, 7Zip) group files together in a single compressed stream:
+
+```
+Solid Archive Layout:
+[Header] [Compressed Stream] [Footer]
+ ├─ File1 compressed data
+ ├─ File2 compressed data
+ ├─ File3 compressed data
+ └─ File4 compressed data
+```
+
+Extracting File3 requires decompressing File1 and File2 first.
+
+### Sequential vs Random Extraction
+
+**Random Extraction (Slow):**
+```csharp
+using (var archive = RarArchive.Open("solid.rar"))
+{
+ foreach (var entry in archive.Entries)
+ {
+ entry.WriteToFile(@"C:\output\" + entry.Key); // ✗ Slow!
+ // Each entry triggers full decompression from start
+ }
+}
+```
+
+**Sequential Extraction (Fast):**
+```csharp
+using (var archive = RarArchive.Open("solid.rar"))
+{
+ // Method 1: Use WriteToDirectory (recommended)
+ archive.WriteToDirectory(@"C:\output", new ExtractionOptions
+ {
+ ExtractFullPath = true,
+ Overwrite = true
+ });
+
+ // Method 2: Use ExtractAllEntries
+ archive.ExtractAllEntries();
+
+ // Method 3: Use Reader API (also sequential)
+ using (var reader = RarReader.Open(File.OpenRead("solid.rar")))
+ {
+ while (reader.MoveToNextEntry())
+ {
+ reader.WriteEntryToDirectory(@"C:\output");
+ }
+ }
+}
+```
+
+**Performance Impact:**
+- Random extraction: O(n²) - very slow for many files
+- Sequential extraction: O(n) - 10-100x faster
+
+### Best Practices for Solid Archives
+
+1. **Always extract sequentially** when possible
+2. **Use Reader API** for large solid archives
+3. **Process entries in order** from the archive
+4. **Consider using 7Zip command-line** for scripted extractions
+
+---
+
+## Compression Level Trade-offs
+
+### Deflate/GZip Levels
+
+```csharp
+// Level 1 = Fastest, largest size
+// Level 6 = Default (balanced)
+// Level 9 = Slowest, best compression
+
+// Write with different compression levels
+using (var archive = ZipArchive.Create())
+{
+ archive.AddAllFromDirectory(@"D:\data");
+
+ // Fast compression (level 1)
+ archive.SaveTo("fast.zip", new WriterOptions(CompressionType.Deflate)
+ {
+ CompressionLevel = 1
+ });
+
+ // Default compression (level 6)
+ archive.SaveTo("default.zip", CompressionType.Deflate);
+
+ // Best compression (level 9)
+ archive.SaveTo("best.zip", new WriterOptions(CompressionType.Deflate)
+ {
+ CompressionLevel = 9
+ });
+}
+```
+
+**Speed vs Size:**
+| Level | Speed | Size | Use Case |
+|-------|-------|------|----------|
+| 1 | 10x | 90% | Network, streaming |
+| 6 | 1x | 75% | Default (good balance) |
+| 9 | 0.1x | 65% | Archival, static storage |
+
+### BZip2 Block Size
+
+```csharp
+// BZip2 block size affects memory and compression
+// 100K to 900K (default 900K)
+
+// Smaller block size = lower memory, faster
+// Larger block size = better compression, slower
+
+using (var archive = TarArchive.Create())
+{
+ archive.AddAllFromDirectory(@"D:\data");
+
+ // These are preset in WriterOptions via CompressionLevel
+ archive.SaveTo("archive.tar.bz2", CompressionType.BZip2);
+}
+```
+
+### LZMA Settings
+
+LZMA compression is very powerful but memory-intensive:
+
+```csharp
+// LZMA (7Zip, .tar.lzma):
+// - Dictionary size: 16 KB to 1 GB (default 32 MB)
+// - Faster preset: smaller dictionary
+// - Better compression: larger dictionary
+
+// Preset via CompressionType
+using (var archive = TarArchive.Create())
+{
+ archive.AddAllFromDirectory(@"D:\data");
+ archive.SaveTo("archive.tar.xz", CompressionType.LZMA); // Default settings
+}
+```
+
+---
+
+## Async Performance
+
+### When Async Helps
+
+Async is beneficial when:
+- **Long I/O operations** (network, slow disks)
+- **UI responsiveness** needed (Windows Forms, WPF, Blazor)
+- **Server applications** (ASP.NET, multiple concurrent operations)
+
+```csharp
+// Async extraction (non-blocking)
+using (var archive = ZipArchive.Open("archive.zip"))
+{
+ await archive.WriteToDirectoryAsync(
+ @"C:\output",
+ new ExtractionOptions { ExtractFullPath = true, Overwrite = true },
+ cancellationToken
+ );
+}
+// Thread can handle other work while I/O happens
+```
+
+### When Async Doesn't Help
+
+Async doesn't improve performance for:
+- **CPU-bound operations** (already fast)
+- **Local SSD I/O** (I/O is fast enough)
+- **Single-threaded scenarios** (no parallelism benefit)
+
+```csharp
+// Sync extraction (simpler, same performance on fast I/O)
+using (var archive = ZipArchive.Open("archive.zip"))
+{
+ archive.WriteToDirectory(
+ @"C:\output",
+ new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
+ );
+}
+// Simple and fast - no async needed
+```
+
+### Cancellation Pattern
+
+```csharp
+var cts = new CancellationTokenSource();
+
+// Cancel after 5 minutes
+cts.CancelAfter(TimeSpan.FromMinutes(5));
+
+try
+{
+ using (var archive = ZipArchive.Open("archive.zip"))
+ {
+ await archive.WriteToDirectoryAsync(
+ @"C:\output",
+ new ExtractionOptions { ExtractFullPath = true, Overwrite = true },
+ cts.Token
+ );
+ }
+}
+catch (OperationCanceledException)
+{
+ Console.WriteLine("Extraction cancelled");
+ // Clean up partial extraction if needed
+}
+```
+
+---
+
+## Practical Performance Tips
+
+### 1. Choose the Right API
+
+| Scenario | API | Why |
+|----------|-----|-----|
+| Small archives | Archive | Faster random access |
+| Large archives | Reader | Lower memory |
+| Streaming | Reader | Works on non-seekable streams |
+| Download streams | Reader | Async extraction while downloading |
+
+### 2. Batch Operations
+
+```csharp
+// ✗ Slow - opens each archive separately
+foreach (var file in files)
+{
+ using (var archive = ZipArchive.Open("archive.zip"))
+ {
+ archive.WriteToDirectory(@"C:\output");
+ }
+}
+
+// ✓ Better - process multiple entries at once
+using (var archive = ZipArchive.Open("archive.zip"))
+{
+ archive.WriteToDirectory(@"C:\output");
+}
+```
+
+### 3. Profile Your Code
+
+```csharp
+var sw = Stopwatch.StartNew();
+using (var archive = ZipArchive.Open("large.zip"))
+{
+ archive.WriteToDirectory(@"C:\output");
+}
+sw.Stop();
+
+Console.WriteLine($"Extraction took {sw.ElapsedMilliseconds}ms");
+
+// Measure memory before/after
+var beforeMem = GC.GetTotalMemory(true);
+// ... do work ...
+var afterMem = GC.GetTotalMemory(true);
+Console.WriteLine($"Memory used: {(afterMem - beforeMem) / 1024 / 1024}MB");
+```
+
+---
+
+## Troubleshooting Performance
+
+### Extraction is Slow
+
+1. **Check if solid archive** → Use sequential extraction
+2. **Check API** → Reader API might be faster for large files
+3. **Check compression level** → Higher levels are slower to decompress
+4. **Check I/O** → Network drives are much slower than SSD
+5. **Check buffer size** → May need larger buffers for network
+
+### High Memory Usage
+
+1. **Use Reader API** instead of Archive API
+2. **Process entries immediately** rather than buffering
+3. **Reduce compression level** if writing
+4. **Check for memory leaks** in your code
+
+### CPU Usage at 100%
+
+1. **Normal for compression** - especially with high compression levels
+2. **Consider lower level** for faster processing
+3. **Reduce parallelism** if processing multiple archives
+4. **Check if awaiting properly** in async code
+
+---
+
+## Related Documentation
+
+- [PERFORMANCE.md](USAGE.md) - Usage examples with performance considerations
+- [FORMATS.md](FORMATS.md) - Format-specific performance notes
diff --git a/USAGE.md b/docs/USAGE.md
similarity index 90%
rename from USAGE.md
rename to docs/USAGE.md
index 1e3a6c78f..8e636cab8 100644
--- a/USAGE.md
+++ b/docs/USAGE.md
@@ -1,6 +1,6 @@
# SharpCompress Usage
-## Async/Await Support
+## Async/Await Support (Beta)
SharpCompress now provides full async/await support for all I/O operations. All `Read`, `Write`, and extraction operations have async equivalents ending in `Async` that accept an optional `CancellationToken`. This enables better performance and scalability for I/O-bound operations.
@@ -13,7 +13,7 @@ SharpCompress now provides full async/await support for all I/O operations. All
See [Async Examples](#async-examples) section below for usage patterns.
-## Stream Rules (changed with 0.21)
+## Stream Rules
When dealing with Streams, the rule should be that you don't close a stream you didn't create. This, in effect, should mean you should always put a Stream in a using block to dispose it.
@@ -113,38 +113,26 @@ using (var archive = RarArchive.Open("Test.rar"))
}
```
-### Extract solid Rar or 7Zip archives with manual progress reporting
+### Extract solid Rar or 7Zip archives with progress reporting
`ExtractAllEntries` only works for solid archives (Rar) or 7Zip archives. For optimal performance with these archive types, use this method:
```C#
-using (var archive = RarArchive.Open("archive.rar")) // Must be solid Rar or 7Zip
+using SharpCompress.Common;
+using SharpCompress.Readers;
+
+var progress = new Progress(report =>
{
- if (archive.IsSolid || archive.Type == ArchiveType.SevenZip)
- {
- // Calculate total size for progress reporting
- double totalSize = archive.Entries.Where(e => !e.IsDirectory).Sum(e => e.Size);
- long completed = 0;
+ Console.WriteLine($"Extracting {report.EntryPath}: {report.PercentComplete}%");
+});
- using (var reader = archive.ExtractAllEntries())
- {
- while (reader.MoveToNextEntry())
- {
- if (!reader.Entry.IsDirectory)
- {
- reader.WriteEntryToDirectory(@"D:\output", new ExtractionOptions()
- {
- ExtractFullPath = true,
- Overwrite = true
- });
-
- completed += reader.Entry.Size;
- double progress = completed / totalSize;
- Console.WriteLine($"Progress: {progress:P}");
- }
- }
- }
- }
+using (var archive = RarArchive.Open("archive.rar", new ReaderOptions { Progress = progress })) // Must be solid Rar or 7Zip
+{
+ archive.WriteToDirectory(@"D:\output", new ExtractionOptions()
+ {
+ ExtractFullPath = true,
+ Overwrite = true
+ });
}
```
diff --git a/src/SharpCompress/Archives/AbstractArchive.cs b/src/SharpCompress/Archives/AbstractArchive.cs
index 672382ffc..12ab15a6b 100644
--- a/src/SharpCompress/Archives/AbstractArchive.cs
+++ b/src/SharpCompress/Archives/AbstractArchive.cs
@@ -1,14 +1,13 @@
-using System;
using System.Collections.Generic;
-using System.IO;
using System.Linq;
+using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.IO;
using SharpCompress.Readers;
namespace SharpCompress.Archives;
-public abstract class AbstractArchive : IArchive
+public abstract class AbstractArchive : IArchive, IAsyncArchive
where TEntry : IArchiveEntry
where TVolume : IVolume
{
@@ -26,6 +25,12 @@ internal AbstractArchive(ArchiveType type, SourceStream sourceStream)
_sourceStream = sourceStream;
_lazyVolumes = new LazyReadOnlyCollection(LoadVolumes(_sourceStream));
_lazyEntries = new LazyReadOnlyCollection(LoadEntries(Volumes));
+ _lazyVolumesAsync = new LazyAsyncReadOnlyCollection(
+ LoadVolumesAsync(_sourceStream)
+ );
+ _lazyEntriesAsync = new LazyAsyncReadOnlyCollection(
+ LoadEntriesAsync(_lazyVolumesAsync)
+ );
}
internal AbstractArchive(ArchiveType type)
@@ -34,19 +39,16 @@ internal AbstractArchive(ArchiveType type)
ReaderOptions = new();
_lazyVolumes = new LazyReadOnlyCollection(Enumerable.Empty());
_lazyEntries = new LazyReadOnlyCollection(Enumerable.Empty());
+ _lazyVolumesAsync = new LazyAsyncReadOnlyCollection(
+ AsyncEnumerableEx.Empty()
+ );
+ _lazyEntriesAsync = new LazyAsyncReadOnlyCollection(
+ AsyncEnumerableEx.Empty()
+ );
}
public ArchiveType Type { get; }
- private static Stream CheckStreams(Stream stream)
- {
- if (!stream.CanSeek || !stream.CanRead)
- {
- throw new ArchiveException("Archive streams must be Readable and Seekable");
- }
- return stream;
- }
-
///
/// Returns an ReadOnlyCollection of all the RarArchiveEntries across the one or many parts of the RarArchive.
///
@@ -72,6 +74,19 @@ private static Stream CheckStreams(Stream stream)
protected abstract IEnumerable LoadVolumes(SourceStream sourceStream);
protected abstract IEnumerable LoadEntries(IEnumerable volumes);
+ protected virtual IAsyncEnumerable LoadVolumesAsync(SourceStream sourceStream) =>
+ LoadVolumes(sourceStream).ToAsyncEnumerable();
+
+ protected virtual async IAsyncEnumerable LoadEntriesAsync(
+ IAsyncEnumerable volumes
+ )
+ {
+ foreach (var item in LoadEntries(await volumes.ToListAsync()))
+ {
+ yield return item;
+ }
+ }
+
IEnumerable IArchive.Entries => Entries.Cast();
IEnumerable IArchive.Volumes => _lazyVolumes.Cast();
@@ -118,6 +133,7 @@ public IReader ExtractAllEntries()
}
protected abstract IReader CreateReaderForSolidExtraction();
+ protected abstract ValueTask CreateReaderForSolidExtractionAsync();
///
/// Archive is SOLID (this means the Archive saved bytes by reusing information which helps for archives containing many small files).
@@ -140,4 +156,67 @@ public bool IsComplete
return Entries.All(x => x.IsComplete);
}
}
+
+ #region Async Support
+
+ private readonly LazyAsyncReadOnlyCollection _lazyVolumesAsync;
+ private readonly LazyAsyncReadOnlyCollection _lazyEntriesAsync;
+
+ public virtual async ValueTask DisposeAsync()
+ {
+ if (!_disposed)
+ {
+ await foreach (var v in _lazyVolumesAsync)
+ {
+ v.Dispose();
+ }
+ foreach (var v in _lazyEntriesAsync.GetLoaded().Cast())
+ {
+ v.Close();
+ }
+ _sourceStream?.Dispose();
+
+ _disposed = true;
+ }
+ }
+
+ private async ValueTask EnsureEntriesLoadedAsync()
+ {
+ await _lazyEntriesAsync.EnsureFullyLoaded();
+ await _lazyVolumesAsync.EnsureFullyLoaded();
+ }
+
+ public virtual IAsyncEnumerable EntriesAsync => _lazyEntriesAsync;
+ IAsyncEnumerable IAsyncArchive.EntriesAsync =>
+ EntriesAsync.Cast();
+
+ public IAsyncEnumerable VolumesAsync => _lazyVolumesAsync.Cast();
+
+ public async ValueTask ExtractAllEntriesAsync()
+ {
+ if (!IsSolid && 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();
+ }
+
+ public virtual ValueTask IsSolidAsync() => new(false);
+
+ public async ValueTask IsCompleteAsync()
+ {
+ await EnsureEntriesLoadedAsync();
+ return await EntriesAsync.All(x => x.IsComplete);
+ }
+
+ public async ValueTask TotalSizeAsync() =>
+ await EntriesAsync.Aggregate(0L, (total, cf) => total + cf.CompressedSize);
+
+ public async ValueTask TotalUncompressSizeAsync() =>
+ await EntriesAsync.Aggregate(0L, (total, cf) => total + cf.Size);
+
+ #endregion
}
diff --git a/src/SharpCompress/Archives/AbstractWritableArchive.cs b/src/SharpCompress/Archives/AbstractWritableArchive.cs
index 744d4ee2a..13fb66f9a 100644
--- a/src/SharpCompress/Archives/AbstractWritableArchive.cs
+++ b/src/SharpCompress/Archives/AbstractWritableArchive.cs
@@ -162,7 +162,7 @@ public void SaveTo(Stream stream, WriterOptions options)
SaveTo(stream, options, OldEntries, newEntries);
}
- public async Task SaveToAsync(
+ public async ValueTask SaveToAsync(
Stream stream,
WriterOptions options,
CancellationToken cancellationToken = default
@@ -208,7 +208,7 @@ protected abstract void SaveTo(
IEnumerable newEntries
);
- protected abstract Task SaveToAsync(
+ protected abstract ValueTask SaveToAsync(
Stream stream,
WriterOptions options,
IEnumerable oldEntries,
diff --git a/src/SharpCompress/Archives/ArchiveFactory.cs b/src/SharpCompress/Archives/ArchiveFactory.cs
index 94368ece5..e052c3e80 100644
--- a/src/SharpCompress/Archives/ArchiveFactory.cs
+++ b/src/SharpCompress/Archives/ArchiveFactory.cs
@@ -2,6 +2,8 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Factories;
using SharpCompress.IO;
@@ -24,6 +26,28 @@ public static IArchive Open(Stream stream, ReaderOptions? readerOptions = null)
return FindFactory(stream).Open(stream, readerOptions);
}
+ ///
+ /// Opens an Archive for random access asynchronously
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static async ValueTask OpenAsync(
+ Stream stream,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ readerOptions ??= new ReaderOptions();
+ stream = SharpCompressStream.Create(stream, bufferSize: readerOptions.BufferSize);
+ var factory = await FindFactoryAsync(stream, cancellationToken)
+ .ConfigureAwait(false);
+ return await factory
+ .OpenAsync(stream, readerOptions, cancellationToken)
+ .ConfigureAwait(false);
+ }
+
public static IWritableArchive Create(ArchiveType type)
{
var factory = Factory
@@ -49,6 +73,22 @@ public static IArchive Open(string filePath, ReaderOptions? options = null)
return Open(new FileInfo(filePath), options);
}
+ ///
+ /// Opens an Archive from a filepath asynchronously.
+ ///
+ ///
+ ///
+ ///
+ public static ValueTask OpenAsync(
+ string filePath,
+ ReaderOptions? options = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ filePath.NotNullOrEmpty(nameof(filePath));
+ return OpenAsync(new FileInfo(filePath), options, cancellationToken);
+ }
+
///
/// Constructor with a FileInfo object to an existing file.
///
@@ -61,6 +101,25 @@ public static IArchive Open(FileInfo fileInfo, ReaderOptions? options = null)
return FindFactory(fileInfo).Open(fileInfo, options);
}
+ ///
+ /// Opens an Archive from a FileInfo object asynchronously.
+ ///
+ ///
+ ///
+ ///
+ public static async ValueTask OpenAsync(
+ FileInfo fileInfo,
+ ReaderOptions? options = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ options ??= new ReaderOptions { LeaveStreamOpen = false };
+
+ var factory = await FindFactoryAsync(fileInfo, cancellationToken)
+ .ConfigureAwait(false);
+ return await factory.OpenAsync(fileInfo, options, cancellationToken).ConfigureAwait(false);
+ }
+
///
/// Constructor with IEnumerable FileInfo objects, multi and split support.
///
@@ -87,6 +146,40 @@ public static IArchive Open(IEnumerable fileInfos, ReaderOptions? opti
return FindFactory(fileInfo).Open(filesArray, options);
}
+ ///
+ /// Opens a multi-part archive from files asynchronously.
+ ///
+ ///
+ ///
+ ///
+ public static async ValueTask OpenAsync(
+ IEnumerable fileInfos,
+ ReaderOptions? options = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ fileInfos.NotNull(nameof(fileInfos));
+ var filesArray = fileInfos.ToArray();
+ if (filesArray.Length == 0)
+ {
+ throw new InvalidOperationException("No files to open");
+ }
+
+ var fileInfo = filesArray[0];
+ if (filesArray.Length == 1)
+ {
+ return await OpenAsync(fileInfo, options, cancellationToken).ConfigureAwait(false);
+ }
+
+ fileInfo.NotNull(nameof(fileInfo));
+ options ??= new ReaderOptions { LeaveStreamOpen = false };
+
+ var factory = FindFactory(fileInfo);
+ return await factory
+ .OpenAsync(filesArray, options, cancellationToken)
+ .ConfigureAwait(false);
+ }
+
///
/// Constructor with IEnumerable FileInfo objects, multi and split support.
///
@@ -113,6 +206,41 @@ public static IArchive Open(IEnumerable streams, ReaderOptions? options
return FindFactory(firstStream).Open(streamsArray, options);
}
+ ///
+ /// Opens a multi-part archive from streams asynchronously.
+ ///
+ ///
+ ///
+ ///
+ public static async ValueTask OpenAsync(
+ IEnumerable streams,
+ ReaderOptions? options = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ streams.NotNull(nameof(streams));
+ var streamsArray = streams.ToArray();
+ if (streamsArray.Length == 0)
+ {
+ throw new InvalidOperationException("No streams");
+ }
+
+ var firstStream = streamsArray[0];
+ if (streamsArray.Length == 1)
+ {
+ return await OpenAsync(firstStream, options, cancellationToken).ConfigureAwait(false);
+ }
+
+ firstStream.NotNull(nameof(firstStream));
+ options ??= new ReaderOptions();
+
+ var factory = FindFactory(firstStream);
+ return await factory
+ .OpenAsync(streamsArray, options, cancellationToken)
+ .ConfigureAwait(false);
+ }
+
///
/// Extract to specific directory, retaining filename
///
@@ -166,6 +294,52 @@ private static T FindFactory(Stream stream)
);
}
+ private static async ValueTask FindFactoryAsync(
+ FileInfo finfo,
+ CancellationToken cancellationToken
+ )
+ where T : IFactory
+ {
+ finfo.NotNull(nameof(finfo));
+ using Stream stream = finfo.OpenRead();
+ return await FindFactoryAsync(stream, cancellationToken);
+ }
+
+ private static async ValueTask FindFactoryAsync(
+ Stream stream,
+ CancellationToken cancellationToken
+ )
+ where T : IFactory
+ {
+ stream.NotNull(nameof(stream));
+ if (!stream.CanRead || !stream.CanSeek)
+ {
+ throw new ArgumentException("Stream should be readable and seekable");
+ }
+
+ var factories = Factory.Factories.OfType();
+
+ var startPosition = stream.Position;
+
+ foreach (var factory in factories)
+ {
+ stream.Seek(startPosition, SeekOrigin.Begin);
+
+ if (await factory.IsArchiveAsync(stream, cancellationToken: cancellationToken))
+ {
+ stream.Seek(startPosition, SeekOrigin.Begin);
+
+ return factory;
+ }
+ }
+
+ var extensions = string.Join(", ", factories.Select(item => item.Name));
+
+ throw new InvalidOperationException(
+ $"Cannot determine compressed stream type. Supported Archive Formats: {extensions}"
+ );
+ }
+
public static bool IsArchive(
string filePath,
out ArchiveType? type,
diff --git a/src/SharpCompress/Archives/AutoArchiveFactory.cs b/src/SharpCompress/Archives/AutoArchiveFactory.cs
index 78313df54..7751c7e01 100644
--- a/src/SharpCompress/Archives/AutoArchiveFactory.cs
+++ b/src/SharpCompress/Archives/AutoArchiveFactory.cs
@@ -1,12 +1,14 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Readers;
namespace SharpCompress.Archives;
-class AutoArchiveFactory : IArchiveFactory
+internal class AutoArchiveFactory : IArchiveFactory
{
public string Name => nameof(AutoArchiveFactory);
@@ -20,11 +22,30 @@ public bool IsArchive(
int bufferSize = ReaderOptions.DefaultBufferSize
) => throw new NotSupportedException();
+ public ValueTask IsArchiveAsync(
+ Stream stream,
+ string? password = null,
+ int bufferSize = ReaderOptions.DefaultBufferSize,
+ CancellationToken cancellationToken = default
+ ) => throw new NotSupportedException();
+
public FileInfo? GetFilePart(int index, FileInfo part1) => throw new NotSupportedException();
public IArchive Open(Stream stream, ReaderOptions? readerOptions = null) =>
ArchiveFactory.Open(stream, readerOptions);
+ public async ValueTask OpenAsync(
+ Stream stream,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ ) => await ArchiveFactory.OpenAsync(stream, readerOptions, cancellationToken);
+
public IArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null) =>
ArchiveFactory.Open(fileInfo, readerOptions);
+
+ public async ValueTask OpenAsync(
+ FileInfo fileInfo,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ ) => await ArchiveFactory.OpenAsync(fileInfo, readerOptions, cancellationToken);
}
diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.cs b/src/SharpCompress/Archives/GZip/GZipArchive.cs
index 34e4c6484..7c8c08f7c 100644
--- a/src/SharpCompress/Archives/GZip/GZipArchive.cs
+++ b/src/SharpCompress/Archives/GZip/GZipArchive.cs
@@ -102,6 +102,70 @@ public static GZipArchive Open(Stream stream, ReaderOptions? readerOptions = nul
);
}
+ ///
+ /// Opens a GZipArchive asynchronously from a stream.
+ ///
+ ///
+ ///
+ ///
+ public static ValueTask OpenAsync(
+ Stream stream,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return new(Open(stream, readerOptions));
+ }
+
+ ///
+ /// Opens a GZipArchive asynchronously from a FileInfo.
+ ///
+ ///
+ ///
+ ///
+ public static ValueTask OpenAsync(
+ FileInfo fileInfo,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return new(Open(fileInfo, readerOptions));
+ }
+
+ ///
+ /// Opens a GZipArchive asynchronously from multiple streams.
+ ///
+ ///
+ ///
+ ///
+ public static ValueTask OpenAsync(
+ IReadOnlyList streams,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return new(Open(streams, readerOptions));
+ }
+
+ ///
+ /// Opens a GZipArchive asynchronously from multiple FileInfo objects.
+ ///
+ ///
+ ///
+ ///
+ public static ValueTask OpenAsync(
+ IReadOnlyList fileInfos,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return new(Open(fileInfos, readerOptions));
+ }
+
public static GZipArchive Create() => new();
///
@@ -138,10 +202,13 @@ public void SaveTo(FileInfo fileInfo)
SaveTo(stream, new WriterOptions(CompressionType.GZip));
}
- public Task SaveToAsync(string filePath, CancellationToken cancellationToken = default) =>
+ public ValueTask SaveToAsync(string filePath, CancellationToken cancellationToken = default) =>
SaveToAsync(new FileInfo(filePath), cancellationToken);
- public async Task SaveToAsync(FileInfo fileInfo, CancellationToken cancellationToken = default)
+ public async ValueTask SaveToAsync(
+ FileInfo fileInfo,
+ CancellationToken cancellationToken = default
+ )
{
using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write);
await SaveToAsync(stream, new WriterOptions(CompressionType.GZip), cancellationToken)
@@ -167,6 +234,28 @@ public static bool IsGZipFile(Stream stream)
return true;
}
+ public static async ValueTask IsGZipFileAsync(
+ Stream stream,
+ CancellationToken cancellationToken = default
+ )
+ {
+ // read the header on the first read
+ byte[] header = new byte[10];
+
+ // workitem 8501: handle edge case (decompress empty stream)
+ if (!await stream.ReadFullyAsync(header, cancellationToken).ConfigureAwait(false))
+ {
+ return false;
+ }
+
+ if (header[0] != 0x1F || header[1] != 0x8B || header[2] != 8)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
internal GZipArchive()
: base(ArchiveType.GZip) { }
@@ -213,7 +302,7 @@ IEnumerable newEntries
}
}
- protected override async Task SaveToAsync(
+ protected override async ValueTask SaveToAsync(
Stream stream,
WriterOptions options,
IEnumerable oldEntries,
@@ -250,4 +339,11 @@ protected override IReader CreateReaderForSolidExtraction()
stream.Position = 0;
return GZipReader.Open(stream);
}
+
+ protected override ValueTask CreateReaderForSolidExtractionAsync()
+ {
+ var stream = Volumes.Single().Stream;
+ stream.Position = 0;
+ return new(GZipReader.Open(stream));
+ }
}
diff --git a/src/SharpCompress/Archives/GZip/GZipArchiveEntry.cs b/src/SharpCompress/Archives/GZip/GZipArchiveEntry.cs
index 62e4760b3..049c7262a 100644
--- a/src/SharpCompress/Archives/GZip/GZipArchiveEntry.cs
+++ b/src/SharpCompress/Archives/GZip/GZipArchiveEntry.cs
@@ -23,10 +23,12 @@ public virtual Stream OpenEntryStream()
return Parts.Single().GetCompressedStream().NotNull();
}
- public virtual Task OpenEntryStreamAsync(CancellationToken cancellationToken = default)
+ public async ValueTask OpenEntryStreamAsync(
+ CancellationToken cancellationToken = default
+ )
{
// GZip synchronous implementation is fast enough, just wrap it
- return Task.FromResult(OpenEntryStream());
+ return OpenEntryStream();
}
#region IArchiveEntry Members
diff --git a/src/SharpCompress/Archives/IArchiveEntry.cs b/src/SharpCompress/Archives/IArchiveEntry.cs
index 69b3a674e..a38e65a0c 100644
--- a/src/SharpCompress/Archives/IArchiveEntry.cs
+++ b/src/SharpCompress/Archives/IArchiveEntry.cs
@@ -17,7 +17,7 @@ public interface IArchiveEntry : IEntry
/// Opens the current entry as a stream that will decompress as it is read asynchronously.
/// Read the entire stream or use SkipEntry on EntryStream.
///
- Task OpenEntryStreamAsync(CancellationToken cancellationToken = default);
+ ValueTask OpenEntryStreamAsync(CancellationToken cancellationToken = default);
///
/// The archive can find all the parts of the archive needed to extract this entry.
diff --git a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs
index af2c9be45..3bf940351 100644
--- a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs
+++ b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs
@@ -37,7 +37,7 @@ public void WriteTo(Stream streamToWriteTo, IProgress? progress
/// The stream to write the entry content to.
/// Cancellation token.
/// Optional progress reporter for tracking extraction progress.
- public async Task WriteToAsync(
+ public async ValueTask WriteToAsync(
Stream streamToWriteTo,
IProgress? progress = null,
CancellationToken cancellationToken = default
@@ -110,18 +110,20 @@ public void WriteToDirectory(
///
/// Extract to specific directory asynchronously, retaining filename
///
- public Task WriteToDirectoryAsync(
+ public async ValueTask WriteToDirectoryAsync(
string destinationDirectory,
ExtractionOptions? options = null,
CancellationToken cancellationToken = default
) =>
- ExtractionMethods.WriteEntryToDirectoryAsync(
- entry,
- destinationDirectory,
- options,
- entry.WriteToFileAsync,
- cancellationToken
- );
+ await ExtractionMethods
+ .WriteEntryToDirectoryAsync(
+ entry,
+ destinationDirectory,
+ options,
+ entry.WriteToFileAsync,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
///
/// Extract to specific file
@@ -141,21 +143,23 @@ public void WriteToFile(string destinationFileName, ExtractionOptions? options =
///
/// Extract to specific file asynchronously
///
- public Task WriteToFileAsync(
+ public async ValueTask WriteToFileAsync(
string destinationFileName,
ExtractionOptions? options = null,
CancellationToken cancellationToken = default
) =>
- ExtractionMethods.WriteEntryToFileAsync(
- entry,
- destinationFileName,
- options,
- async (x, fm, ct) =>
- {
- using var fs = File.Open(destinationFileName, fm);
- await entry.WriteToAsync(fs, null, ct).ConfigureAwait(false);
- },
- cancellationToken
- );
+ await ExtractionMethods
+ .WriteEntryToFileAsync(
+ entry,
+ destinationFileName,
+ options,
+ async (x, fm, ct) =>
+ {
+ using var fs = File.Open(destinationFileName, fm);
+ await entry.WriteToAsync(fs, null, ct).ConfigureAwait(false);
+ },
+ cancellationToken
+ )
+ .ConfigureAwait(false);
}
}
diff --git a/src/SharpCompress/Archives/IArchiveExtensions.cs b/src/SharpCompress/Archives/IArchiveExtensions.cs
index 0d39c6e2c..c1d2ac987 100644
--- a/src/SharpCompress/Archives/IArchiveExtensions.cs
+++ b/src/SharpCompress/Archives/IArchiveExtensions.cs
@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Readers;
@@ -80,89 +78,5 @@ private void WriteToDirectoryInternal(
);
}
}
-
- ///
- /// Extract to specific directory asynchronously with progress reporting and cancellation support
- ///
- /// The folder to extract into.
- /// Extraction options.
- /// Optional progress reporter for tracking extraction progress.
- /// Optional cancellation token.
- public async Task WriteToDirectoryAsync(
- string destinationDirectory,
- ExtractionOptions? options = null,
- IProgress? progress = null,
- CancellationToken cancellationToken = default
- )
- {
- // For solid archives (Rar, 7Zip), use the optimized reader-based approach
- if (archive.IsSolid || archive.Type == ArchiveType.SevenZip)
- {
- using var reader = archive.ExtractAllEntries();
- await reader.WriteAllToDirectoryAsync(
- destinationDirectory,
- options,
- cancellationToken
- );
- }
- else
- {
- // For non-solid archives, extract entries directly
- await archive.WriteToDirectoryAsyncInternal(
- destinationDirectory,
- options,
- progress,
- cancellationToken
- );
- }
- }
-
- private async Task WriteToDirectoryAsyncInternal(
- string destinationDirectory,
- ExtractionOptions? options,
- IProgress? progress,
- CancellationToken cancellationToken
- )
- {
- // Prepare for progress reporting
- var totalBytes = archive.TotalUncompressSize;
- var bytesRead = 0L;
-
- // Tracking for created directories.
- var seenDirectories = new HashSet();
-
- // Extract
- foreach (var entry in archive.Entries)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- if (entry.IsDirectory)
- {
- var dirPath = Path.Combine(
- destinationDirectory,
- entry.Key.NotNull("Entry Key is null")
- );
- if (
- Path.GetDirectoryName(dirPath + "/") is { } parentDirectory
- && seenDirectories.Add(dirPath)
- )
- {
- Directory.CreateDirectory(parentDirectory);
- }
- continue;
- }
-
- // Use the entry's WriteToDirectoryAsync method which respects ExtractionOptions
- await entry
- .WriteToDirectoryAsync(destinationDirectory, options, cancellationToken)
- .ConfigureAwait(false);
-
- // Update progress
- bytesRead += entry.Size;
- progress?.Report(
- new ProgressReport(entry.Key ?? string.Empty, bytesRead, totalBytes)
- );
- }
- }
}
}
diff --git a/src/SharpCompress/Archives/IArchiveFactory.cs b/src/SharpCompress/Archives/IArchiveFactory.cs
index 370e5c9fe..1c1253f6a 100644
--- a/src/SharpCompress/Archives/IArchiveFactory.cs
+++ b/src/SharpCompress/Archives/IArchiveFactory.cs
@@ -1,4 +1,6 @@
using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
using SharpCompress.Factories;
using SharpCompress.Readers;
@@ -26,10 +28,34 @@ public interface IArchiveFactory : IFactory
/// reading options.
IArchive Open(Stream stream, ReaderOptions? readerOptions = null);
+ ///
+ /// Opens an Archive for random access asynchronously.
+ ///
+ /// An open, readable and seekable stream.
+ /// reading options.
+ /// Cancellation token.
+ ValueTask OpenAsync(
+ Stream stream,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ );
+
///
/// Constructor with a FileInfo object to an existing file.
///
/// the file to open.
/// reading options.
IArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null);
+
+ ///
+ /// Opens an Archive from a FileInfo object asynchronously.
+ ///
+ /// the file to open.
+ /// reading options.
+ /// Cancellation token.
+ ValueTask OpenAsync(
+ FileInfo fileInfo,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ );
}
diff --git a/src/SharpCompress/Archives/IAsyncArchive.cs b/src/SharpCompress/Archives/IAsyncArchive.cs
new file mode 100644
index 000000000..bd3f290ea
--- /dev/null
+++ b/src/SharpCompress/Archives/IAsyncArchive.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using SharpCompress.Common;
+using SharpCompress.Readers;
+
+namespace SharpCompress.Archives;
+
+public interface IAsyncArchive : IAsyncDisposable
+{
+ IAsyncEnumerable EntriesAsync { get; }
+ IAsyncEnumerable VolumesAsync { get; }
+
+ ArchiveType Type { 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
+ /// extracted sequentially for the best performance.
+ ///
+ ValueTask ExtractAllEntriesAsync();
+
+ ///
+ /// Archive is SOLID (this means the Archive saved bytes by reusing information which helps for archives containing many small files).
+ /// Rar Archives can be SOLID while all 7Zip archives are considered SOLID.
+ ///
+ ValueTask IsSolidAsync();
+
+ ///
+ /// This checks to see if all the known entries have IsComplete = true
+ ///
+ ValueTask IsCompleteAsync();
+
+ ///
+ /// The total size of the files compressed in the archive.
+ ///
+ ValueTask TotalSizeAsync();
+
+ ///
+ /// The total size of the files as uncompressed in the archive.
+ ///
+ ValueTask TotalUncompressSizeAsync();
+}
diff --git a/src/SharpCompress/Archives/IAsyncArchiveExtensions.cs b/src/SharpCompress/Archives/IAsyncArchiveExtensions.cs
new file mode 100644
index 000000000..b6b0cad1e
--- /dev/null
+++ b/src/SharpCompress/Archives/IAsyncArchiveExtensions.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using SharpCompress.Common;
+using SharpCompress.Readers;
+
+namespace SharpCompress.Archives;
+
+public static class IAsyncArchiveExtensions
+{
+ ///
+ /// Extract to specific directory asynchronously with progress reporting and cancellation support
+ ///
+ /// The archive to extract.
+ /// The folder to extract into.
+ /// Extraction options.
+ /// Optional progress reporter for tracking extraction progress.
+ /// Optional cancellation token.
+ public static async Task WriteToDirectoryAsync(
+ this IAsyncArchive archive,
+ string destinationDirectory,
+ ExtractionOptions? options = null,
+ IProgress? progress = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ // For solid archives (Rar, 7Zip), use the optimized reader-based approach
+ if (await archive.IsSolidAsync() || archive.Type == ArchiveType.SevenZip)
+ {
+ await using var reader = await archive.ExtractAllEntriesAsync();
+ await reader.WriteAllToDirectoryAsync(destinationDirectory, options, cancellationToken);
+ }
+ else
+ {
+ // For non-solid archives, extract entries directly
+ await archive.WriteToDirectoryAsyncInternal(
+ destinationDirectory,
+ options,
+ progress,
+ cancellationToken
+ );
+ }
+ }
+
+ private static async Task WriteToDirectoryAsyncInternal(
+ this IAsyncArchive archive,
+ string destinationDirectory,
+ ExtractionOptions? options,
+ IProgress? progress,
+ CancellationToken cancellationToken
+ )
+ {
+ // Prepare for progress reporting
+ var totalBytes = await archive.TotalUncompressSizeAsync();
+ var bytesRead = 0L;
+
+ // Tracking for created directories.
+ var seenDirectories = new HashSet();
+
+ // Extract
+ await foreach (var entry in archive.EntriesAsync.WithCancellation(cancellationToken))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (entry.IsDirectory)
+ {
+ var dirPath = Path.Combine(
+ destinationDirectory,
+ entry.Key.NotNull("Entry Key is null")
+ );
+ if (
+ Path.GetDirectoryName(dirPath + "/") is { } parentDirectory
+ && seenDirectories.Add(dirPath)
+ )
+ {
+ Directory.CreateDirectory(parentDirectory);
+ }
+ continue;
+ }
+
+ // Use the entry's WriteToDirectoryAsync method which respects ExtractionOptions
+ await entry
+ .WriteToDirectoryAsync(destinationDirectory, options, cancellationToken)
+ .ConfigureAwait(false);
+
+ // Update progress
+ bytesRead += entry.Size;
+ progress?.Report(new ProgressReport(entry.Key ?? string.Empty, bytesRead, totalBytes));
+ }
+ }
+}
diff --git a/src/SharpCompress/Archives/IMultiArchiveFactory.cs b/src/SharpCompress/Archives/IMultiArchiveFactory.cs
index c26b649f1..4fa94d7fe 100644
--- a/src/SharpCompress/Archives/IMultiArchiveFactory.cs
+++ b/src/SharpCompress/Archives/IMultiArchiveFactory.cs
@@ -1,5 +1,7 @@
using System.Collections.Generic;
using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
using SharpCompress.Factories;
using SharpCompress.Readers;
@@ -27,10 +29,34 @@ public interface IMultiArchiveFactory : IFactory
/// reading options.
IArchive Open(IReadOnlyList streams, ReaderOptions? readerOptions = null);
+ ///
+ /// Opens a multi-part archive from streams asynchronously.
+ ///
+ ///
+ /// reading options.
+ /// Cancellation token.
+ ValueTask OpenAsync(
+ IReadOnlyList streams,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ );
+
///
/// Constructor with IEnumerable Stream objects, multi and split support.
///
///
/// reading options.
IArchive Open(IReadOnlyList fileInfos, ReaderOptions? readerOptions = null);
+
+ ///
+ /// Opens a multi-part archive from files asynchronously.
+ ///
+ ///
+ /// reading options.
+ /// Cancellation token.
+ ValueTask OpenAsync(
+ IReadOnlyList fileInfos,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ );
}
diff --git a/src/SharpCompress/Archives/IWritableArchive.cs b/src/SharpCompress/Archives/IWritableArchive.cs
index dde22a032..74d8da763 100644
--- a/src/SharpCompress/Archives/IWritableArchive.cs
+++ b/src/SharpCompress/Archives/IWritableArchive.cs
@@ -22,7 +22,7 @@ IArchiveEntry AddEntry(
void SaveTo(Stream stream, WriterOptions options);
- Task SaveToAsync(
+ ValueTask SaveToAsync(
Stream stream,
WriterOptions options,
CancellationToken cancellationToken = default
diff --git a/src/SharpCompress/Archives/IWritableArchiveExtensions.cs b/src/SharpCompress/Archives/IWritableArchiveExtensions.cs
index 4defe6049..60ec83d85 100644
--- a/src/SharpCompress/Archives/IWritableArchiveExtensions.cs
+++ b/src/SharpCompress/Archives/IWritableArchiveExtensions.cs
@@ -44,14 +44,14 @@ WriterOptions options
writableArchive.SaveTo(stream, options);
}
- public static Task SaveToAsync(
+ public static ValueTask SaveToAsync(
this IWritableArchive writableArchive,
string filePath,
WriterOptions options,
CancellationToken cancellationToken = default
) => writableArchive.SaveToAsync(new FileInfo(filePath), options, cancellationToken);
- public static async Task SaveToAsync(
+ public static async ValueTask SaveToAsync(
this IWritableArchive writableArchive,
FileInfo fileInfo,
WriterOptions options,
diff --git a/src/SharpCompress/Archives/Rar/RarArchive.cs b/src/SharpCompress/Archives/Rar/RarArchive.cs
index 9acfdccc5..03b8d4d90 100644
--- a/src/SharpCompress/Archives/Rar/RarArchive.cs
+++ b/src/SharpCompress/Archives/Rar/RarArchive.cs
@@ -2,6 +2,8 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Rar;
using SharpCompress.Common.Rar.Headers;
@@ -65,7 +67,13 @@ protected override IEnumerable LoadVolumes(SourceStream sourceStream)
return new StreamRarArchiveVolume(sourceStream, ReaderOptions, i++).AsEnumerable();
}
- protected override IReader CreateReaderForSolidExtraction()
+ protected override IReader CreateReaderForSolidExtraction() =>
+ CreateReaderForSolidExtractionInternal();
+
+ protected override ValueTask CreateReaderForSolidExtractionAsync() =>
+ new(CreateReaderForSolidExtractionInternal());
+
+ private RarReader CreateReaderForSolidExtractionInternal()
{
if (this.IsMultipartVolume())
{
@@ -181,6 +189,70 @@ public static RarArchive Open(IEnumerable streams, ReaderOptions? reader
);
}
+ ///
+ /// Opens a RarArchive asynchronously from a stream.
+ ///
+ ///
+ ///
+ ///
+ public static ValueTask OpenAsync(
+ Stream stream,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return new(Open(stream, readerOptions));
+ }
+
+ ///
+ /// Opens a RarArchive asynchronously from a FileInfo.
+ ///
+ ///
+ ///
+ ///
+ public static ValueTask OpenAsync(
+ FileInfo fileInfo,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return new(Open(fileInfo, readerOptions));
+ }
+
+ ///
+ /// Opens a RarArchive asynchronously from multiple streams.
+ ///
+ ///
+ ///
+ ///
+ public static ValueTask OpenAsync(
+ IReadOnlyList streams,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return new(Open(streams, readerOptions));
+ }
+
+ ///
+ /// Opens a RarArchive asynchronously from multiple FileInfo objects.
+ ///
+ ///
+ ///
+ ///
+ public static ValueTask OpenAsync(
+ IReadOnlyList fileInfos,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return new(Open(fileInfos, readerOptions));
+ }
+
public static bool IsRarFile(string filePath) => IsRarFile(new FileInfo(filePath));
public static bool IsRarFile(FileInfo fileInfo)
diff --git a/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs b/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs
index 69c54f310..0fe259cca 100644
--- a/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs
+++ b/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs
@@ -92,7 +92,9 @@ public Stream OpenEntryStream()
return stream;
}
- public async Task OpenEntryStreamAsync(CancellationToken cancellationToken = default)
+ public async ValueTask OpenEntryStreamAsync(
+ CancellationToken cancellationToken = default
+ )
{
RarStream stream;
if (IsRarV3)
diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs
index d9b5ba1aa..43f49abed 100644
--- a/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs
+++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchive.cs
@@ -105,6 +105,70 @@ public static SevenZipArchive Open(Stream stream, ReaderOptions? readerOptions =
);
}
+ ///
+ /// Opens a SevenZipArchive asynchronously from a stream.
+ ///
+ ///
+ ///
+ ///
+ public static ValueTask OpenAsync(
+ Stream stream,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return new(Open(stream, readerOptions));
+ }
+
+ ///
+ /// Opens a SevenZipArchive asynchronously from a FileInfo.
+ ///
+ ///
+ ///
+ ///
+ public static ValueTask OpenAsync(
+ FileInfo fileInfo,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return new(Open(fileInfo, readerOptions));
+ }
+
+ ///
+ /// Opens a SevenZipArchive asynchronously from multiple streams.
+ ///
+ ///
+ ///
+ ///
+ public static ValueTask OpenAsync(
+ IReadOnlyList streams,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return new(Open(streams, readerOptions));
+ }
+
+ ///
+ /// Opens a SevenZipArchive asynchronously from multiple FileInfo objects.
+ ///
+ ///
+ ///
+ ///
+ public static ValueTask OpenAsync(
+ IReadOnlyList fileInfos,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return new(Open(fileInfos, readerOptions));
+ }
+
///
/// Constructor with a SourceStream able to handle FileInfo and Streams.
///
@@ -201,6 +265,9 @@ private static bool SignatureMatch(Stream stream)
protected override IReader CreateReaderForSolidExtraction() =>
new SevenZipReader(ReaderOptions, this);
+ protected override ValueTask CreateReaderForSolidExtractionAsync() =>
+ new(new SevenZipReader(ReaderOptions, this));
+
public override bool IsSolid =>
Entries
.Where(x => !x.IsDirectory)
diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchiveEntry.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchiveEntry.cs
index 754c8c637..a0d4a50d8 100644
--- a/src/SharpCompress/Archives/SevenZip/SevenZipArchiveEntry.cs
+++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchiveEntry.cs
@@ -12,8 +12,9 @@ internal SevenZipArchiveEntry(SevenZipArchive archive, SevenZipFilePart part)
public Stream OpenEntryStream() => FilePart.GetCompressedStream();
- public Task OpenEntryStreamAsync(CancellationToken cancellationToken = default) =>
- Task.FromResult(OpenEntryStream());
+ public async ValueTask OpenEntryStreamAsync(
+ CancellationToken cancellationToken = default
+ ) => OpenEntryStream();
public IArchive Archive { get; }
diff --git a/src/SharpCompress/Archives/Tar/TarArchive.cs b/src/SharpCompress/Archives/Tar/TarArchive.cs
index 2754fd9bb..1aeaf9a7a 100644
--- a/src/SharpCompress/Archives/Tar/TarArchive.cs
+++ b/src/SharpCompress/Archives/Tar/TarArchive.cs
@@ -103,6 +103,70 @@ public static TarArchive Open(Stream stream, ReaderOptions? readerOptions = null
);
}
+ ///
+ /// Opens a TarArchive asynchronously from a stream.
+ ///
+ ///
+ ///
+ ///
+ public static ValueTask OpenAsync(
+ Stream stream,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return new(Open(stream, readerOptions));
+ }
+
+ ///
+ /// Opens a TarArchive asynchronously from a FileInfo.
+ ///
+ ///
+ ///
+ ///
+ public static ValueTask OpenAsync(
+ FileInfo fileInfo,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return new(Open(fileInfo, readerOptions));
+ }
+
+ ///
+ /// Opens a TarArchive asynchronously from multiple streams.
+ ///
+ ///
+ ///
+ ///
+ public static ValueTask OpenAsync(
+ IReadOnlyList streams,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return new(Open(streams, readerOptions));
+ }
+
+ ///
+ /// Opens a TarArchive asynchronously from multiple FileInfo objects.
+ ///
+ ///
+ ///
+ ///
+ public static ValueTask OpenAsync(
+ IReadOnlyList fileInfos,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return new(Open(fileInfos, readerOptions));
+ }
+
public static bool IsTarFile(string filePath) => IsTarFile(new FileInfo(filePath));
public static bool IsTarFile(FileInfo fileInfo)
@@ -259,7 +323,7 @@ IEnumerable newEntries
}
}
- protected override async Task SaveToAsync(
+ protected override async ValueTask SaveToAsync(
Stream stream,
WriterOptions options,
IEnumerable oldEntries,
@@ -302,4 +366,11 @@ protected override IReader CreateReaderForSolidExtraction()
stream.Position = 0;
return TarReader.Open(stream);
}
+
+ protected override ValueTask CreateReaderForSolidExtractionAsync()
+ {
+ var stream = Volumes.Single().Stream;
+ stream.Position = 0;
+ return new(TarReader.Open(stream));
+ }
}
diff --git a/src/SharpCompress/Archives/Tar/TarArchiveEntry.cs b/src/SharpCompress/Archives/Tar/TarArchiveEntry.cs
index 8c0827917..cbea2c717 100644
--- a/src/SharpCompress/Archives/Tar/TarArchiveEntry.cs
+++ b/src/SharpCompress/Archives/Tar/TarArchiveEntry.cs
@@ -14,9 +14,9 @@ internal TarArchiveEntry(TarArchive archive, TarFilePart? part, CompressionType
public virtual Stream OpenEntryStream() => Parts.Single().GetCompressedStream().NotNull();
- public virtual Task OpenEntryStreamAsync(
+ public async ValueTask OpenEntryStreamAsync(
CancellationToken cancellationToken = default
- ) => Task.FromResult(OpenEntryStream());
+ ) => OpenEntryStream();
#region IArchiveEntry Members
diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.cs b/src/SharpCompress/Archives/Zip/ZipArchive.cs
index 57db85c2a..756bc8863 100644
--- a/src/SharpCompress/Archives/Zip/ZipArchive.cs
+++ b/src/SharpCompress/Archives/Zip/ZipArchive.cs
@@ -124,6 +124,70 @@ public static ZipArchive Open(Stream stream, ReaderOptions? readerOptions = null
);
}
+ ///
+ /// Opens a ZipArchive asynchronously from a stream.
+ ///
+ ///
+ ///
+ ///
+ public static ValueTask OpenAsync(
+ Stream stream,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return new(Open(stream, readerOptions));
+ }
+
+ ///
+ /// Opens a ZipArchive asynchronously from a FileInfo.
+ ///
+ ///
+ ///
+ ///
+ public static ValueTask OpenAsync(
+ FileInfo fileInfo,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return new(Open(fileInfo, readerOptions));
+ }
+
+ ///
+ /// Opens a ZipArchive asynchronously from multiple streams.
+ ///
+ ///
+ ///
+ ///
+ public static ValueTask OpenAsync(
+ IReadOnlyList streams,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return new(Open(streams, readerOptions));
+ }
+
+ ///
+ /// Opens a ZipArchive asynchronously from multiple FileInfo objects.
+ ///
+ ///
+ ///
+ ///
+ public static ValueTask OpenAsync(
+ IReadOnlyList fileInfos,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return new(Open(fileInfos, readerOptions));
+ }
+
public static bool IsZipFile(
string filePath,
string? password = null,
@@ -199,7 +263,95 @@ public static bool IsZipMulti(
if (stream.CanSeek) //could be multipart. Test for central directory - might not be z64 safe
{
var z = new SeekableZipHeaderFactory(password, new ArchiveEncoding());
- var x = z.ReadSeekableHeader(stream).FirstOrDefault();
+ var x = z.ReadSeekableHeader(stream, useSync: true).FirstOrDefault();
+ return x?.ZipHeaderType == ZipHeaderType.DirectoryEntry;
+ }
+ else
+ {
+ return false;
+ }
+ }
+ return Enum.IsDefined(typeof(ZipHeaderType), header.ZipHeaderType);
+ }
+ catch (CryptographicException)
+ {
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ public static async ValueTask IsZipFileAsync(
+ Stream stream,
+ string? password = null,
+ int bufferSize = ReaderOptions.DefaultBufferSize,
+ CancellationToken cancellationToken = default
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ var headerFactory = new StreamingZipHeaderFactory(password, new ArchiveEncoding(), null);
+ try
+ {
+ if (stream is not SharpCompressStream)
+ {
+ stream = new SharpCompressStream(stream, bufferSize: bufferSize);
+ }
+
+ var header = await headerFactory
+ .ReadStreamHeaderAsync(stream)
+ .Where(x => x.ZipHeaderType != ZipHeaderType.Split)
+ .FirstOrDefaultAsync();
+ if (header is null)
+ {
+ return false;
+ }
+ return Enum.IsDefined(typeof(ZipHeaderType), header.ZipHeaderType);
+ }
+ catch (CryptographicException)
+ {
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ public static async ValueTask IsZipMultiAsync(
+ Stream stream,
+ string? password = null,
+ int bufferSize = ReaderOptions.DefaultBufferSize,
+ CancellationToken cancellationToken = default
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ var headerFactory = new StreamingZipHeaderFactory(password, new ArchiveEncoding(), null);
+ try
+ {
+ if (stream is not SharpCompressStream)
+ {
+ stream = new SharpCompressStream(stream, bufferSize: bufferSize);
+ }
+
+ var header = headerFactory
+ .ReadStreamHeader(stream)
+ .FirstOrDefault(x => x.ZipHeaderType != ZipHeaderType.Split);
+ if (header is null)
+ {
+ if (stream.CanSeek) //could be multipart. Test for central directory - might not be z64 safe
+ {
+ var z = new SeekableZipHeaderFactory(password, new ArchiveEncoding());
+ ZipHeader? x = null;
+ await foreach (
+ var h in z.ReadSeekableHeaderAsync(stream)
+ .WithCancellation(cancellationToken)
+ )
+ {
+ x = h;
+ break;
+ }
return x?.ZipHeaderType == ZipHeaderType.DirectoryEntry;
}
else
@@ -254,7 +406,9 @@ internal ZipArchive()
protected override IEnumerable LoadEntries(IEnumerable volumes)
{
var vols = volumes.ToArray();
- foreach (var h in headerFactory.NotNull().ReadSeekableHeader(vols.Last().Stream))
+ foreach (
+ var h in headerFactory.NotNull().ReadSeekableHeader(vols.Last().Stream, useSync: true)
+ )
{
if (h != null)
{
@@ -298,6 +452,59 @@ protected override IEnumerable LoadEntries(IEnumerable LoadEntriesAsync(
+ IAsyncEnumerable volumes
+ )
+ {
+ var vols = await volumes.ToListAsync();
+ var volsArray = vols.ToArray();
+
+ await foreach (
+ var h in headerFactory.NotNull().ReadSeekableHeaderAsync(volsArray.Last().Stream)
+ )
+ {
+ if (h != null)
+ {
+ switch (h.ZipHeaderType)
+ {
+ case ZipHeaderType.DirectoryEntry:
+ {
+ var deh = (DirectoryEntryHeader)h;
+ Stream s;
+ if (
+ deh.RelativeOffsetOfEntryHeader + deh.CompressedSize
+ > volsArray[deh.DiskNumberStart].Stream.Length
+ )
+ {
+ var v = volsArray.Skip(deh.DiskNumberStart).ToArray();
+ s = new SourceStream(
+ v[0].Stream,
+ i => i < v.Length ? v[i].Stream : null,
+ new ReaderOptions() { LeaveStreamOpen = true }
+ );
+ }
+ else
+ {
+ s = volsArray[deh.DiskNumberStart].Stream;
+ }
+
+ yield return new ZipArchiveEntry(
+ this,
+ new SeekableZipFilePart(headerFactory.NotNull(), deh, s)
+ );
+ }
+ break;
+ case ZipHeaderType.DirectoryEnd:
+ {
+ var bytes = ((DirectoryEndHeader)h).Comment ?? Array.Empty();
+ volsArray.Last().Comment = ReaderOptions.ArchiveEncoding.Decode(bytes);
+ yield break;
+ }
+ }
+ }
+ }
+ }
+
public void SaveTo(Stream stream) => SaveTo(stream, new WriterOptions(CompressionType.Deflate));
protected override void SaveTo(
@@ -329,7 +536,7 @@ IEnumerable newEntries
}
}
- protected override async Task SaveToAsync(
+ protected override async ValueTask SaveToAsync(
Stream stream,
WriterOptions options,
IEnumerable oldEntries,
@@ -385,4 +592,11 @@ protected override IReader CreateReaderForSolidExtraction()
((IStreamStack)stream).StackSeek(0);
return ZipReader.Open(stream, ReaderOptions, Entries);
}
+
+ protected override ValueTask CreateReaderForSolidExtractionAsync()
+ {
+ var stream = Volumes.Single().Stream;
+ stream.Position = 0;
+ return new(ZipReader.Open(stream));
+ }
}
diff --git a/src/SharpCompress/Archives/Zip/ZipArchiveEntry.cs b/src/SharpCompress/Archives/Zip/ZipArchiveEntry.cs
index a6baf34b3..f59da4f66 100644
--- a/src/SharpCompress/Archives/Zip/ZipArchiveEntry.cs
+++ b/src/SharpCompress/Archives/Zip/ZipArchiveEntry.cs
@@ -13,9 +13,17 @@ internal ZipArchiveEntry(ZipArchive archive, SeekableZipFilePart? part)
public virtual Stream OpenEntryStream() => Parts.Single().GetCompressedStream().NotNull();
- public virtual Task OpenEntryStreamAsync(
+ public async ValueTask OpenEntryStreamAsync(
CancellationToken cancellationToken = default
- ) => Task.FromResult(OpenEntryStream());
+ )
+ {
+ var part = Parts.Single();
+ if (part is SeekableZipFilePart seekablePart)
+ {
+ return (await seekablePart.GetCompressedStreamAsync(cancellationToken)).NotNull();
+ }
+ return OpenEntryStream();
+ }
#region IArchiveEntry Members
diff --git a/src/SharpCompress/Common/Ace/Headers/AceFileHeader.cs b/src/SharpCompress/Common/Ace/Headers/AceFileHeader.cs
index 9fd34c63d..575281710 100644
--- a/src/SharpCompress/Common/Ace/Headers/AceFileHeader.cs
+++ b/src/SharpCompress/Common/Ace/Headers/AceFileHeader.cs
@@ -46,7 +46,7 @@ public int DictionarySize
}
}
- public AceFileHeader(ArchiveEncoding archiveEncoding)
+ public AceFileHeader(IArchiveEncoding archiveEncoding)
: base(archiveEncoding, AceHeaderType.FILE) { }
///
diff --git a/src/SharpCompress/Common/Ace/Headers/AceHeader.cs b/src/SharpCompress/Common/Ace/Headers/AceHeader.cs
index 0fa816e5b..e51d11e5b 100644
--- a/src/SharpCompress/Common/Ace/Headers/AceHeader.cs
+++ b/src/SharpCompress/Common/Ace/Headers/AceHeader.cs
@@ -31,13 +31,13 @@ public abstract class AceHeader
(byte)'*',
];
- public AceHeader(ArchiveEncoding archiveEncoding, AceHeaderType type)
+ public AceHeader(IArchiveEncoding archiveEncoding, AceHeaderType type)
{
AceHeaderType = type;
ArchiveEncoding = archiveEncoding;
}
- public ArchiveEncoding ArchiveEncoding { get; }
+ public IArchiveEncoding ArchiveEncoding { get; }
public AceHeaderType AceHeaderType { get; }
public ushort HeaderFlags { get; set; }
diff --git a/src/SharpCompress/Common/Ace/Headers/AceMainHeader.cs b/src/SharpCompress/Common/Ace/Headers/AceMainHeader.cs
index c2fc68159..f05706056 100644
--- a/src/SharpCompress/Common/Ace/Headers/AceMainHeader.cs
+++ b/src/SharpCompress/Common/Ace/Headers/AceMainHeader.cs
@@ -22,7 +22,7 @@ public sealed class AceMainHeader : AceHeader
public List Comment { get; set; } = new();
public byte AceVersion { get; private set; }
- public AceMainHeader(ArchiveEncoding archiveEncoding)
+ public AceMainHeader(IArchiveEncoding archiveEncoding)
: base(archiveEncoding, AceHeaderType.MAIN) { }
///
diff --git a/src/SharpCompress/Common/Arc/ArcEntryHeader.cs b/src/SharpCompress/Common/Arc/ArcEntryHeader.cs
index 1a6492ecf..e50c0e49c 100644
--- a/src/SharpCompress/Common/Arc/ArcEntryHeader.cs
+++ b/src/SharpCompress/Common/Arc/ArcEntryHeader.cs
@@ -7,7 +7,7 @@ namespace SharpCompress.Common.Arc
{
public class ArcEntryHeader
{
- public ArchiveEncoding ArchiveEncoding { get; }
+ public IArchiveEncoding ArchiveEncoding { get; }
public CompressionType CompressionMethod { get; private set; }
public string? Name { get; private set; }
public long CompressedSize { get; private set; }
@@ -16,7 +16,7 @@ public class ArcEntryHeader
public long OriginalSize { get; private set; }
public long DataStartPosition { get; private set; }
- public ArcEntryHeader(ArchiveEncoding archiveEncoding)
+ public ArcEntryHeader(IArchiveEncoding archiveEncoding)
{
this.ArchiveEncoding = archiveEncoding;
}
diff --git a/src/SharpCompress/Common/ArchiveEncoding.cs b/src/SharpCompress/Common/ArchiveEncoding.cs
index 3701a93bf..2fd4a4936 100644
--- a/src/SharpCompress/Common/ArchiveEncoding.cs
+++ b/src/SharpCompress/Common/ArchiveEncoding.cs
@@ -3,55 +3,11 @@
namespace SharpCompress.Common;
-public class ArchiveEncoding
+public class ArchiveEncoding : IArchiveEncoding
{
- ///
- /// Default encoding to use when archive format doesn't specify one.
- ///
- public Encoding? Default { get; set; }
-
- ///
- /// ArchiveEncoding used by encryption schemes which don't comply with RFC 2898.
- ///
- public Encoding? Password { get; set; }
-
- ///
- /// Set this encoding when you want to force it for all encoding operations.
- ///
+ public Encoding Default { get; set; } = Encoding.Default;
+ public Encoding Password { get; set; } = Encoding.Default;
+ public Encoding UTF8 { get; set; } = Encoding.UTF8;
public Encoding? Forced { get; set; }
-
- ///
- /// Set this when you want to use a custom method for all decoding operations.
- ///
- /// string Func(bytes, index, length)
- public Func? CustomDecoder { get; set; }
-
- public ArchiveEncoding()
- : this(Encoding.Default, Encoding.Default) { }
-
- public ArchiveEncoding(Encoding def, Encoding password)
- {
- Default = def;
- Password = password;
- }
-
-#if !NETFRAMEWORK
- static ArchiveEncoding() => Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
-#endif
-
- public string Decode(byte[] bytes) => Decode(bytes, 0, bytes.Length);
-
- public string Decode(byte[] bytes, int start, int length) =>
- GetDecoder().Invoke(bytes, start, length);
-
- public string DecodeUTF8(byte[] bytes) => Encoding.UTF8.GetString(bytes, 0, bytes.Length);
-
- public byte[] Encode(string str) => GetEncoding().GetBytes(str);
-
- public Encoding GetEncoding() => Forced ?? Default ?? Encoding.UTF8;
-
- public Encoding GetPasswordEncoding() => Password ?? Encoding.UTF8;
-
- public Func GetDecoder() =>
- CustomDecoder ?? ((bytes, index, count) => GetEncoding().GetString(bytes, index, count));
+ public Func? CustomDecoder { get; set; }
}
diff --git a/src/SharpCompress/Common/ArchiveEncodingExtensions.cs b/src/SharpCompress/Common/ArchiveEncodingExtensions.cs
new file mode 100644
index 000000000..88dc35b49
--- /dev/null
+++ b/src/SharpCompress/Common/ArchiveEncodingExtensions.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Text;
+
+namespace SharpCompress.Common;
+
+///
+/// Specifies the type of encoding to use.
+///
+public enum EncodingType
+{
+ ///
+ /// Uses the default encoding.
+ ///
+ Default,
+
+ ///
+ /// Uses UTF-8 encoding.
+ ///
+ UTF8,
+}
+
+///
+/// Provides extension methods for archive encoding.
+///
+public static class ArchiveEncodingExtensions
+{
+#if !NETFRAMEWORK
+ ///
+ /// Registers the code pages encoding provider.
+ ///
+ static ArchiveEncodingExtensions() =>
+ Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
+#endif
+
+ extension(IArchiveEncoding encoding)
+ {
+ ///
+ /// Gets the encoding based on the archive encoding settings.
+ ///
+ /// Whether to use UTF-8.
+ /// The encoding.
+ public Encoding GetEncoding(bool useUtf8 = false) =>
+ encoding.Forced ?? (useUtf8 ? encoding.UTF8 : encoding.Default);
+
+ ///
+ /// Gets the decoder function for the archive encoding.
+ ///
+ /// The decoder function.
+ public Func GetDecoder() =>
+ encoding.CustomDecoder
+ ?? (
+ (bytes, index, count, type) =>
+ encoding.GetEncoding(type == EncodingType.UTF8).GetString(bytes, index, count)
+ );
+
+ ///
+ /// Encodes a string using the default encoding.
+ ///
+ /// The string to encode.
+ /// The encoded bytes.
+ public byte[] Encode(string str) => encoding.Default.GetBytes(str);
+
+ ///
+ /// Decodes bytes using the specified encoding type.
+ ///
+ /// The bytes to decode.
+ /// The encoding type.
+ /// The decoded string.
+ public string Decode(byte[] bytes, EncodingType type = EncodingType.Default) =>
+ encoding.Decode(bytes, 0, bytes.Length, type);
+
+ ///
+ /// Decodes a portion of bytes using the specified encoding type.
+ ///
+ /// The bytes to decode.
+ /// The start index.
+ /// The length.
+ /// The encoding type.
+ /// The decoded string.
+ public string Decode(
+ byte[] bytes,
+ int start,
+ int length,
+ EncodingType type = EncodingType.Default
+ ) => encoding.GetDecoder()(bytes, start, length, type);
+ }
+}
diff --git a/src/SharpCompress/Common/AsyncBinaryReader.cs b/src/SharpCompress/Common/AsyncBinaryReader.cs
new file mode 100644
index 000000000..51da5d5cd
--- /dev/null
+++ b/src/SharpCompress/Common/AsyncBinaryReader.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Buffers.Binary;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace SharpCompress.Common
+{
+ public sealed class AsyncBinaryReader : IDisposable
+ {
+ private readonly Stream _stream;
+ private readonly Stream _originalStream;
+ private readonly bool _leaveOpen;
+ private readonly byte[] _buffer = new byte[8];
+ private bool _disposed;
+
+ public AsyncBinaryReader(Stream stream, bool leaveOpen = false, int bufferSize = 4096)
+ {
+ _originalStream = stream ?? throw new ArgumentNullException(nameof(stream));
+ _leaveOpen = leaveOpen;
+
+ // Use the stream directly without wrapping in BufferedStream
+ // BufferedStream uses synchronous Read internally which doesn't work with async-only streams
+ // SharpCompress uses SharpCompressStream for buffering which supports true async reads
+ _stream = stream;
+ }
+
+ public Stream BaseStream => _stream;
+
+ public async ValueTask ReadByteAsync(CancellationToken ct = default)
+ {
+ await _stream.ReadExactAsync(_buffer, 0, 1, ct).ConfigureAwait(false);
+ return _buffer[0];
+ }
+
+ public async ValueTask ReadUInt16Async(CancellationToken ct = default)
+ {
+ await _stream.ReadExactAsync(_buffer, 0, 2, ct).ConfigureAwait(false);
+ return BinaryPrimitives.ReadUInt16LittleEndian(_buffer);
+ }
+
+ public async ValueTask ReadUInt32Async(CancellationToken ct = default)
+ {
+ await _stream.ReadExactAsync(_buffer, 0, 4, ct).ConfigureAwait(false);
+ return BinaryPrimitives.ReadUInt32LittleEndian(_buffer);
+ }
+
+ public async ValueTask ReadUInt64Async(CancellationToken ct = default)
+ {
+ await _stream.ReadExactAsync(_buffer, 0, 8, ct).ConfigureAwait(false);
+ return BinaryPrimitives.ReadUInt64LittleEndian(_buffer);
+ }
+
+ public async ValueTask ReadBytesAsync(int count, CancellationToken ct = default)
+ {
+ var result = new byte[count];
+ await _stream.ReadExactAsync(result, 0, count, ct).ConfigureAwait(false);
+ return result;
+ }
+
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+
+ // Dispose the original stream if we own it
+ if (!_leaveOpen)
+ {
+ _originalStream.Dispose();
+ }
+ }
+
+#if NET6_0_OR_GREATER
+ public async ValueTask DisposeAsync()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+
+ // Dispose the original stream if we own it
+ if (!_leaveOpen)
+ {
+ await _originalStream.DisposeAsync().ConfigureAwait(false);
+ }
+ }
+#endif
+ }
+}
diff --git a/src/SharpCompress/Common/EntryStream.cs b/src/SharpCompress/Common/EntryStream.cs
index 9e87e25e0..11d0e898f 100644
--- a/src/SharpCompress/Common/EntryStream.cs
+++ b/src/SharpCompress/Common/EntryStream.cs
@@ -56,7 +56,7 @@ public void SkipEntry()
///
/// Asynchronously skip the rest of the entry stream.
///
- public async Task SkipEntryAsync(CancellationToken cancellationToken = default)
+ public async ValueTask SkipEntryAsync(CancellationToken cancellationToken = default)
{
await this.SkipAsync(cancellationToken).ConfigureAwait(false);
_completed = true;
diff --git a/src/SharpCompress/Common/ExtractionMethods.cs b/src/SharpCompress/Common/ExtractionMethods.cs
index 509524b15..787771de9 100644
--- a/src/SharpCompress/Common/ExtractionMethods.cs
+++ b/src/SharpCompress/Common/ExtractionMethods.cs
@@ -124,11 +124,11 @@ Action openAndWrite
}
}
- public static async Task WriteEntryToDirectoryAsync(
+ public static async ValueTask WriteEntryToDirectoryAsync(
IEntry entry,
string destinationDirectory,
ExtractionOptions? options,
- Func writeAsync,
+ Func writeAsync,
CancellationToken cancellationToken = default
)
{
@@ -197,11 +197,11 @@ public static async Task WriteEntryToDirectoryAsync(
}
}
- public static async Task WriteEntryToFileAsync(
+ public static async ValueTask WriteEntryToFileAsync(
IEntry entry,
string destinationFileName,
ExtractionOptions? options,
- Func openAndWriteAsync,
+ Func openAndWriteAsync,
CancellationToken cancellationToken = default
)
{
diff --git a/src/SharpCompress/Common/FilePart.cs b/src/SharpCompress/Common/FilePart.cs
index 54e3c9f9c..583dbf4b4 100644
--- a/src/SharpCompress/Common/FilePart.cs
+++ b/src/SharpCompress/Common/FilePart.cs
@@ -1,12 +1,14 @@
using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
namespace SharpCompress.Common;
public abstract class FilePart
{
- protected FilePart(ArchiveEncoding archiveEncoding) => ArchiveEncoding = archiveEncoding;
+ protected FilePart(IArchiveEncoding archiveEncoding) => ArchiveEncoding = archiveEncoding;
- internal ArchiveEncoding ArchiveEncoding { get; }
+ internal IArchiveEncoding ArchiveEncoding { get; }
internal abstract string? FilePartName { get; }
public int Index { get; set; }
@@ -14,4 +16,8 @@ public abstract class FilePart
internal abstract Stream? GetCompressedStream();
internal abstract Stream? GetRawStream();
internal bool Skipped { get; set; }
+
+ internal virtual ValueTask GetCompressedStreamAsync(
+ CancellationToken cancellationToken = default
+ ) => new(GetCompressedStream());
}
diff --git a/src/SharpCompress/Common/GZip/GZipFilePart.cs b/src/SharpCompress/Common/GZip/GZipFilePart.cs
index 690aad802..f1d0eaf1a 100644
--- a/src/SharpCompress/Common/GZip/GZipFilePart.cs
+++ b/src/SharpCompress/Common/GZip/GZipFilePart.cs
@@ -13,7 +13,7 @@ internal sealed class GZipFilePart : FilePart
private string? _name;
private readonly Stream _stream;
- internal GZipFilePart(Stream stream, ArchiveEncoding archiveEncoding)
+ internal GZipFilePart(Stream stream, IArchiveEncoding archiveEncoding)
: base(archiveEncoding)
{
_stream = stream;
diff --git a/src/SharpCompress/Common/IArchiveEncoding.cs b/src/SharpCompress/Common/IArchiveEncoding.cs
new file mode 100644
index 000000000..92fe00932
--- /dev/null
+++ b/src/SharpCompress/Common/IArchiveEncoding.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Text;
+
+namespace SharpCompress.Common;
+
+///
+/// Defines the encoding settings for archives.
+///
+public interface IArchiveEncoding
+{
+ ///
+ /// Default encoding to use when archive format doesn't specify one. Required and defaults to Encoding.Default.
+ ///
+ public Encoding Default { get; set; }
+
+ ///
+ /// ArchiveEncoding used by encryption schemes which don't comply with RFC 2898. Required and defaults to Encoding.Default.
+ ///
+ public Encoding Password { get; set; }
+
+ ///
+ /// Default encoding to use when archive format specifies UTF-8 encoding. Required and defaults to Encoding.UTF8.
+ ///
+ public Encoding UTF8 { get; set; }
+
+ ///
+ /// Set this encoding when you want to force it for all encoding operations.
+ ///
+ public Encoding? Forced { get; set; }
+
+ ///
+ /// Set this when you want to use a custom method for all decoding operations.
+ ///
+ /// string Func(bytes, index, length, EncodingType)
+ public Func? CustomDecoder { get; set; }
+}
diff --git a/src/SharpCompress/Common/OptionsBase.cs b/src/SharpCompress/Common/OptionsBase.cs
index 8d1cbd1cf..ed13e146b 100644
--- a/src/SharpCompress/Common/OptionsBase.cs
+++ b/src/SharpCompress/Common/OptionsBase.cs
@@ -7,5 +7,5 @@ public class OptionsBase
///
public bool LeaveStreamOpen { get; set; } = true;
- public ArchiveEncoding ArchiveEncoding { get; set; } = new();
+ public IArchiveEncoding ArchiveEncoding { get; set; } = new ArchiveEncoding();
}
diff --git a/src/SharpCompress/Common/Rar/Headers/RarHeader.cs b/src/SharpCompress/Common/Rar/Headers/RarHeader.cs
index 2ae1b9f31..3ca76ab92 100644
--- a/src/SharpCompress/Common/Rar/Headers/RarHeader.cs
+++ b/src/SharpCompress/Common/Rar/Headers/RarHeader.cs
@@ -13,7 +13,7 @@ internal class RarHeader : IRarHeader
internal static RarHeader? TryReadBase(
RarCrcBinaryReader reader,
bool isRar5,
- ArchiveEncoding archiveEncoding
+ IArchiveEncoding archiveEncoding
)
{
try
@@ -26,7 +26,7 @@ ArchiveEncoding archiveEncoding
}
}
- private RarHeader(RarCrcBinaryReader reader, bool isRar5, ArchiveEncoding archiveEncoding)
+ private RarHeader(RarCrcBinaryReader reader, bool isRar5, IArchiveEncoding archiveEncoding)
{
_headerType = HeaderType.Null;
_isRar5 = isRar5;
@@ -115,7 +115,7 @@ private void VerifyHeaderCrc(uint crc32)
protected int HeaderSize { get; }
- internal ArchiveEncoding ArchiveEncoding { get; }
+ internal IArchiveEncoding ArchiveEncoding { get; }
///
/// Extra header size.
diff --git a/src/SharpCompress/Common/SevenZip/SevenZipFilePart.cs b/src/SharpCompress/Common/SevenZip/SevenZipFilePart.cs
index 156d3e711..c41fd6985 100644
--- a/src/SharpCompress/Common/SevenZip/SevenZipFilePart.cs
+++ b/src/SharpCompress/Common/SevenZip/SevenZipFilePart.cs
@@ -15,7 +15,7 @@ internal SevenZipFilePart(
ArchiveDatabase database,
int index,
CFileItem fileEntry,
- ArchiveEncoding archiveEncoding
+ IArchiveEncoding archiveEncoding
)
: base(archiveEncoding)
{
@@ -55,7 +55,7 @@ internal override Stream GetCompressedStream()
{
folderStream.Skip(skipSize);
}
- return new ReadOnlySubStream(folderStream, Header.Size);
+ return new ReadOnlySubStream(folderStream, Header.Size, leaveOpen: false);
}
public CompressionType CompressionType
diff --git a/src/SharpCompress/Common/Tar/Headers/TarHeader.cs b/src/SharpCompress/Common/Tar/Headers/TarHeader.cs
index e6b7c265c..9fbe863f6 100644
--- a/src/SharpCompress/Common/Tar/Headers/TarHeader.cs
+++ b/src/SharpCompress/Common/Tar/Headers/TarHeader.cs
@@ -11,7 +11,7 @@ internal sealed class TarHeader
internal static readonly DateTime EPOCH = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public TarHeader(
- ArchiveEncoding archiveEncoding,
+ IArchiveEncoding archiveEncoding,
TarHeaderWriteFormat writeFormat = TarHeaderWriteFormat.GNU_TAR_LONG_LINK
)
{
@@ -30,7 +30,7 @@ public TarHeader(
internal DateTime LastModifiedTime { get; set; }
internal EntryType EntryType { get; set; }
internal Stream? PackedStream { get; set; }
- internal ArchiveEncoding ArchiveEncoding { get; }
+ internal IArchiveEncoding ArchiveEncoding { get; }
internal const int BLOCK_SIZE = 512;
diff --git a/src/SharpCompress/Common/Tar/TarEntry.cs b/src/SharpCompress/Common/Tar/TarEntry.cs
index 2597b837e..9dc5e4711 100644
--- a/src/SharpCompress/Common/Tar/TarEntry.cs
+++ b/src/SharpCompress/Common/Tar/TarEntry.cs
@@ -54,7 +54,7 @@ internal static IEnumerable GetEntries(
StreamingMode mode,
Stream stream,
CompressionType compressionType,
- ArchiveEncoding archiveEncoding
+ IArchiveEncoding archiveEncoding
)
{
foreach (var header in TarHeaderFactory.ReadHeader(mode, stream, archiveEncoding))
diff --git a/src/SharpCompress/Common/Tar/TarHeaderFactory.cs b/src/SharpCompress/Common/Tar/TarHeaderFactory.cs
index 13813353d..c2af1e533 100644
--- a/src/SharpCompress/Common/Tar/TarHeaderFactory.cs
+++ b/src/SharpCompress/Common/Tar/TarHeaderFactory.cs
@@ -10,7 +10,7 @@ internal static class TarHeaderFactory
internal static IEnumerable ReadHeader(
StreamingMode mode,
Stream stream,
- ArchiveEncoding archiveEncoding
+ IArchiveEncoding archiveEncoding
)
{
while (true)
diff --git a/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.cs b/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.cs
index 2e54a6ddd..7d35f3ea6 100644
--- a/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.cs
+++ b/src/SharpCompress/Common/Zip/Headers/DirectoryEndHeader.cs
@@ -1,4 +1,5 @@
using System.IO;
+using System.Threading.Tasks;
namespace SharpCompress.Common.Zip.Headers;
@@ -19,6 +20,18 @@ internal override void Read(BinaryReader reader)
Comment = reader.ReadBytes(CommentLength);
}
+ 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();
+ Comment = await reader.ReadBytesAsync(CommentLength);
+ }
+
public ushort VolumeNumber { get; private set; }
public ushort FirstVolumeWithDirectory { get; private set; }
diff --git a/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.cs b/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.cs
index 8cf4f4ad7..a9ed564f6 100644
--- a/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.cs
+++ b/src/SharpCompress/Common/Zip/Headers/DirectoryEntryHeader.cs
@@ -1,11 +1,12 @@
using System.IO;
using System.Linq;
+using System.Threading.Tasks;
namespace SharpCompress.Common.Zip.Headers;
internal class DirectoryEntryHeader : ZipFileEntry
{
- public DirectoryEntryHeader(ArchiveEncoding archiveEncoding)
+ public DirectoryEntryHeader(IArchiveEncoding archiveEncoding)
: base(ZipHeaderType.DirectoryEntry, archiveEncoding) { }
internal override void Read(BinaryReader reader)
@@ -31,7 +32,37 @@ internal override void Read(BinaryReader reader)
var extra = reader.ReadBytes(extraLength);
var comment = reader.ReadBytes(commentLength);
- // According to .ZIP File Format Specification
+ ProcessReadData(name, extra, comment);
+ }
+
+ 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();
+
+ var name = await reader.ReadBytesAsync(nameLength);
+ var extra = await reader.ReadBytesAsync(extraLength);
+ var comment = await reader.ReadBytesAsync(commentLength);
+
+ ProcessReadData(name, extra, comment);
+ }
+
+ private void ProcessReadData(byte[] name, byte[] extra, byte[] comment)
+ {
//
// For example: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
//
@@ -41,8 +72,8 @@ internal override void Read(BinaryReader reader)
if (Flags.HasFlag(HeaderFlags.Efs))
{
- Name = ArchiveEncoding.DecodeUTF8(name);
- Comment = ArchiveEncoding.DecodeUTF8(comment);
+ Name = ArchiveEncoding.Decode(name, EncodingType.UTF8);
+ Comment = ArchiveEncoding.Decode(comment, EncodingType.UTF8);
}
else
{
diff --git a/src/SharpCompress/Common/Zip/Headers/IgnoreHeader.cs b/src/SharpCompress/Common/Zip/Headers/IgnoreHeader.cs
index 5a587a7bc..9c648baf3 100644
--- a/src/SharpCompress/Common/Zip/Headers/IgnoreHeader.cs
+++ b/src/SharpCompress/Common/Zip/Headers/IgnoreHeader.cs
@@ -1,4 +1,5 @@
using System.IO;
+using System.Threading.Tasks;
namespace SharpCompress.Common.Zip.Headers;
@@ -8,4 +9,6 @@ public IgnoreHeader(ZipHeaderType type)
: base(type) { }
internal override void Read(BinaryReader reader) { }
+
+ internal override ValueTask Read(AsyncBinaryReader reader) => default;
}
diff --git a/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs b/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs
index 1e3dc62da..9091d454e 100644
--- a/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs
+++ b/src/SharpCompress/Common/Zip/Headers/LocalEntryHeader.cs
@@ -1,13 +1,12 @@
using System.IO;
using System.Linq;
+using System.Threading.Tasks;
namespace SharpCompress.Common.Zip.Headers;
-internal class LocalEntryHeader : ZipFileEntry
+internal class LocalEntryHeader(IArchiveEncoding archiveEncoding)
+ : ZipFileEntry(ZipHeaderType.LocalEntry, archiveEncoding)
{
- public LocalEntryHeader(ArchiveEncoding archiveEncoding)
- : base(ZipHeaderType.LocalEntry, archiveEncoding) { }
-
internal override void Read(BinaryReader reader)
{
Version = reader.ReadUInt16();
@@ -23,7 +22,29 @@ internal override void Read(BinaryReader reader)
var name = reader.ReadBytes(nameLength);
var extra = reader.ReadBytes(extraLength);
- // According to .ZIP File Format Specification
+ ProcessReadData(name, extra);
+ }
+
+ 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();
+ var name = await reader.ReadBytesAsync(nameLength);
+ var extra = await reader.ReadBytesAsync(extraLength);
+
+ ProcessReadData(name, extra);
+ }
+
+ private void ProcessReadData(byte[] name, byte[] extra)
+ {
//
// For example: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
//
@@ -33,7 +54,7 @@ internal override void Read(BinaryReader reader)
if (Flags.HasFlag(HeaderFlags.Efs))
{
- Name = ArchiveEncoding.DecodeUTF8(name);
+ Name = ArchiveEncoding.Decode(name, EncodingType.UTF8);
}
else
{
diff --git a/src/SharpCompress/Common/Zip/Headers/SplitHeader.cs b/src/SharpCompress/Common/Zip/Headers/SplitHeader.cs
index 4151a6cbf..29aaabaae 100644
--- a/src/SharpCompress/Common/Zip/Headers/SplitHeader.cs
+++ b/src/SharpCompress/Common/Zip/Headers/SplitHeader.cs
@@ -1,5 +1,6 @@
using System;
using System.IO;
+using System.Threading.Tasks;
namespace SharpCompress.Common.Zip.Headers;
@@ -9,4 +10,7 @@ public SplitHeader()
: base(ZipHeaderType.Split) { }
internal override void Read(BinaryReader reader) => throw new NotImplementedException();
+
+ internal override ValueTask Read(AsyncBinaryReader reader) =>
+ throw new NotImplementedException();
}
diff --git a/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndHeader.cs b/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndHeader.cs
index a74b4d1f0..b15b6f162 100644
--- a/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndHeader.cs
+++ b/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndHeader.cs
@@ -1,4 +1,5 @@
using System.IO;
+using System.Threading.Tasks;
namespace SharpCompress.Common.Zip.Headers;
@@ -26,6 +27,25 @@ internal override void Read(BinaryReader reader)
);
}
+ 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();
+ DataSector = await reader.ReadBytesAsync(
+ (int)(
+ SizeOfDirectoryEndRecord
+ - SIZE_OF_FIXED_HEADER_DATA_EXCEPT_SIGNATURE_AND_SIZE_FIELDS
+ )
+ );
+ }
+
private const int SIZE_OF_FIXED_HEADER_DATA_EXCEPT_SIGNATURE_AND_SIZE_FIELDS = 44;
public long SizeOfDirectoryEndRecord { get; private set; }
diff --git a/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.cs b/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.cs
index 3020d377e..8326be99a 100644
--- a/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.cs
+++ b/src/SharpCompress/Common/Zip/Headers/Zip64DirectoryEndLocatorHeader.cs
@@ -1,12 +1,10 @@
using System.IO;
+using System.Threading.Tasks;
namespace SharpCompress.Common.Zip.Headers;
-internal class Zip64DirectoryEndLocatorHeader : ZipHeader
+internal class Zip64DirectoryEndLocatorHeader() : ZipHeader(ZipHeaderType.Zip64DirectoryEndLocator)
{
- public Zip64DirectoryEndLocatorHeader()
- : base(ZipHeaderType.Zip64DirectoryEndLocator) { }
-
internal override void Read(BinaryReader reader)
{
FirstVolumeWithDirectory = reader.ReadUInt32();
@@ -14,6 +12,13 @@ internal override void Read(BinaryReader reader)
TotalNumberOfVolumes = reader.ReadUInt32();
}
+ internal override async ValueTask Read(AsyncBinaryReader reader)
+ {
+ FirstVolumeWithDirectory = await reader.ReadUInt32Async();
+ RelativeOffsetOfTheEndOfDirectoryRecord = (long)await reader.ReadUInt64Async();
+ TotalNumberOfVolumes = await reader.ReadUInt32Async();
+ }
+
public uint FirstVolumeWithDirectory { get; private set; }
public long RelativeOffsetOfTheEndOfDirectoryRecord { get; private set; }
diff --git a/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs b/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs
index f37690d60..edcb29767 100644
--- a/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs
+++ b/src/SharpCompress/Common/Zip/Headers/ZipFileEntry.cs
@@ -2,18 +2,14 @@
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
namespace SharpCompress.Common.Zip.Headers;
-internal abstract class ZipFileEntry : ZipHeader
+internal abstract class ZipFileEntry(ZipHeaderType type, IArchiveEncoding archiveEncoding)
+ : ZipHeader(type)
{
- protected ZipFileEntry(ZipHeaderType type, ArchiveEncoding archiveEncoding)
- : base(type)
- {
- Extra = new List();
- ArchiveEncoding = archiveEncoding;
- }
-
internal bool IsDirectory
{
get
@@ -30,7 +26,7 @@ internal bool IsDirectory
internal Stream? PackedStream { get; set; }
- internal ArchiveEncoding ArchiveEncoding { get; }
+ internal IArchiveEncoding ArchiveEncoding { get; } = archiveEncoding;
internal string? Name { get; set; }
@@ -44,7 +40,7 @@ internal bool IsDirectory
internal long UncompressedSize { get; set; }
- internal List Extra { get; set; }
+ internal List Extra { get; set; } = new();
public string? Password { get; set; }
@@ -63,6 +59,24 @@ internal PkwareTraditionalEncryptionData ComposeEncryptionData(Stream archiveStr
return encryptionData;
}
+ internal async ValueTask ComposeEncryptionDataAsync(
+ Stream archiveStream,
+ CancellationToken cancellationToken = default
+ )
+ {
+ if (archiveStream is null)
+ {
+ throw new ArgumentNullException(nameof(archiveStream));
+ }
+
+ var buffer = new byte[12];
+ await archiveStream.ReadFullyAsync(buffer, 0, 12, cancellationToken).ConfigureAwait(false);
+
+ var encryptionData = PkwareTraditionalEncryptionData.ForRead(Password!, this, buffer);
+
+ return encryptionData;
+ }
+
internal WinzipAesEncryptionData? WinzipAesEncryptionData { get; set; }
///
diff --git a/src/SharpCompress/Common/Zip/Headers/ZipHeader.cs b/src/SharpCompress/Common/Zip/Headers/ZipHeader.cs
index 36d40a821..9ce1caa3a 100644
--- a/src/SharpCompress/Common/Zip/Headers/ZipHeader.cs
+++ b/src/SharpCompress/Common/Zip/Headers/ZipHeader.cs
@@ -1,18 +1,14 @@
using System.IO;
+using System.Threading.Tasks;
namespace SharpCompress.Common.Zip.Headers;
-internal abstract class ZipHeader
+internal abstract class ZipHeader(ZipHeaderType type)
{
- protected ZipHeader(ZipHeaderType type)
- {
- ZipHeaderType = type;
- HasData = true;
- }
-
- internal ZipHeaderType ZipHeaderType { get; }
+ internal ZipHeaderType ZipHeaderType { get; } = type;
internal abstract void Read(BinaryReader reader);
+ internal abstract ValueTask Read(AsyncBinaryReader reader);
- internal bool HasData { get; set; }
+ internal bool HasData { get; set; } = true;
}
diff --git a/src/SharpCompress/Common/Zip/PkwareTraditionalEncryptionData.cs b/src/SharpCompress/Common/Zip/PkwareTraditionalEncryptionData.cs
index 7b5178579..c0c64524b 100644
--- a/src/SharpCompress/Common/Zip/PkwareTraditionalEncryptionData.cs
+++ b/src/SharpCompress/Common/Zip/PkwareTraditionalEncryptionData.cs
@@ -8,9 +8,9 @@ internal class PkwareTraditionalEncryptionData
{
private static readonly CRC32 CRC32 = new();
private readonly uint[] _keys = { 0x12345678, 0x23456789, 0x34567890 };
- private readonly ArchiveEncoding _archiveEncoding;
+ private readonly IArchiveEncoding _archiveEncoding;
- private PkwareTraditionalEncryptionData(string password, ArchiveEncoding archiveEncoding)
+ private PkwareTraditionalEncryptionData(string password, IArchiveEncoding archiveEncoding)
{
_archiveEncoding = archiveEncoding;
Initialize(password);
@@ -103,7 +103,7 @@ private void Initialize(string password)
internal byte[] StringToByteArray(string value)
{
- var a = _archiveEncoding.GetPasswordEncoding().GetBytes(value);
+ var a = _archiveEncoding.Password.GetBytes(value);
return a;
}
diff --git a/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs b/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs
index e75727112..7dbf93baa 100644
--- a/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs
+++ b/src/SharpCompress/Common/Zip/SeekableZipFilePart.cs
@@ -1,4 +1,6 @@
using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
using SharpCompress.Common.Zip.Headers;
namespace SharpCompress.Common.Zip;
@@ -25,9 +27,24 @@ internal override Stream GetCompressedStream()
return base.GetCompressedStream();
}
+ internal override async ValueTask GetCompressedStreamAsync(
+ CancellationToken cancellationToken = default
+ )
+ {
+ if (!_isLocalHeaderLoaded)
+ {
+ await LoadLocalHeaderAsync(cancellationToken);
+ _isLocalHeaderLoaded = true;
+ }
+ return await base.GetCompressedStreamAsync(cancellationToken);
+ }
+
private void LoadLocalHeader() =>
Header = _headerFactory.GetLocalHeader(BaseStream, (DirectoryEntryHeader)Header);
+ private async ValueTask LoadLocalHeaderAsync(CancellationToken cancellationToken = default) =>
+ Header = await _headerFactory.GetLocalHeaderAsync(BaseStream, (DirectoryEntryHeader)Header);
+
protected override Stream CreateBaseStream()
{
BaseStream.Position = Header.DataStartPosition.NotNull();
diff --git a/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs b/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs
index 005f64801..908566229 100644
--- a/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs
+++ b/src/SharpCompress/Common/Zip/SeekableZipHeaderFactory.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Threading.Tasks;
using SharpCompress.Common.Zip.Headers;
using SharpCompress.IO;
@@ -15,10 +16,77 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory
private const int MAX_SEARCH_LENGTH_FOR_EOCD = 65557;
private bool _zip64;
- internal SeekableZipHeaderFactory(string? password, ArchiveEncoding archiveEncoding)
+ internal SeekableZipHeaderFactory(string? password, IArchiveEncoding archiveEncoding)
: base(StreamingMode.Seekable, password, archiveEncoding) { }
- internal IEnumerable ReadSeekableHeader(Stream stream)
+ internal async IAsyncEnumerable ReadSeekableHeaderAsync(Stream stream)
+ {
+ var reader = new AsyncBinaryReader(stream);
+
+ await SeekBackToHeaderAsync(stream, reader);
+
+ var eocd_location = stream.Position;
+ var entry = new DirectoryEndHeader();
+ await entry.Read(reader);
+
+ if (entry.IsZip64)
+ {
+ _zip64 = true;
+
+ // 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();
+ 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);
+
+ stream.Seek(zip64Locator.RelativeOffsetOfTheEndOfDirectoryRecord, SeekOrigin.Begin);
+ var zip64Signature = await reader.ReadUInt32Async();
+ if (zip64Signature != ZIP64_END_OF_CENTRAL_DIRECTORY)
+ {
+ throw new ArchiveException("Failed to locate the Zip64 Header");
+ }
+
+ var zip64Entry = new Zip64DirectoryEndHeader();
+ await zip64Entry.Read(reader);
+ stream.Seek(zip64Entry.DirectoryStartOffsetRelativeToDisk, SeekOrigin.Begin);
+ }
+ else
+ {
+ stream.Seek(entry.DirectoryStartOffsetRelativeToDisk, SeekOrigin.Begin);
+ }
+
+ var position = stream.Position;
+ while (true)
+ {
+ stream.Position = position;
+ var signature = await reader.ReadUInt32Async();
+ var nextHeader = await ReadHeader(signature, reader, _zip64);
+ position = stream.Position;
+
+ if (nextHeader is null)
+ {
+ yield break;
+ }
+
+ if (nextHeader is DirectoryEntryHeader entryHeader)
+ {
+ //entry could be zero bytes so we need to know that.
+ entryHeader.HasData = entryHeader.CompressedSize != 0;
+ yield return entryHeader;
+ }
+ else if (nextHeader is DirectoryEndHeader endHeader)
+ {
+ yield return endHeader;
+ }
+ }
+ }
+
+ internal IEnumerable ReadSeekableHeader(Stream stream, bool useSync)
{
var reader = new BinaryReader(stream);
@@ -85,6 +153,73 @@ internal IEnumerable ReadSeekableHeader(Stream stream)
}
}
+ internal async IAsyncEnumerable ReadSeekableHeaderAsync(Stream stream, bool useSync)
+ {
+ var reader = new AsyncBinaryReader(stream);
+
+ await SeekBackToHeaderAsync(stream, reader);
+
+ var eocd_location = stream.Position;
+ var entry = new DirectoryEndHeader();
+ await entry.Read(reader);
+
+ if (entry.IsZip64)
+ {
+ _zip64 = true;
+
+ // ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR should be before the EOCD
+ stream.Seek(eocd_location - ZIP64_EOCD_LENGTH - 4, SeekOrigin.Begin);
+ var zip64_locator = await reader.ReadUInt32Async();
+ 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);
+
+ stream.Seek(zip64Locator.RelativeOffsetOfTheEndOfDirectoryRecord, SeekOrigin.Begin);
+ var zip64Signature = await reader.ReadUInt32Async();
+ if (zip64Signature != ZIP64_END_OF_CENTRAL_DIRECTORY)
+ {
+ throw new ArchiveException("Failed to locate the Zip64 Header");
+ }
+
+ var zip64Entry = new Zip64DirectoryEndHeader();
+ await zip64Entry.Read(reader);
+ stream.Seek(zip64Entry.DirectoryStartOffsetRelativeToDisk, SeekOrigin.Begin);
+ }
+ else
+ {
+ stream.Seek(entry.DirectoryStartOffsetRelativeToDisk, SeekOrigin.Begin);
+ }
+
+ var position = stream.Position;
+ while (true)
+ {
+ stream.Position = position;
+ var signature = await reader.ReadUInt32Async();
+ var nextHeader = await ReadHeader(signature, reader, _zip64);
+ position = stream.Position;
+
+ if (nextHeader is null)
+ {
+ yield break;
+ }
+
+ if (nextHeader is DirectoryEntryHeader entryHeader)
+ {
+ //entry could be zero bytes so we need to know that.
+ entryHeader.HasData = entryHeader.CompressedSize != 0;
+ yield return entryHeader;
+ }
+ else if (nextHeader is DirectoryEndHeader endHeader)
+ {
+ yield return endHeader;
+ }
+ }
+ }
+
private static bool IsMatch(byte[] haystack, int position, byte[] needle)
{
for (var i = 0; i < needle.Length; i++)
@@ -98,6 +233,45 @@ private static bool IsMatch(byte[] haystack, int position, byte[] needle)
return true;
}
+ private static async ValueTask SeekBackToHeaderAsync(Stream stream, AsyncBinaryReader reader)
+ {
+ // Minimum EOCD length
+ if (stream.Length < MINIMUM_EOCD_LENGTH)
+ {
+ throw new ArchiveException(
+ "Could not find Zip file Directory at the end of the file. File may be corrupted."
+ );
+ }
+
+ var len =
+ stream.Length < MAX_SEARCH_LENGTH_FOR_EOCD
+ ? (int)stream.Length
+ : MAX_SEARCH_LENGTH_FOR_EOCD;
+ // We search for marker in reverse to find the first occurance
+ byte[] needle = { 0x06, 0x05, 0x4b, 0x50 };
+
+ stream.Seek(-len, SeekOrigin.End);
+
+ var seek = await reader.ReadBytesAsync(len);
+
+ // Search in reverse
+ Array.Reverse(seek);
+
+ // don't exclude the minimum eocd region, otherwise you fail to locate the header in empty zip files
+ var max_search_area = len; // - MINIMUM_EOCD_LENGTH;
+
+ for (var pos_from_end = 0; pos_from_end < max_search_area; ++pos_from_end)
+ {
+ if (IsMatch(seek, pos_from_end, needle))
+ {
+ stream.Seek(-pos_from_end, SeekOrigin.End);
+ return;
+ }
+ }
+
+ throw new ArchiveException("Failed to locate the Zip Header");
+ }
+
private static void SeekBackToHeader(Stream stream, BinaryReader reader)
{
// Minimum EOCD length
@@ -163,4 +337,31 @@ DirectoryEntryHeader directoryEntryHeader
}
return localEntryHeader;
}
+
+ internal async ValueTask GetLocalHeaderAsync(
+ Stream stream,
+ DirectoryEntryHeader directoryEntryHeader
+ )
+ {
+ stream.Seek(directoryEntryHeader.RelativeOffsetOfEntryHeader, SeekOrigin.Begin);
+ var reader = new AsyncBinaryReader(stream);
+ var signature = await reader.ReadUInt32Async();
+ if (await ReadHeader(signature, reader, _zip64) is not LocalEntryHeader localEntryHeader)
+ {
+ throw new InvalidOperationException();
+ }
+
+ // populate fields only known from the DirectoryEntryHeader
+ localEntryHeader.HasData = directoryEntryHeader.HasData;
+ localEntryHeader.ExternalFileAttributes = directoryEntryHeader.ExternalFileAttributes;
+ localEntryHeader.Comment = directoryEntryHeader.Comment;
+
+ if (FlagUtility.HasFlag(localEntryHeader.Flags, HeaderFlags.UsePostDataDescriptor))
+ {
+ localEntryHeader.Crc = directoryEntryHeader.Crc;
+ localEntryHeader.CompressedSize = directoryEntryHeader.CompressedSize;
+ localEntryHeader.UncompressedSize = directoryEntryHeader.UncompressedSize;
+ }
+ return localEntryHeader;
+ }
}
diff --git a/src/SharpCompress/Common/Zip/StreamingZipFilePart.cs b/src/SharpCompress/Common/Zip/StreamingZipFilePart.cs
index 5464a9cc8..312ea1263 100644
--- a/src/SharpCompress/Common/Zip/StreamingZipFilePart.cs
+++ b/src/SharpCompress/Common/Zip/StreamingZipFilePart.cs
@@ -1,4 +1,6 @@
using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
using SharpCompress.Common.Zip.Headers;
using SharpCompress.Compressors.Deflate;
using SharpCompress.IO;
@@ -31,6 +33,28 @@ internal override Stream GetCompressedStream()
return _decompressionStream;
}
+ internal override async ValueTask GetCompressedStreamAsync(
+ CancellationToken cancellationToken = default
+ )
+ {
+ if (!Header.HasData)
+ {
+ return Stream.Null;
+ }
+ _decompressionStream = await CreateDecompressionStreamAsync(
+ await GetCryptoStreamAsync(CreateBaseStream(), cancellationToken)
+ .ConfigureAwait(false),
+ Header.CompressionMethod,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+ if (LeaveStreamOpen)
+ {
+ return SharpCompressStream.Create(_decompressionStream, leaveOpen: true);
+ }
+ return _decompressionStream;
+ }
+
internal BinaryReader FixStreamedFileLocation(ref SharpCompressStream rewindableStream)
{
if (Header.IsDirectory)
diff --git a/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs b/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs
index 031287ed1..479a5c2a1 100644
--- a/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs
+++ b/src/SharpCompress/Common/Zip/StreamingZipHeaderFactory.cs
@@ -2,6 +2,9 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using SharpCompress.Common;
using SharpCompress.Common.Zip.Headers;
using SharpCompress.IO;
@@ -13,7 +16,7 @@ internal class StreamingZipHeaderFactory : ZipHeaderFactory
internal StreamingZipHeaderFactory(
string? password,
- ArchiveEncoding archiveEncoding,
+ IArchiveEncoding archiveEncoding,
IEnumerable? entries
)
: base(StreamingMode.Streaming, password, archiveEncoding) => _entries = entries;
@@ -200,4 +203,331 @@ internal IEnumerable ReadStreamHeader(Stream stream)
yield return header;
}
}
+
+ ///
+ /// Reads ZIP headers asynchronously for streams that do not support synchronous reads.
+ ///
+ internal IAsyncEnumerable ReadStreamHeaderAsync(Stream stream) =>
+ new StreamHeaderAsyncEnumerable(this, stream);
+
+ ///
+ /// Invokes the shared async header parsing logic on the base factory.
+ ///
+ private ValueTask ReadHeaderAsyncInternal(
+ uint headerBytes,
+ AsyncBinaryReader reader
+ ) => ReadHeader(headerBytes, reader);
+
+ ///
+ /// Exposes the last parsed local entry header to the async enumerator so it can handle streaming data descriptors.
+ ///
+ private LocalEntryHeader? LastEntryHeader
+ {
+ get => _lastEntryHeader;
+ set => _lastEntryHeader = value;
+ }
+
+ ///
+ /// Produces an async enumerator for streaming ZIP headers.
+ ///
+ private sealed class StreamHeaderAsyncEnumerable : IAsyncEnumerable
+ {
+ private readonly StreamingZipHeaderFactory _headerFactory;
+ private readonly Stream _stream;
+
+ public StreamHeaderAsyncEnumerable(StreamingZipHeaderFactory headerFactory, Stream stream)
+ {
+ _headerFactory = headerFactory;
+ _stream = stream;
+ }
+
+ public IAsyncEnumerator GetAsyncEnumerator(
+ CancellationToken cancellationToken = default
+ ) => new StreamHeaderAsyncEnumerator(_headerFactory, _stream, cancellationToken);
+ }
+
+ ///
+ /// Async implementation of using to avoid sync reads.
+ ///
+ private sealed class StreamHeaderAsyncEnumerator : IAsyncEnumerator, IDisposable
+ {
+ private readonly StreamingZipHeaderFactory _headerFactory;
+ private readonly SharpCompressStream _rewindableStream;
+ private readonly AsyncBinaryReader _reader;
+ private readonly CancellationToken _cancellationToken;
+ private bool _completed;
+
+ public StreamHeaderAsyncEnumerator(
+ StreamingZipHeaderFactory headerFactory,
+ Stream stream,
+ CancellationToken cancellationToken
+ )
+ {
+ _headerFactory = headerFactory;
+ _rewindableStream = EnsureSharpCompressStream(stream);
+ _reader = new AsyncBinaryReader(_rewindableStream, leaveOpen: true);
+ _cancellationToken = cancellationToken;
+ }
+
+ private ZipHeader? _current;
+
+ public ZipHeader Current =>
+ _current ?? throw new InvalidOperationException("No current header is available.");
+
+ ///
+ /// Advances to the next ZIP header in the stream, honoring streaming data descriptors where applicable.
+ ///
+ public async ValueTask MoveNextAsync()
+ {
+ if (_completed)
+ {
+ return false;
+ }
+
+ while (true)
+ {
+ _cancellationToken.ThrowIfCancellationRequested();
+
+ uint headerBytes;
+ var lastEntryHeader = _headerFactory.LastEntryHeader;
+ if (
+ lastEntryHeader != null
+ && FlagUtility.HasFlag(lastEntryHeader.Flags, HeaderFlags.UsePostDataDescriptor)
+ )
+ {
+ if (lastEntryHeader.Part is null)
+ {
+ continue;
+ }
+
+ var pos = _rewindableStream.CanSeek ? (long?)_rewindableStream.Position : null;
+
+ var crc = await _reader
+ .ReadUInt32Async(_cancellationToken)
+ .ConfigureAwait(false);
+ if (crc == POST_DATA_DESCRIPTOR)
+ {
+ crc = await _reader
+ .ReadUInt32Async(_cancellationToken)
+ .ConfigureAwait(false);
+ }
+ lastEntryHeader.Crc = crc;
+
+ //attempt 32bit read
+ ulong compressedSize = await _reader
+ .ReadUInt32Async(_cancellationToken)
+ .ConfigureAwait(false);
+ ulong uncompressedSize = await _reader
+ .ReadUInt32Async(_cancellationToken)
+ .ConfigureAwait(false);
+ headerBytes = await _reader
+ .ReadUInt32Async(_cancellationToken)
+ .ConfigureAwait(false);
+
+ //check for zip64 sentinel or unexpected header
+ bool isSentinel =
+ compressedSize == 0xFFFFFFFF || uncompressedSize == 0xFFFFFFFF;
+ bool isHeader = headerBytes == 0x04034b50 || headerBytes == 0x02014b50;
+
+ if (!isHeader && !isSentinel)
+ {
+ //reshuffle into 64-bit values
+ compressedSize = (uncompressedSize << 32) | compressedSize;
+ uncompressedSize =
+ ((ulong)headerBytes << 32)
+ | await _reader
+ .ReadUInt32Async(_cancellationToken)
+ .ConfigureAwait(false);
+ headerBytes = await _reader
+ .ReadUInt32Async(_cancellationToken)
+ .ConfigureAwait(false);
+ }
+ else if (isSentinel)
+ {
+ //standards-compliant zip64 descriptor
+ compressedSize = await _reader
+ .ReadUInt64Async(_cancellationToken)
+ .ConfigureAwait(false);
+ uncompressedSize = await _reader
+ .ReadUInt64Async(_cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ lastEntryHeader.CompressedSize = (long)compressedSize;
+ lastEntryHeader.UncompressedSize = (long)uncompressedSize;
+
+ if (pos.HasValue)
+ {
+ lastEntryHeader.DataStartPosition = pos - lastEntryHeader.CompressedSize;
+ }
+ }
+ else if (lastEntryHeader != null && lastEntryHeader.IsZip64)
+ {
+ if (lastEntryHeader.Part is null)
+ {
+ continue;
+ }
+
+ var pos = _rewindableStream.CanSeek ? (long?)_rewindableStream.Position : null;
+
+ headerBytes = await _reader
+ .ReadUInt32Async(_cancellationToken)
+ .ConfigureAwait(false);
+
+ _ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // version
+ _ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // flags
+ _ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // compressionMethod
+ _ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // lastModifiedDate
+ _ = await _reader.ReadUInt16Async(_cancellationToken).ConfigureAwait(false); // lastModifiedTime
+
+ var crc = await _reader
+ .ReadUInt32Async(_cancellationToken)
+ .ConfigureAwait(false);
+
+ if (crc == POST_DATA_DESCRIPTOR)
+ {
+ crc = await _reader
+ .ReadUInt32Async(_cancellationToken)
+ .ConfigureAwait(false);
+ }
+ lastEntryHeader.Crc = crc;
+
+ // The DataDescriptor can be either 64bit or 32bit
+ var compressedSize = await _reader
+ .ReadUInt32Async(_cancellationToken)
+ .ConfigureAwait(false);
+ var uncompressedSize = await _reader
+ .ReadUInt32Async(_cancellationToken)
+ .ConfigureAwait(false);
+
+ // Check if we have header or 64bit DataDescriptor
+ var testHeader = !(headerBytes == 0x04034b50 || headerBytes == 0x02014b50);
+
+ var test64Bit = ((long)uncompressedSize << 32) | compressedSize;
+ if (test64Bit == lastEntryHeader.CompressedSize && testHeader)
+ {
+ lastEntryHeader.UncompressedSize =
+ (
+ (long)
+ await _reader
+ .ReadUInt32Async(_cancellationToken)
+ .ConfigureAwait(false) << 32
+ ) | headerBytes;
+ headerBytes = await _reader
+ .ReadUInt32Async(_cancellationToken)
+ .ConfigureAwait(false);
+ }
+ else
+ {
+ lastEntryHeader.UncompressedSize = uncompressedSize;
+ }
+
+ if (pos.HasValue)
+ {
+ lastEntryHeader.DataStartPosition = pos - lastEntryHeader.CompressedSize;
+
+ // 4 = First 4 bytes of the entry header (i.e. 50 4B 03 04)
+ _rewindableStream.Position = pos.Value + 4;
+ }
+ }
+ else
+ {
+ headerBytes = await _reader
+ .ReadUInt32Async(_cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ _headerFactory.LastEntryHeader = null;
+ var header = await _headerFactory
+ .ReadHeaderAsyncInternal(headerBytes, _reader)
+ .ConfigureAwait(false);
+ if (header is null)
+ {
+ _completed = true;
+ return false;
+ }
+
+ //entry could be zero bytes so we need to know that.
+ if (header.ZipHeaderType == ZipHeaderType.LocalEntry)
+ {
+ var localHeader = (LocalEntryHeader)header;
+ var directoryHeader = _headerFactory._entries?.FirstOrDefault(entry =>
+ entry.Key == localHeader.Name
+ && localHeader.CompressedSize == 0
+ && localHeader.UncompressedSize == 0
+ && localHeader.Crc == 0
+ && localHeader.IsDirectory == false
+ );
+
+ if (directoryHeader != null)
+ {
+ localHeader.UncompressedSize = directoryHeader.Size;
+ localHeader.CompressedSize = directoryHeader.CompressedSize;
+ localHeader.Crc = (uint)directoryHeader.Crc;
+ }
+
+ // If we have CompressedSize, there is data to be read
+ if (localHeader.CompressedSize > 0)
+ {
+ header.HasData = true;
+ } // Check if zip is streaming ( Length is 0 and is declared in PostDataDescriptor )
+ else if (localHeader.Flags.HasFlag(HeaderFlags.UsePostDataDescriptor))
+ {
+ var nextHeaderBytes = await _reader
+ .ReadUInt32Async(_cancellationToken)
+ .ConfigureAwait(false);
+ ((IStreamStack)_rewindableStream).Rewind(sizeof(uint));
+
+ // Check if next data is PostDataDescriptor, streamed file with 0 length
+ header.HasData = !IsHeader(nextHeaderBytes);
+ }
+ else // We are not streaming and compressed size is 0, we have no data
+ {
+ header.HasData = false;
+ }
+ }
+
+ _current = header;
+ return true;
+ }
+ }
+
+ public ValueTask DisposeAsync()
+ {
+ Dispose();
+ return default;
+ }
+
+ ///
+ /// Disposes the underlying reader (without closing the archive stream).
+ ///
+ public void Dispose()
+ {
+ _reader.Dispose();
+ }
+
+ ///
+ /// Ensures the stream is a so header parsing can use rewind/buffer helpers.
+ ///
+ private static SharpCompressStream EnsureSharpCompressStream(Stream stream)
+ {
+ if (stream is SharpCompressStream sharpCompressStream)
+ {
+ return sharpCompressStream;
+ }
+
+ // Ensure the stream is already a SharpCompressStream so the buffer/size is set.
+ // The original code wrapped this with RewindableStream; use SharpCompressStream so we can get the buffer size.
+ if (stream is SourceStream src)
+ {
+ return new SharpCompressStream(
+ stream,
+ src.ReaderOptions.LeaveStreamOpen,
+ bufferSize: src.ReaderOptions.BufferSize
+ );
+ }
+
+ throw new ArgumentException("Stream must be a SharpCompressStream", nameof(stream));
+ }
+ }
}
diff --git a/src/SharpCompress/Common/Zip/WinzipAesEncryptionData.cs b/src/SharpCompress/Common/Zip/WinzipAesEncryptionData.cs
index b91d72912..da37501b9 100644
--- a/src/SharpCompress/Common/Zip/WinzipAesEncryptionData.cs
+++ b/src/SharpCompress/Common/Zip/WinzipAesEncryptionData.cs
@@ -20,7 +20,7 @@ string password
{
_keySize = keySize;
-#if NETFRAMEWORK
+#if NETFRAMEWORK || NETSTANDARD2_0
var rfc2898 = new Rfc2898DeriveBytes(password, salt, RFC2898_ITERATIONS);
KeyBytes = rfc2898.GetBytes(KeySizeInBytes);
IvBytes = rfc2898.GetBytes(KeySizeInBytes);
diff --git a/src/SharpCompress/Common/Zip/ZipFilePart.cs b/src/SharpCompress/Common/Zip/ZipFilePart.cs
index 16eb8e1a9..219f24fae 100644
--- a/src/SharpCompress/Common/Zip/ZipFilePart.cs
+++ b/src/SharpCompress/Common/Zip/ZipFilePart.cs
@@ -2,6 +2,8 @@
using System.Buffers.Binary;
using System.IO;
using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
using SharpCompress.Common.Zip.Headers;
using SharpCompress.Compressors;
using SharpCompress.Compressors.BZip2;
@@ -264,4 +266,244 @@ protected Stream GetCryptoStream(Stream plainStream)
}
return plainStream;
}
+
+ internal override async ValueTask GetCompressedStreamAsync(
+ CancellationToken cancellationToken = default
+ )
+ {
+ if (!Header.HasData)
+ {
+ return Stream.Null;
+ }
+ var decompressionStream = await CreateDecompressionStreamAsync(
+ await GetCryptoStreamAsync(CreateBaseStream(), cancellationToken)
+ .ConfigureAwait(false),
+ Header.CompressionMethod,
+ cancellationToken
+ )
+ .ConfigureAwait(false);
+ if (LeaveStreamOpen)
+ {
+ return SharpCompressStream.Create(decompressionStream, leaveOpen: true);
+ }
+ return decompressionStream;
+ }
+
+ protected async Task GetCryptoStreamAsync(
+ Stream plainStream,
+ CancellationToken cancellationToken = default
+ )
+ {
+ var isFileEncrypted = FlagUtility.HasFlag(Header.Flags, HeaderFlags.Encrypted);
+
+ if (Header.CompressedSize == 0 && isFileEncrypted)
+ {
+ throw new NotSupportedException("Cannot encrypt file with unknown size at start.");
+ }
+
+ if (
+ (
+ Header.CompressedSize == 0
+ && FlagUtility.HasFlag(Header.Flags, HeaderFlags.UsePostDataDescriptor)
+ ) || Header.IsZip64
+ )
+ {
+ plainStream = SharpCompressStream.Create(plainStream, leaveOpen: true); //make sure AES doesn't close
+ }
+ else
+ {
+ plainStream = new ReadOnlySubStream(plainStream, Header.CompressedSize); //make sure AES doesn't close
+ }
+
+ if (isFileEncrypted)
+ {
+ switch (Header.CompressionMethod)
+ {
+ case ZipCompressionMethod.None:
+ case ZipCompressionMethod.Shrink:
+ case ZipCompressionMethod.Reduce1:
+ case ZipCompressionMethod.Reduce2:
+ case ZipCompressionMethod.Reduce3:
+ case ZipCompressionMethod.Reduce4:
+ case ZipCompressionMethod.Deflate:
+ case ZipCompressionMethod.Deflate64:
+ case ZipCompressionMethod.BZip2:
+ case ZipCompressionMethod.LZMA:
+ case ZipCompressionMethod.PPMd:
+ {
+ return new PkwareTraditionalCryptoStream(
+ plainStream,
+ await Header
+ .ComposeEncryptionDataAsync(plainStream, cancellationToken)
+ .ConfigureAwait(false),
+ CryptoMode.Decrypt
+ );
+ }
+
+ case ZipCompressionMethod.WinzipAes:
+ {
+ if (Header.WinzipAesEncryptionData != null)
+ {
+ return new WinzipAesCryptoStream(
+ plainStream,
+ Header.WinzipAesEncryptionData,
+ Header.CompressedSize - 10
+ );
+ }
+ return plainStream;
+ }
+
+ default:
+ {
+ throw new InvalidOperationException("Header.CompressionMethod is invalid");
+ }
+ }
+ }
+ return plainStream;
+ }
+
+ protected async Task CreateDecompressionStreamAsync(
+ Stream stream,
+ ZipCompressionMethod method,
+ CancellationToken cancellationToken = default
+ )
+ {
+ switch (method)
+ {
+ case ZipCompressionMethod.None:
+ {
+ if (Header.CompressedSize is 0)
+ {
+ return new DataDescriptorStream(stream);
+ }
+
+ return stream;
+ }
+ case ZipCompressionMethod.Shrink:
+ {
+ return new ShrinkStream(
+ stream,
+ CompressionMode.Decompress,
+ Header.CompressedSize,
+ Header.UncompressedSize
+ );
+ }
+ case ZipCompressionMethod.Reduce1:
+ {
+ return new ReduceStream(stream, Header.CompressedSize, Header.UncompressedSize, 1);
+ }
+ case ZipCompressionMethod.Reduce2:
+ {
+ return new ReduceStream(stream, Header.CompressedSize, Header.UncompressedSize, 2);
+ }
+ case ZipCompressionMethod.Reduce3:
+ {
+ return new ReduceStream(stream, Header.CompressedSize, Header.UncompressedSize, 3);
+ }
+ case ZipCompressionMethod.Reduce4:
+ {
+ return new ReduceStream(stream, Header.CompressedSize, Header.UncompressedSize, 4);
+ }
+ case ZipCompressionMethod.Explode:
+ {
+ return new ExplodeStream(
+ stream,
+ Header.CompressedSize,
+ Header.UncompressedSize,
+ Header.Flags
+ );
+ }
+
+ case ZipCompressionMethod.Deflate:
+ {
+ return new DeflateStream(stream, CompressionMode.Decompress);
+ }
+ case ZipCompressionMethod.Deflate64:
+ {
+ return new Deflate64Stream(stream, CompressionMode.Decompress);
+ }
+ case ZipCompressionMethod.BZip2:
+ {
+ return new BZip2Stream(stream, CompressionMode.Decompress, false);
+ }
+ case ZipCompressionMethod.LZMA:
+ {
+ if (FlagUtility.HasFlag(Header.Flags, HeaderFlags.Encrypted))
+ {
+ throw new NotSupportedException("LZMA with pkware encryption.");
+ }
+ var buffer = new byte[4];
+ await stream.ReadFullyAsync(buffer, 0, 4, cancellationToken).ConfigureAwait(false);
+ var version = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(0, 2));
+ var propsSize = BinaryPrimitives.ReadUInt16LittleEndian(buffer.AsSpan(2, 2));
+ var props = new byte[propsSize];
+ await stream
+ .ReadFullyAsync(props, 0, propsSize, cancellationToken)
+ .ConfigureAwait(false);
+ return new LzmaStream(
+ props,
+ stream,
+ Header.CompressedSize > 0 ? Header.CompressedSize - 4 - props.Length : -1,
+ FlagUtility.HasFlag(Header.Flags, HeaderFlags.Bit1)
+ ? -1
+ : Header.UncompressedSize
+ );
+ }
+ case ZipCompressionMethod.Xz:
+ {
+ return new XZStream(stream);
+ }
+ case ZipCompressionMethod.ZStandard:
+ {
+ return new DecompressionStream(stream);
+ }
+ case ZipCompressionMethod.PPMd:
+ {
+ var props = new byte[2];
+ await stream.ReadFullyAsync(props, 0, 2, cancellationToken).ConfigureAwait(false);
+ return new PpmdStream(new PpmdProperties(props), stream, false);
+ }
+ case ZipCompressionMethod.WinzipAes:
+ {
+ var data = Header.Extra.SingleOrDefault(x => x.Type == ExtraDataType.WinZipAes);
+ if (data is null)
+ {
+ throw new InvalidFormatException("No Winzip AES extra data found.");
+ }
+
+ if (data.Length != 7)
+ {
+ throw new InvalidFormatException("Winzip data length is not 7.");
+ }
+
+ var compressedMethod = BinaryPrimitives.ReadUInt16LittleEndian(data.DataBytes);
+
+ if (compressedMethod != 0x01 && compressedMethod != 0x02)
+ {
+ throw new InvalidFormatException(
+ "Unexpected vendor version number for WinZip AES metadata"
+ );
+ }
+
+ var vendorId = BinaryPrimitives.ReadUInt16LittleEndian(data.DataBytes.AsSpan(2));
+ if (vendorId != 0x4541)
+ {
+ throw new InvalidFormatException(
+ "Unexpected vendor ID for WinZip AES metadata"
+ );
+ }
+
+ return await CreateDecompressionStreamAsync(
+ stream,
+ (ZipCompressionMethod)
+ BinaryPrimitives.ReadUInt16LittleEndian(data.DataBytes.AsSpan(5)),
+ cancellationToken
+ );
+ }
+ default:
+ {
+ throw new NotSupportedException("CompressionMethod: " + Header.CompressionMethod);
+ }
+ }
+ }
}
diff --git a/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs b/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs
index 45869ff6a..7238dda5c 100644
--- a/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs
+++ b/src/SharpCompress/Common/Zip/ZipHeaderFactory.cs
@@ -1,6 +1,8 @@
using System;
using System.IO;
using System.Linq;
+using System.Threading.Tasks;
+using SharpCompress;
using SharpCompress.Common.Zip.Headers;
using SharpCompress.IO;
@@ -21,12 +23,12 @@ internal class ZipHeaderFactory
protected LocalEntryHeader? _lastEntryHeader;
private readonly string? _password;
private readonly StreamingMode _mode;
- private readonly ArchiveEncoding _archiveEncoding;
+ private readonly IArchiveEncoding _archiveEncoding;
protected ZipHeaderFactory(
StreamingMode mode,
string? password,
- ArchiveEncoding archiveEncoding
+ IArchiveEncoding archiveEncoding
)
{
_mode = mode;
@@ -34,6 +36,82 @@ ArchiveEncoding archiveEncoding
_archiveEncoding = archiveEncoding;
}
+ protected async ValueTask ReadHeader(
+ uint headerBytes,
+ AsyncBinaryReader reader,
+ bool zip64 = false
+ )
+ {
+ switch (headerBytes)
+ {
+ case ENTRY_HEADER_BYTES:
+ {
+ var entryHeader = new LocalEntryHeader(_archiveEncoding);
+ await entryHeader.Read(reader);
+ await LoadHeaderAsync(entryHeader, reader.BaseStream).ConfigureAwait(false);
+
+ _lastEntryHeader = entryHeader;
+ return entryHeader;
+ }
+ case DIRECTORY_START_HEADER_BYTES:
+ {
+ var entry = new DirectoryEntryHeader(_archiveEncoding);
+ await entry.Read(reader);
+ return entry;
+ }
+ case POST_DATA_DESCRIPTOR:
+ {
+ if (
+ _lastEntryHeader != null
+ && FlagUtility.HasFlag(
+ _lastEntryHeader.NotNull().Flags,
+ HeaderFlags.UsePostDataDescriptor
+ )
+ )
+ {
+ _lastEntryHeader.Crc = await reader.ReadUInt32Async();
+ _lastEntryHeader.CompressedSize = zip64
+ ? (long)await reader.ReadUInt64Async()
+ : await reader.ReadUInt32Async();
+ _lastEntryHeader.UncompressedSize = zip64
+ ? (long)await reader.ReadUInt64Async()
+ : await reader.ReadUInt32Async();
+ }
+ else
+ {
+ await reader.ReadBytesAsync(zip64 ? 20 : 12);
+ }
+ return null;
+ }
+ case DIGITAL_SIGNATURE:
+ return null;
+ case DIRECTORY_END_HEADER_BYTES:
+ {
+ var entry = new DirectoryEndHeader();
+ await entry.Read(reader);
+ return entry;
+ }
+ case SPLIT_ARCHIVE_HEADER_BYTES:
+ {
+ return new SplitHeader();
+ }
+ case ZIP64_END_OF_CENTRAL_DIRECTORY:
+ {
+ var entry = new Zip64DirectoryEndHeader();
+ await entry.Read(reader);
+ return entry;
+ }
+ case ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR:
+ {
+ var entry = new Zip64DirectoryEndLocatorHeader();
+ await entry.Read(reader);
+ return entry;
+ }
+ default:
+ return null;
+ }
+ }
+
protected ZipHeader? ReadHeader(uint headerBytes, BinaryReader reader, bool zip64 = false)
{
switch (headerBytes)
@@ -205,4 +283,82 @@ private void LoadHeader(ZipFileEntry entryHeader, Stream stream)
//}
}
+
+ ///
+ /// Loads encryption metadata and stream positioning for a header using async reads where needed.
+ ///
+ private async ValueTask LoadHeaderAsync(ZipFileEntry entryHeader, Stream stream)
+ {
+ if (FlagUtility.HasFlag(entryHeader.Flags, HeaderFlags.Encrypted))
+ {
+ if (
+ !entryHeader.IsDirectory
+ && entryHeader.CompressedSize == 0
+ && FlagUtility.HasFlag(entryHeader.Flags, HeaderFlags.UsePostDataDescriptor)
+ )
+ {
+ throw new NotSupportedException(
+ "SharpCompress cannot currently read non-seekable Zip Streams with encrypted data that has been written in a non-seekable manner."
+ );
+ }
+
+ if (_password is null)
+ {
+ throw new CryptographicException("No password supplied for encrypted zip.");
+ }
+
+ entryHeader.Password = _password;
+
+ if (entryHeader.CompressionMethod == ZipCompressionMethod.WinzipAes)
+ {
+ var data = entryHeader.Extra.SingleOrDefault(x =>
+ x.Type == ExtraDataType.WinZipAes
+ );
+ if (data != null)
+ {
+ var keySize = (WinzipAesKeySize)data.DataBytes[4];
+
+ var salt = new byte[WinzipAesEncryptionData.KeyLengthInBytes(keySize) / 2];
+ var passwordVerifyValue = new byte[2];
+ await stream.ReadExactAsync(salt, 0, salt.Length).ConfigureAwait(false);
+ await stream.ReadExactAsync(passwordVerifyValue, 0, 2).ConfigureAwait(false);
+
+ entryHeader.WinzipAesEncryptionData = new WinzipAesEncryptionData(
+ keySize,
+ salt,
+ passwordVerifyValue,
+ _password
+ );
+
+ entryHeader.CompressedSize -= (uint)(salt.Length + 2);
+ }
+ }
+ }
+
+ if (entryHeader.IsDirectory)
+ {
+ return;
+ }
+
+ switch (_mode)
+ {
+ case StreamingMode.Seekable:
+ {
+ entryHeader.DataStartPosition = stream.Position;
+ stream.Position += entryHeader.CompressedSize;
+ break;
+ }
+
+ case StreamingMode.Streaming:
+ {
+ entryHeader.PackedStream = stream;
+ break;
+ }
+
+ default:
+ {
+ throw new InvalidFormatException("Invalid StreamingMode");
+ }
+ }
+ }
}
diff --git a/src/SharpCompress/Compressors/ADC/ADCBase.cs b/src/SharpCompress/Compressors/ADC/ADCBase.cs
index 35301b526..ad5218986 100644
--- a/src/SharpCompress/Compressors/ADC/ADCBase.cs
+++ b/src/SharpCompress/Compressors/ADC/ADCBase.cs
@@ -104,7 +104,7 @@ public static int Decompress(byte[] input, out byte[]? output, int bufferSize =
/// Max size for decompressed data
/// Cancellation token
/// Result containing bytes read and decompressed data
- public static async Task DecompressAsync(
+ public static async ValueTask DecompressAsync(
byte[] input,
int bufferSize = 262144,
CancellationToken cancellationToken = default
@@ -117,7 +117,7 @@ public static async Task DecompressAsync(
/// Max size for decompressed data
/// Cancellation token
/// Result containing bytes read and decompressed data
- public static async Task DecompressAsync(
+ public static async ValueTask DecompressAsync(
Stream input,
int bufferSize = 262144,
CancellationToken cancellationToken = default
diff --git a/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs b/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs
index e2a757c62..d3c10f9b7 100644
--- a/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs
+++ b/src/SharpCompress/Compressors/Deflate/ZlibBaseStream.cs
@@ -400,7 +400,7 @@ private void finish()
}
}
- private async Task finishAsync(CancellationToken cancellationToken = default)
+ private async ValueTask finishAsync(CancellationToken cancellationToken = default)
{
if (_z is null)
{
@@ -646,7 +646,9 @@ private string ReadZeroTerminatedString()
return _encoding.GetString(buffer, 0, buffer.Length);
}
- private async Task ReadZeroTerminatedStringAsync(CancellationToken cancellationToken)
+ private async ValueTask ReadZeroTerminatedStringAsync(
+ CancellationToken cancellationToken
+ )
{
var list = new List();
var done = false;
@@ -729,7 +731,9 @@ private int _ReadAndValidateGzipHeader()
return totalBytesRead;
}
- private async Task _ReadAndValidateGzipHeaderAsync(CancellationToken cancellationToken)
+ private async ValueTask _ReadAndValidateGzipHeaderAsync(
+ CancellationToken cancellationToken
+ )
{
var totalBytesRead = 0;
diff --git a/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs b/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs
index c65cc6834..0866f718f 100644
--- a/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs
+++ b/src/SharpCompress/Compressors/LZMA/LZ/LzOutWindow.cs
@@ -87,7 +87,7 @@ public void ReleaseStream()
_stream = null;
}
- public async Task ReleaseStreamAsync(CancellationToken cancellationToken = default)
+ public async ValueTask ReleaseStreamAsync(CancellationToken cancellationToken = default)
{
await FlushAsync(cancellationToken).ConfigureAwait(false);
_stream = null;
@@ -112,7 +112,7 @@ private void Flush()
_streamPos = _pos;
}
- private async Task FlushAsync(CancellationToken cancellationToken = default)
+ private async ValueTask FlushAsync(CancellationToken cancellationToken = default)
{
if (_stream is null)
{
@@ -153,7 +153,7 @@ public void CopyPending()
_pendingLen = rem;
}
- public async Task CopyPendingAsync(CancellationToken cancellationToken = default)
+ public async ValueTask CopyPendingAsync(CancellationToken cancellationToken = default)
{
if (_pendingLen < 1)
{
@@ -206,7 +206,7 @@ public void CopyBlock(int distance, int len)
_pendingDist = distance;
}
- public async Task CopyBlockAsync(
+ public async ValueTask CopyBlockAsync(
int distance,
int len,
CancellationToken cancellationToken = default
@@ -253,7 +253,7 @@ public void PutByte(byte b)
}
}
- public async Task PutByteAsync(byte b, CancellationToken cancellationToken = default)
+ public async ValueTask PutByteAsync(byte b, CancellationToken cancellationToken = default)
{
_buffer[_pos++] = b;
_total++;
@@ -303,7 +303,7 @@ public int CopyStream(Stream stream, int len)
return len - size;
}
- public async Task CopyStreamAsync(
+ public async ValueTask CopyStreamAsync(
Stream stream,
int len,
CancellationToken cancellationToken = default
@@ -369,6 +369,28 @@ public int Read(byte[] buffer, int offset, int count)
return size;
}
+ public int Read(Memory buffer, int offset, int count)
+ {
+ if (_streamPos >= _pos)
+ {
+ return 0;
+ }
+
+ var size = _pos - _streamPos;
+ if (size > count)
+ {
+ size = count;
+ }
+ _buffer.AsMemory(_streamPos, size).CopyTo(buffer.Slice(offset, size));
+ _streamPos += size;
+ if (_streamPos >= _windowSize)
+ {
+ _pos = 0;
+ _streamPos = 0;
+ }
+ return size;
+ }
+
public int ReadByte()
{
if (_streamPos >= _pos)
diff --git a/src/SharpCompress/Compressors/LZMA/LZipStream.cs b/src/SharpCompress/Compressors/LZMA/LZipStream.cs
index e7059003e..e409e39e6 100644
--- a/src/SharpCompress/Compressors/LZMA/LZipStream.cs
+++ b/src/SharpCompress/Compressors/LZMA/LZipStream.cs
@@ -45,10 +45,12 @@ void IStreamStack.SetPosition(long position) { }
private bool _finished;
private long _writeCount;
+ private readonly Stream? _originalStream;
public LZipStream(Stream stream, CompressionMode mode)
{
Mode = mode;
+ _originalStream = stream;
if (mode == CompressionMode.Decompress)
{
@@ -125,6 +127,10 @@ protected override void Dispose(bool disposing)
{
Finish();
_stream.Dispose();
+ if (Mode == CompressionMode.Compress)
+ {
+ _originalStream?.Dispose();
+ }
}
}
diff --git a/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs b/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs
index fed40533d..95d3d027b 100644
--- a/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs
+++ b/src/SharpCompress/Compressors/LZMA/LzmaDecoder.cs
@@ -3,6 +3,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
+using System.Threading.Tasks;
using SharpCompress.Compressors.LZMA.LZ;
using SharpCompress.Compressors.LZMA.RangeCoder;
@@ -475,7 +476,7 @@ internal bool Code(int dictionarySize, OutWindow outWindow, RangeCoder.Decoder r
return false;
}
- internal async System.Threading.Tasks.Task CodeAsync(
+ internal async ValueTask CodeAsync(
int dictionarySize,
OutWindow outWindow,
RangeCoder.Decoder rangeDecoder,
diff --git a/src/SharpCompress/Compressors/LZMA/LzmaStream.cs b/src/SharpCompress/Compressors/LZMA/LzmaStream.cs
index e9d5877ff..26079966c 100644
--- a/src/SharpCompress/Compressors/LZMA/LzmaStream.cs
+++ b/src/SharpCompress/Compressors/LZMA/LzmaStream.cs
@@ -425,11 +425,11 @@ private void DecodeChunkHeader()
}
}
- private async Task DecodeChunkHeaderAsync(CancellationToken cancellationToken = default)
+ private async ValueTask DecodeChunkHeaderAsync(CancellationToken cancellationToken = default)
{
var controlBuffer = new byte[1];
await _inputStream
- .ReadExactlyAsync(controlBuffer, 0, 1, cancellationToken)
+ .ReadExactAsync(controlBuffer, 0, 1, cancellationToken)
.ConfigureAwait(false);
var control = controlBuffer[0];
_inputPosition++;
@@ -458,13 +458,13 @@ await _inputStream
_availableBytes = (control & 0x1F) << 16;
var buffer = new byte[2];
await _inputStream
- .ReadExactlyAsync(buffer, 0, 2, cancellationToken)
+ .ReadExactAsync(buffer, 0, 2, cancellationToken)
.ConfigureAwait(false);
_availableBytes += (buffer[0] << 8) + buffer[1] + 1;
_inputPosition += 2;
await _inputStream
- .ReadExactlyAsync(buffer, 0, 2, cancellationToken)
+ .ReadExactAsync(buffer, 0, 2, cancellationToken)
.ConfigureAwait(false);
_rangeDecoderLimit = (buffer[0] << 8) + buffer[1] + 1;
_inputPosition += 2;
@@ -473,7 +473,7 @@ await _inputStream
{
_needProps = false;
await _inputStream
- .ReadExactlyAsync(controlBuffer, 0, 1, cancellationToken)
+ .ReadExactAsync(controlBuffer, 0, 1, cancellationToken)
.ConfigureAwait(false);
Properties[0] = controlBuffer[0];
_inputPosition++;
@@ -502,7 +502,7 @@ await _inputStream
_uncompressedChunk = true;
var buffer = new byte[2];
await _inputStream
- .ReadExactlyAsync(buffer, 0, 2, cancellationToken)
+ .ReadExactAsync(buffer, 0, 2, cancellationToken)
.ConfigureAwait(false);
_availableBytes = (buffer[0] << 8) + buffer[1] + 1;
_inputPosition += 2;
@@ -632,6 +632,119 @@ await _decoder
return total;
}
+#if !NETFRAMEWORK && !NETSTANDARD2_0
+ public override async ValueTask ReadAsync(
+ Memory buffer,
+ CancellationToken cancellationToken = default
+ )
+ {
+ if (_endReached)
+ {
+ return 0;
+ }
+
+ var total = 0;
+ var offset = 0;
+ var count = buffer.Length;
+ while (total < count)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (_availableBytes == 0)
+ {
+ if (_isLzma2)
+ {
+ await DecodeChunkHeaderAsync(cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ _endReached = true;
+ }
+ if (_endReached)
+ {
+ break;
+ }
+ }
+
+ var toProcess = count - total;
+ if (toProcess > _availableBytes)
+ {
+ toProcess = (int)_availableBytes;
+ }
+
+ _outWindow.SetLimit(toProcess);
+ if (_uncompressedChunk)
+ {
+ _inputPosition += await _outWindow
+ .CopyStreamAsync(_inputStream, toProcess, cancellationToken)
+ .ConfigureAwait(false);
+ }
+ else if (
+ await _decoder
+ .CodeAsync(_dictionarySize, _outWindow, _rangeDecoder, cancellationToken)
+ .ConfigureAwait(false)
+ && _outputSize < 0
+ )
+ {
+ _availableBytes = _outWindow.AvailableBytes;
+ }
+
+ var read = _outWindow.Read(buffer, offset, toProcess);
+ total += read;
+ offset += read;
+ _position += read;
+ _availableBytes -= read;
+
+ if (_availableBytes == 0 && !_uncompressedChunk)
+ {
+ if (
+ !_rangeDecoder.IsFinished
+ || (_rangeDecoderLimit >= 0 && _rangeDecoder._total != _rangeDecoderLimit)
+ )
+ {
+ _outWindow.SetLimit(toProcess + 1);
+ if (
+ !await _decoder
+ .CodeAsync(
+ _dictionarySize,
+ _outWindow,
+ _rangeDecoder,
+ cancellationToken
+ )
+ .ConfigureAwait(false)
+ )
+ {
+ _rangeDecoder.ReleaseStream();
+ throw new DataErrorException();
+ }
+ }
+
+ _rangeDecoder.ReleaseStream();
+
+ _inputPosition += _rangeDecoder._total;
+ if (_outWindow.HasPending)
+ {
+ throw new DataErrorException();
+ }
+ }
+ }
+
+ if (_endReached)
+ {
+ if (_inputSize >= 0 && _inputPosition != _inputSize)
+ {
+ throw new DataErrorException();
+ }
+ if (_outputSize >= 0 && _position != _outputSize)
+ {
+ throw new DataErrorException();
+ }
+ }
+
+ return total;
+ }
+#endif
+
public override Task WriteAsync(
byte[] buffer,
int offset,
diff --git a/src/SharpCompress/Compressors/LZMA/Utilites/Utils.cs b/src/SharpCompress/Compressors/LZMA/Utilites/Utils.cs
index 19b0f3748..b57cd53f5 100644
--- a/src/SharpCompress/Compressors/LZMA/Utilites/Utils.cs
+++ b/src/SharpCompress/Compressors/LZMA/Utilites/Utils.cs
@@ -53,39 +53,4 @@ public static void Assert(bool expression)
throw new InvalidOperationException("Assertion failed.");
}
}
-
- public static void ReadExact(this Stream stream, byte[] buffer, int offset, int length)
- {
- if (stream is null)
- {
- throw new ArgumentNullException(nameof(stream));
- }
-
- if (buffer is null)
- {
- throw new ArgumentNullException(nameof(buffer));
- }
-
- if (offset < 0 || offset > buffer.Length)
- {
- throw new ArgumentOutOfRangeException(nameof(offset));
- }
-
- if (length < 0 || length > buffer.Length - offset)
- {
- throw new ArgumentOutOfRangeException(nameof(length));
- }
-
- while (length > 0)
- {
- var fetched = stream.Read(buffer, offset, length);
- if (fetched <= 0)
- {
- throw new EndOfStreamException();
- }
-
- offset += fetched;
- length -= fetched;
- }
- }
}
diff --git a/src/SharpCompress/Compressors/Rar/RarStream.cs b/src/SharpCompress/Compressors/Rar/RarStream.cs
index a4869075a..7f258bc5b 100644
--- a/src/SharpCompress/Compressors/Rar/RarStream.cs
+++ b/src/SharpCompress/Compressors/Rar/RarStream.cs
@@ -68,7 +68,7 @@ public void Initialize()
_position = 0;
}
- public async Task InitializeAsync(CancellationToken cancellationToken = default)
+ public async ValueTask InitializeAsync(CancellationToken cancellationToken = default)
{
fetch = true;
await unpack.DoUnpackAsync(fileHeader, readStream, this, cancellationToken);
diff --git a/src/SharpCompress/Compressors/Xz/MultiByteIntegers.cs b/src/SharpCompress/Compressors/Xz/MultiByteIntegers.cs
index f5613d661..6f7a863ba 100644
--- a/src/SharpCompress/Compressors/Xz/MultiByteIntegers.cs
+++ b/src/SharpCompress/Compressors/Xz/MultiByteIntegers.cs
@@ -58,7 +58,7 @@ public static async Task ReadXZIntegerAsync(
MaxBytes = 9;
}
- var LastByte = await ReadByteAsync(reader, cancellationToken).ConfigureAwait(false);
+ var LastByte = await reader.ReadByteAsync(cancellationToken).ConfigureAwait(false);
var Output = (ulong)LastByte & 0x7F;
var i = 0;
@@ -69,7 +69,7 @@ public static async Task ReadXZIntegerAsync(
throw new InvalidFormatException();
}
- LastByte = await ReadByteAsync(reader, cancellationToken).ConfigureAwait(false);
+ LastByte = await reader.ReadByteAsync(cancellationToken).ConfigureAwait(false);
if (LastByte == 0)
{
throw new InvalidFormatException();
@@ -79,37 +79,4 @@ public static async Task ReadXZIntegerAsync(
}
return Output;
}
-
- public static async Task ReadByteAsync(
- this BinaryReader reader,
- CancellationToken cancellationToken = default
- )
- {
- var buffer = new byte[1];
- var bytesRead = await reader
- .BaseStream.ReadAsync(buffer, 0, 1, cancellationToken)
- .ConfigureAwait(false);
- if (bytesRead != 1)
- {
- throw new EndOfStreamException();
- }
- return buffer[0];
- }
-
- public static async Task ReadBytesAsync(
- this BinaryReader reader,
- int count,
- CancellationToken cancellationToken = default
- )
- {
- var buffer = new byte[count];
- var bytesRead = await reader
- .BaseStream.ReadAsync(buffer, 0, count, cancellationToken)
- .ConfigureAwait(false);
- if (bytesRead != count)
- {
- throw new EndOfStreamException();
- }
- return buffer;
- }
}
diff --git a/src/SharpCompress/Compressors/Xz/XZBlock.cs b/src/SharpCompress/Compressors/Xz/XZBlock.cs
index 45e11745a..7c18e3b41 100644
--- a/src/SharpCompress/Compressors/Xz/XZBlock.cs
+++ b/src/SharpCompress/Compressors/Xz/XZBlock.cs
@@ -132,7 +132,7 @@ private void SkipPadding()
_paddingSkipped = true;
}
- private async Task SkipPaddingAsync(CancellationToken cancellationToken = default)
+ private async ValueTask SkipPaddingAsync(CancellationToken cancellationToken = default)
{
var bytes = (BaseStream.Position - _startPosition) % 4;
if (bytes > 0)
@@ -158,7 +158,7 @@ private void CheckCrc()
_crcChecked = true;
}
- private async Task CheckCrcAsync(CancellationToken cancellationToken = default)
+ private async ValueTask CheckCrcAsync(CancellationToken cancellationToken = default)
{
var crc = new byte[_checkSize];
await BaseStream.ReadAsync(crc, 0, _checkSize, cancellationToken).ConfigureAwait(false);
@@ -194,7 +194,7 @@ private void LoadHeader()
HeaderIsLoaded = true;
}
- private async Task LoadHeaderAsync(CancellationToken cancellationToken = default)
+ private async ValueTask LoadHeaderAsync(CancellationToken cancellationToken = default)
{
await ReadHeaderSizeAsync(cancellationToken).ConfigureAwait(false);
var headerCache = await CacheHeaderAsync(cancellationToken).ConfigureAwait(false);
@@ -218,7 +218,7 @@ private void ReadHeaderSize()
}
}
- private async Task ReadHeaderSizeAsync(CancellationToken cancellationToken = default)
+ private async ValueTask ReadHeaderSizeAsync(CancellationToken cancellationToken = default)
{
var buffer = new byte[1];
await BaseStream.ReadAsync(buffer, 0, 1, cancellationToken).ConfigureAwait(false);
@@ -249,7 +249,7 @@ private byte[] CacheHeader()
return blockHeaderWithoutCrc;
}
- private async Task CacheHeaderAsync(CancellationToken cancellationToken = default)
+ private async ValueTask CacheHeaderAsync(CancellationToken cancellationToken = default)
{
var blockHeaderWithoutCrc = new byte[BlockHeaderSize - 4];
blockHeaderWithoutCrc[0] = _blockHeaderSizeByte;
diff --git a/src/SharpCompress/Compressors/Xz/XZFooter.cs b/src/SharpCompress/Compressors/Xz/XZFooter.cs
index 09c95b1a0..67b68be7c 100644
--- a/src/SharpCompress/Compressors/Xz/XZFooter.cs
+++ b/src/SharpCompress/Compressors/Xz/XZFooter.cs
@@ -62,7 +62,7 @@ public void Process()
}
}
- public async Task ProcessAsync(CancellationToken cancellationToken = default)
+ public async ValueTask ProcessAsync(CancellationToken cancellationToken = default)
{
var crc = await _reader
.BaseStream.ReadLittleEndianUInt32Async(cancellationToken)
diff --git a/src/SharpCompress/Compressors/Xz/XZHeader.cs b/src/SharpCompress/Compressors/Xz/XZHeader.cs
index 1aee619bb..39f706b1d 100644
--- a/src/SharpCompress/Compressors/Xz/XZHeader.cs
+++ b/src/SharpCompress/Compressors/Xz/XZHeader.cs
@@ -41,7 +41,7 @@ public void Process()
ProcessStreamFlags();
}
- public async Task ProcessAsync(CancellationToken cancellationToken = default)
+ public async ValueTask ProcessAsync(CancellationToken cancellationToken = default)
{
CheckMagicBytes(await _reader.ReadBytesAsync(6, cancellationToken).ConfigureAwait(false));
await ProcessStreamFlagsAsync(cancellationToken).ConfigureAwait(false);
@@ -65,7 +65,7 @@ private void ProcessStreamFlags()
}
}
- private async Task ProcessStreamFlagsAsync(CancellationToken cancellationToken = default)
+ private async ValueTask ProcessStreamFlagsAsync(CancellationToken cancellationToken = default)
{
var streamFlags = await _reader.ReadBytesAsync(2, cancellationToken).ConfigureAwait(false);
var crc = await _reader
diff --git a/src/SharpCompress/Compressors/Xz/XZIndex.cs b/src/SharpCompress/Compressors/Xz/XZIndex.cs
index 9c5230911..3bc8ea422 100644
--- a/src/SharpCompress/Compressors/Xz/XZIndex.cs
+++ b/src/SharpCompress/Compressors/Xz/XZIndex.cs
@@ -41,7 +41,7 @@ public static XZIndex FromStream(Stream stream, bool indexMarkerAlreadyVerified)
return index;
}
- public static async Task FromStreamAsync(
+ public static async ValueTask FromStreamAsync(
Stream stream,
bool indexMarkerAlreadyVerified,
CancellationToken cancellationToken = default
@@ -71,7 +71,7 @@ public void Process()
VerifyCrc32();
}
- public async Task ProcessAsync(CancellationToken cancellationToken = default)
+ public async ValueTask ProcessAsync(CancellationToken cancellationToken = default)
{
if (!_indexMarkerAlreadyVerified)
{
@@ -100,7 +100,7 @@ private void VerifyIndexMarker()
}
}
- private async Task VerifyIndexMarkerAsync(CancellationToken cancellationToken = default)
+ private async ValueTask VerifyIndexMarkerAsync(CancellationToken cancellationToken = default)
{
var marker = await _reader.ReadByteAsync(cancellationToken).ConfigureAwait(false);
if (marker != 0)
@@ -122,7 +122,7 @@ private void SkipPadding()
}
}
- private async Task SkipPaddingAsync(CancellationToken cancellationToken = default)
+ private async ValueTask SkipPaddingAsync(CancellationToken cancellationToken = default)
{
var bytes = (int)(_reader.BaseStream.Position - StreamStartPosition) % 4;
if (bytes > 0)
@@ -143,7 +143,7 @@ private void VerifyCrc32()
// TODO verify this matches
}
- private async Task VerifyCrc32Async(CancellationToken cancellationToken = default)
+ private async ValueTask VerifyCrc32Async(CancellationToken cancellationToken = default)
{
var crc = await _reader
.BaseStream.ReadLittleEndianUInt32Async(cancellationToken)
diff --git a/src/SharpCompress/Compressors/Xz/XZStream.cs b/src/SharpCompress/Compressors/Xz/XZStream.cs
index ebd0924ed..1e3051d6b 100644
--- a/src/SharpCompress/Compressors/Xz/XZStream.cs
+++ b/src/SharpCompress/Compressors/Xz/XZStream.cs
@@ -142,7 +142,7 @@ private void ReadHeader()
HeaderIsRead = true;
}
- private async Task ReadHeaderAsync(CancellationToken cancellationToken = default)
+ private async ValueTask ReadHeaderAsync(CancellationToken cancellationToken = default)
{
Header = await XZHeader
.FromStreamAsync(BaseStream, cancellationToken)
@@ -153,7 +153,7 @@ private async Task ReadHeaderAsync(CancellationToken cancellationToken = default
private void ReadIndex() => Index = XZIndex.FromStream(BaseStream, true);
- private async Task ReadIndexAsync(CancellationToken cancellationToken = default) =>
+ private async ValueTask ReadIndexAsync(CancellationToken cancellationToken = default) =>
Index = await XZIndex
.FromStreamAsync(BaseStream, true, cancellationToken)
.ConfigureAwait(false);
@@ -162,7 +162,7 @@ private async Task ReadIndexAsync(CancellationToken cancellationToken = default)
private void ReadFooter() => Footer = XZFooter.FromStream(BaseStream);
// TODO verify footer
- private async Task ReadFooterAsync(CancellationToken cancellationToken = default) =>
+ private async ValueTask ReadFooterAsync(CancellationToken cancellationToken = default) =>
Footer = await XZFooter
.FromStreamAsync(BaseStream, cancellationToken)
.ConfigureAwait(false);
@@ -202,7 +202,7 @@ private int ReadBlocks(byte[] buffer, int offset, int count)
return bytesRead;
}
- private async Task ReadBlocksAsync(
+ private async ValueTask ReadBlocksAsync(
byte[] buffer,
int offset,
int count,
diff --git a/src/SharpCompress/Compressors/ZStandard/CompressionStream.cs b/src/SharpCompress/Compressors/ZStandard/CompressionStream.cs
index 92de03b34..af8865b41 100644
--- a/src/SharpCompress/Compressors/ZStandard/CompressionStream.cs
+++ b/src/SharpCompress/Compressors/ZStandard/CompressionStream.cs
@@ -77,7 +77,7 @@ public void LoadDictionary(byte[] dict)
#if !NETSTANDARD2_0 && !NETFRAMEWORK
public override async ValueTask DisposeAsync()
#else
- public async Task DisposeAsync()
+ public async ValueTask DisposeAsync()
#endif
{
if (compressor == null)
@@ -137,7 +137,7 @@ await FlushInternalAsync(ZSTD_EndDirective.ZSTD_e_flush, cancellationToken)
private void FlushInternal(ZSTD_EndDirective directive) => WriteInternal(null, directive);
- private async Task FlushInternalAsync(
+ private async ValueTask FlushInternalAsync(
ZSTD_EndDirective directive,
CancellationToken cancellationToken = default
) => await WriteInternalAsync(null, directive, cancellationToken).ConfigureAwait(false);
@@ -183,7 +183,7 @@ private async ValueTask WriteInternalAsync(
CancellationToken cancellationToken = default
)
#else
- private async Task WriteInternalAsync(
+ private async ValueTask WriteInternalAsync(
ReadOnlyMemory? buffer,
ZSTD_EndDirective directive,
CancellationToken cancellationToken = default
@@ -235,14 +235,16 @@ await WriteInternalAsync(buffer, ZSTD_EndDirective.ZSTD_e_continue, cancellation
.ConfigureAwait(false);
#else
- public override Task WriteAsync(
+ public override async Task WriteAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
- ) => WriteAsync(new ReadOnlyMemory(buffer, offset, count), cancellationToken);
+ ) =>
+ await WriteAsync(new ReadOnlyMemory(buffer, offset, count), cancellationToken)
+ .ConfigureAwait(false);
- public async Task WriteAsync(
+ public async ValueTask WriteAsync(
ReadOnlyMemory buffer,
CancellationToken cancellationToken = default
) =>
diff --git a/src/SharpCompress/Compressors/ZStandard/DecompressionStream.cs b/src/SharpCompress/Compressors/ZStandard/DecompressionStream.cs
index 9864a8055..78af43513 100644
--- a/src/SharpCompress/Compressors/ZStandard/DecompressionStream.cs
+++ b/src/SharpCompress/Compressors/ZStandard/DecompressionStream.cs
@@ -177,9 +177,9 @@ public override Task ReadAsync(
int offset,
int count,
CancellationToken cancellationToken
- ) => ReadAsync(new Memory(buffer, offset, count), cancellationToken);
+ ) => ReadAsync(new Memory(buffer, offset, count), cancellationToken).AsTask();
- public async Task ReadAsync(
+ public async ValueTask ReadAsync(
Memory buffer,
CancellationToken cancellationToken = default
)
diff --git a/src/SharpCompress/Factories/AceFactory.cs b/src/SharpCompress/Factories/AceFactory.cs
index 5b80ae24f..95f647ddb 100644
--- a/src/SharpCompress/Factories/AceFactory.cs
+++ b/src/SharpCompress/Factories/AceFactory.cs
@@ -3,6 +3,7 @@
using System.IO;
using System.Linq;
using System.Text;
+using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Ace.Headers;
@@ -26,12 +27,21 @@ public override bool IsArchive(
Stream stream,
string? password = null,
int bufferSize = ReaderOptions.DefaultBufferSize
- )
- {
- return AceHeader.IsArchive(stream);
- }
+ ) => AceHeader.IsArchive(stream);
public IReader OpenReader(Stream stream, ReaderOptions? options) =>
AceReader.Open(stream, options);
+
+ public ValueTask OpenReaderAsync(
+ Stream stream,
+ ReaderOptions? options,
+ CancellationToken cancellationToken = default
+ ) => new(AceReader.Open(stream, options));
+
+ public override ValueTask IsArchiveAsync(
+ Stream stream,
+ string? password = null,
+ int bufferSize = ReaderOptions.DefaultBufferSize
+ ) => new(IsArchive(stream, password, bufferSize));
}
}
diff --git a/src/SharpCompress/Factories/ArcFactory.cs b/src/SharpCompress/Factories/ArcFactory.cs
index b5180afae..37984112a 100644
--- a/src/SharpCompress/Factories/ArcFactory.cs
+++ b/src/SharpCompress/Factories/ArcFactory.cs
@@ -4,6 +4,7 @@
using System.Linq;
using System.Security.Cryptography;
using System.Text;
+using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Readers;
@@ -42,5 +43,17 @@ public override bool IsArchive(
public IReader OpenReader(Stream stream, ReaderOptions? options) =>
ArcReader.Open(stream, options);
+
+ public ValueTask OpenReaderAsync(
+ Stream stream,
+ ReaderOptions? options,
+ CancellationToken cancellationToken = default
+ ) => new(ArcReader.Open(stream, options));
+
+ public override ValueTask IsArchiveAsync(
+ Stream stream,
+ string? password = null,
+ int bufferSize = ReaderOptions.DefaultBufferSize
+ ) => new(IsArchive(stream, password, bufferSize));
}
}
diff --git a/src/SharpCompress/Factories/ArjFactory.cs b/src/SharpCompress/Factories/ArjFactory.cs
index f6f7a3934..6e5f7a309 100644
--- a/src/SharpCompress/Factories/ArjFactory.cs
+++ b/src/SharpCompress/Factories/ArjFactory.cs
@@ -3,6 +3,7 @@
using System.IO;
using System.Linq;
using System.Text;
+using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Arj.Headers;
@@ -33,5 +34,17 @@ public override bool IsArchive(
public IReader OpenReader(Stream stream, ReaderOptions? options) =>
ArjReader.Open(stream, options);
+
+ public ValueTask OpenReaderAsync(
+ Stream stream,
+ ReaderOptions? options,
+ CancellationToken cancellationToken = default
+ ) => new(ArjReader.Open(stream, options));
+
+ public override ValueTask IsArchiveAsync(
+ Stream stream,
+ string? password = null,
+ int bufferSize = ReaderOptions.DefaultBufferSize
+ ) => new(IsArchive(stream, password, bufferSize));
}
}
diff --git a/src/SharpCompress/Factories/Factory.cs b/src/SharpCompress/Factories/Factory.cs
index 4651ccb22..f28f3d249 100644
--- a/src/SharpCompress/Factories/Factory.cs
+++ b/src/SharpCompress/Factories/Factory.cs
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.IO;
using SharpCompress.Readers;
@@ -57,6 +59,24 @@ public abstract bool IsArchive(
int bufferSize = ReaderOptions.DefaultBufferSize
);
+ public abstract ValueTask IsArchiveAsync(
+ Stream stream,
+ string? password = null,
+ int bufferSize = ReaderOptions.DefaultBufferSize
+ );
+
+ ///
+ public virtual ValueTask IsArchiveAsync(
+ Stream stream,
+ string? password = null,
+ int bufferSize = ReaderOptions.DefaultBufferSize,
+ CancellationToken cancellationToken = default
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return new(IsArchive(stream, password, bufferSize));
+ }
+
///
public virtual FileInfo? GetFilePart(int index, FileInfo part1) => null;
@@ -92,4 +112,34 @@ out IReader? reader
return false;
}
+
+ internal virtual async ValueTask<(bool, IAsyncReader?)> TryOpenReaderAsync(
+ SharpCompressStream stream,
+ ReaderOptions options,
+ CancellationToken cancellationToken
+ )
+ {
+ if (this is IReaderFactory readerFactory)
+ {
+ long pos = ((IStreamStack)stream).GetPosition();
+
+ if (
+ await IsArchiveAsync(
+ stream,
+ options.Password,
+ options.BufferSize,
+ cancellationToken
+ )
+ )
+ {
+ ((IStreamStack)stream).StackSeek(pos);
+ return (
+ true,
+ await readerFactory.OpenReaderAsync(stream, options, cancellationToken)
+ );
+ }
+ }
+
+ return (false, null);
+ }
}
diff --git a/src/SharpCompress/Factories/GZipFactory.cs b/src/SharpCompress/Factories/GZipFactory.cs
index 17f344cf0..48f5c63e5 100644
--- a/src/SharpCompress/Factories/GZipFactory.cs
+++ b/src/SharpCompress/Factories/GZipFactory.cs
@@ -1,6 +1,8 @@
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
+using System.Threading;
+using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Archives.GZip;
using SharpCompress.Archives.Tar;
@@ -46,6 +48,14 @@ public override bool IsArchive(
int bufferSize = ReaderOptions.DefaultBufferSize
) => GZipArchive.IsGZipFile(stream);
+ ///
+ public override ValueTask IsArchiveAsync(
+ Stream stream,
+ string? password = null,
+ int bufferSize = ReaderOptions.DefaultBufferSize,
+ CancellationToken cancellationToken = default
+ ) => GZipArchive.IsGZipFileAsync(stream, cancellationToken);
+
#endregion
#region IArchiveFactory
@@ -54,10 +64,30 @@ public override bool IsArchive(
public IArchive Open(Stream stream, ReaderOptions? readerOptions = null) =>
GZipArchive.Open(stream, readerOptions);
+ ///
+ public ValueTask OpenAsync(
+ Stream stream,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ ) => GZipArchive.OpenAsync(stream, readerOptions, cancellationToken);
+
+ public override ValueTask IsArchiveAsync(
+ Stream stream,
+ string? password = null,
+ int bufferSize = ReaderOptions.DefaultBufferSize
+ ) => new(IsArchive(stream, password, bufferSize));
+
///
public IArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null) =>
GZipArchive.Open(fileInfo, readerOptions);
+ ///
+ public ValueTask OpenAsync(
+ FileInfo fileInfo,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ ) => GZipArchive.OpenAsync(fileInfo, readerOptions, cancellationToken);
+
#endregion
#region IMultiArchiveFactory
@@ -66,10 +96,24 @@ public IArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null) =>
public IArchive Open(IReadOnlyList streams, ReaderOptions? readerOptions = null) =>
GZipArchive.Open(streams, readerOptions);
+ ///
+ public ValueTask OpenAsync(
+ IReadOnlyList streams,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ ) => GZipArchive.OpenAsync(streams, readerOptions, cancellationToken);
+
///
public IArchive Open(IReadOnlyList fileInfos, ReaderOptions? readerOptions = null) =>
GZipArchive.Open(fileInfos, readerOptions);
+ ///
+ public ValueTask OpenAsync(
+ IReadOnlyList fileInfos,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ ) => GZipArchive.OpenAsync(fileInfos, readerOptions, cancellationToken);
+
#endregion
#region IReaderFactory
@@ -108,6 +152,17 @@ out IReader? reader
public IReader OpenReader(Stream stream, ReaderOptions? options) =>
GZipReader.Open(stream, options);
+ ///
+ public ValueTask OpenReaderAsync(
+ Stream stream,
+ ReaderOptions? options,
+ CancellationToken cancellationToken = default
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return new(GZipReader.Open(stream, options));
+ }
+
#endregion
#region IWriterFactory
@@ -122,6 +177,17 @@ public IWriter Open(Stream stream, WriterOptions writerOptions)
return new GZipWriter(stream, new GZipWriterOptions(writerOptions));
}
+ ///
+ public ValueTask OpenAsync(
+ Stream stream,
+ WriterOptions writerOptions,
+ CancellationToken cancellationToken = default
+ )
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ return new(Open(stream, writerOptions));
+ }
+
#endregion
#region IWriteableArchiveFactory
diff --git a/src/SharpCompress/Factories/IFactory.cs b/src/SharpCompress/Factories/IFactory.cs
index 63d5eeec6..a9dd4f5ac 100644
--- a/src/SharpCompress/Factories/IFactory.cs
+++ b/src/SharpCompress/Factories/IFactory.cs
@@ -1,5 +1,7 @@
using System.Collections.Generic;
using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
using SharpCompress.Readers;
namespace SharpCompress.Factories;
@@ -42,6 +44,20 @@ bool IsArchive(
int bufferSize = ReaderOptions.DefaultBufferSize
);
+ ///
+ /// Returns true if the stream represents an archive of the format defined by this type asynchronously.
+ ///
+ /// A stream, pointing to the beginning of the archive.
+ /// optional password
+ /// buffer size for reading
+ /// cancellation token
+ ValueTask IsArchiveAsync(
+ Stream stream,
+ string? password = null,
+ int bufferSize = ReaderOptions.DefaultBufferSize,
+ CancellationToken cancellationToken = default
+ );
+
///
/// From a passed in archive (zip, rar, 7z, 001), return all parts.
///
diff --git a/src/SharpCompress/Factories/RarFactory.cs b/src/SharpCompress/Factories/RarFactory.cs
index 610999057..fb9e03abb 100644
--- a/src/SharpCompress/Factories/RarFactory.cs
+++ b/src/SharpCompress/Factories/RarFactory.cs
@@ -1,5 +1,7 @@
using System.Collections.Generic;
using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Archives.Rar;
using SharpCompress.Common;
@@ -47,10 +49,30 @@ public override bool IsArchive(
public IArchive Open(Stream stream, ReaderOptions? readerOptions = null) =>
RarArchive.Open(stream, readerOptions);
+ ///
+ public ValueTask OpenAsync(
+ Stream stream,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ ) => RarArchive.OpenAsync(stream, readerOptions, cancellationToken);
+
///
public IArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null) =>
RarArchive.Open(fileInfo, readerOptions);
+ ///
+ public ValueTask OpenAsync(
+ FileInfo fileInfo,
+ ReaderOptions? readerOptions = null,
+ CancellationToken cancellationToken = default
+ ) => RarArchive.OpenAsync(fileInfo, readerOptions, cancellationToken);
+
+ public override ValueTask IsArchiveAsync(
+ Stream stream,
+ string? password = null,
+ int bufferSize = ReaderOptions.DefaultBufferSize
+ ) => new(IsArchive(stream, password, bufferSize));
+
#endregion
#region IMultiArchiveFactory
@@ -59,10 +81,24 @@ public IArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null) =>
public IArchive Open(IReadOnlyList streams, ReaderOptions? readerOptions = null) =>
RarArchive.Open(streams, readerOptions);
+ ///
+ public ValueTask OpenAsync(
+ IReadOnlyList