Compare commits

..

5 Commits

Author SHA1 Message Date
Adam Hathcock
d5a8c37113 Merge pull request #1154 from adamhathcock/adam/1151-release
Adam/1151 release cherry pick
2026-01-23 09:31:03 +00:00
Adam Hathcock
21ce9a38e6 fix up tests 2026-01-23 09:04:55 +00:00
Adam Hathcock
7732fbb698 Merge pull request #1151 from adamhathcock/copilot/fix-entrystream-flush-issue
Fix EntryStream.Dispose() throwing NotSupportedException on non-seekable streams
2026-01-23 08:59:56 +00:00
Adam Hathcock
97879f18b6 Merge pull request #1146 from adamhathcock/adam/pr-1145-release
Merge pull request #1145 from adamhathcock/copilot/add-leaveopen-para…
2026-01-19 10:35:33 +00:00
Adam Hathcock
d74454f7e9 Merge pull request #1145 from adamhathcock/copilot/add-leaveopen-parameter-lzipstream
Add leaveOpen parameter to LZipStream and BZip2Stream
2026-01-19 09:58:10 +00:00
256 changed files with 3275 additions and 11729 deletions

View File

@@ -307,6 +307,7 @@ dotnet_diagnostic.CS8602.severity = error
dotnet_diagnostic.CS8604.severity = error
dotnet_diagnostic.CS8618.severity = error
dotnet_diagnostic.CS0618.severity = suggestion
dotnet_diagnostic.CS1998.severity = error
dotnet_diagnostic.CS4014.severity = error
dotnet_diagnostic.CS8600.severity = error
dotnet_diagnostic.CS8603.severity = error
@@ -367,9 +368,6 @@ 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
##########################################

4
.gitignore vendored
View File

@@ -4,8 +4,8 @@ _ReSharper.SharpCompress/
bin/
*.suo
*.user
tests/TestArchives/Scratch/
tests/TestArchives/Scratch2/
TestArchives/Scratch/
TestArchives/Scratch2/
TestResults/
*.nupkg
packages/*/

View File

@@ -14,7 +14,6 @@ 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.
@@ -111,7 +110,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 [docs/FORMATS.md](docs/FORMATS.md) for complete format support matrix
- See FORMATS.md for complete format support matrix
### Stream Handling Rules
- **Disposal**: As of version 0.21, SharpCompress closes wrapped streams by default
@@ -178,4 +177,5 @@ SharpCompress supports multiple archive and compression formats:
2. **Solid archives (Rar, 7Zip)** - Use `ExtractAllEntries()` for best performance, not individual entry extraction
3. **Stream disposal** - Always set `LeaveStreamOpen` explicitly when needed (default is to close)
4. **Tar + non-seekable stream** - Must provide file size or it will throw
5. **Multi-framework differences** - Some features differ between .NET Framework and modern .NET (e.g., Mono.Posix)
6. **Format detection** - Use `ReaderFactory.Open()` for auto-detection, test with actual archive files

View File

@@ -12,6 +12,5 @@
<RunAnalyzersDuringBuild>False</RunAnalyzersDuringBuild>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
</Project>

View File

@@ -5,7 +5,6 @@
<PackageVersion Include="Glob" Version="1.1.9" />
<PackageVersion Include="JetBrains.Profiler.SelfApi" Version="2.5.15" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.0" />
<PackageVersion Include="Microsoft.NET.ILLink.Task" Version="10.0.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageVersion Include="Mono.Posix.NETStandard" Version="1.0.0" />
<PackageVersion Include="SimpleExec" Version="13.0.0" />
@@ -14,11 +13,8 @@
<PackageVersion Include="System.Memory" Version="4.6.3" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<GlobalPackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<GlobalPackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" />
<GlobalPackageReference
Include="Microsoft.VisualStudio.Threading.Analyzers"
Version="17.14.15"
/>
<PackageVersion Include="Microsoft.NET.ILLink.Tasks" Version="10.0.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<PackageVersion Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" />
</ItemGroup>
</Project>

View File

@@ -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 [USAGE.md](docs/USAGE.md#async-examples) for examples.
**NEW:** All I/O operations now support async/await for improved performance and scalability. See the [USAGE.md](USAGE.md#async-examples) for examples.
GitHub Actions Build -
[![SharpCompress](https://github.com/adamhathcock/sharpcompress/actions/workflows/dotnetcore.yml/badge.svg)](https://github.com/adamhathcock/sharpcompress/actions/workflows/dotnetcore.yml)
@@ -14,7 +14,7 @@ GitHub Actions Build -
Post Issues on Github!
Check the [Supported Formats](docs/FORMATS.md) and [Basic Usage.](docs/USAGE.md)
Check the [Supported Formats](FORMATS.md) and [Basic Usage.](USAGE.md)
## Recommended Formats

View File

@@ -18,11 +18,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Config", "Config", "{CDB425
Directory.Build.props = Directory.Build.props
global.json = global.json
.editorconfig = .editorconfig
.gitignore = .gitignore
Directory.Packages.props = Directory.Packages.props
NuGet.config = NuGet.config
.github\workflows\nuget-release.yml = .github\workflows\nuget-release.yml
USAGE.md = USAGE.md
README.md = README.md
FORMATS.md = FORMATS.md
AGENTS.md = AGENTS.md
EndProjectSection
EndProject

View File

@@ -1,6 +1,6 @@
# SharpCompress Usage
## Async/Await Support (Beta)
## Async/Await Support
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
## Stream Rules (changed with 0.21)
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.
@@ -48,7 +48,7 @@ Also, look over the tests for more thorough [examples](https://github.com/adamha
### Create Zip Archive from multiple files
```C#
using(var archive = ZipArchive.CreateArchive())
using(var archive = ZipArchive.Create())
{
archive.AddEntry("file01.txt", "C:\\file01.txt");
archive.AddEntry("file02.txt", "C:\\file02.txt");
@@ -61,7 +61,7 @@ using(var archive = ZipArchive.CreateArchive())
### Create Zip Archive from all files in a directory to a file
```C#
using (var archive = ZipArchive.CreateArchive())
using (var archive = ZipArchive.Create())
{
archive.AddAllFromDirectory("D:\\temp");
archive.SaveTo("C:\\temp.zip", CompressionType.Deflate);
@@ -72,7 +72,7 @@ using (var archive = ZipArchive.CreateArchive())
```C#
var memoryStream = new MemoryStream();
using (var archive = ZipArchive.CreateArchive())
using (var archive = ZipArchive.Create())
{
archive.AddAllFromDirectory("D:\\temp");
archive.SaveTo(memoryStream, new WriterOptions(CompressionType.Deflate)
@@ -90,7 +90,7 @@ Note: Extracting a solid rar or 7z file needs to be done in sequential order to
`ExtractAllEntries` is primarily intended for solid archives (like solid Rar) or 7Zip archives, where sequential extraction provides the best performance. For general/simple extraction with any supported archive type, use `archive.WriteToDirectory()` instead.
```C#
using (var archive = RarArchive.OpenArchive("Test.rar"))
using (var archive = RarArchive.Open("Test.rar"))
{
// Simple extraction with RarArchive; this WriteToDirectory pattern works for all archive types
archive.WriteToDirectory(@"D:\temp", new ExtractionOptions()
@@ -104,7 +104,7 @@ using (var archive = RarArchive.OpenArchive("Test.rar"))
### Iterate over all files from a Rar file using RarArchive
```C#
using (var archive = RarArchive.OpenArchive("Test.rar"))
using (var archive = RarArchive.Open("Test.rar"))
{
foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
{
@@ -126,7 +126,7 @@ var progress = new Progress<ProgressReport>(report =>
Console.WriteLine($"Extracting {report.EntryPath}: {report.PercentComplete}%");
});
using (var archive = RarArchive.OpenArchive("archive.rar", new ReaderOptions { Progress = progress })) // Must be solid Rar or 7Zip
using (var archive = RarArchive.Open("archive.rar", new ReaderOptions { Progress = progress })) // Must be solid Rar or 7Zip
{
archive.WriteToDirectory(@"D:\output", new ExtractionOptions()
{
@@ -140,7 +140,7 @@ using (var archive = RarArchive.OpenArchive("archive.rar", new ReaderOptions { P
```C#
using (Stream stream = File.OpenRead("Tar.tar.bz2"))
using (var reader = ReaderFactory.OpenReader(stream))
using (var reader = ReaderFactory.Open(stream))
{
while (reader.MoveToNextEntry())
{
@@ -161,7 +161,7 @@ using (var reader = ReaderFactory.OpenReader(stream))
```C#
using (Stream stream = File.OpenRead("Tar.tar.bz2"))
using (var reader = ReaderFactory.OpenReader(stream))
using (var reader = ReaderFactory.Open(stream))
{
while (reader.MoveToNextEntry())
{
@@ -180,7 +180,7 @@ using (var reader = ReaderFactory.OpenReader(stream))
```C#
using (Stream stream = File.OpenWrite("C:\\temp.tgz"))
using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Tar, new WriterOptions(CompressionType.GZip)
using (var writer = WriterFactory.Open(stream, ArchiveType.Tar, new WriterOptions(CompressionType.GZip)
{
LeaveOpenStream = true
}))
@@ -199,7 +199,7 @@ opts.ArchiveEncoding.CustomDecoder = (data, x, y) =>
{
return encoding.GetString(data);
};
var tr = SharpCompress.Archives.Zip.ZipArchive.OpenArchive("test.zip", opts);
var tr = SharpCompress.Archives.Zip.ZipArchive.Open("test.zip", opts);
foreach(var entry in tr.Entries)
{
Console.WriteLine($"{entry.Key}");
@@ -213,7 +213,7 @@ foreach(var entry in tr.Entries)
**Extract single entry asynchronously:**
```C#
using (Stream stream = File.OpenRead("archive.zip"))
using (var reader = ReaderFactory.OpenReader(stream))
using (var reader = ReaderFactory.Open(stream))
{
while (reader.MoveToNextEntry())
{
@@ -234,7 +234,7 @@ using (var reader = ReaderFactory.OpenReader(stream))
**Extract all entries asynchronously:**
```C#
using (Stream stream = File.OpenRead("archive.tar.gz"))
using (var reader = ReaderFactory.OpenReader(stream))
using (var reader = ReaderFactory.Open(stream))
{
await reader.WriteAllToDirectoryAsync(
@"D:\temp",
@@ -250,7 +250,7 @@ using (var reader = ReaderFactory.OpenReader(stream))
**Open and process entry stream asynchronously:**
```C#
using (var archive = ZipArchive.OpenArchive("archive.zip"))
using (var archive = ZipArchive.Open("archive.zip"))
{
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
{
@@ -268,7 +268,7 @@ using (var archive = ZipArchive.OpenArchive("archive.zip"))
**Write single file asynchronously:**
```C#
using (Stream archiveStream = File.OpenWrite("output.zip"))
using (var writer = WriterFactory.OpenWriter(archiveStream, ArchiveType.Zip, CompressionType.Deflate))
using (var writer = WriterFactory.Open(archiveStream, ArchiveType.Zip, CompressionType.Deflate))
{
using (Stream fileStream = File.OpenRead("input.txt"))
{
@@ -280,7 +280,7 @@ using (var writer = WriterFactory.OpenWriter(archiveStream, ArchiveType.Zip, Com
**Write entire directory asynchronously:**
```C#
using (Stream stream = File.OpenWrite("backup.tar.gz"))
using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Tar, new WriterOptions(CompressionType.GZip)))
using (var writer = WriterFactory.Open(stream, ArchiveType.Tar, new WriterOptions(CompressionType.GZip)))
{
await writer.WriteAllAsync(
@"D:\files",
@@ -299,7 +299,7 @@ var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromMinutes(5));
using (Stream stream = File.OpenWrite("archive.zip"))
using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Zip, CompressionType.Deflate))
using (var writer = WriterFactory.Open(stream, ArchiveType.Zip, CompressionType.Deflate))
{
try
{
@@ -316,7 +316,7 @@ using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Zip, Compressio
**Extract from archive asynchronously:**
```C#
using (var archive = ZipArchive.OpenArchive("archive.zip"))
using (var archive = ZipArchive.Open("archive.zip"))
{
// Simple async extraction - works for all archive types
await archive.WriteToDirectoryAsync(

View File

@@ -14,51 +14,11 @@
"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=="
}
}
}

View File

@@ -1,512 +0,0 @@
# API Quick Reference
Quick reference for commonly used SharpCompress APIs.
## Factory Methods
### Opening Archives
```csharp
// Auto-detect format
using (var reader = ReaderFactory.OpenReader(stream))
{
// Works with Zip, Tar, GZip, Rar, 7Zip, etc.
}
// Specific format - Archive API
using (var archive = ZipArchive.OpenArchive("file.zip"))
using (var archive = TarArchive.OpenArchive("file.tar"))
using (var archive = RarArchive.OpenArchive("file.rar"))
using (var archive = SevenZipArchive.OpenArchive("file.7z"))
using (var archive = GZipArchive.OpenArchive("file.gz"))
// With options
var options = new ReaderOptions
{
Password = "password",
LeaveStreamOpen = true,
ArchiveEncoding = new ArchiveEncoding { Default = Encoding.GetEncoding(932) }
};
using (var archive = ZipArchive.OpenArchive("encrypted.zip", options))
```
### Creating Archives
```csharp
// Writer Factory
using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Zip, CompressionType.Deflate))
{
// Write entries
}
// Specific writer
using (var archive = ZipArchive.CreateArchive())
using (var archive = TarArchive.CreateArchive())
using (var archive = GZipArchive.CreateArchive())
// With options
var options = new WriterOptions(CompressionType.Deflate)
{
CompressionLevel = 9,
LeaveStreamOpen = false
};
using (var archive = ZipArchive.CreateArchive())
{
archive.SaveTo("output.zip", options);
}
```
---
## Archive API Methods
### Reading/Extracting
```csharp
using (var archive = ZipArchive.OpenArchive("file.zip"))
{
// Get all entries
IEnumerable<IArchiveEntry> 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 extraction (requires IAsyncArchive)
using (var asyncArchive = await ZipArchive.OpenAsyncArchive("file.zip"))
{
await asyncArchive.WriteToDirectoryAsync(
@"C:\output",
new ExtractionOptions { ExtractFullPath = true, Overwrite = true },
cancellationToken: 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.CreateArchive())
{
// 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.OpenReader(stream))
{
while (reader.MoveToNextEntry())
{
IArchiveEntry 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 (use OpenAsyncReader to get IAsyncReader)
using (var stream = File.OpenRead("file.zip"))
using (var reader = await ReaderFactory.OpenAsyncReader(stream))
{
while (await reader.MoveToNextEntryAsync())
{
await reader.WriteEntryToFileAsync(
@"C:\output\" + reader.Entry.Key,
cancellationToken: cancellationToken
);
}
// Async extraction of all entries
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.OpenWriter(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.OpenArchive("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.Deflate64 // Deflate64
CompressionType.BZip2 // BZip2
CompressionType.LZMA // LZMA (for 7Zip, LZip, XZ)
CompressionType.PPMd // PPMd (for ZIP)
CompressionType.Rar // RAR compression (read-only)
CompressionType.ZStandard // ZStandard
ArchiveType.Arc
ArchiveType.Arj
ArchiveType.Ace
// For Tar archives with compression
// Use WriterFactory to create compressed tar archives
using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Tar, CompressionType.GZip)) // Tar.GZip
using (var writer = WriterFactory.OpenWriter(stream, ArchiveType.Tar, 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<ProgressReport>(report =>
{
Console.WriteLine($"Extracting {report.EntryPath}: {report.PercentComplete}%");
});
var options = new ReaderOptions { Progress = progress };
using (var archive = ZipArchive.OpenArchive("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 = await ZipArchive.OpenAsyncArchive("archive.zip"))
{
await archive.WriteToDirectoryAsync(
@"C:\output",
new ExtractionOptions { ExtractFullPath = true, Overwrite = true },
cancellationToken: cts.Token
);
}
}
catch (OperationCanceledException)
{
Console.WriteLine("Extraction cancelled");
}
```
### Create with Custom Compression
```csharp
using (var archive = ZipArchive.CreateArchive())
{
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.CreateArchive())
{
// 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.OpenArchive("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.OpenArchive("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.OpenArchive(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.OpenArchive(stream))
{
archive.WriteToDirectory(@"C:\output");
}
// Both properly disposed
```
### ✗ Wrong - Mixing API styles
```csharp
// Loading entire archive then iterating
using (var archive = ZipArchive.OpenArchive("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.OpenReader(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

View File

@@ -1,659 +0,0 @@
# 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.OpenReader(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.OpenReader(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.CreateArchive();
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<IEntry> entries = archive.Entries;
foreach (var entry in entries)
{
// Random access - entries already in memory
}
// Reader API - provides iterator
IReader reader = ReaderFactory.OpenReader(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<IEntry> Entries { get; }
void WriteToDirectory(string destinationDirectory,
ExtractionOptions options = null);
IEntry FirstOrDefault(Func<IEntry, bool> 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<NewFormatEntry> _entries;
public static NewFormatArchive OpenArchive(Stream stream)
{
var archive = new NewFormatArchive();
archive._header = NewFormatHeader.Read(stream);
archive.LoadEntries(stream);
return archive;
}
public override IEnumerable<IEntry> 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.OpenArchive(stream);
public IReader CreateReader(Stream stream, ReaderOptions options)
=> new NewFormatReader(stream) { Options = options };
public bool Matches(Stream stream, ReadOnlySpan<byte> 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.OpenArchive(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.OpenArchive(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.OpenArchive(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.OpenArchive(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

View File

@@ -1,610 +0,0 @@
# 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.OpenArchive("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.OpenArchive("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.OpenReader(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.OpenArchive("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.OpenArchive("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.OpenArchive("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.OpenArchive("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.OpenArchive("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.OpenArchive("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.OpenArchive("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.OpenArchive("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.OpenReader(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.CreateArchive())
{
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

View File

@@ -1,474 +0,0 @@
# 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.OpenArchive("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.OpenReader(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.OpenReader(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.OpenArchive(archivePath))
{
archive.WriteToDirectory(@"C:\output");
}
// Stream during download (on-the-fly extraction)
using (var httpStream = await httpClient.GetStreamAsync(url))
using (var reader = ReaderFactory.OpenReader(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.OpenArchive("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.OpenArchive("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.CreateArchive())
{
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.CreateArchive())
{
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.CreateArchive())
{
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.OpenArchive("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.OpenArchive("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.OpenArchive("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.OpenArchive("archive.zip"))
{
archive.WriteToDirectory(@"C:\output");
}
}
// ✓ Better - process multiple entries at once
using (var archive = ZipArchive.OpenArchive("archive.zip"))
{
archive.WriteToDirectory(@"C:\output");
}
```
### 3. Profile Your Code
```csharp
var sw = Stopwatch.StartNew();
using (var archive = ZipArchive.OpenArchive("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

View File

@@ -1,7 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
#if !LEGACY_DOTNET
#if !NETSTANDARD2_0 && !NETSTANDARD2_1 && !NETFRAMEWORK
#define SUPPORTS_RUNTIME_INTRINSICS
#define SUPPORTS_HOTPATH
#endif

View File

@@ -1,13 +1,14 @@
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<TEntry, TVolume> : IArchive, IAsyncArchive
public abstract class AbstractArchive<TEntry, TVolume> : IArchive
where TEntry : IArchiveEntry
where TVolume : IVolume
{
@@ -25,12 +26,6 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IAsyncArchive
_sourceStream = sourceStream;
_lazyVolumes = new LazyReadOnlyCollection<TVolume>(LoadVolumes(_sourceStream));
_lazyEntries = new LazyReadOnlyCollection<TEntry>(LoadEntries(Volumes));
_lazyVolumesAsync = new LazyAsyncReadOnlyCollection<TVolume>(
LoadVolumesAsync(_sourceStream)
);
_lazyEntriesAsync = new LazyAsyncReadOnlyCollection<TEntry>(
LoadEntriesAsync(_lazyVolumesAsync)
);
}
internal AbstractArchive(ArchiveType type)
@@ -39,16 +34,19 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IAsyncArchive
ReaderOptions = new();
_lazyVolumes = new LazyReadOnlyCollection<TVolume>(Enumerable.Empty<TVolume>());
_lazyEntries = new LazyReadOnlyCollection<TEntry>(Enumerable.Empty<TEntry>());
_lazyVolumesAsync = new LazyAsyncReadOnlyCollection<TVolume>(
AsyncEnumerableEx.Empty<TVolume>()
);
_lazyEntriesAsync = new LazyAsyncReadOnlyCollection<TEntry>(
AsyncEnumerableEx.Empty<TEntry>()
);
}
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;
}
/// <summary>
/// Returns an ReadOnlyCollection of all the RarArchiveEntries across the one or many parts of the RarArchive.
/// </summary>
@@ -68,25 +66,12 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IAsyncArchive
/// <summary>
/// The total size of the files as uncompressed in the archive.
/// </summary>
public virtual long TotalUncompressedSize =>
public virtual long TotalUncompressSize =>
Entries.Aggregate(0L, (total, cf) => total + cf.Size);
protected abstract IEnumerable<TVolume> LoadVolumes(SourceStream sourceStream);
protected abstract IEnumerable<TEntry> LoadEntries(IEnumerable<TVolume> volumes);
protected virtual IAsyncEnumerable<TVolume> LoadVolumesAsync(SourceStream sourceStream) =>
LoadVolumes(sourceStream).ToAsyncEnumerable();
protected virtual async IAsyncEnumerable<TEntry> LoadEntriesAsync(
IAsyncEnumerable<TVolume> volumes
)
{
foreach (var item in LoadEntries(await volumes.ToListAsync()))
{
yield return item;
}
}
IEnumerable<IArchiveEntry> IArchive.Entries => Entries.Cast<IArchiveEntry>();
IEnumerable<IVolume> IArchive.Volumes => _lazyVolumes.Cast<IVolume>();
@@ -133,7 +118,6 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IAsyncArchive
}
protected abstract IReader CreateReaderForSolidExtraction();
protected abstract ValueTask<IAsyncReader> CreateReaderForSolidExtractionAsync();
/// <summary>
/// Archive is SOLID (this means the Archive saved bytes by reusing information which helps for archives containing many small files).
@@ -145,19 +129,6 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IAsyncArchive
/// </summary>
public virtual bool IsEncrypted => false;
/// <summary>
/// Returns whether multi-threaded extraction is supported for this archive.
/// Multi-threading is supported when:
/// 1. The archive is opened from a FileInfo or file path (not a stream)
/// 2. Multi-threading is explicitly enabled in ReaderOptions
/// 3. The archive is not SOLID (SOLID archives should use sequential extraction)
/// </summary>
public virtual bool SupportsMultiThreadedExtraction =>
_sourceStream is not null
&& _sourceStream.IsFileMode
&& ReaderOptions.EnableMultiThreadedExtraction
&& !IsSolid;
/// <summary>
/// The archive can find all the parts of the archive needed to fully extract the archive. This forces the parsing of the entire archive.
/// </summary>
@@ -169,85 +140,4 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IAsyncArchive
return Entries.All(x => x.IsComplete);
}
}
#region Async Support
private readonly LazyAsyncReadOnlyCollection<TVolume> _lazyVolumesAsync;
private readonly LazyAsyncReadOnlyCollection<TEntry> _lazyEntriesAsync;
public virtual async ValueTask DisposeAsync()
{
if (!_disposed)
{
await foreach (var v in _lazyVolumesAsync)
{
v.Dispose();
}
foreach (var v in _lazyEntriesAsync.GetLoaded().Cast<Entry>())
{
v.Close();
}
_sourceStream?.Dispose();
_disposed = true;
}
}
private async ValueTask EnsureEntriesLoadedAsync()
{
await _lazyEntriesAsync.EnsureFullyLoaded();
await _lazyVolumesAsync.EnsureFullyLoaded();
}
public virtual IAsyncEnumerable<TEntry> EntriesAsync => _lazyEntriesAsync;
private async IAsyncEnumerable<IArchiveEntry> EntriesAsyncCast()
{
await foreach (var entry in EntriesAsync)
{
yield return entry;
}
}
IAsyncEnumerable<IArchiveEntry> IAsyncArchive.EntriesAsync => EntriesAsyncCast();
private async IAsyncEnumerable<IVolume> VolumesAsyncCast()
{
await foreach (var volume in VolumesAsync)
{
yield return volume;
}
}
public IAsyncEnumerable<IVolume> VolumesAsync => VolumesAsyncCast();
public async ValueTask<IAsyncReader> 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<bool> IsSolidAsync() => new(false);
public async ValueTask<bool> IsCompleteAsync()
{
await EnsureEntriesLoadedAsync();
return await EntriesAsync.AllAsync(x => x.IsComplete);
}
public async ValueTask<long> TotalSizeAsync() =>
await EntriesAsync.AggregateAsync(0L, (total, cf) => total + cf.CompressedSize);
public async ValueTask<long> TotalUncompressedSizeAsync() =>
await EntriesAsync.AggregateAsync(0L, (total, cf) => total + cf.Size);
public ValueTask<bool> IsEncryptedAsync() => new(IsEncrypted);
#endregion
}

View File

@@ -12,8 +12,7 @@ namespace SharpCompress.Archives;
public abstract class AbstractWritableArchive<TEntry, TVolume>
: AbstractArchive<TEntry, TVolume>,
IWritableArchive,
IWritableAsyncArchive
IWritableArchive
where TEntry : IArchiveEntry
where TVolume : IVolume
{
@@ -84,12 +83,12 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
}
}
void IWritableArchiveCommon.RemoveEntry(IArchiveEntry entry) => RemoveEntry((TEntry)entry);
void IWritableArchive.RemoveEntry(IArchiveEntry entry) => RemoveEntry((TEntry)entry);
public TEntry AddEntry(string key, Stream source, long size = 0, DateTime? modified = null) =>
AddEntry(key, source, false, size, modified);
IArchiveEntry IWritableArchiveCommon.AddEntry(
IArchiveEntry IWritableArchive.AddEntry(
string key,
Stream source,
bool closeStream,
@@ -97,7 +96,7 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
DateTime? modified
) => AddEntry(key, source, closeStream, size, modified);
IArchiveEntry IWritableArchiveCommon.AddDirectoryEntry(string key, DateTime? modified) =>
IArchiveEntry IWritableArchive.AddDirectoryEntry(string key, DateTime? modified) =>
AddDirectoryEntry(key, modified);
public TEntry AddEntry(
@@ -163,7 +162,7 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
SaveTo(stream, options, OldEntries, newEntries);
}
public async ValueTask SaveToAsync(
public async Task SaveToAsync(
Stream stream,
WriterOptions options,
CancellationToken cancellationToken = default
@@ -209,7 +208,7 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
IEnumerable<TEntry> newEntries
);
protected abstract ValueTask SaveToAsync(
protected abstract Task SaveToAsync(
Stream stream,
WriterOptions options,
IEnumerable<TEntry> oldEntries,

View File

@@ -2,8 +2,6 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Factories;
using SharpCompress.IO;
@@ -13,26 +11,20 @@ namespace SharpCompress.Archives;
public static class ArchiveFactory
{
public static IArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null)
/// <summary>
/// Opens an Archive for random access
/// </summary>
/// <param name="stream"></param>
/// <param name="readerOptions"></param>
/// <returns></returns>
public static IArchive Open(Stream stream, ReaderOptions? readerOptions = null)
{
readerOptions ??= new ReaderOptions();
stream = SharpCompressStream.Create(stream, bufferSize: readerOptions.BufferSize);
return FindFactory<IArchiveFactory>(stream).OpenArchive(stream, readerOptions);
return FindFactory<IArchiveFactory>(stream).Open(stream, readerOptions);
}
public static async ValueTask<IAsyncArchive> OpenAsyncArchive(
Stream stream,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
readerOptions ??= new ReaderOptions();
stream = SharpCompressStream.Create(stream, bufferSize: readerOptions.BufferSize);
var factory = await FindFactoryAsync<IArchiveFactory>(stream, cancellationToken);
return factory.OpenAsyncArchive(stream, readerOptions);
}
public static IWritableArchive CreateArchive(ArchiveType type)
public static IWritableArchive Create(ArchiveType type)
{
var factory = Factory
.Factories.OfType<IWriteableArchiveFactory>()
@@ -40,51 +32,41 @@ public static class ArchiveFactory
if (factory != null)
{
return factory.CreateArchive();
return factory.CreateWriteableArchive();
}
throw new NotSupportedException("Cannot create Archives of type: " + type);
}
public static IArchive OpenArchive(string filePath, ReaderOptions? options = null)
/// <summary>
/// Constructor expects a filepath to an existing file.
/// </summary>
/// <param name="filePath"></param>
/// <param name="options"></param>
public static IArchive Open(string filePath, ReaderOptions? options = null)
{
filePath.NotNullOrEmpty(nameof(filePath));
return OpenArchive(new FileInfo(filePath), options);
return Open(new FileInfo(filePath), options);
}
public static ValueTask<IAsyncArchive> OpenAsyncArchive(
string filePath,
ReaderOptions? options = null,
CancellationToken cancellationToken = default
)
{
filePath.NotNullOrEmpty(nameof(filePath));
return OpenAsyncArchive(new FileInfo(filePath), options, cancellationToken);
}
public static IArchive OpenArchive(FileInfo fileInfo, ReaderOptions? options = null)
/// <summary>
/// Constructor with a FileInfo object to an existing file.
/// </summary>
/// <param name="fileInfo"></param>
/// <param name="options"></param>
public static IArchive Open(FileInfo fileInfo, ReaderOptions? options = null)
{
options ??= new ReaderOptions { LeaveStreamOpen = false };
return FindFactory<IArchiveFactory>(fileInfo).OpenArchive(fileInfo, options);
return FindFactory<IArchiveFactory>(fileInfo).Open(fileInfo, options);
}
public static async ValueTask<IAsyncArchive> OpenAsyncArchive(
FileInfo fileInfo,
ReaderOptions? options = null,
CancellationToken cancellationToken = default
)
{
options ??= new ReaderOptions { LeaveStreamOpen = false };
var factory = await FindFactoryAsync<IArchiveFactory>(fileInfo, cancellationToken);
return factory.OpenAsyncArchive(fileInfo, options, cancellationToken);
}
public static IArchive OpenArchive(
IEnumerable<FileInfo> fileInfos,
ReaderOptions? options = null
)
/// <summary>
/// Constructor with IEnumerable FileInfo objects, multi and split support.
/// </summary>
/// <param name="fileInfos"></param>
/// <param name="options"></param>
public static IArchive Open(IEnumerable<FileInfo> fileInfos, ReaderOptions? options = null)
{
fileInfos.NotNull(nameof(fileInfos));
var filesArray = fileInfos.ToArray();
@@ -96,42 +78,21 @@ public static class ArchiveFactory
var fileInfo = filesArray[0];
if (filesArray.Length == 1)
{
return OpenArchive(fileInfo, options);
return Open(fileInfo, options);
}
fileInfo.NotNull(nameof(fileInfo));
options ??= new ReaderOptions { LeaveStreamOpen = false };
return FindFactory<IMultiArchiveFactory>(fileInfo).OpenArchive(filesArray, options);
return FindFactory<IMultiArchiveFactory>(fileInfo).Open(filesArray, options);
}
public static async ValueTask<IAsyncArchive> OpenAsyncArchive(
IEnumerable<FileInfo> 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 OpenAsyncArchive(fileInfo, options, cancellationToken);
}
fileInfo.NotNull(nameof(fileInfo));
options ??= new ReaderOptions { LeaveStreamOpen = false };
var factory = await FindFactoryAsync<IMultiArchiveFactory>(fileInfo, cancellationToken);
return factory.OpenAsyncArchive(filesArray, options, cancellationToken);
}
public static IArchive OpenArchive(IEnumerable<Stream> streams, ReaderOptions? options = null)
/// <summary>
/// Constructor with IEnumerable FileInfo objects, multi and split support.
/// </summary>
/// <param name="streams"></param>
/// <param name="options"></param>
public static IArchive Open(IEnumerable<Stream> streams, ReaderOptions? options = null)
{
streams.NotNull(nameof(streams));
var streamsArray = streams.ToArray();
@@ -143,49 +104,25 @@ public static class ArchiveFactory
var firstStream = streamsArray[0];
if (streamsArray.Length == 1)
{
return OpenArchive(firstStream, options);
return Open(firstStream, options);
}
firstStream.NotNull(nameof(firstStream));
options ??= new ReaderOptions();
return FindFactory<IMultiArchiveFactory>(firstStream).OpenArchive(streamsArray, options);
}
public static async ValueTask<IAsyncArchive> OpenAsyncArchive(
IEnumerable<Stream> 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 OpenAsyncArchive(firstStream, options, cancellationToken);
}
firstStream.NotNull(nameof(firstStream));
options ??= new ReaderOptions();
var factory = FindFactory<IMultiArchiveFactory>(firstStream);
return factory.OpenAsyncArchive(streamsArray, options);
return FindFactory<IMultiArchiveFactory>(firstStream).Open(streamsArray, options);
}
/// <summary>
/// Extract to specific directory, retaining filename
/// </summary>
public static void WriteToDirectory(
string sourceArchive,
string destinationDirectory,
ExtractionOptions? options = null
)
{
using var archive = OpenArchive(sourceArchive);
using var archive = Open(sourceArchive);
archive.WriteToDirectory(destinationDirectory, options);
}
@@ -229,52 +166,6 @@ public static class ArchiveFactory
);
}
private static async ValueTask<T> FindFactoryAsync<T>(
FileInfo finfo,
CancellationToken cancellationToken
)
where T : IFactory
{
finfo.NotNull(nameof(finfo));
using Stream stream = finfo.OpenRead();
return await FindFactoryAsync<T>(stream, cancellationToken);
}
private static async ValueTask<T> FindFactoryAsync<T>(
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<T>();
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,
@@ -317,12 +208,22 @@ public static class ArchiveFactory
return false;
}
/// <summary>
/// From a passed in archive (zip, rar, 7z, 001), return all parts.
/// </summary>
/// <param name="part1"></param>
/// <returns></returns>
public static IEnumerable<string> GetFileParts(string part1)
{
part1.NotNullOrEmpty(nameof(part1));
return GetFileParts(new FileInfo(part1)).Select(a => a.FullName);
}
/// <summary>
/// From a passed in archive (zip, rar, 7z, 001), return all parts.
/// </summary>
/// <param name="part1"></param>
/// <returns></returns>
public static IEnumerable<FileInfo> GetFileParts(FileInfo part1)
{
part1.NotNull(nameof(part1));
@@ -336,7 +237,7 @@ public static class ArchiveFactory
if (part != null)
{
yield return part;
while ((part = factory.GetFilePart(i++, part1)) != null)
while ((part = factory.GetFilePart(i++, part1)) != null) //tests split too
{
yield return part;
}

View File

@@ -1,14 +1,12 @@
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;
internal class AutoArchiveFactory : IArchiveFactory
class AutoArchiveFactory : IArchiveFactory
{
public string Name => nameof(AutoArchiveFactory);
@@ -22,31 +20,11 @@ internal class AutoArchiveFactory : IArchiveFactory
int bufferSize = ReaderOptions.DefaultBufferSize
) => throw new NotSupportedException();
public ValueTask<bool> 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 OpenArchive(Stream stream, ReaderOptions? readerOptions = null) =>
ArchiveFactory.OpenArchive(stream, readerOptions);
public IArchive Open(Stream stream, ReaderOptions? readerOptions = null) =>
ArchiveFactory.Open(stream, readerOptions);
public IAsyncArchive OpenAsyncArchive(Stream stream, ReaderOptions? readerOptions = null) =>
(IAsyncArchive)OpenArchive(stream, readerOptions);
public IArchive OpenArchive(FileInfo fileInfo, ReaderOptions? readerOptions = null) =>
ArchiveFactory.OpenArchive(fileInfo, readerOptions);
public IAsyncArchive OpenAsyncArchive(
FileInfo fileInfo,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return (IAsyncArchive)OpenArchive(fileInfo, readerOptions);
}
public IArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null) =>
ArchiveFactory.Open(fileInfo, readerOptions);
}

View File

@@ -1,197 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.GZip;
using SharpCompress.IO;
using SharpCompress.Readers;
using SharpCompress.Readers.GZip;
using SharpCompress.Writers;
using SharpCompress.Writers.GZip;
namespace SharpCompress.Archives.GZip;
public partial class GZipArchive
#if NET8_0_OR_GREATER
: IWritableArchiveOpenable,
IMultiArchiveOpenable<IWritableArchive, IWritableAsyncArchive>
#endif
{
public static IWritableAsyncArchive OpenAsyncArchive(
string path,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
path.NotNullOrEmpty(nameof(path));
return (IWritableAsyncArchive)OpenArchive(
new FileInfo(path),
readerOptions ?? new ReaderOptions()
);
}
public static IWritableArchive OpenArchive(string filePath, ReaderOptions? readerOptions = null)
{
filePath.NotNullOrEmpty(nameof(filePath));
return OpenArchive(new FileInfo(filePath), readerOptions ?? new ReaderOptions());
}
public static IWritableArchive OpenArchive(
FileInfo fileInfo,
ReaderOptions? readerOptions = null
)
{
fileInfo.NotNull(nameof(fileInfo));
return new GZipArchive(
new SourceStream(
fileInfo,
i => ArchiveVolumeFactory.GetFilePart(i, fileInfo),
readerOptions ?? new ReaderOptions()
)
);
}
public static IWritableArchive OpenArchive(
IEnumerable<FileInfo> fileInfos,
ReaderOptions? readerOptions = null
)
{
fileInfos.NotNull(nameof(fileInfos));
var files = fileInfos.ToArray();
return new GZipArchive(
new SourceStream(
files[0],
i => i < files.Length ? files[i] : null,
readerOptions ?? new ReaderOptions()
)
);
}
public static IWritableArchive OpenArchive(
IEnumerable<Stream> streams,
ReaderOptions? readerOptions = null
)
{
streams.NotNull(nameof(streams));
var strms = streams.ToArray();
return new GZipArchive(
new SourceStream(
strms[0],
i => i < strms.Length ? strms[i] : null,
readerOptions ?? new ReaderOptions()
)
);
}
public static IWritableArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null)
{
stream.NotNull(nameof(stream));
if (stream is not { CanSeek: true })
{
throw new ArgumentException("Stream must be seekable", nameof(stream));
}
return new GZipArchive(
new SourceStream(stream, _ => null, readerOptions ?? new ReaderOptions())
);
}
public static IWritableAsyncArchive OpenAsyncArchive(
Stream stream,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return (IWritableAsyncArchive)OpenArchive(stream, readerOptions);
}
public static IWritableAsyncArchive OpenAsyncArchive(
FileInfo fileInfo,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return (IWritableAsyncArchive)OpenArchive(fileInfo, readerOptions);
}
public static IWritableAsyncArchive OpenAsyncArchive(
IReadOnlyList<Stream> streams,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return (IWritableAsyncArchive)OpenArchive(streams, readerOptions);
}
public static IWritableAsyncArchive OpenAsyncArchive(
IReadOnlyList<FileInfo> fileInfos,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return (IWritableAsyncArchive)OpenArchive(fileInfos, readerOptions);
}
public static IWritableArchive CreateArchive() => new GZipArchive();
public static IWritableAsyncArchive CreateAsyncArchive() => new GZipArchive();
public static bool IsGZipFile(string filePath) => IsGZipFile(new FileInfo(filePath));
public static bool IsGZipFile(FileInfo fileInfo)
{
if (!fileInfo.Exists)
{
return false;
}
using Stream stream = fileInfo.OpenRead();
return IsGZipFile(stream);
}
public static bool IsGZipFile(Stream stream)
{
Span<byte> header = stackalloc byte[10];
if (!stream.ReadFully(header))
{
return false;
}
if (header[0] != 0x1F || header[1] != 0x8B || header[2] != 8)
{
return false;
}
return true;
}
public static async ValueTask<bool> IsGZipFileAsync(
Stream stream,
CancellationToken cancellationToken = default
)
{
byte[] header = new byte[10];
if (!await stream.ReadFullyAsync(header, cancellationToken).ConfigureAwait(false))
{
return false;
}
if (header[0] != 0x1F || header[1] != 0x8B || header[2] != 8)
{
return false;
}
return true;
}
}

View File

@@ -14,20 +14,122 @@ using SharpCompress.Writers.GZip;
namespace SharpCompress.Archives.GZip;
public partial class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
{
/// <summary>
/// Constructor expects a filepath to an existing file.
/// </summary>
/// <param name="filePath"></param>
/// <param name="readerOptions"></param>
public static GZipArchive Open(string filePath, ReaderOptions? readerOptions = null)
{
filePath.NotNullOrEmpty(nameof(filePath));
return Open(new FileInfo(filePath), readerOptions ?? new ReaderOptions());
}
/// <summary>
/// Constructor with a FileInfo object to an existing file.
/// </summary>
/// <param name="fileInfo"></param>
/// <param name="readerOptions"></param>
public static GZipArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
fileInfo.NotNull(nameof(fileInfo));
return new GZipArchive(
new SourceStream(
fileInfo,
i => ArchiveVolumeFactory.GetFilePart(i, fileInfo),
readerOptions ?? new ReaderOptions()
)
);
}
/// <summary>
/// Constructor with all file parts passed in
/// </summary>
/// <param name="fileInfos"></param>
/// <param name="readerOptions"></param>
public static GZipArchive Open(
IEnumerable<FileInfo> fileInfos,
ReaderOptions? readerOptions = null
)
{
fileInfos.NotNull(nameof(fileInfos));
var files = fileInfos.ToArray();
return new GZipArchive(
new SourceStream(
files[0],
i => i < files.Length ? files[i] : null,
readerOptions ?? new ReaderOptions()
)
);
}
/// <summary>
/// Constructor with all stream parts passed in
/// </summary>
/// <param name="streams"></param>
/// <param name="readerOptions"></param>
public static GZipArchive Open(IEnumerable<Stream> streams, ReaderOptions? readerOptions = null)
{
streams.NotNull(nameof(streams));
var strms = streams.ToArray();
return new GZipArchive(
new SourceStream(
strms[0],
i => i < strms.Length ? strms[i] : null,
readerOptions ?? new ReaderOptions()
)
);
}
/// <summary>
/// Takes a seekable Stream as a source
/// </summary>
/// <param name="stream"></param>
/// <param name="readerOptions"></param>
public static GZipArchive Open(Stream stream, ReaderOptions? readerOptions = null)
{
stream.NotNull(nameof(stream));
if (stream is not { CanSeek: true })
{
throw new ArgumentException("Stream must be seekable", nameof(stream));
}
return new GZipArchive(
new SourceStream(stream, _ => null, readerOptions ?? new ReaderOptions())
);
}
public static GZipArchive Create() => new();
/// <summary>
/// Constructor with a SourceStream able to handle FileInfo and Streams.
/// </summary>
/// <param name="sourceStream"></param>
private GZipArchive(SourceStream sourceStream)
: base(ArchiveType.GZip, sourceStream) { }
internal GZipArchive()
: base(ArchiveType.GZip) { }
protected override IEnumerable<GZipVolume> LoadVolumes(SourceStream sourceStream)
{
sourceStream.LoadAllParts();
return sourceStream.Streams.Select(a => new GZipVolume(a, ReaderOptions, 0));
}
public static bool IsGZipFile(string filePath) => IsGZipFile(new FileInfo(filePath));
public static bool IsGZipFile(FileInfo fileInfo)
{
if (!fileInfo.Exists)
{
return false;
}
using Stream stream = fileInfo.OpenRead();
return IsGZipFile(stream);
}
public void SaveTo(string filePath) => SaveTo(new FileInfo(filePath));
public void SaveTo(FileInfo fileInfo)
@@ -36,19 +138,38 @@ public partial class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZi
SaveTo(stream, new WriterOptions(CompressionType.GZip));
}
public ValueTask SaveToAsync(string filePath, CancellationToken cancellationToken = default) =>
public Task SaveToAsync(string filePath, CancellationToken cancellationToken = default) =>
SaveToAsync(new FileInfo(filePath), cancellationToken);
public async ValueTask SaveToAsync(
FileInfo fileInfo,
CancellationToken cancellationToken = default
)
public async Task SaveToAsync(FileInfo fileInfo, CancellationToken cancellationToken = default)
{
using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write);
await SaveToAsync(stream, new WriterOptions(CompressionType.GZip), cancellationToken)
.ConfigureAwait(false);
}
public static bool IsGZipFile(Stream stream)
{
// read the header on the first read
Span<byte> header = stackalloc byte[10];
// workitem 8501: handle edge case (decompress empty stream)
if (!stream.ReadFully(header))
{
return false;
}
if (header[0] != 0x1F || header[1] != 0x8B || header[2] != 8)
{
return false;
}
return true;
}
internal GZipArchive()
: base(ArchiveType.GZip) { }
protected override GZipArchiveEntry CreateEntryInternal(
string filePath,
Stream source,
@@ -92,7 +213,7 @@ public partial class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZi
}
}
protected override async ValueTask SaveToAsync(
protected override async Task SaveToAsync(
Stream stream,
WriterOptions options,
IEnumerable<GZipArchiveEntry> oldEntries,
@@ -119,18 +240,7 @@ public partial class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZi
var stream = volumes.Single().Stream;
yield return new GZipArchiveEntry(
this,
GZipFilePart.Create(stream, ReaderOptions.ArchiveEncoding)
);
}
protected override async IAsyncEnumerable<GZipArchiveEntry> LoadEntriesAsync(
IAsyncEnumerable<GZipVolume> volumes
)
{
var stream = (await volumes.SingleAsync()).Stream;
yield return new GZipArchiveEntry(
this,
await GZipFilePart.CreateAsync(stream, ReaderOptions.ArchiveEncoding)
new GZipFilePart(stream, ReaderOptions.ArchiveEncoding)
);
}
@@ -138,13 +248,6 @@ public partial class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZi
{
var stream = Volumes.Single().Stream;
stream.Position = 0;
return GZipReader.OpenReader(stream);
}
protected override ValueTask<IAsyncReader> CreateReaderForSolidExtractionAsync()
{
var stream = Volumes.Single().Stream;
stream.Position = 0;
return new((IAsyncReader)GZipReader.OpenReader(stream));
return GZipReader.Open(stream);
}
}

View File

@@ -23,10 +23,10 @@ public class GZipArchiveEntry : GZipEntry, IArchiveEntry
return Parts.Single().GetCompressedStream().NotNull();
}
public ValueTask<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default)
public virtual Task<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default)
{
// GZip synchronous implementation is fast enough, just wrap it
return new(OpenEntryStream());
return Task.FromResult(OpenEntryStream());
}
#region IArchiveEntry Members

View File

@@ -38,18 +38,5 @@ public interface IArchive : IDisposable
/// <summary>
/// The total size of the files as uncompressed in the archive.
/// </summary>
long TotalUncompressedSize { get; }
/// <summary>
/// Returns whether the archive is encrypted.
/// </summary>
bool IsEncrypted { get; }
/// <summary>
/// Returns whether multi-threaded extraction is supported for this archive.
/// Multi-threading is supported when the archive is opened from a FileInfo or file path
/// (not a stream) and the format supports random access (e.g., Zip, Tar, Rar).
/// SOLID archives (some Rar, all 7Zip) should use sequential extraction for best performance.
/// </summary>
bool SupportsMultiThreadedExtraction { get; }
long TotalUncompressSize { get; }
}

View File

@@ -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.
/// </summary>
ValueTask<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default);
Task<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default);
/// <summary>
/// The archive can find all the parts of the archive needed to extract this entry.

View File

@@ -37,7 +37,7 @@ public static class IArchiveEntryExtensions
/// <param name="streamToWriteTo">The stream to write the entry content to.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <param name="progress">Optional progress reporter for tracking extraction progress.</param>
public async ValueTask WriteToAsync(
public async Task WriteToAsync(
Stream streamToWriteTo,
IProgress<ProgressReport>? progress = null,
CancellationToken cancellationToken = default
@@ -110,20 +110,18 @@ public static class IArchiveEntryExtensions
/// <summary>
/// Extract to specific directory asynchronously, retaining filename
/// </summary>
public async ValueTask WriteToDirectoryAsync(
public Task WriteToDirectoryAsync(
string destinationDirectory,
ExtractionOptions? options = null,
CancellationToken cancellationToken = default
) =>
await ExtractionMethods
.WriteEntryToDirectoryAsync(
entry,
destinationDirectory,
options,
entry.WriteToFileAsync,
cancellationToken
)
.ConfigureAwait(false);
ExtractionMethods.WriteEntryToDirectoryAsync(
entry,
destinationDirectory,
options,
entry.WriteToFileAsync,
cancellationToken
);
/// <summary>
/// Extract to specific file
@@ -143,23 +141,21 @@ public static class IArchiveEntryExtensions
/// <summary>
/// Extract to specific file asynchronously
/// </summary>
public async ValueTask WriteToFileAsync(
public Task WriteToFileAsync(
string destinationFileName,
ExtractionOptions? options = null,
CancellationToken cancellationToken = default
) =>
await ExtractionMethods
.WriteEntryToFileAsync(
entry,
destinationFileName,
options,
async (x, fm, ct) =>
{
using var fs = File.Open(destinationFileName, fm);
await entry.WriteToAsync(fs, null, ct).ConfigureAwait(false);
},
cancellationToken
)
.ConfigureAwait(false);
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
);
}
}

View File

@@ -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.Readers;
@@ -8,6 +10,7 @@ namespace SharpCompress.Archives;
public static class IArchiveExtensions
{
/// <param name="archive">The archive to extract.</param>
extension(IArchive archive)
{
/// <summary>
@@ -22,6 +25,7 @@ public static class IArchiveExtensions
IProgress<ProgressReport>? progress = null
)
{
// For solid archives (Rar, 7Zip), use the optimized reader-based approach
if (archive.IsSolid || archive.Type == ArchiveType.SevenZip)
{
using var reader = archive.ExtractAllEntries();
@@ -29,6 +33,7 @@ public static class IArchiveExtensions
}
else
{
// For non-solid archives, extract entries directly
archive.WriteToDirectoryInternal(destinationDirectory, options, progress);
}
}
@@ -39,10 +44,14 @@ public static class IArchiveExtensions
IProgress<ProgressReport>? progress
)
{
var totalBytes = archive.TotalUncompressedSize;
// Prepare for progress reporting
var totalBytes = archive.TotalUncompressSize;
var bytesRead = 0L;
// Tracking for created directories.
var seenDirectories = new HashSet<string>();
// Extract
foreach (var entry in archive.Entries)
{
if (entry.IsDirectory)
@@ -61,8 +70,94 @@ public static class IArchiveExtensions
continue;
}
// Use the entry's WriteToDirectory method which respects ExtractionOptions
entry.WriteToDirectory(destinationDirectory, options);
// Update progress
bytesRead += entry.Size;
progress?.Report(
new ProgressReport(entry.Key ?? string.Empty, bytesRead, totalBytes)
);
}
}
/// <summary>
/// Extract to specific directory asynchronously with progress reporting and cancellation support
/// </summary>
/// <param name="destinationDirectory">The folder to extract into.</param>
/// <param name="options">Extraction options.</param>
/// <param name="progress">Optional progress reporter for tracking extraction progress.</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
public async Task WriteToDirectoryAsync(
string destinationDirectory,
ExtractionOptions? options = null,
IProgress<ProgressReport>? 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<ProgressReport>? progress,
CancellationToken cancellationToken
)
{
// Prepare for progress reporting
var totalBytes = archive.TotalUncompressSize;
var bytesRead = 0L;
// Tracking for created directories.
var seenDirectories = new HashSet<string>();
// 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)

View File

@@ -1,5 +1,4 @@
using System.IO;
using System.Threading;
using SharpCompress.Factories;
using SharpCompress.Readers;
@@ -25,31 +24,12 @@ public interface IArchiveFactory : IFactory
/// </summary>
/// <param name="stream">An open, readable and seekable stream.</param>
/// <param name="readerOptions">reading options.</param>
IArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null);
/// <summary>
/// Opens an Archive for random access asynchronously.
/// </summary>
/// <param name="stream">An open, readable and seekable stream.</param>
/// <param name="readerOptions">reading options.</param>
IAsyncArchive OpenAsyncArchive(Stream stream, ReaderOptions? readerOptions = null);
IArchive Open(Stream stream, ReaderOptions? readerOptions = null);
/// <summary>
/// Constructor with a FileInfo object to an existing file.
/// </summary>
/// <param name="fileInfo">the file to open.</param>
/// <param name="readerOptions">reading options.</param>
IArchive OpenArchive(FileInfo fileInfo, ReaderOptions? readerOptions = null);
/// <summary>
/// Opens an Archive from a FileInfo object asynchronously.
/// </summary>
/// <param name="fileInfo">the file to open.</param>
/// <param name="readerOptions">reading options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
IAsyncArchive OpenAsyncArchive(
FileInfo fileInfo,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
);
IArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null);
}

View File

@@ -1,40 +0,0 @@
#if NET8_0_OR_GREATER
using System.IO;
using System.Threading;
using SharpCompress.Readers;
namespace SharpCompress.Archives;
public interface IArchiveOpenable<TSync, TASync>
where TSync : IArchive
where TASync : IAsyncArchive
{
public static abstract TSync OpenArchive(string filePath, ReaderOptions? readerOptions = null);
public static abstract TSync OpenArchive(
FileInfo fileInfo,
ReaderOptions? readerOptions = null
);
public static abstract TSync OpenArchive(Stream stream, ReaderOptions? readerOptions = null);
public static abstract TASync OpenAsyncArchive(
string path,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
);
public static abstract TASync OpenAsyncArchive(
Stream stream,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
);
public static abstract TASync OpenAsyncArchive(
FileInfo fileInfo,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
);
}
#endif

View File

@@ -1,48 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Readers;
namespace SharpCompress.Archives;
public interface IAsyncArchive : IAsyncDisposable
{
IAsyncEnumerable<IArchiveEntry> EntriesAsync { get; }
IAsyncEnumerable<IVolume> VolumesAsync { get; }
ArchiveType Type { get; }
/// <summary>
/// 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.
/// </summary>
ValueTask<IAsyncReader> ExtractAllEntriesAsync();
/// <summary>
/// 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.
/// </summary>
ValueTask<bool> IsSolidAsync();
/// <summary>
/// This checks to see if all the known entries have IsComplete = true
/// </summary>
ValueTask<bool> IsCompleteAsync();
/// <summary>
/// The total size of the files compressed in the archive.
/// </summary>
ValueTask<long> TotalSizeAsync();
/// <summary>
/// The total size of the files as uncompressed in the archive.
/// </summary>
ValueTask<long> TotalUncompressedSizeAsync();
/// <summary>
/// Returns whether the archive is encrypted.
/// </summary>
ValueTask<bool> IsEncryptedAsync();
}

View File

@@ -1,92 +0,0 @@
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
{
extension(IAsyncArchive archive)
{
/// <summary>
/// Extract to specific directory asynchronously with progress reporting and cancellation support
/// </summary>
/// <param name="archive">The archive to extract.</param>
/// <param name="destinationDirectory">The folder to extract into.</param>
/// <param name="options">Extraction options.</param>
/// <param name="progress">Optional progress reporter for tracking extraction progress.</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
public async Task WriteToDirectoryAsync(
string destinationDirectory,
ExtractionOptions? options = null,
IProgress<ProgressReport>? progress = null,
CancellationToken cancellationToken = default
)
{
if (await archive.IsSolidAsync() || archive.Type == ArchiveType.SevenZip)
{
await using var reader = await archive.ExtractAllEntriesAsync();
await reader.WriteAllToDirectoryAsync(
destinationDirectory,
options,
cancellationToken
);
}
else
{
await archive.WriteToDirectoryAsyncInternal(
destinationDirectory,
options,
progress,
cancellationToken
);
}
}
private async Task WriteToDirectoryAsyncInternal(
string destinationDirectory,
ExtractionOptions? options,
IProgress<ProgressReport>? progress,
CancellationToken cancellationToken
)
{
var totalBytes = await archive.TotalUncompressedSizeAsync();
var bytesRead = 0L;
var seenDirectories = new HashSet<string>();
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;
}
await entry
.WriteToDirectoryAsync(destinationDirectory, options, cancellationToken)
.ConfigureAwait(false);
bytesRead += entry.Size;
progress?.Report(
new ProgressReport(entry.Key ?? string.Empty, bytesRead, totalBytes)
);
}
}
}
}

View File

@@ -1,6 +1,5 @@
using System.Collections.Generic;
using System.IO;
using System.Threading;
using SharpCompress.Factories;
using SharpCompress.Readers;
@@ -26,34 +25,12 @@ public interface IMultiArchiveFactory : IFactory
/// </summary>
/// <param name="streams"></param>
/// <param name="readerOptions">reading options.</param>
IArchive OpenArchive(IReadOnlyList<Stream> streams, ReaderOptions? readerOptions = null);
/// <summary>
/// Opens a multi-part archive from streams asynchronously.
/// </summary>
/// <param name="streams"></param>
/// <param name="readerOptions">reading options.</param>
IAsyncArchive OpenAsyncArchive(
IReadOnlyList<Stream> streams,
ReaderOptions? readerOptions = null
);
IArchive Open(IReadOnlyList<Stream> streams, ReaderOptions? readerOptions = null);
/// <summary>
/// Constructor with IEnumerable Stream objects, multi and split support.
/// </summary>
/// <param name="fileInfos"></param>
/// <param name="readerOptions">reading options.</param>
IArchive OpenArchive(IReadOnlyList<FileInfo> fileInfos, ReaderOptions? readerOptions = null);
/// <summary>
/// Opens a multi-part archive from files asynchronously.
/// </summary>
/// <param name="fileInfos"></param>
/// <param name="readerOptions">reading options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
IAsyncArchive OpenAsyncArchive(
IReadOnlyList<FileInfo> fileInfos,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
);
IArchive Open(IReadOnlyList<FileInfo> fileInfos, ReaderOptions? readerOptions = null);
}

View File

@@ -1,35 +0,0 @@
#if NET8_0_OR_GREATER
using System.Collections.Generic;
using System.IO;
using System.Threading;
using SharpCompress.Readers;
namespace SharpCompress.Archives;
public interface IMultiArchiveOpenable<TSync, TASync>
where TSync : IArchive
where TASync : IAsyncArchive
{
public static abstract TSync OpenArchive(
IEnumerable<FileInfo> fileInfos,
ReaderOptions? readerOptions = null
);
public static abstract TSync OpenArchive(
IEnumerable<Stream> streams,
ReaderOptions? readerOptions = null
);
public static abstract TASync OpenAsyncArchive(
IReadOnlyList<Stream> streams,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
);
public static abstract TASync OpenAsyncArchive(
IReadOnlyList<FileInfo> fileInfos,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
);
}
#endif

View File

@@ -6,17 +6,8 @@ using SharpCompress.Writers;
namespace SharpCompress.Archives;
public interface IWritableArchiveCommon
public interface IWritableArchive : IArchive
{
/// <summary>
/// Use this to pause entry rebuilding when adding large collections of entries. Dispose when complete. A using statement is recommended.
/// </summary>
/// <returns>IDisposeable to resume entry rebuilding</returns>
IDisposable PauseEntryRebuilding();
/// <summary>
/// Removes the specified entry from the archive.
/// </summary>
void RemoveEntry(IArchiveEntry entry);
IArchiveEntry AddEntry(
@@ -28,24 +19,18 @@ public interface IWritableArchiveCommon
);
IArchiveEntry AddDirectoryEntry(string key, DateTime? modified = null);
}
public interface IWritableArchive : IArchive, IWritableArchiveCommon
{
/// <summary>
/// Saves the archive to the specified stream using the given writer options.
/// </summary>
void SaveTo(Stream stream, WriterOptions options);
}
public interface IWritableAsyncArchive : IAsyncArchive, IWritableArchiveCommon
{
/// <summary>
/// Asynchronously saves the archive to the specified stream using the given writer options.
/// </summary>
ValueTask SaveToAsync(
Task SaveToAsync(
Stream stream,
WriterOptions options,
CancellationToken cancellationToken = default
);
/// <summary>
/// Use this to pause entry rebuilding when adding large collections of entries. Dispose when complete. A using statement is recommended.
/// </summary>
/// <returns>IDisposeable to resume entry rebuilding</returns>
IDisposable PauseEntryRebuilding();
}

View File

@@ -1,59 +0,0 @@
using System;
using System.IO;
namespace SharpCompress.Archives;
public static class IWritableArchiveCommonExtensions
{
extension(IWritableArchiveCommon writableArchive)
{
public void AddAllFromDirectory(
string filePath,
string searchPattern = "*.*",
SearchOption searchOption = SearchOption.AllDirectories
)
{
using (writableArchive.PauseEntryRebuilding())
{
foreach (
var path in Directory.EnumerateFiles(filePath, searchPattern, searchOption)
)
{
var fileInfo = new FileInfo(path);
writableArchive.AddEntry(
path.Substring(filePath.Length),
fileInfo.OpenRead(),
true,
fileInfo.Length,
fileInfo.LastWriteTime
);
}
}
}
public IArchiveEntry AddEntry(string key, string file) =>
writableArchive.AddEntry(key, new FileInfo(file));
public IArchiveEntry AddEntry(
string key,
Stream source,
long size = 0,
DateTime? modified = null
) => writableArchive.AddEntry(key, source, false, size, modified);
public IArchiveEntry AddEntry(string key, FileInfo fileInfo)
{
if (!fileInfo.Exists)
{
throw new ArgumentException("FileInfo does not exist.");
}
return writableArchive.AddEntry(
key,
fileInfo.OpenRead(),
true,
fileInfo.Length,
fileInfo.LastWriteTime
);
}
}
}

View File

@@ -1,20 +1,106 @@
using System;
using System.IO;
using SharpCompress.Common;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Writers;
namespace SharpCompress.Archives;
public static class IWritableArchiveExtensions
{
extension(IWritableArchive writableArchive)
public static void AddEntry(
this IWritableArchive writableArchive,
string entryPath,
string filePath
)
{
public void SaveTo(string filePath, WriterOptions? options = null) =>
writableArchive.SaveTo(new FileInfo(filePath), options ?? new(CompressionType.Deflate));
public void SaveTo(FileInfo fileInfo, WriterOptions? options = null)
var fileInfo = new FileInfo(filePath);
if (!fileInfo.Exists)
{
using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write);
writableArchive.SaveTo(stream, options ?? new(CompressionType.Deflate));
throw new FileNotFoundException("Could not AddEntry: " + filePath);
}
writableArchive.AddEntry(
entryPath,
new FileInfo(filePath).OpenRead(),
true,
fileInfo.Length,
fileInfo.LastWriteTime
);
}
public static void SaveTo(
this IWritableArchive writableArchive,
string filePath,
WriterOptions options
) => writableArchive.SaveTo(new FileInfo(filePath), options);
public static void SaveTo(
this IWritableArchive writableArchive,
FileInfo fileInfo,
WriterOptions options
)
{
using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write);
writableArchive.SaveTo(stream, options);
}
public static Task SaveToAsync(
this IWritableArchive writableArchive,
string filePath,
WriterOptions options,
CancellationToken cancellationToken = default
) => writableArchive.SaveToAsync(new FileInfo(filePath), options, cancellationToken);
public static async Task SaveToAsync(
this IWritableArchive writableArchive,
FileInfo fileInfo,
WriterOptions options,
CancellationToken cancellationToken = default
)
{
using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write);
await writableArchive.SaveToAsync(stream, options, cancellationToken).ConfigureAwait(false);
}
public static void AddAllFromDirectory(
this IWritableArchive writableArchive,
string filePath,
string searchPattern = "*.*",
SearchOption searchOption = SearchOption.AllDirectories
)
{
using (writableArchive.PauseEntryRebuilding())
{
foreach (var path in Directory.EnumerateFiles(filePath, searchPattern, searchOption))
{
var fileInfo = new FileInfo(path);
writableArchive.AddEntry(
path.Substring(filePath.Length),
fileInfo.OpenRead(),
true,
fileInfo.Length,
fileInfo.LastWriteTime
);
}
}
}
public static IArchiveEntry AddEntry(
this IWritableArchive writableArchive,
string key,
FileInfo fileInfo
)
{
if (!fileInfo.Exists)
{
throw new ArgumentException("FileInfo does not exist.");
}
return writableArchive.AddEntry(
key,
fileInfo.OpenRead(),
true,
fileInfo.Length,
fileInfo.LastWriteTime
);
}
}

View File

@@ -1,10 +0,0 @@
#if NET8_0_OR_GREATER
namespace SharpCompress.Archives;
public interface IWritableArchiveOpenable
: IArchiveOpenable<IWritableArchive, IWritableAsyncArchive>
{
public static abstract IWritableArchive CreateArchive();
public static abstract IWritableAsyncArchive CreateAsyncArchive();
}
#endif

View File

@@ -1,36 +0,0 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Writers;
namespace SharpCompress.Archives;
public static class IWritableAsyncArchiveExtensions
{
extension(IWritableAsyncArchive writableArchive)
{
public ValueTask SaveToAsync(
string filePath,
WriterOptions? options = null,
CancellationToken cancellationToken = default
) =>
writableArchive.SaveToAsync(
new FileInfo(filePath),
options ?? new(CompressionType.Deflate),
cancellationToken
);
public async ValueTask SaveToAsync(
FileInfo fileInfo,
WriterOptions? options = null,
CancellationToken cancellationToken = default
)
{
using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write);
await writableArchive
.SaveToAsync(stream, options ?? new(CompressionType.Deflate), cancellationToken)
.ConfigureAwait(false);
}
}
}

View File

@@ -16,5 +16,5 @@ public interface IWriteableArchiveFactory : Factories.IFactory
/// Creates a new, empty archive, ready to be written.
/// </summary>
/// <returns></returns>
IWritableArchive CreateArchive();
IWritableArchive CreateWriteableArchive();
}

View File

@@ -1,36 +1,18 @@
using System.Linq;
using System.Threading.Tasks;
using SharpCompress.Common.Rar;
using System.Linq;
namespace SharpCompress.Archives.Rar;
public static class RarArchiveExtensions
{
extension(IRarArchive archive)
{
/// <summary>
/// RarArchive is the first volume of a multi-part archive. If MultipartVolume is true and IsFirstVolume is false then the first volume file must be missing.
/// </summary>
public bool IsFirstVolume() => archive.Volumes.Cast<RarVolume>().First().IsFirstVolume;
/// <summary>
/// RarArchive is the first volume of a multi-part archive. If MultipartVolume is true and IsFirstVolume is false then the first volume file must be missing.
/// </summary>
public static bool IsFirstVolume(this RarArchive archive) =>
archive.Volumes.First().IsFirstVolume;
/// <summary>
/// RarArchive is part of a multi-part archive.
/// </summary>
public bool IsMultipartVolume() => archive.Volumes.Cast<RarVolume>().First().IsMultiVolume;
}
extension(IRarAsyncArchive archive)
{
/// <summary>
/// RarArchive is the first volume of a multi-part archive. If MultipartVolume is true and IsFirstVolume is false then the first volume file must be missing.
/// </summary>
public async ValueTask<bool> IsFirstVolumeAsync() =>
(await archive.VolumesAsync.CastAsync<RarVolume>().FirstAsync()).IsFirstVolume;
/// <summary>
/// RarArchive is part of a multi-part archive.
/// </summary>
public async ValueTask<bool> IsMultipartVolumeAsync() =>
(await archive.VolumesAsync.CastAsync<RarVolume>().FirstAsync()).IsMultiVolume;
}
/// <summary>
/// RarArchive is part of a multi-part archive.
/// </summary>
public static bool IsMultipartVolume(this RarArchive archive) =>
archive.Volumes.First().IsMultiVolume;
}

View File

@@ -1,166 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using SharpCompress.Common;
using SharpCompress.Common.Rar;
using SharpCompress.Common.Rar.Headers;
using SharpCompress.Compressors.Rar;
using SharpCompress.IO;
using SharpCompress.Readers;
using SharpCompress.Readers.Rar;
namespace SharpCompress.Archives.Rar;
public partial class RarArchive
#if NET8_0_OR_GREATER
: IArchiveOpenable<IRarArchive, IRarAsyncArchive>,
IMultiArchiveOpenable<IRarArchive, IRarAsyncArchive>
#endif
{
public static IRarAsyncArchive OpenAsyncArchive(
string path,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
path.NotNullOrEmpty(nameof(path));
return (IRarAsyncArchive)OpenArchive(new FileInfo(path), readerOptions);
}
public static IRarArchive OpenArchive(string filePath, ReaderOptions? options = null)
{
filePath.NotNullOrEmpty(nameof(filePath));
var fileInfo = new FileInfo(filePath);
return new RarArchive(
new SourceStream(
fileInfo,
i => RarArchiveVolumeFactory.GetFilePart(i, fileInfo),
options ?? new ReaderOptions()
)
);
}
public static IRarArchive OpenArchive(FileInfo fileInfo, ReaderOptions? options = null)
{
fileInfo.NotNull(nameof(fileInfo));
return new RarArchive(
new SourceStream(
fileInfo,
i => RarArchiveVolumeFactory.GetFilePart(i, fileInfo),
options ?? new ReaderOptions()
)
);
}
public static IRarArchive OpenArchive(Stream stream, ReaderOptions? options = null)
{
stream.NotNull(nameof(stream));
if (stream is not { CanSeek: true })
{
throw new ArgumentException("Stream must be seekable", nameof(stream));
}
return new RarArchive(new SourceStream(stream, _ => null, options ?? new ReaderOptions()));
}
public static IRarArchive OpenArchive(
IEnumerable<FileInfo> fileInfos,
ReaderOptions? readerOptions = null
)
{
fileInfos.NotNull(nameof(fileInfos));
var files = fileInfos.ToArray();
return new RarArchive(
new SourceStream(
files[0],
i => i < files.Length ? files[i] : null,
readerOptions ?? new ReaderOptions()
)
);
}
public static IRarArchive OpenArchive(
IEnumerable<Stream> streams,
ReaderOptions? readerOptions = null
)
{
streams.NotNull(nameof(streams));
var strms = streams.ToArray();
return new RarArchive(
new SourceStream(
strms[0],
i => i < strms.Length ? strms[i] : null,
readerOptions ?? new ReaderOptions()
)
);
}
public static IRarAsyncArchive OpenAsyncArchive(
Stream stream,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return (IRarAsyncArchive)OpenArchive(stream, readerOptions);
}
public static IRarAsyncArchive OpenAsyncArchive(
FileInfo fileInfo,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return (IRarAsyncArchive)OpenArchive(fileInfo, readerOptions);
}
public static IRarAsyncArchive OpenAsyncArchive(
IReadOnlyList<Stream> streams,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return (IRarAsyncArchive)OpenArchive(streams, readerOptions);
}
public static IRarAsyncArchive OpenAsyncArchive(
IReadOnlyList<FileInfo> fileInfos,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return (IRarAsyncArchive)OpenArchive(fileInfos, readerOptions);
}
public static bool IsRarFile(string filePath) => IsRarFile(new FileInfo(filePath));
public static bool IsRarFile(FileInfo fileInfo)
{
if (!fileInfo.Exists)
{
return false;
}
using Stream stream = fileInfo.OpenRead();
return IsRarFile(stream);
}
public static bool IsRarFile(Stream stream, ReaderOptions? options = null)
{
try
{
MarkHeader.Read(stream, true, false);
return true;
}
catch
{
return false;
}
}
}

View File

@@ -2,8 +2,6 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Rar;
using SharpCompress.Common.Rar.Headers;
@@ -14,23 +12,17 @@ using SharpCompress.Readers.Rar;
namespace SharpCompress.Archives.Rar;
public interface IRarArchiveCommon
{
int MinVersion { get; }
int MaxVersion { get; }
}
public interface IRarArchive : IArchive, IRarArchiveCommon { }
public interface IRarAsyncArchive : IAsyncArchive, IRarArchiveCommon { }
public partial class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>, IRarArchive
public class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>
{
private bool _disposed;
internal Lazy<IRarUnpack> UnpackV2017 { get; } =
new(() => new Compressors.Rar.UnpackV2017.Unpack());
internal Lazy<IRarUnpack> UnpackV1 { get; } = new(() => new Compressors.Rar.UnpackV1.Unpack());
/// <summary>
/// Constructor with a SourceStream able to handle FileInfo and Streams.
/// </summary>
/// <param name="sourceStream"></param>
private RarArchive(SourceStream sourceStream)
: base(ArchiveType.Rar, sourceStream) { }
@@ -48,29 +40,15 @@ public partial class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>, I
}
}
public override async ValueTask DisposeAsync()
{
if (!_disposed)
{
if (UnpackV1.IsValueCreated && UnpackV1.Value is IDisposable unpackV1)
{
unpackV1.Dispose();
}
_disposed = true;
await base.DisposeAsync();
}
}
protected override IEnumerable<RarArchiveEntry> LoadEntries(IEnumerable<RarVolume> volumes) =>
RarArchiveEntryFactory.GetEntries(this, volumes, ReaderOptions);
protected override IEnumerable<RarVolume> LoadVolumes(SourceStream sourceStream)
{
sourceStream.LoadAllParts();
sourceStream.LoadAllParts(); //request all streams
var streams = sourceStream.Streams.ToArray();
var i = 0;
if (streams.Length > 1 && IsRarFile(streams[1], ReaderOptions))
if (streams.Length > 1 && IsRarFile(streams[1], ReaderOptions)) //test part 2 - true = multipart not split
{
sourceStream.IsVolumes = true;
streams[1].Position = 0;
@@ -83,16 +61,11 @@ public partial class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>, I
));
}
//split mode or single file
return new StreamRarArchiveVolume(sourceStream, ReaderOptions, i++).AsEnumerable();
}
protected override IReader CreateReaderForSolidExtraction() =>
CreateReaderForSolidExtractionInternal();
protected override ValueTask<IAsyncReader> CreateReaderForSolidExtractionAsync() =>
new(CreateReaderForSolidExtractionInternal());
private RarReader CreateReaderForSolidExtractionInternal()
protected override IReader CreateReaderForSolidExtraction()
{
if (this.IsMultipartVolume())
{
@@ -101,12 +74,12 @@ public partial class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>, I
volume.Stream.Position = 0;
return volume.Stream;
});
return (RarReader)RarReader.OpenReader(streams, ReaderOptions);
return RarReader.Open(streams, ReaderOptions);
}
var stream = Volumes.First().Stream;
stream.Position = 0;
return (RarReader)RarReader.OpenReader(stream, ReaderOptions);
return RarReader.Open(stream, ReaderOptions);
}
public override bool IsSolid => Volumes.First().IsSolidArchive;
@@ -115,4 +88,123 @@ public partial class RarArchive : AbstractArchive<RarArchiveEntry, RarVolume>, I
public virtual int MinVersion => Volumes.First().MinVersion;
public virtual int MaxVersion => Volumes.First().MaxVersion;
#region Creation
/// <summary>
/// Constructor with a FileInfo object to an existing file.
/// </summary>
/// <param name="filePath"></param>
/// <param name="options"></param>
public static RarArchive Open(string filePath, ReaderOptions? options = null)
{
filePath.NotNullOrEmpty(nameof(filePath));
var fileInfo = new FileInfo(filePath);
return new RarArchive(
new SourceStream(
fileInfo,
i => RarArchiveVolumeFactory.GetFilePart(i, fileInfo),
options ?? new ReaderOptions()
)
);
}
/// <summary>
/// Constructor with a FileInfo object to an existing file.
/// </summary>
/// <param name="fileInfo"></param>
/// <param name="options"></param>
public static RarArchive Open(FileInfo fileInfo, ReaderOptions? options = null)
{
fileInfo.NotNull(nameof(fileInfo));
return new RarArchive(
new SourceStream(
fileInfo,
i => RarArchiveVolumeFactory.GetFilePart(i, fileInfo),
options ?? new ReaderOptions()
)
);
}
/// <summary>
/// Takes a seekable Stream as a source
/// </summary>
/// <param name="stream"></param>
/// <param name="options"></param>
public static RarArchive Open(Stream stream, ReaderOptions? options = null)
{
stream.NotNull(nameof(stream));
if (stream is not { CanSeek: true })
{
throw new ArgumentException("Stream must be seekable", nameof(stream));
}
return new RarArchive(new SourceStream(stream, _ => null, options ?? new ReaderOptions()));
}
/// <summary>
/// Constructor with all file parts passed in
/// </summary>
/// <param name="fileInfos"></param>
/// <param name="readerOptions"></param>
public static RarArchive Open(
IEnumerable<FileInfo> fileInfos,
ReaderOptions? readerOptions = null
)
{
fileInfos.NotNull(nameof(fileInfos));
var files = fileInfos.ToArray();
return new RarArchive(
new SourceStream(
files[0],
i => i < files.Length ? files[i] : null,
readerOptions ?? new ReaderOptions()
)
);
}
/// <summary>
/// Constructor with all stream parts passed in
/// </summary>
/// <param name="streams"></param>
/// <param name="readerOptions"></param>
public static RarArchive Open(IEnumerable<Stream> streams, ReaderOptions? readerOptions = null)
{
streams.NotNull(nameof(streams));
var strms = streams.ToArray();
return new RarArchive(
new SourceStream(
strms[0],
i => i < strms.Length ? strms[i] : null,
readerOptions ?? new ReaderOptions()
)
);
}
public static bool IsRarFile(string filePath) => IsRarFile(new FileInfo(filePath));
public static bool IsRarFile(FileInfo fileInfo)
{
if (!fileInfo.Exists)
{
return false;
}
using Stream stream = fileInfo.OpenRead();
return IsRarFile(stream);
}
public static bool IsRarFile(Stream stream, ReaderOptions? options = null)
{
try
{
MarkHeader.Read(stream, true, false);
return true;
}
catch
{
return false;
}
}
#endregion
}

View File

@@ -92,9 +92,7 @@ public class RarArchiveEntry : RarEntry, IArchiveEntry
return stream;
}
public async ValueTask<Stream> OpenEntryStreamAsync(
CancellationToken cancellationToken = default
)
public async Task<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default)
{
RarStream stream;
if (IsRarV3)

View File

@@ -1,7 +1,6 @@
using System.IO;
using SharpCompress.Common.Rar;
using SharpCompress.Common.Rar.Headers;
using SharpCompress.IO;
namespace SharpCompress.Archives.Rar;
@@ -25,76 +24,6 @@ internal class SeekableFilePart : RarFilePart
internal override Stream GetCompressedStream()
{
Stream streamToUse;
// If the stream is a SourceStream in file mode with multi-threading enabled,
// create an independent stream to support concurrent extraction
if (
_stream is SourceStream sourceStream
&& sourceStream.IsFileMode
&& sourceStream.ReaderOptions.EnableMultiThreadedExtraction
)
{
var independentStream = sourceStream.CreateIndependentStream(0);
if (independentStream is not null)
{
streamToUse = independentStream;
streamToUse.Position = FileHeader.DataStartPosition;
if (FileHeader.R4Salt != null)
{
var cryptKey = new CryptKey3(_password!);
return new RarCryptoWrapper(streamToUse, FileHeader.R4Salt, cryptKey);
}
if (FileHeader.Rar5CryptoInfo != null)
{
var cryptKey = new CryptKey5(_password!, FileHeader.Rar5CryptoInfo);
return new RarCryptoWrapper(
streamToUse,
FileHeader.Rar5CryptoInfo.Salt,
cryptKey
);
}
return streamToUse;
}
}
// Check if the stream wraps a FileStream
Stream? underlyingStream = _stream;
if (_stream is IStreamStack streamStack)
{
underlyingStream = streamStack.BaseStream();
}
if (underlyingStream is FileStream fileStream)
{
// Create a new independent stream from the file
streamToUse = new FileStream(
fileStream.Name,
FileMode.Open,
FileAccess.Read,
FileShare.Read
);
streamToUse.Position = FileHeader.DataStartPosition;
if (FileHeader.R4Salt != null)
{
var cryptKey = new CryptKey3(_password!);
return new RarCryptoWrapper(streamToUse, FileHeader.R4Salt, cryptKey);
}
if (FileHeader.Rar5CryptoInfo != null)
{
var cryptKey = new CryptKey5(_password!, FileHeader.Rar5CryptoInfo);
return new RarCryptoWrapper(streamToUse, FileHeader.Rar5CryptoInfo.Salt, cryptKey);
}
return streamToUse;
}
// Fall back to existing behavior for stream-based sources
_stream.Position = FileHeader.DataStartPosition;
if (FileHeader.R4Salt != null)

View File

@@ -1,169 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.SevenZip;
using SharpCompress.Compressors.LZMA.Utilites;
using SharpCompress.IO;
using SharpCompress.Readers;
namespace SharpCompress.Archives.SevenZip;
public partial class SevenZipArchive
#if NET8_0_OR_GREATER
: IArchiveOpenable<IArchive, IAsyncArchive>,
IMultiArchiveOpenable<IArchive, IAsyncArchive>
#endif
{
public static IAsyncArchive OpenAsyncArchive(
string path,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
path.NotNullOrEmpty("path");
return (IAsyncArchive)OpenArchive(new FileInfo(path), readerOptions ?? new ReaderOptions());
}
public static IArchive OpenArchive(string filePath, ReaderOptions? readerOptions = null)
{
filePath.NotNullOrEmpty("filePath");
return OpenArchive(new FileInfo(filePath), readerOptions ?? new ReaderOptions());
}
public static IArchive OpenArchive(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
fileInfo.NotNull("fileInfo");
return new SevenZipArchive(
new SourceStream(
fileInfo,
i => ArchiveVolumeFactory.GetFilePart(i, fileInfo),
readerOptions ?? new ReaderOptions()
)
);
}
public static IArchive OpenArchive(
IEnumerable<FileInfo> fileInfos,
ReaderOptions? readerOptions = null
)
{
fileInfos.NotNull(nameof(fileInfos));
var files = fileInfos.ToArray();
return new SevenZipArchive(
new SourceStream(
files[0],
i => i < files.Length ? files[i] : null,
readerOptions ?? new ReaderOptions()
)
);
}
public static IArchive OpenArchive(
IEnumerable<Stream> streams,
ReaderOptions? readerOptions = null
)
{
streams.NotNull(nameof(streams));
var strms = streams.ToArray();
return new SevenZipArchive(
new SourceStream(
strms[0],
i => i < strms.Length ? strms[i] : null,
readerOptions ?? new ReaderOptions()
)
);
}
public static IArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null)
{
stream.NotNull("stream");
if (stream is not { CanSeek: true })
{
throw new ArgumentException("Stream must be seekable", nameof(stream));
}
return new SevenZipArchive(
new SourceStream(stream, _ => null, readerOptions ?? new ReaderOptions())
);
}
public static IAsyncArchive OpenAsyncArchive(
Stream stream,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return (IAsyncArchive)OpenArchive(stream, readerOptions);
}
public static IAsyncArchive OpenAsyncArchive(
FileInfo fileInfo,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return (IAsyncArchive)OpenArchive(fileInfo, readerOptions);
}
public static IAsyncArchive OpenAsyncArchive(
IReadOnlyList<Stream> streams,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return (IAsyncArchive)OpenArchive(streams, readerOptions);
}
public static IAsyncArchive OpenAsyncArchive(
IReadOnlyList<FileInfo> fileInfos,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return (IAsyncArchive)OpenArchive(fileInfos, readerOptions);
}
public static bool IsSevenZipFile(string filePath) => IsSevenZipFile(new FileInfo(filePath));
public static bool IsSevenZipFile(FileInfo fileInfo)
{
if (!fileInfo.Exists)
{
return false;
}
using Stream stream = fileInfo.OpenRead();
return IsSevenZipFile(stream);
}
public static bool IsSevenZipFile(Stream stream)
{
try
{
return SignatureMatch(stream);
}
catch
{
return false;
}
}
private static ReadOnlySpan<byte> Signature =>
new byte[] { (byte)'7', (byte)'z', 0xBC, 0xAF, 0x27, 0x1C };
private static bool SignatureMatch(Stream stream)
{
var reader = new BinaryReader(stream);
ReadOnlySpan<byte> signatureBytes = reader.ReadBytes(6);
return signatureBytes.SequenceEqual(Signature);
}
}

View File

@@ -12,22 +12,127 @@ using SharpCompress.Readers;
namespace SharpCompress.Archives.SevenZip;
public partial class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, SevenZipVolume>
public class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, SevenZipVolume>
{
private ArchiveDatabase? _database;
/// <summary>
/// Constructor expects a filepath to an existing file.
/// </summary>
/// <param name="filePath"></param>
/// <param name="readerOptions"></param>
public static SevenZipArchive Open(string filePath, ReaderOptions? readerOptions = null)
{
filePath.NotNullOrEmpty("filePath");
return Open(new FileInfo(filePath), readerOptions ?? new ReaderOptions());
}
/// <summary>
/// Constructor with a FileInfo object to an existing file.
/// </summary>
/// <param name="fileInfo"></param>
/// <param name="readerOptions"></param>
public static SevenZipArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
fileInfo.NotNull("fileInfo");
return new SevenZipArchive(
new SourceStream(
fileInfo,
i => ArchiveVolumeFactory.GetFilePart(i, fileInfo),
readerOptions ?? new ReaderOptions()
)
);
}
/// <summary>
/// Constructor with all file parts passed in
/// </summary>
/// <param name="fileInfos"></param>
/// <param name="readerOptions"></param>
public static SevenZipArchive Open(
IEnumerable<FileInfo> fileInfos,
ReaderOptions? readerOptions = null
)
{
fileInfos.NotNull(nameof(fileInfos));
var files = fileInfos.ToArray();
return new SevenZipArchive(
new SourceStream(
files[0],
i => i < files.Length ? files[i] : null,
readerOptions ?? new ReaderOptions()
)
);
}
/// <summary>
/// Constructor with all stream parts passed in
/// </summary>
/// <param name="streams"></param>
/// <param name="readerOptions"></param>
public static SevenZipArchive Open(
IEnumerable<Stream> streams,
ReaderOptions? readerOptions = null
)
{
streams.NotNull(nameof(streams));
var strms = streams.ToArray();
return new SevenZipArchive(
new SourceStream(
strms[0],
i => i < strms.Length ? strms[i] : null,
readerOptions ?? new ReaderOptions()
)
);
}
/// <summary>
/// Takes a seekable Stream as a source
/// </summary>
/// <param name="stream"></param>
/// <param name="readerOptions"></param>
public static SevenZipArchive Open(Stream stream, ReaderOptions? readerOptions = null)
{
stream.NotNull("stream");
if (stream is not { CanSeek: true })
{
throw new ArgumentException("Stream must be seekable", nameof(stream));
}
return new SevenZipArchive(
new SourceStream(stream, _ => null, readerOptions ?? new ReaderOptions())
);
}
/// <summary>
/// Constructor with a SourceStream able to handle FileInfo and Streams.
/// </summary>
/// <param name="sourceStream"></param>
private SevenZipArchive(SourceStream sourceStream)
: base(ArchiveType.SevenZip, sourceStream) { }
internal SevenZipArchive()
: base(ArchiveType.SevenZip) { }
protected override IEnumerable<SevenZipVolume> LoadVolumes(SourceStream sourceStream)
{
sourceStream.NotNull("SourceStream is null").LoadAllParts();
return new SevenZipVolume(sourceStream, ReaderOptions, 0).AsEnumerable();
sourceStream.NotNull("SourceStream is null").LoadAllParts(); //request all streams
return new SevenZipVolume(sourceStream, ReaderOptions, 0).AsEnumerable(); //simple single volume or split, multivolume not supported
}
public static bool IsSevenZipFile(string filePath) => IsSevenZipFile(new FileInfo(filePath));
public static bool IsSevenZipFile(FileInfo fileInfo)
{
if (!fileInfo.Exists)
{
return false;
}
using Stream stream = fileInfo.OpenRead();
return IsSevenZipFile(stream);
}
internal SevenZipArchive()
: base(ArchiveType.SevenZip) { }
protected override IEnumerable<SevenZipArchiveEntry> LoadEntries(
IEnumerable<SevenZipVolume> volumes
)
@@ -53,7 +158,7 @@ public partial class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, Sev
foreach (var entry in group)
{
entry.IsSolid = isSolid;
isSolid = true;
isSolid = true; //mark others in this group as solid - same as rar behaviour.
}
}
@@ -71,12 +176,31 @@ public partial class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, Sev
}
}
public static bool IsSevenZipFile(Stream stream)
{
try
{
return SignatureMatch(stream);
}
catch
{
return false;
}
}
private static ReadOnlySpan<byte> Signature =>
new byte[] { (byte)'7', (byte)'z', 0xBC, 0xAF, 0x27, 0x1C };
private static bool SignatureMatch(Stream stream)
{
var reader = new BinaryReader(stream);
ReadOnlySpan<byte> signatureBytes = reader.ReadBytes(6);
return signatureBytes.SequenceEqual(Signature);
}
protected override IReader CreateReaderForSolidExtraction() =>
new SevenZipReader(ReaderOptions, this);
protected override ValueTask<IAsyncReader> CreateReaderForSolidExtractionAsync() =>
new(new SevenZipReader(ReaderOptions, this));
public override bool IsSolid =>
Entries
.Where(x => !x.IsDirectory)
@@ -107,6 +231,9 @@ public partial class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, Sev
_currentEntry = dir;
yield return dir;
}
// For non-directory entries, yield them without creating shared streams
// Each call to GetEntryStream() will create a fresh decompression stream
// to avoid state corruption issues with async operations
foreach (var entry in entries.Where(x => !x.IsDirectory))
{
_currentEntry = entry;
@@ -116,6 +243,13 @@ public partial class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, Sev
protected override EntryStream GetEntryStream()
{
// Create a fresh decompression stream for each file (no state sharing).
// However, the LZMA decoder has bugs in its async implementation that cause
// state corruption even on fresh streams. The SyncOnlyStream wrapper
// works around these bugs by forcing async operations to use sync equivalents.
//
// TODO: Fix the LZMA decoder async bugs (in LzmaStream, Decoder, OutWindow)
// so this wrapper is no longer necessary.
var entry = _currentEntry.NotNull("currentEntry is not null");
if (entry.IsDirectory)
{
@@ -125,6 +259,15 @@ public partial class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, Sev
}
}
/// <summary>
/// WORKAROUND: Forces async operations to use synchronous equivalents.
/// This is necessary because the LZMA decoder has bugs in its async implementation
/// that cause state corruption (IndexOutOfRangeException, DataErrorException).
///
/// The proper fix would be to repair the LZMA decoder's async methods
/// (LzmaStream.ReadAsync, Decoder.CodeAsync, OutWindow async operations),
/// but that requires deep changes to the decoder state machine.
/// </summary>
private sealed class SyncOnlyStream : Stream
{
private readonly Stream _baseStream;
@@ -154,6 +297,7 @@ public partial class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, Sev
public override void Write(byte[] buffer, int offset, int count) =>
_baseStream.Write(buffer, offset, count);
// Force async operations to use sync equivalents to avoid LZMA decoder bugs
public override Task<int> ReadAsync(
byte[] buffer,
int offset,
@@ -184,7 +328,7 @@ public partial class SevenZipArchive : AbstractArchive<SevenZipArchiveEntry, Sev
return Task.CompletedTask;
}
#if !LEGACY_DOTNET
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default

View File

@@ -12,8 +12,8 @@ public class SevenZipArchiveEntry : SevenZipEntry, IArchiveEntry
public Stream OpenEntryStream() => FilePart.GetCompressedStream();
public ValueTask<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default) =>
new(OpenEntryStream());
public Task<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default) =>
Task.FromResult(OpenEntryStream());
public IArchive Archive { get; }

View File

@@ -1,171 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Tar;
using SharpCompress.Common.Tar.Headers;
using SharpCompress.IO;
using SharpCompress.Readers;
using SharpCompress.Writers;
using SharpCompress.Writers.Tar;
namespace SharpCompress.Archives.Tar;
public partial class TarArchive
#if NET8_0_OR_GREATER
: IWritableArchiveOpenable,
IMultiArchiveOpenable<IWritableArchive, IWritableAsyncArchive>
#endif
{
public static IWritableArchive OpenArchive(string filePath, ReaderOptions? readerOptions = null)
{
filePath.NotNullOrEmpty(nameof(filePath));
return OpenArchive(new FileInfo(filePath), readerOptions ?? new ReaderOptions());
}
public static IWritableArchive OpenArchive(
FileInfo fileInfo,
ReaderOptions? readerOptions = null
)
{
fileInfo.NotNull(nameof(fileInfo));
return new TarArchive(
new SourceStream(
fileInfo,
i => ArchiveVolumeFactory.GetFilePart(i, fileInfo),
readerOptions ?? new ReaderOptions()
)
);
}
public static IWritableArchive OpenArchive(
IEnumerable<FileInfo> fileInfos,
ReaderOptions? readerOptions = null
)
{
fileInfos.NotNull(nameof(fileInfos));
var files = fileInfos.ToArray();
return new TarArchive(
new SourceStream(
files[0],
i => i < files.Length ? files[i] : null,
readerOptions ?? new ReaderOptions()
)
);
}
public static IWritableArchive OpenArchive(
IEnumerable<Stream> streams,
ReaderOptions? readerOptions = null
)
{
streams.NotNull(nameof(streams));
var strms = streams.ToArray();
return new TarArchive(
new SourceStream(
strms[0],
i => i < strms.Length ? strms[i] : null,
readerOptions ?? new ReaderOptions()
)
);
}
public static IWritableArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null)
{
stream.NotNull(nameof(stream));
if (stream is not { CanSeek: true })
{
throw new ArgumentException("Stream must be seekable", nameof(stream));
}
return new TarArchive(
new SourceStream(stream, i => null, readerOptions ?? new ReaderOptions())
);
}
public static IWritableAsyncArchive OpenAsyncArchive(
Stream stream,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return (IWritableAsyncArchive)OpenArchive(stream, readerOptions);
}
public static IWritableAsyncArchive OpenAsyncArchive(
string path,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return (IWritableAsyncArchive)OpenArchive(new FileInfo(path), readerOptions);
}
public static IWritableAsyncArchive OpenAsyncArchive(
FileInfo fileInfo,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return (IWritableAsyncArchive)OpenArchive(fileInfo, readerOptions);
}
public static IWritableAsyncArchive OpenAsyncArchive(
IReadOnlyList<Stream> streams,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return (IWritableAsyncArchive)OpenArchive(streams, readerOptions);
}
public static IWritableAsyncArchive OpenAsyncArchive(
IReadOnlyList<FileInfo> fileInfos,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return (IWritableAsyncArchive)OpenArchive(fileInfos, readerOptions);
}
public static bool IsTarFile(string filePath) => IsTarFile(new FileInfo(filePath));
public static bool IsTarFile(FileInfo fileInfo)
{
if (!fileInfo.Exists)
{
return false;
}
using Stream stream = fileInfo.OpenRead();
return IsTarFile(stream);
}
public static bool IsTarFile(Stream stream)
{
try
{
var tarHeader = new TarHeader(new ArchiveEncoding());
var readSucceeded = tarHeader.Read(new BinaryReader(stream));
var isEmptyArchive =
tarHeader.Name?.Length == 0
&& tarHeader.Size == 0
&& Enum.IsDefined(typeof(EntryType), tarHeader.EntryType);
return readSucceeded || isEmptyArchive;
}
catch { }
return false;
}
public static IWritableArchive CreateArchive() => new TarArchive();
public static IWritableAsyncArchive CreateAsyncArchive() => new TarArchive();
}

View File

@@ -15,14 +15,132 @@ using SharpCompress.Writers.Tar;
namespace SharpCompress.Archives.Tar;
public partial class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
{
protected override IEnumerable<TarVolume> LoadVolumes(SourceStream sourceStream)
/// <summary>
/// Constructor expects a filepath to an existing file.
/// </summary>
/// <param name="filePath"></param>
/// <param name="readerOptions"></param>
public static TarArchive Open(string filePath, ReaderOptions? readerOptions = null)
{
sourceStream.NotNull("SourceStream is null").LoadAllParts();
return new TarVolume(sourceStream, ReaderOptions, 1).AsEnumerable();
filePath.NotNullOrEmpty(nameof(filePath));
return Open(new FileInfo(filePath), readerOptions ?? new ReaderOptions());
}
/// <summary>
/// Constructor with a FileInfo object to an existing file.
/// </summary>
/// <param name="fileInfo"></param>
/// <param name="readerOptions"></param>
public static TarArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
fileInfo.NotNull(nameof(fileInfo));
return new TarArchive(
new SourceStream(
fileInfo,
i => ArchiveVolumeFactory.GetFilePart(i, fileInfo),
readerOptions ?? new ReaderOptions()
)
);
}
/// <summary>
/// Constructor with all file parts passed in
/// </summary>
/// <param name="fileInfos"></param>
/// <param name="readerOptions"></param>
public static TarArchive Open(
IEnumerable<FileInfo> fileInfos,
ReaderOptions? readerOptions = null
)
{
fileInfos.NotNull(nameof(fileInfos));
var files = fileInfos.ToArray();
return new TarArchive(
new SourceStream(
files[0],
i => i < files.Length ? files[i] : null,
readerOptions ?? new ReaderOptions()
)
);
}
/// <summary>
/// Constructor with all stream parts passed in
/// </summary>
/// <param name="streams"></param>
/// <param name="readerOptions"></param>
public static TarArchive Open(IEnumerable<Stream> streams, ReaderOptions? readerOptions = null)
{
streams.NotNull(nameof(streams));
var strms = streams.ToArray();
return new TarArchive(
new SourceStream(
strms[0],
i => i < strms.Length ? strms[i] : null,
readerOptions ?? new ReaderOptions()
)
);
}
/// <summary>
/// Takes a seekable Stream as a source
/// </summary>
/// <param name="stream"></param>
/// <param name="readerOptions"></param>
public static TarArchive Open(Stream stream, ReaderOptions? readerOptions = null)
{
stream.NotNull(nameof(stream));
if (stream is not { CanSeek: true })
{
throw new ArgumentException("Stream must be seekable", nameof(stream));
}
return new TarArchive(
new SourceStream(stream, i => null, readerOptions ?? new ReaderOptions())
);
}
public static bool IsTarFile(string filePath) => IsTarFile(new FileInfo(filePath));
public static bool IsTarFile(FileInfo fileInfo)
{
if (!fileInfo.Exists)
{
return false;
}
using Stream stream = fileInfo.OpenRead();
return IsTarFile(stream);
}
public static bool IsTarFile(Stream stream)
{
try
{
var tarHeader = new TarHeader(new ArchiveEncoding());
var readSucceeded = tarHeader.Read(new BinaryReader(stream));
var isEmptyArchive =
tarHeader.Name?.Length == 0
&& tarHeader.Size == 0
&& Enum.IsDefined(typeof(EntryType), tarHeader.EntryType);
return readSucceeded || isEmptyArchive;
}
catch { }
return false;
}
protected override IEnumerable<TarVolume> LoadVolumes(SourceStream sourceStream)
{
sourceStream.NotNull("SourceStream is null").LoadAllParts(); //request all streams
return new TarVolume(sourceStream, ReaderOptions, 1).AsEnumerable(); //simple single volume or split, multivolume not supported
}
/// <summary>
/// Constructor with a SourceStream able to handle FileInfo and Streams.
/// </summary>
/// <param name="sourceStream"></param>
private TarArchive(SourceStream sourceStream)
: base(ArchiveType.Tar, sourceStream) { }
@@ -87,6 +205,8 @@ public partial class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVo
}
}
public static TarArchive Create() => new();
protected override TarArchiveEntry CreateEntryInternal(
string filePath,
Stream source,
@@ -139,7 +259,7 @@ public partial class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVo
}
}
protected override async ValueTask SaveToAsync(
protected override async Task SaveToAsync(
Stream stream,
WriterOptions options,
IEnumerable<TarArchiveEntry> oldEntries,
@@ -180,13 +300,6 @@ public partial class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVo
{
var stream = Volumes.Single().Stream;
stream.Position = 0;
return TarReader.OpenReader(stream);
}
protected override ValueTask<IAsyncReader> CreateReaderForSolidExtractionAsync()
{
var stream = Volumes.Single().Stream;
stream.Position = 0;
return new((IAsyncReader)TarReader.OpenReader(stream));
return TarReader.Open(stream);
}
}

View File

@@ -14,8 +14,9 @@ public class TarArchiveEntry : TarEntry, IArchiveEntry
public virtual Stream OpenEntryStream() => Parts.Single().GetCompressedStream().NotNull();
public ValueTask<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default) =>
new(OpenEntryStream());
public virtual Task<Stream> OpenEntryStreamAsync(
CancellationToken cancellationToken = default
) => Task.FromResult(OpenEntryStream());
#region IArchiveEntry Members

View File

@@ -1,324 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Zip;
using SharpCompress.Common.Zip.Headers;
using SharpCompress.IO;
using SharpCompress.Readers;
namespace SharpCompress.Archives.Zip;
public partial class ZipArchive
#if NET8_0_OR_GREATER
: IWritableArchiveOpenable,
IMultiArchiveOpenable<IWritableArchive, IWritableAsyncArchive>
#endif
{
public static IWritableArchive OpenArchive(string filePath, ReaderOptions? readerOptions = null)
{
filePath.NotNullOrEmpty(nameof(filePath));
return OpenArchive(new FileInfo(filePath), readerOptions ?? new ReaderOptions());
}
public static IWritableArchive OpenArchive(
FileInfo fileInfo,
ReaderOptions? readerOptions = null
)
{
fileInfo.NotNull(nameof(fileInfo));
return new ZipArchive(
new SourceStream(
fileInfo,
i => ZipArchiveVolumeFactory.GetFilePart(i, fileInfo),
readerOptions ?? new ReaderOptions()
)
);
}
public static IWritableArchive OpenArchive(
IEnumerable<FileInfo> fileInfos,
ReaderOptions? readerOptions = null
)
{
fileInfos.NotNull(nameof(fileInfos));
var files = fileInfos.ToArray();
return new ZipArchive(
new SourceStream(
files[0],
i => i < files.Length ? files[i] : null,
readerOptions ?? new ReaderOptions()
)
);
}
public static IWritableArchive OpenArchive(
IEnumerable<Stream> streams,
ReaderOptions? readerOptions = null
)
{
streams.NotNull(nameof(streams));
var strms = streams.ToArray();
return new ZipArchive(
new SourceStream(
strms[0],
i => i < strms.Length ? strms[i] : null,
readerOptions ?? new ReaderOptions()
)
);
}
public static IWritableArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null)
{
stream.NotNull(nameof(stream));
if (stream is not { CanSeek: true })
{
throw new ArgumentException("Stream must be seekable", nameof(stream));
}
return new ZipArchive(
new SourceStream(stream, i => null, readerOptions ?? new ReaderOptions())
);
}
public static IWritableAsyncArchive OpenAsyncArchive(
string path,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return (IWritableAsyncArchive)OpenArchive(path, readerOptions);
}
public static IWritableAsyncArchive OpenAsyncArchive(
Stream stream,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return (IWritableAsyncArchive)OpenArchive(stream, readerOptions);
}
public static IWritableAsyncArchive OpenAsyncArchive(
FileInfo fileInfo,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return (IWritableAsyncArchive)OpenArchive(fileInfo, readerOptions);
}
public static IWritableAsyncArchive OpenAsyncArchive(
IReadOnlyList<Stream> streams,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return (IWritableAsyncArchive)OpenArchive(streams, readerOptions);
}
public static IWritableAsyncArchive OpenAsyncArchive(
IReadOnlyList<FileInfo> fileInfos,
ReaderOptions? readerOptions = null,
CancellationToken cancellationToken = default
)
{
cancellationToken.ThrowIfCancellationRequested();
return (IWritableAsyncArchive)OpenArchive(fileInfos, readerOptions);
}
public static bool IsZipFile(
string filePath,
string? password = null,
int bufferSize = ReaderOptions.DefaultBufferSize
) => IsZipFile(new FileInfo(filePath), password, bufferSize);
public static bool IsZipFile(
FileInfo fileInfo,
string? password = null,
int bufferSize = ReaderOptions.DefaultBufferSize
)
{
if (!fileInfo.Exists)
{
return false;
}
using Stream stream = fileInfo.OpenRead();
return IsZipFile(stream, password, bufferSize);
}
public static bool IsZipFile(
Stream stream,
string? password = null,
int bufferSize = ReaderOptions.DefaultBufferSize
)
{
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)
{
return false;
}
return Enum.IsDefined(typeof(ZipHeaderType), header.ZipHeaderType);
}
catch (CryptographicException)
{
return true;
}
catch
{
return false;
}
}
public static bool IsZipMulti(
Stream stream,
string? password = null,
int bufferSize = ReaderOptions.DefaultBufferSize
)
{
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)
{
var z = new SeekableZipHeaderFactory(password, new ArchiveEncoding());
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<bool> 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(cancellationToken);
if (header is null)
{
return false;
}
return Enum.IsDefined(typeof(ZipHeaderType), header.ZipHeaderType);
}
catch (CryptographicException)
{
return true;
}
catch
{
return false;
}
}
public static IWritableArchive CreateArchive() => new ZipArchive();
public static IWritableAsyncArchive CreateAsyncArchive() => new ZipArchive();
public static async ValueTask<bool> 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)
{
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
{
return false;
}
}
return Enum.IsDefined(typeof(ZipHeaderType), header.ZipHeaderType);
}
catch (CryptographicException)
{
return true;
}
catch
{
return false;
}
}
}

View File

@@ -16,12 +16,21 @@ using SharpCompress.Writers.Zip;
namespace SharpCompress.Archives.Zip;
public partial class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
{
private readonly SeekableZipHeaderFactory? headerFactory;
/// <summary>
/// Gets or sets the compression level applied to files added to the archive,
/// if the compression method is set to deflate
/// </summary>
public CompressionLevel DeflateCompressionLevel { get; set; }
/// <summary>
/// Constructor with a SourceStream able to handle FileInfo and Streams.
/// </summary>
/// <param name="sourceStream"></param>
/// <param name="options"></param>
internal ZipArchive(SourceStream sourceStream)
: base(ArchiveType.Zip, sourceStream) =>
headerFactory = new SeekableZipHeaderFactory(
@@ -29,42 +38,223 @@ public partial class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVo
sourceStream.ReaderOptions.ArchiveEncoding
);
internal ZipArchive()
: base(ArchiveType.Zip) { }
/// <summary>
/// Constructor expects a filepath to an existing file.
/// </summary>
/// <param name="filePath"></param>
/// <param name="readerOptions"></param>
public static ZipArchive Open(string filePath, ReaderOptions? readerOptions = null)
{
filePath.NotNullOrEmpty(nameof(filePath));
return Open(new FileInfo(filePath), readerOptions ?? new ReaderOptions());
}
/// <summary>
/// Constructor with a FileInfo object to an existing file.
/// </summary>
/// <param name="fileInfo"></param>
/// <param name="readerOptions"></param>
public static ZipArchive Open(FileInfo fileInfo, ReaderOptions? readerOptions = null)
{
fileInfo.NotNull(nameof(fileInfo));
return new ZipArchive(
new SourceStream(
fileInfo,
i => ZipArchiveVolumeFactory.GetFilePart(i, fileInfo),
readerOptions ?? new ReaderOptions()
)
);
}
/// <summary>
/// Constructor with all file parts passed in
/// </summary>
/// <param name="fileInfos"></param>
/// <param name="readerOptions"></param>
public static ZipArchive Open(
IEnumerable<FileInfo> fileInfos,
ReaderOptions? readerOptions = null
)
{
fileInfos.NotNull(nameof(fileInfos));
var files = fileInfos.ToArray();
return new ZipArchive(
new SourceStream(
files[0],
i => i < files.Length ? files[i] : null,
readerOptions ?? new ReaderOptions()
)
);
}
/// <summary>
/// Constructor with all stream parts passed in
/// </summary>
/// <param name="streams"></param>
/// <param name="readerOptions"></param>
public static ZipArchive Open(IEnumerable<Stream> streams, ReaderOptions? readerOptions = null)
{
streams.NotNull(nameof(streams));
var strms = streams.ToArray();
return new ZipArchive(
new SourceStream(
strms[0],
i => i < strms.Length ? strms[i] : null,
readerOptions ?? new ReaderOptions()
)
);
}
/// <summary>
/// Takes a seekable Stream as a source
/// </summary>
/// <param name="stream"></param>
/// <param name="readerOptions"></param>
public static ZipArchive Open(Stream stream, ReaderOptions? readerOptions = null)
{
stream.NotNull(nameof(stream));
if (stream is not { CanSeek: true })
{
throw new ArgumentException("Stream must be seekable", nameof(stream));
}
return new ZipArchive(
new SourceStream(stream, i => null, readerOptions ?? new ReaderOptions())
);
}
public static bool IsZipFile(
string filePath,
string? password = null,
int bufferSize = ReaderOptions.DefaultBufferSize
) => IsZipFile(new FileInfo(filePath), password, bufferSize);
public static bool IsZipFile(
FileInfo fileInfo,
string? password = null,
int bufferSize = ReaderOptions.DefaultBufferSize
)
{
if (!fileInfo.Exists)
{
return false;
}
using Stream stream = fileInfo.OpenRead();
return IsZipFile(stream, password, bufferSize);
}
public static bool IsZipFile(
Stream stream,
string? password = null,
int bufferSize = ReaderOptions.DefaultBufferSize
)
{
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)
{
return false;
}
return Enum.IsDefined(typeof(ZipHeaderType), header.ZipHeaderType);
}
catch (CryptographicException)
{
return true;
}
catch
{
return false;
}
}
public static bool IsZipMulti(
Stream stream,
string? password = null,
int bufferSize = ReaderOptions.DefaultBufferSize
)
{
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());
var x = z.ReadSeekableHeader(stream).FirstOrDefault();
return x?.ZipHeaderType == ZipHeaderType.DirectoryEntry;
}
else
{
return false;
}
}
return Enum.IsDefined(typeof(ZipHeaderType), header.ZipHeaderType);
}
catch (CryptographicException)
{
return true;
}
catch
{
return false;
}
}
protected override IEnumerable<ZipVolume> LoadVolumes(SourceStream stream)
{
stream.LoadAllParts();
stream.LoadAllParts(); //request all streams
stream.Position = 0;
var streams = stream.Streams.ToList();
var idx = 0;
if (streams.Count() > 1)
if (streams.Count() > 1) //test part 2 - true = multipart not split
{
streams[1].Position += 4;
streams[1].Position += 4; //skip the POST_DATA_DESCRIPTOR to prevent an exception
var isZip = IsZipFile(streams[1], ReaderOptions.Password, ReaderOptions.BufferSize);
streams[1].Position -= 4;
if (isZip)
{
stream.IsVolumes = true;
var tmp = streams[0];
var tmp = streams[0]; //arcs as zip, z01 ... swap the zip the end
streams.RemoveAt(0);
streams.Add(tmp);
//streams[0].Position = 4; //skip the POST_DATA_DESCRIPTOR to prevent an exception
return streams.Select(a => new ZipVolume(a, ReaderOptions, idx++));
}
}
//split mode or single file
return new ZipVolume(stream, ReaderOptions, idx++).AsEnumerable();
}
internal ZipArchive()
: base(ArchiveType.Zip) { }
protected override IEnumerable<ZipArchiveEntry> LoadEntries(IEnumerable<ZipVolume> volumes)
{
var vols = volumes.ToArray();
foreach (
var h in headerFactory.NotNull().ReadSeekableHeader(vols.Last().Stream, useSync: true)
)
foreach (var h in headerFactory.NotNull().ReadSeekableHeader(vols.Last().Stream))
{
if (h != null)
{
@@ -108,59 +298,6 @@ public partial class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVo
}
}
protected override async IAsyncEnumerable<ZipArchiveEntry> LoadEntriesAsync(
IAsyncEnumerable<ZipVolume> 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<byte>();
volsArray.Last().Comment = ReaderOptions.ArchiveEncoding.Decode(bytes);
yield break;
}
}
}
}
}
public void SaveTo(Stream stream) => SaveTo(stream, new WriterOptions(CompressionType.Deflate));
protected override void SaveTo(
@@ -192,7 +329,7 @@ public partial class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVo
}
}
protected override async ValueTask SaveToAsync(
protected override async Task SaveToAsync(
Stream stream,
WriterOptions options,
IEnumerable<ZipArchiveEntry> oldEntries,
@@ -240,17 +377,12 @@ public partial class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVo
DateTime? modified
) => new ZipWritableArchiveEntry(this, directoryPath, modified);
public static ZipArchive Create() => new();
protected override IReader CreateReaderForSolidExtraction()
{
var stream = Volumes.Single().Stream;
((IStreamStack)stream).StackSeek(0);
return ZipReader.OpenReader(stream, ReaderOptions, Entries);
}
protected override ValueTask<IAsyncReader> CreateReaderForSolidExtractionAsync()
{
var stream = Volumes.Single().Stream;
stream.Position = 0;
return new((IAsyncReader)ZipReader.OpenReader(stream));
return ZipReader.Open(stream, ReaderOptions, Entries);
}
}

View File

@@ -13,17 +13,9 @@ public class ZipArchiveEntry : ZipEntry, IArchiveEntry
public virtual Stream OpenEntryStream() => Parts.Single().GetCompressedStream().NotNull();
public async ValueTask<Stream> OpenEntryStreamAsync(
public virtual Task<Stream> OpenEntryStreamAsync(
CancellationToken cancellationToken = default
)
{
var part = Parts.Single();
if (part is SeekableZipFilePart seekablePart)
{
return (await seekablePart.GetCompressedStreamAsync(cancellationToken)).NotNull();
}
return OpenEntryStream();
}
) => Task.FromResult(OpenEntryStream());
#region IArchiveEntry Members

View File

@@ -46,7 +46,7 @@ namespace SharpCompress.Common.Ace.Headers
}
}
public AceFileHeader(IArchiveEncoding archiveEncoding)
public AceFileHeader(ArchiveEncoding archiveEncoding)
: base(archiveEncoding, AceHeaderType.FILE) { }
/// <summary>

View File

@@ -31,13 +31,13 @@ namespace SharpCompress.Common.Ace.Headers
(byte)'*',
];
public AceHeader(IArchiveEncoding archiveEncoding, AceHeaderType type)
public AceHeader(ArchiveEncoding archiveEncoding, AceHeaderType type)
{
AceHeaderType = type;
ArchiveEncoding = archiveEncoding;
}
public IArchiveEncoding ArchiveEncoding { get; }
public ArchiveEncoding ArchiveEncoding { get; }
public AceHeaderType AceHeaderType { get; }
public ushort HeaderFlags { get; set; }

View File

@@ -22,7 +22,7 @@ namespace SharpCompress.Common.Ace.Headers
public List<byte> Comment { get; set; } = new();
public byte AceVersion { get; private set; }
public AceMainHeader(IArchiveEncoding archiveEncoding)
public AceMainHeader(ArchiveEncoding archiveEncoding)
: base(archiveEncoding, AceHeaderType.MAIN) { }
/// <summary>

View File

@@ -7,7 +7,7 @@ namespace SharpCompress.Common.Arc
{
public class ArcEntryHeader
{
public IArchiveEncoding ArchiveEncoding { get; }
public ArchiveEncoding ArchiveEncoding { get; }
public CompressionType CompressionMethod { get; private set; }
public string? Name { get; private set; }
public long CompressedSize { get; private set; }
@@ -16,7 +16,7 @@ namespace SharpCompress.Common.Arc
public long OriginalSize { get; private set; }
public long DataStartPosition { get; private set; }
public ArcEntryHeader(IArchiveEncoding archiveEncoding)
public ArcEntryHeader(ArchiveEncoding archiveEncoding)
{
this.ArchiveEncoding = archiveEncoding;
}

View File

@@ -3,11 +3,55 @@ using System.Text;
namespace SharpCompress.Common;
public class ArchiveEncoding : IArchiveEncoding
public class ArchiveEncoding
{
public Encoding Default { get; set; } = Encoding.Default;
public Encoding Password { get; set; } = Encoding.Default;
public Encoding UTF8 { get; set; } = Encoding.UTF8;
/// <summary>
/// Default encoding to use when archive format doesn't specify one.
/// </summary>
public Encoding? Default { get; set; }
/// <summary>
/// ArchiveEncoding used by encryption schemes which don't comply with RFC 2898.
/// </summary>
public Encoding? Password { get; set; }
/// <summary>
/// Set this encoding when you want to force it for all encoding operations.
/// </summary>
public Encoding? Forced { get; set; }
public Func<byte[], int, int, EncodingType, string>? CustomDecoder { get; set; }
/// <summary>
/// Set this when you want to use a custom method for all decoding operations.
/// </summary>
/// <returns>string Func(bytes, index, length)</returns>
public Func<byte[], int, int, string>? 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<byte[], int, int, string> GetDecoder() =>
CustomDecoder ?? ((bytes, index, count) => GetEncoding().GetString(bytes, index, count));
}

View File

@@ -1,87 +0,0 @@
using System;
using System.Text;
namespace SharpCompress.Common;
/// <summary>
/// Specifies the type of encoding to use.
/// </summary>
public enum EncodingType
{
/// <summary>
/// Uses the default encoding.
/// </summary>
Default,
/// <summary>
/// Uses UTF-8 encoding.
/// </summary>
UTF8,
}
/// <summary>
/// Provides extension methods for archive encoding.
/// </summary>
public static class ArchiveEncodingExtensions
{
#if !NETFRAMEWORK
/// <summary>
/// Registers the code pages encoding provider.
/// </summary>
static ArchiveEncodingExtensions() =>
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
#endif
extension(IArchiveEncoding encoding)
{
/// <summary>
/// Gets the encoding based on the archive encoding settings.
/// </summary>
/// <param name="useUtf8">Whether to use UTF-8.</param>
/// <returns>The encoding.</returns>
public Encoding GetEncoding(bool useUtf8 = false) =>
encoding.Forced ?? (useUtf8 ? encoding.UTF8 : encoding.Default);
/// <summary>
/// Gets the decoder function for the archive encoding.
/// </summary>
/// <returns>The decoder function.</returns>
public Func<byte[], int, int, EncodingType, string> GetDecoder() =>
encoding.CustomDecoder
?? (
(bytes, index, count, type) =>
encoding.GetEncoding(type == EncodingType.UTF8).GetString(bytes, index, count)
);
/// <summary>
/// Encodes a string using the default encoding.
/// </summary>
/// <param name="str">The string to encode.</param>
/// <returns>The encoded bytes.</returns>
public byte[] Encode(string str) => encoding.Default.GetBytes(str);
/// <summary>
/// Decodes bytes using the specified encoding type.
/// </summary>
/// <param name="bytes">The bytes to decode.</param>
/// <param name="type">The encoding type.</param>
/// <returns>The decoded string.</returns>
public string Decode(byte[] bytes, EncodingType type = EncodingType.Default) =>
encoding.Decode(bytes, 0, bytes.Length, type);
/// <summary>
/// Decodes a portion of bytes using the specified encoding type.
/// </summary>
/// <param name="bytes">The bytes to decode.</param>
/// <param name="start">The start index.</param>
/// <param name="length">The length.</param>
/// <param name="type">The encoding type.</param>
/// <returns>The decoded string.</returns>
public string Decode(
byte[] bytes,
int start,
int length,
EncodingType type = EncodingType.Default
) => encoding.GetDecoder()(bytes, start, length, type);
}
}

View File

@@ -1,103 +0,0 @@
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<byte> ReadByteAsync(CancellationToken ct = default)
{
await _stream.ReadExactAsync(_buffer, 0, 1, ct).ConfigureAwait(false);
return _buffer[0];
}
public async ValueTask<ushort> ReadUInt16Async(CancellationToken ct = default)
{
await _stream.ReadExactAsync(_buffer, 0, 2, ct).ConfigureAwait(false);
return BinaryPrimitives.ReadUInt16LittleEndian(_buffer);
}
public async ValueTask<uint> ReadUInt32Async(CancellationToken ct = default)
{
await _stream.ReadExactAsync(_buffer, 0, 4, ct).ConfigureAwait(false);
return BinaryPrimitives.ReadUInt32LittleEndian(_buffer);
}
public async ValueTask<ulong> ReadUInt64Async(CancellationToken ct = default)
{
await _stream.ReadExactAsync(_buffer, 0, 8, ct).ConfigureAwait(false);
return BinaryPrimitives.ReadUInt64LittleEndian(_buffer);
}
public async ValueTask ReadBytesAsync(
byte[] bytes,
int offset,
int count,
CancellationToken ct = default
)
{
await _stream.ReadExactAsync(bytes, offset, count, ct).ConfigureAwait(false);
}
public async ValueTask SkipAsync(int count, CancellationToken ct = default)
{
await _stream.SkipAsync(count, ct).ConfigureAwait(false);
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
// Dispose the original stream if we own it
if (!_leaveOpen)
{
_originalStream.Dispose();
}
}
#if NET8_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
}
}

View File

@@ -56,7 +56,7 @@ public class EntryStream : Stream, IStreamStack
/// <summary>
/// Asynchronously skip the rest of the entry stream.
/// </summary>
public async ValueTask SkipEntryAsync(CancellationToken cancellationToken = default)
public async Task SkipEntryAsync(CancellationToken cancellationToken = default)
{
await this.SkipAsync(cancellationToken).ConfigureAwait(false);
_completed = true;
@@ -79,11 +79,25 @@ public class EntryStream : Stream, IStreamStack
{
if (ss.BaseStream() is SharpCompress.Compressors.Deflate.DeflateStream deflateStream)
{
deflateStream.Flush(); //Deflate over reads. Knock it back
try
{
deflateStream.Flush(); //Deflate over reads. Knock it back
}
catch (NotSupportedException)
{
// Ignore: underlying stream does not support required operations for Flush
}
}
else if (ss.BaseStream() is SharpCompress.Compressors.LZMA.LzmaStream lzmaStream)
{
lzmaStream.Flush(); //Lzma over reads. Knock it back
try
{
lzmaStream.Flush(); //Lzma over reads. Knock it back
}
catch (NotSupportedException)
{
// Ignore: underlying stream does not support required operations for Flush
}
}
}
#if DEBUG_STREAMS
@@ -93,7 +107,7 @@ public class EntryStream : Stream, IStreamStack
_stream.Dispose();
}
#if !LEGACY_DOTNET
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async ValueTask DisposeAsync()
{
if (_isDisposed)
@@ -111,11 +125,25 @@ public class EntryStream : Stream, IStreamStack
{
if (ss.BaseStream() is SharpCompress.Compressors.Deflate.DeflateStream deflateStream)
{
await deflateStream.FlushAsync().ConfigureAwait(false);
try
{
await deflateStream.FlushAsync().ConfigureAwait(false);
}
catch (NotSupportedException)
{
// Ignore: underlying stream does not support required operations for Flush
}
}
else if (ss.BaseStream() is SharpCompress.Compressors.LZMA.LzmaStream lzmaStream)
{
await lzmaStream.FlushAsync().ConfigureAwait(false);
try
{
await lzmaStream.FlushAsync().ConfigureAwait(false);
}
catch (NotSupportedException)
{
// Ignore: underlying stream does not support required operations for Flush
}
}
}
#if DEBUG_STREAMS
@@ -171,7 +199,7 @@ public class EntryStream : Stream, IStreamStack
return read;
}
#if !LEGACY_DOTNET
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default

View File

@@ -124,11 +124,11 @@ internal static class ExtractionMethods
}
}
public static async ValueTask WriteEntryToDirectoryAsync(
public static async Task WriteEntryToDirectoryAsync(
IEntry entry,
string destinationDirectory,
ExtractionOptions? options,
Func<string, ExtractionOptions?, CancellationToken, ValueTask> writeAsync,
Func<string, ExtractionOptions?, CancellationToken, Task> writeAsync,
CancellationToken cancellationToken = default
)
{
@@ -197,11 +197,11 @@ internal static class ExtractionMethods
}
}
public static async ValueTask WriteEntryToFileAsync(
public static async Task WriteEntryToFileAsync(
IEntry entry,
string destinationFileName,
ExtractionOptions? options,
Func<string, FileMode, CancellationToken, ValueTask> openAndWriteAsync,
Func<string, FileMode, CancellationToken, Task> openAndWriteAsync,
CancellationToken cancellationToken = default
)
{

View File

@@ -1,14 +1,12 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace SharpCompress.Common;
public abstract class FilePart
{
protected FilePart(IArchiveEncoding archiveEncoding) => ArchiveEncoding = archiveEncoding;
protected FilePart(ArchiveEncoding archiveEncoding) => ArchiveEncoding = archiveEncoding;
internal IArchiveEncoding ArchiveEncoding { get; }
internal ArchiveEncoding ArchiveEncoding { get; }
internal abstract string? FilePartName { get; }
public int Index { get; set; }
@@ -16,8 +14,4 @@ public abstract class FilePart
internal abstract Stream? GetCompressedStream();
internal abstract Stream? GetRawStream();
internal bool Skipped { get; set; }
internal virtual ValueTask<Stream?> GetCompressedStreamAsync(
CancellationToken cancellationToken = default
) => new(GetCompressedStream());
}

View File

@@ -40,6 +40,6 @@ public class GZipEntry : Entry
internal static IEnumerable<GZipEntry> GetEntries(Stream stream, OptionsBase options)
{
yield return new GZipEntry(GZipFilePart.Create(stream, options.ArchiveEncoding));
yield return new GZipEntry(new GZipFilePart(stream, options.ArchiveEncoding));
}
}

View File

@@ -2,8 +2,6 @@ using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common.Tar.Headers;
using SharpCompress.Compressors;
using SharpCompress.Compressors.Deflate;
@@ -15,58 +13,28 @@ internal sealed class GZipFilePart : FilePart
private string? _name;
private readonly Stream _stream;
internal static GZipFilePart Create(Stream stream, IArchiveEncoding archiveEncoding)
internal GZipFilePart(Stream stream, ArchiveEncoding archiveEncoding)
: base(archiveEncoding)
{
var part = new GZipFilePart(stream, archiveEncoding);
part.ReadAndValidateGzipHeader();
_stream = stream;
ReadAndValidateGzipHeader();
if (stream.CanSeek)
{
var position = stream.Position;
stream.Position = stream.Length - 8;
part.ReadTrailer();
ReadTrailer();
stream.Position = position;
part.EntryStartPosition = position;
EntryStartPosition = position;
}
else
{
// For non-seekable streams, we can't read the trailer or track position.
// Set to 0 since the stream will be read sequentially from its current position.
part.EntryStartPosition = 0;
EntryStartPosition = 0;
}
return part;
}
internal static async ValueTask<GZipFilePart> CreateAsync(
Stream stream,
IArchiveEncoding archiveEncoding,
CancellationToken cancellationToken = default
)
{
var part = new GZipFilePart(stream, archiveEncoding);
await part.ReadAndValidateGzipHeaderAsync(cancellationToken);
if (stream.CanSeek)
{
var position = stream.Position;
stream.Position = stream.Length - 8;
await part.ReadTrailerAsync(cancellationToken);
stream.Position = position;
part.EntryStartPosition = position;
}
else
{
// For non-seekable streams, we can't read the trailer or track position.
// Set to 0 since the stream will be read sequentially from its current position.
part.EntryStartPosition = 0;
}
return part;
}
private GZipFilePart(Stream stream, IArchiveEncoding archiveEncoding)
: base(archiveEncoding) => _stream = stream;
internal long EntryStartPosition { get; private set; }
internal long EntryStartPosition { get; }
internal DateTime? DateModified { get; private set; }
internal uint? Crc { get; private set; }
@@ -83,22 +51,12 @@ internal sealed class GZipFilePart : FilePart
{
// Read and potentially verify the GZIP trailer: CRC32 and size mod 2^32
Span<byte> trailer = stackalloc byte[8];
_stream.ReadFully(trailer);
var n = _stream.Read(trailer);
Crc = BinaryPrimitives.ReadUInt32LittleEndian(trailer);
UncompressedSize = BinaryPrimitives.ReadUInt32LittleEndian(trailer.Slice(4));
}
private async ValueTask ReadTrailerAsync(CancellationToken cancellationToken = default)
{
// Read and potentially verify the GZIP trailer: CRC32 and size mod 2^32
var trailer = new byte[8];
_ = await _stream.ReadFullyAsync(trailer, 0, 8, cancellationToken);
Crc = BinaryPrimitives.ReadUInt32LittleEndian(trailer);
UncompressedSize = BinaryPrimitives.ReadUInt32LittleEndian(trailer.AsSpan().Slice(4));
}
private void ReadAndValidateGzipHeader()
{
// read the header on the first read
@@ -151,61 +109,6 @@ internal sealed class GZipFilePart : FilePart
}
}
private async ValueTask ReadAndValidateGzipHeaderAsync(
CancellationToken cancellationToken = default
)
{
// read the header on the first read
var header = new byte[10];
var n = await _stream.ReadAsync(header, 0, 10, cancellationToken);
// workitem 8501: handle edge case (decompress empty stream)
if (n == 0)
{
return;
}
if (n != 10)
{
throw new ZlibException("Not a valid GZIP stream.");
}
if (header[0] != 0x1F || header[1] != 0x8B || header[2] != 8)
{
throw new ZlibException("Bad GZIP header.");
}
var timet = BinaryPrimitives.ReadInt32LittleEndian(header.AsSpan().Slice(4));
DateModified = TarHeader.EPOCH.AddSeconds(timet);
if ((header[3] & 0x04) == 0x04)
{
// read and discard extra field
var lengthField = new byte[2];
_ = await _stream.ReadAsync(lengthField, 0, 2, cancellationToken);
var extraLength = (short)(lengthField[0] + (lengthField[1] * 256));
var extra = new byte[extraLength];
if (!await _stream.ReadFullyAsync(extra, cancellationToken))
{
throw new ZlibException("Unexpected end-of-file reading GZIP header.");
}
}
if ((header[3] & 0x08) == 0x08)
{
_name = await ReadZeroTerminatedStringAsync(_stream, cancellationToken);
}
if ((header[3] & 0x10) == 0x010)
{
await ReadZeroTerminatedStringAsync(_stream, cancellationToken);
}
if ((header[3] & 0x02) == 0x02)
{
var buf = new byte[1];
_ = await _stream.ReadAsync(buf, 0, 1, cancellationToken); // CRC16, ignore
}
}
private string ReadZeroTerminatedString(Stream stream)
{
Span<byte> buf1 = stackalloc byte[1];
@@ -231,33 +134,4 @@ internal sealed class GZipFilePart : FilePart
var buffer = list.ToArray();
return ArchiveEncoding.Decode(buffer);
}
private async ValueTask<string> ReadZeroTerminatedStringAsync(
Stream stream,
CancellationToken cancellationToken = default
)
{
var buf1 = new byte[1];
var list = new List<byte>();
var done = false;
do
{
// workitem 7740
var n = await stream.ReadAsync(buf1, 0, 1, cancellationToken);
if (n != 1)
{
throw new ZlibException("Unexpected EOF reading GZIP header.");
}
if (buf1[0] == 0)
{
done = true;
}
else
{
list.Add(buf1[0]);
}
} while (!done);
var buffer = list.ToArray();
return ArchiveEncoding.Decode(buffer);
}
}

View File

@@ -1,36 +0,0 @@
using System;
using System.Text;
namespace SharpCompress.Common;
/// <summary>
/// Defines the encoding settings for archives.
/// </summary>
public interface IArchiveEncoding
{
/// <summary>
/// Default encoding to use when archive format doesn't specify one. Required and defaults to Encoding.Default.
/// </summary>
public Encoding Default { get; set; }
/// <summary>
/// ArchiveEncoding used by encryption schemes which don't comply with RFC 2898. Required and defaults to Encoding.Default.
/// </summary>
public Encoding Password { get; set; }
/// <summary>
/// Default encoding to use when archive format specifies UTF-8 encoding. Required and defaults to Encoding.UTF8.
/// </summary>
public Encoding UTF8 { get; set; }
/// <summary>
/// Set this encoding when you want to force it for all encoding operations.
/// </summary>
public Encoding? Forced { get; set; }
/// <summary>
/// Set this when you want to use a custom method for all decoding operations.
/// </summary>
/// <returns>string Func(bytes, index, length, EncodingType)</returns>
public Func<byte[], int, int, EncodingType, string>? CustomDecoder { get; set; }
}

View File

@@ -7,5 +7,5 @@ public class OptionsBase
/// </summary>
public bool LeaveStreamOpen { get; set; } = true;
public IArchiveEncoding ArchiveEncoding { get; set; } = new ArchiveEncoding();
public ArchiveEncoding ArchiveEncoding { get; set; } = new();
}

View File

@@ -13,7 +13,7 @@ internal class RarHeader : IRarHeader
internal static RarHeader? TryReadBase(
RarCrcBinaryReader reader,
bool isRar5,
IArchiveEncoding archiveEncoding
ArchiveEncoding archiveEncoding
)
{
try
@@ -26,7 +26,7 @@ internal class RarHeader : IRarHeader
}
}
private RarHeader(RarCrcBinaryReader reader, bool isRar5, IArchiveEncoding archiveEncoding)
private RarHeader(RarCrcBinaryReader reader, bool isRar5, ArchiveEncoding archiveEncoding)
{
_headerType = HeaderType.Null;
_isRar5 = isRar5;
@@ -115,7 +115,7 @@ internal class RarHeader : IRarHeader
protected int HeaderSize { get; }
internal IArchiveEncoding ArchiveEncoding { get; }
internal ArchiveEncoding ArchiveEncoding { get; }
/// <summary>
/// Extra header size.

View File

@@ -15,7 +15,7 @@ internal class SevenZipFilePart : FilePart
ArchiveDatabase database,
int index,
CFileItem fileEntry,
IArchiveEncoding archiveEncoding
ArchiveEncoding archiveEncoding
)
: base(archiveEncoding)
{

View File

@@ -11,7 +11,7 @@ internal sealed class TarHeader
internal static readonly DateTime EPOCH = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public TarHeader(
IArchiveEncoding archiveEncoding,
ArchiveEncoding archiveEncoding,
TarHeaderWriteFormat writeFormat = TarHeaderWriteFormat.GNU_TAR_LONG_LINK
)
{
@@ -30,7 +30,7 @@ internal sealed class TarHeader
internal DateTime LastModifiedTime { get; set; }
internal EntryType EntryType { get; set; }
internal Stream? PackedStream { get; set; }
internal IArchiveEncoding ArchiveEncoding { get; }
internal ArchiveEncoding ArchiveEncoding { get; }
internal const int BLOCK_SIZE = 512;

View File

@@ -54,7 +54,7 @@ public class TarEntry : Entry
StreamingMode mode,
Stream stream,
CompressionType compressionType,
IArchiveEncoding archiveEncoding
ArchiveEncoding archiveEncoding
)
{
foreach (var header in TarHeaderFactory.ReadHeader(mode, stream, archiveEncoding))

View File

@@ -1,6 +1,5 @@
using System.IO;
using SharpCompress.Common.Tar.Headers;
using SharpCompress.IO;
namespace SharpCompress.Common.Tar;
@@ -21,45 +20,8 @@ internal sealed class TarFilePart : FilePart
internal override Stream GetCompressedStream()
{
if (_seekableStream is not null)
if (_seekableStream != null)
{
// If the seekable stream is a SourceStream in file mode with multi-threading enabled,
// create an independent stream to support concurrent extraction
if (
_seekableStream is SourceStream sourceStream
&& sourceStream.IsFileMode
&& sourceStream.ReaderOptions.EnableMultiThreadedExtraction
)
{
var independentStream = sourceStream.CreateIndependentStream(0);
if (independentStream is not null)
{
independentStream.Position = Header.DataStartPosition ?? 0;
return new TarReadOnlySubStream(independentStream, Header.Size);
}
}
// Check if the seekable stream wraps a FileStream
Stream? underlyingStream = _seekableStream;
if (_seekableStream is IStreamStack streamStack)
{
underlyingStream = streamStack.BaseStream();
}
if (underlyingStream is FileStream fileStream)
{
// Create a new independent stream from the file
var independentStream = new FileStream(
fileStream.Name,
FileMode.Open,
FileAccess.Read,
FileShare.Read
);
independentStream.Position = Header.DataStartPosition ?? 0;
return new TarReadOnlySubStream(independentStream, Header.Size);
}
// Fall back to existing behavior for stream-based sources
_seekableStream.Position = Header.DataStartPosition ?? 0;
return new TarReadOnlySubStream(_seekableStream, Header.Size);
}

View File

@@ -10,7 +10,7 @@ internal static class TarHeaderFactory
internal static IEnumerable<TarHeader?> ReadHeader(
StreamingMode mode,
Stream stream,
IArchiveEncoding archiveEncoding
ArchiveEncoding archiveEncoding
)
{
while (true)

View File

@@ -66,7 +66,7 @@ internal class TarReadOnlySubStream : SharpCompressStream, IStreamStack
base.Dispose(disposing);
}
#if !LEGACY_DOTNET
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async System.Threading.Tasks.ValueTask DisposeAsync()
{
if (_isDisposed)
@@ -170,7 +170,7 @@ internal class TarReadOnlySubStream : SharpCompressStream, IStreamStack
return read;
}
#if !LEGACY_DOTNET
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async System.Threading.Tasks.ValueTask<int> ReadAsync(
System.Memory<byte> buffer,
System.Threading.CancellationToken cancellationToken = default

View File

@@ -1,5 +1,4 @@
using System.IO;
using System.Threading.Tasks;
namespace SharpCompress.Common.Zip.Headers;
@@ -20,19 +19,6 @@ internal class DirectoryEndHeader : ZipHeader
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 = new byte[CommentLength];
await reader.ReadBytesAsync(Comment, 0, CommentLength);
}
public ushort VolumeNumber { get; private set; }
public ushort FirstVolumeWithDirectory { get; private set; }

View File

@@ -1,12 +1,11 @@
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace SharpCompress.Common.Zip.Headers;
internal class DirectoryEntryHeader : ZipFileEntry
{
public DirectoryEntryHeader(IArchiveEncoding archiveEncoding)
public DirectoryEntryHeader(ArchiveEncoding archiveEncoding)
: base(ZipHeaderType.DirectoryEntry, archiveEncoding) { }
internal override void Read(BinaryReader reader)
@@ -32,39 +31,7 @@ internal class DirectoryEntryHeader : ZipFileEntry
var extra = reader.ReadBytes(extraLength);
var comment = reader.ReadBytes(commentLength);
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 = new byte[nameLength];
var extra = new byte[extraLength];
var comment = new byte[commentLength];
await reader.ReadBytesAsync(name, 0, nameLength);
await reader.ReadBytesAsync(extra, 0, extraLength);
await reader.ReadBytesAsync(comment, 0, commentLength);
ProcessReadData(name, extra, comment);
}
private void ProcessReadData(byte[] name, byte[] extra, byte[] comment)
{
// According to .ZIP File Format Specification
//
// For example: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
//
@@ -74,8 +41,8 @@ internal class DirectoryEntryHeader : ZipFileEntry
if (Flags.HasFlag(HeaderFlags.Efs))
{
Name = ArchiveEncoding.Decode(name, EncodingType.UTF8);
Comment = ArchiveEncoding.Decode(comment, EncodingType.UTF8);
Name = ArchiveEncoding.DecodeUTF8(name);
Comment = ArchiveEncoding.DecodeUTF8(comment);
}
else
{

View File

@@ -1,5 +1,4 @@
using System.IO;
using System.Threading.Tasks;
namespace SharpCompress.Common.Zip.Headers;
@@ -9,6 +8,4 @@ internal class IgnoreHeader : ZipHeader
: base(type) { }
internal override void Read(BinaryReader reader) { }
internal override ValueTask Read(AsyncBinaryReader reader) => default;
}

View File

@@ -1,12 +1,13 @@
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace SharpCompress.Common.Zip.Headers;
internal class LocalEntryHeader(IArchiveEncoding archiveEncoding)
: ZipFileEntry(ZipHeaderType.LocalEntry, archiveEncoding)
internal class LocalEntryHeader : ZipFileEntry
{
public LocalEntryHeader(ArchiveEncoding archiveEncoding)
: base(ZipHeaderType.LocalEntry, archiveEncoding) { }
internal override void Read(BinaryReader reader)
{
Version = reader.ReadUInt16();
@@ -22,31 +23,7 @@ internal class LocalEntryHeader(IArchiveEncoding archiveEncoding)
var name = reader.ReadBytes(nameLength);
var extra = reader.ReadBytes(extraLength);
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 = new byte[nameLength];
var extra = new byte[extraLength];
await reader.ReadBytesAsync(name, 0, nameLength);
await reader.ReadBytesAsync(extra, 0, extraLength);
ProcessReadData(name, extra);
}
private void ProcessReadData(byte[] name, byte[] extra)
{
// According to .ZIP File Format Specification
//
// For example: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
//
@@ -56,7 +33,7 @@ internal class LocalEntryHeader(IArchiveEncoding archiveEncoding)
if (Flags.HasFlag(HeaderFlags.Efs))
{
Name = ArchiveEncoding.Decode(name, EncodingType.UTF8);
Name = ArchiveEncoding.DecodeUTF8(name);
}
else
{

View File

@@ -1,6 +1,5 @@
using System;
using System.IO;
using System.Threading.Tasks;
namespace SharpCompress.Common.Zip.Headers;
@@ -10,7 +9,4 @@ internal class SplitHeader : ZipHeader
: base(ZipHeaderType.Split) { }
internal override void Read(BinaryReader reader) => throw new NotImplementedException();
internal override ValueTask Read(AsyncBinaryReader reader) =>
throw new NotImplementedException();
}

View File

@@ -1,5 +1,4 @@
using System.IO;
using System.Threading.Tasks;
namespace SharpCompress.Common.Zip.Headers;
@@ -27,24 +26,6 @@ internal class Zip64DirectoryEndHeader : ZipHeader
);
}
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();
var size = (int)(
SizeOfDirectoryEndRecord - SIZE_OF_FIXED_HEADER_DATA_EXCEPT_SIGNATURE_AND_SIZE_FIELDS
);
DataSector = new byte[size];
await reader.ReadBytesAsync(DataSector, 0, size);
}
private const int SIZE_OF_FIXED_HEADER_DATA_EXCEPT_SIGNATURE_AND_SIZE_FIELDS = 44;
public long SizeOfDirectoryEndRecord { get; private set; }

View File

@@ -1,10 +1,12 @@
using System.IO;
using System.Threading.Tasks;
namespace SharpCompress.Common.Zip.Headers;
internal class Zip64DirectoryEndLocatorHeader() : ZipHeader(ZipHeaderType.Zip64DirectoryEndLocator)
internal class Zip64DirectoryEndLocatorHeader : ZipHeader
{
public Zip64DirectoryEndLocatorHeader()
: base(ZipHeaderType.Zip64DirectoryEndLocator) { }
internal override void Read(BinaryReader reader)
{
FirstVolumeWithDirectory = reader.ReadUInt32();
@@ -12,13 +14,6 @@ internal class Zip64DirectoryEndLocatorHeader() : ZipHeader(ZipHeaderType.Zip64D
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; }

View File

@@ -2,14 +2,18 @@ using System;
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(ZipHeaderType type, IArchiveEncoding archiveEncoding)
: ZipHeader(type)
internal abstract class ZipFileEntry : ZipHeader
{
protected ZipFileEntry(ZipHeaderType type, ArchiveEncoding archiveEncoding)
: base(type)
{
Extra = new List<ExtraData>();
ArchiveEncoding = archiveEncoding;
}
internal bool IsDirectory
{
get
@@ -26,7 +30,7 @@ internal abstract class ZipFileEntry(ZipHeaderType type, IArchiveEncoding archiv
internal Stream? PackedStream { get; set; }
internal IArchiveEncoding ArchiveEncoding { get; } = archiveEncoding;
internal ArchiveEncoding ArchiveEncoding { get; }
internal string? Name { get; set; }
@@ -40,7 +44,7 @@ internal abstract class ZipFileEntry(ZipHeaderType type, IArchiveEncoding archiv
internal long UncompressedSize { get; set; }
internal List<ExtraData> Extra { get; set; } = new();
internal List<ExtraData> Extra { get; set; }
public string? Password { get; set; }
@@ -59,24 +63,6 @@ internal abstract class ZipFileEntry(ZipHeaderType type, IArchiveEncoding archiv
return encryptionData;
}
internal async ValueTask<PkwareTraditionalEncryptionData> 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; }
/// <summary>

View File

@@ -1,14 +1,18 @@
using System.IO;
using System.Threading.Tasks;
namespace SharpCompress.Common.Zip.Headers;
internal abstract class ZipHeader(ZipHeaderType type)
internal abstract class ZipHeader
{
internal ZipHeaderType ZipHeaderType { get; } = type;
protected ZipHeader(ZipHeaderType type)
{
ZipHeaderType = type;
HasData = true;
}
internal ZipHeaderType ZipHeaderType { get; }
internal abstract void Read(BinaryReader reader);
internal abstract ValueTask Read(AsyncBinaryReader reader);
internal bool HasData { get; set; } = true;
internal bool HasData { get; set; }
}

View File

@@ -8,9 +8,9 @@ internal class PkwareTraditionalEncryptionData
{
private static readonly CRC32 CRC32 = new();
private readonly uint[] _keys = { 0x12345678, 0x23456789, 0x34567890 };
private readonly IArchiveEncoding _archiveEncoding;
private readonly ArchiveEncoding _archiveEncoding;
private PkwareTraditionalEncryptionData(string password, IArchiveEncoding archiveEncoding)
private PkwareTraditionalEncryptionData(string password, ArchiveEncoding archiveEncoding)
{
_archiveEncoding = archiveEncoding;
Initialize(password);
@@ -103,7 +103,7 @@ internal class PkwareTraditionalEncryptionData
internal byte[] StringToByteArray(string value)
{
var a = _archiveEncoding.Password.GetBytes(value);
var a = _archiveEncoding.GetPasswordEncoding().GetBytes(value);
return a;
}

View File

@@ -1,17 +1,12 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common.Zip.Headers;
using SharpCompress.IO;
namespace SharpCompress.Common.Zip;
internal class SeekableZipFilePart : ZipFilePart
{
private volatile bool _isLocalHeaderLoaded;
private bool _isLocalHeaderLoaded;
private readonly SeekableZipHeaderFactory _headerFactory;
private readonly object _headerLock = new();
private readonly SemaphoreSlim _asyncHeaderSemaphore = new(1, 1);
internal SeekableZipFilePart(
SeekableZipHeaderFactory headerFactory,
@@ -24,191 +19,19 @@ internal class SeekableZipFilePart : ZipFilePart
{
if (!_isLocalHeaderLoaded)
{
lock (_headerLock)
{
if (!_isLocalHeaderLoaded)
{
LoadLocalHeader();
_isLocalHeaderLoaded = true;
}
}
LoadLocalHeader();
_isLocalHeaderLoaded = true;
}
return base.GetCompressedStream();
}
internal override async ValueTask<Stream?> GetCompressedStreamAsync(
CancellationToken cancellationToken = default
)
{
if (!_isLocalHeaderLoaded)
{
await _asyncHeaderSemaphore.WaitAsync(cancellationToken);
try
{
if (!_isLocalHeaderLoaded)
{
await LoadLocalHeaderAsync(cancellationToken);
_isLocalHeaderLoaded = true;
}
}
finally
{
_asyncHeaderSemaphore.Release();
}
}
return await base.GetCompressedStreamAsync(cancellationToken);
}
private void LoadLocalHeader()
{
// Use an independent stream for loading the header if multi-threading is enabled
Stream streamToUse = BaseStream;
bool disposeStream = false;
if (
BaseStream is SourceStream sourceStream
&& sourceStream.IsFileMode
&& sourceStream.ReaderOptions.EnableMultiThreadedExtraction
)
{
var independentStream = sourceStream.CreateIndependentStream(0);
if (independentStream is not null)
{
streamToUse = independentStream;
disposeStream = true;
}
}
else
{
// Check if BaseStream wraps a FileStream
Stream? underlyingStream = BaseStream;
if (BaseStream is IStreamStack streamStack)
{
underlyingStream = streamStack.BaseStream();
}
if (underlyingStream is FileStream fileStream)
{
streamToUse = new FileStream(
fileStream.Name,
FileMode.Open,
FileAccess.Read,
FileShare.Read
);
disposeStream = true;
}
}
try
{
Header = _headerFactory.GetLocalHeader(streamToUse, (DirectoryEntryHeader)Header);
}
finally
{
if (disposeStream)
{
streamToUse.Dispose();
}
}
}
private async ValueTask LoadLocalHeaderAsync(CancellationToken cancellationToken = default)
{
// Use an independent stream for loading the header if multi-threading is enabled
Stream streamToUse = BaseStream;
bool disposeStream = false;
if (
BaseStream is SourceStream sourceStream
&& sourceStream.IsFileMode
&& sourceStream.ReaderOptions.EnableMultiThreadedExtraction
)
{
var independentStream = sourceStream.CreateIndependentStream(0);
if (independentStream is not null)
{
streamToUse = independentStream;
disposeStream = true;
}
}
else
{
// Check if BaseStream wraps a FileStream
Stream? underlyingStream = BaseStream;
if (BaseStream is IStreamStack streamStack)
{
underlyingStream = streamStack.BaseStream();
}
if (underlyingStream is FileStream fileStream)
{
streamToUse = new FileStream(
fileStream.Name,
FileMode.Open,
FileAccess.Read,
FileShare.Read
);
disposeStream = true;
}
}
try
{
Header = await _headerFactory.GetLocalHeaderAsync(
streamToUse,
(DirectoryEntryHeader)Header
);
}
finally
{
if (disposeStream)
{
streamToUse.Dispose();
}
}
}
private void LoadLocalHeader() =>
Header = _headerFactory.GetLocalHeader(BaseStream, (DirectoryEntryHeader)Header);
protected override Stream CreateBaseStream()
{
// If BaseStream is a SourceStream in file mode with multi-threading enabled,
// create an independent stream to support concurrent extraction
if (
BaseStream is SourceStream sourceStream
&& sourceStream.IsFileMode
&& sourceStream.ReaderOptions.EnableMultiThreadedExtraction
)
{
// Create a new independent stream for this entry
var independentStream = sourceStream.CreateIndependentStream(0);
if (independentStream is not null)
{
independentStream.Position = Header.DataStartPosition.NotNull();
return independentStream;
}
}
// Check if BaseStream wraps a FileStream (for multi-volume archives)
Stream? underlyingStream = BaseStream;
if (BaseStream is IStreamStack streamStack)
{
underlyingStream = streamStack.BaseStream();
}
if (underlyingStream is FileStream fileStream)
{
// Create a new independent stream from the file
var independentStream = new FileStream(
fileStream.Name,
FileMode.Open,
FileAccess.Read,
FileShare.Read
);
independentStream.Position = Header.DataStartPosition.NotNull();
return independentStream;
}
// Fall back to existing behavior for stream-based sources
BaseStream.Position = Header.DataStartPosition.NotNull();
return BaseStream;
}
}

View File

@@ -1,8 +1,6 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using SharpCompress.Common.Zip.Headers;
using SharpCompress.IO;
@@ -17,79 +15,10 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory
private const int MAX_SEARCH_LENGTH_FOR_EOCD = 65557;
private bool _zip64;
private static readonly byte[] needle = { 0x06, 0x05, 0x4b, 0x50 };
internal SeekableZipHeaderFactory(string? password, IArchiveEncoding archiveEncoding)
internal SeekableZipHeaderFactory(string? password, ArchiveEncoding archiveEncoding)
: base(StreamingMode.Seekable, password, archiveEncoding) { }
internal async IAsyncEnumerable<ZipHeader> 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<ZipHeader> ReadSeekableHeader(Stream stream, bool useSync)
internal IEnumerable<ZipHeader> ReadSeekableHeader(Stream stream)
{
var reader = new BinaryReader(stream);
@@ -156,7 +85,7 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory
}
}
private static bool IsMatch(Span<byte> haystack, int position, byte[] needle)
private static bool IsMatch(byte[] haystack, int position, byte[] needle)
{
for (var i = 0; i < needle.Length; i++)
{
@@ -169,51 +98,6 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory
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;
stream.Seek(-len, SeekOrigin.End);
var seek = ArrayPool<byte>.Shared.Rent(len);
try
{
await reader.ReadBytesAsync(seek, 0, len, default);
var memory = new Memory<byte>(seek, 0, len);
var span = memory.Span;
span.Reverse();
// 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(span, pos_from_end, needle))
{
stream.Seek(-pos_from_end, SeekOrigin.End);
return;
}
}
throw new ArchiveException("Failed to locate the Zip Header");
}
finally
{
ArrayPool<byte>.Shared.Return(seek);
}
}
private static void SeekBackToHeader(Stream stream, BinaryReader reader)
{
// Minimum EOCD length
@@ -228,6 +112,9 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory
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 = reader.ReadBytes(len);
@@ -276,31 +163,4 @@ internal sealed class SeekableZipHeaderFactory : ZipHeaderFactory
}
return localEntryHeader;
}
internal async ValueTask<LocalEntryHeader> 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;
}
}

View File

@@ -1,6 +1,4 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common.Zip.Headers;
using SharpCompress.Compressors.Deflate;
using SharpCompress.IO;
@@ -33,28 +31,6 @@ internal sealed class StreamingZipFilePart : ZipFilePart
return _decompressionStream;
}
internal override async ValueTask<Stream?> 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)

View File

@@ -2,9 +2,6 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Zip.Headers;
using SharpCompress.IO;
@@ -16,7 +13,7 @@ internal class StreamingZipHeaderFactory : ZipHeaderFactory
internal StreamingZipHeaderFactory(
string? password,
IArchiveEncoding archiveEncoding,
ArchiveEncoding archiveEncoding,
IEnumerable<ZipEntry>? entries
)
: base(StreamingMode.Streaming, password, archiveEncoding) => _entries = entries;
@@ -203,331 +200,4 @@ internal class StreamingZipHeaderFactory : ZipHeaderFactory
yield return header;
}
}
/// <summary>
/// Reads ZIP headers asynchronously for streams that do not support synchronous reads.
/// </summary>
internal IAsyncEnumerable<ZipHeader> ReadStreamHeaderAsync(Stream stream) =>
new StreamHeaderAsyncEnumerable(this, stream);
/// <summary>
/// Invokes the shared async header parsing logic on the base factory.
/// </summary>
private ValueTask<ZipHeader?> ReadHeaderAsyncInternal(
uint headerBytes,
AsyncBinaryReader reader
) => ReadHeader(headerBytes, reader);
/// <summary>
/// Exposes the last parsed local entry header to the async enumerator so it can handle streaming data descriptors.
/// </summary>
private LocalEntryHeader? LastEntryHeader
{
get => _lastEntryHeader;
set => _lastEntryHeader = value;
}
/// <summary>
/// Produces an async enumerator for streaming ZIP headers.
/// </summary>
private sealed class StreamHeaderAsyncEnumerable : IAsyncEnumerable<ZipHeader>
{
private readonly StreamingZipHeaderFactory _headerFactory;
private readonly Stream _stream;
public StreamHeaderAsyncEnumerable(StreamingZipHeaderFactory headerFactory, Stream stream)
{
_headerFactory = headerFactory;
_stream = stream;
}
public IAsyncEnumerator<ZipHeader> GetAsyncEnumerator(
CancellationToken cancellationToken = default
) => new StreamHeaderAsyncEnumerator(_headerFactory, _stream, cancellationToken);
}
/// <summary>
/// Async implementation of <see cref="ReadStreamHeader"/> using <see cref="AsyncBinaryReader"/> to avoid sync reads.
/// </summary>
private sealed class StreamHeaderAsyncEnumerator : IAsyncEnumerator<ZipHeader>, 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.");
/// <summary>
/// Advances to the next ZIP header in the stream, honoring streaming data descriptors where applicable.
/// </summary>
public async ValueTask<bool> 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;
}
/// <summary>
/// Disposes the underlying reader (without closing the archive stream).
/// </summary>
public void Dispose()
{
_reader.Dispose();
}
/// <summary>
/// Ensures the stream is a <see cref="SharpCompressStream"/> so header parsing can use rewind/buffer helpers.
/// </summary>
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));
}
}
}

View File

@@ -20,7 +20,7 @@ internal class WinzipAesEncryptionData
{
_keySize = keySize;
#if LEGACY_DOTNET
#if NETFRAMEWORK
var rfc2898 = new Rfc2898DeriveBytes(password, salt, RFC2898_ITERATIONS);
KeyBytes = rfc2898.GetBytes(KeySizeInBytes);
IvBytes = rfc2898.GetBytes(KeySizeInBytes);

View File

@@ -2,8 +2,6 @@ using System;
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;
@@ -266,244 +264,4 @@ internal abstract class ZipFilePart : FilePart
}
return plainStream;
}
internal override async ValueTask<Stream?> 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<Stream> 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<Stream> 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);
}
}
}
}

View File

@@ -1,8 +1,6 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using SharpCompress;
using SharpCompress.Common.Zip.Headers;
using SharpCompress.IO;
@@ -23,12 +21,12 @@ internal class ZipHeaderFactory
protected LocalEntryHeader? _lastEntryHeader;
private readonly string? _password;
private readonly StreamingMode _mode;
private readonly IArchiveEncoding _archiveEncoding;
private readonly ArchiveEncoding _archiveEncoding;
protected ZipHeaderFactory(
StreamingMode mode,
string? password,
IArchiveEncoding archiveEncoding
ArchiveEncoding archiveEncoding
)
{
_mode = mode;
@@ -36,82 +34,6 @@ internal class ZipHeaderFactory
_archiveEncoding = archiveEncoding;
}
protected async ValueTask<ZipHeader?> 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.SkipAsync(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)
@@ -283,82 +205,4 @@ internal class ZipHeaderFactory
//}
}
/// <summary>
/// Loads encryption metadata and stream positioning for a header using async reads where needed.
/// </summary>
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");
}
}
}
}

View File

@@ -104,7 +104,7 @@ public static class ADCBase
/// <param name="bufferSize">Max size for decompressed data</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Result containing bytes read and decompressed data</returns>
public static async ValueTask<AdcDecompressResult> DecompressAsync(
public static async Task<AdcDecompressResult> DecompressAsync(
byte[] input,
int bufferSize = 262144,
CancellationToken cancellationToken = default
@@ -117,7 +117,7 @@ public static class ADCBase
/// <param name="bufferSize">Max size for decompressed data</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Result containing bytes read and decompressed data</returns>
public static async ValueTask<AdcDecompressResult> DecompressAsync(
public static async Task<AdcDecompressResult> DecompressAsync(
Stream input,
int bufferSize = 262144,
CancellationToken cancellationToken = default

View File

@@ -30,6 +30,7 @@ public sealed class BZip2Stream : Stream, IStreamStack
private readonly Stream stream;
private bool isDisposed;
private readonly bool leaveOpen;
/// <summary>
/// Create a BZip2Stream
@@ -37,19 +38,30 @@ public sealed class BZip2Stream : Stream, IStreamStack
/// <param name="stream">The stream to read from</param>
/// <param name="compressionMode">Compression Mode</param>
/// <param name="decompressConcatenated">Decompress Concatenated</param>
public BZip2Stream(Stream stream, CompressionMode compressionMode, bool decompressConcatenated)
/// <param name="leaveOpen">Leave the stream open after disposing</param>
public BZip2Stream(
Stream stream,
CompressionMode compressionMode,
bool decompressConcatenated,
bool leaveOpen = false
)
{
#if DEBUG_STREAMS
this.DebugConstruct(typeof(BZip2Stream));
#endif
this.leaveOpen = leaveOpen;
Mode = compressionMode;
if (Mode == CompressionMode.Compress)
{
this.stream = new CBZip2OutputStream(stream);
this.stream = new CBZip2OutputStream(stream, 9, leaveOpen);
}
else
{
this.stream = new CBZip2InputStream(stream, decompressConcatenated);
this.stream = new CBZip2InputStream(
stream,
decompressConcatenated,
leaveOpen: leaveOpen
);
}
}
@@ -98,7 +110,7 @@ public sealed class BZip2Stream : Stream, IStreamStack
public override void SetLength(long value) => stream.SetLength(value);
#if !LEGACY_DOTNET
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override int Read(Span<byte> buffer) => stream.Read(buffer);

View File

@@ -168,6 +168,7 @@ internal class CBZip2InputStream : Stream, IStreamStack
private int computedBlockCRC,
computedCombinedCRC;
private readonly bool decompressConcatenated;
private readonly bool leaveOpen;
private int i2,
count,
@@ -181,9 +182,10 @@ internal class CBZip2InputStream : Stream, IStreamStack
private char z;
private bool isDisposed;
public CBZip2InputStream(Stream zStream, bool decompressConcatenated)
public CBZip2InputStream(Stream zStream, bool decompressConcatenated, bool leaveOpen = false)
{
this.decompressConcatenated = decompressConcatenated;
this.leaveOpen = leaveOpen;
ll8 = null;
tt = null;
BsSetStream(zStream);
@@ -207,7 +209,10 @@ internal class CBZip2InputStream : Stream, IStreamStack
this.DebugDispose(typeof(CBZip2InputStream));
#endif
base.Dispose(disposing);
bsStream?.Dispose();
if (!leaveOpen)
{
bsStream?.Dispose();
}
}
internal static int[][] InitIntArray(int n1, int n2)
@@ -398,7 +403,10 @@ internal class CBZip2InputStream : Stream, IStreamStack
private void BsFinishedWithStream()
{
bsStream?.Dispose();
if (!leaveOpen)
{
bsStream?.Dispose();
}
bsStream = null;
}

View File

@@ -341,12 +341,14 @@ internal sealed class CBZip2OutputStream : Stream, IStreamStack
private int currentChar = -1;
private int runLength;
private readonly bool leaveOpen;
public CBZip2OutputStream(Stream inStream)
: this(inStream, 9) { }
public CBZip2OutputStream(Stream inStream, bool leaveOpen = false)
: this(inStream, 9, leaveOpen) { }
public CBZip2OutputStream(Stream inStream, int inBlockSize)
public CBZip2OutputStream(Stream inStream, int inBlockSize, bool leaveOpen = false)
{
this.leaveOpen = leaveOpen;
block = null;
quadrant = null;
zptr = null;
@@ -481,7 +483,10 @@ internal sealed class CBZip2OutputStream : Stream, IStreamStack
this.DebugDispose(typeof(CBZip2OutputStream));
#endif
Dispose();
bsStream?.Dispose();
if (!leaveOpen)
{
bsStream?.Dispose();
}
bsStream = null;
}
}

View File

@@ -299,7 +299,7 @@ public class DeflateStream : Stream, IStreamStack
await _baseStream.FlushAsync(cancellationToken).ConfigureAwait(false);
}
#if !LEGACY_DOTNET
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async ValueTask DisposeAsync()
{
if (_disposed)
@@ -370,7 +370,7 @@ public class DeflateStream : Stream, IStreamStack
.ConfigureAwait(false);
}
#if !LEGACY_DOTNET
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
@@ -461,7 +461,7 @@ public class DeflateStream : Stream, IStreamStack
.ConfigureAwait(false);
}
#if !LEGACY_DOTNET
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async ValueTask WriteAsync(
ReadOnlyMemory<byte> buffer,
CancellationToken cancellationToken = default

View File

@@ -345,7 +345,7 @@ public class GZipStream : Stream, IStreamStack
return n;
}
#if !LEGACY_DOTNET
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
@@ -454,7 +454,7 @@ public class GZipStream : Stream, IStreamStack
await BaseStream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false);
}
#if !LEGACY_DOTNET
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async ValueTask WriteAsync(
ReadOnlyMemory<byte> buffer,
CancellationToken cancellationToken = default

View File

@@ -64,7 +64,6 @@
// -----------------------------------------------------------------------
using System;
using System.Buffers;
using SharpCompress.Algorithms;
namespace SharpCompress.Compressors.Deflate;
@@ -117,14 +116,14 @@ internal sealed class InflateBlocks
internal int readAt; // window read pointer
internal int table; // table lengths (14 bits)
internal int[] tb = new int[1]; // bit length decoding tree
internal IMemoryOwner<byte> window; // sliding window
internal byte[] window; // sliding window
internal int writeAt; // window write pointer
internal InflateBlocks(ZlibCodec codec, object checkfn, int w)
{
_codec = codec;
hufts = new int[MANY * 3];
window = MemoryPool<byte>.Shared.Rent(w);
window = new byte[w];
end = w;
this.checkfn = checkfn;
mode = InflateBlockMode.TYPE;
@@ -341,7 +340,7 @@ internal sealed class InflateBlocks
{
t = m;
}
_codec.InputBuffer.AsSpan(p, t).CopyTo(window.Memory.Span.Slice(q));
Array.Copy(_codec.InputBuffer, p, window, q, t);
p += t;
n -= t;
q += t;
@@ -716,14 +715,13 @@ internal sealed class InflateBlocks
internal void Free()
{
Reset();
window?.Dispose();
window = null;
hufts = null;
}
internal void SetDictionary(byte[] d, int start, int n)
{
d.AsSpan(start, n).CopyTo(window.Memory.Span.Slice(0, n));
Array.Copy(d, start, window, 0, n);
readAt = writeAt = n;
}
@@ -776,16 +774,11 @@ internal sealed class InflateBlocks
// update check information
if (checkfn != null)
{
_codec._adler32 = check = Adler32.Calculate(
check,
window.Memory.Span.Slice(readAt, nBytes)
);
_codec._adler32 = check = Adler32.Calculate(check, window.AsSpan(readAt, nBytes));
}
// copy as far as end of window
window
.Memory.Span.Slice(readAt, nBytes)
.CopyTo(_codec.OutputBuffer.AsSpan(_codec.NextOut));
Array.Copy(window, readAt, _codec.OutputBuffer, _codec.NextOut, nBytes);
_codec.NextOut += nBytes;
readAt += nBytes;
@@ -1220,7 +1213,7 @@ internal sealed class InflateCodes
}
}
blocks.window.Memory.Span[q++] = blocks.window.Memory.Span[f++];
blocks.window[q++] = blocks.window[f++];
m--;
if (f == blocks.end)
@@ -1266,7 +1259,7 @@ internal sealed class InflateCodes
}
r = ZlibConstants.Z_OK;
blocks.window.Memory.Span[q++] = (byte)lit;
blocks.window[q++] = (byte)lit;
m--;
mode = START;
@@ -1403,7 +1396,7 @@ internal sealed class InflateCodes
b >>= (tp[tp_index_t_3 + 1]);
k -= (tp[tp_index_t_3 + 1]);
s.window.Memory.Span[q++] = (byte)tp[tp_index_t_3 + 2];
s.window[q++] = (byte)tp[tp_index_t_3 + 2];
m--;
continue;
}
@@ -1468,14 +1461,13 @@ internal sealed class InflateCodes
r = q - d;
if (q - r > 0 && 2 > (q - r))
{
s.window.Memory.Span[q++] = s.window.Memory.Span[r++]; // minimum count is three,
s.window.Memory.Span[q++] = s.window.Memory.Span[r++]; // so unroll loop a little
s.window[q++] = s.window[r++]; // minimum count is three,
s.window[q++] = s.window[r++]; // so unroll loop a little
c -= 2;
}
else
{
s.window.Memory.Span.Slice(r, 2)
.CopyTo(s.window.Memory.Span.Slice(q));
Array.Copy(s.window, r, s.window, q, 2);
q += 2;
r += 2;
c -= 2;
@@ -1498,13 +1490,12 @@ internal sealed class InflateCodes
{
do
{
s.window.Memory.Span[q++] = s.window.Memory.Span[r++];
s.window[q++] = s.window[r++];
} while (--e != 0);
}
else
{
s.window.Memory.Span.Slice(r, e)
.CopyTo(s.window.Memory.Span.Slice(q));
Array.Copy(s.window, r, s.window, q, e);
q += e;
r += e;
e = 0;
@@ -1518,13 +1509,12 @@ internal sealed class InflateCodes
{
do
{
s.window.Memory.Span[q++] = s.window.Memory.Span[r++];
s.window[q++] = s.window[r++];
} while (--c != 0);
}
else
{
s.window.Memory.Span.Slice(r, c)
.CopyTo(s.window.Memory.Span.Slice(q));
Array.Copy(s.window, r, s.window, q, c);
q += c;
r += c;
c = 0;
@@ -1570,7 +1560,7 @@ internal sealed class InflateCodes
{
b >>= (tp[tp_index_t_3 + 1]);
k -= (tp[tp_index_t_3 + 1]);
s.window.Memory.Span[q++] = (byte)tp[tp_index_t_3 + 2];
s.window[q++] = (byte)tp[tp_index_t_3 + 2];
m--;
break;
}

View File

@@ -400,7 +400,7 @@ internal class ZlibBaseStream : Stream, IStreamStack
}
}
private async ValueTask finishAsync(CancellationToken cancellationToken = default)
private async Task finishAsync(CancellationToken cancellationToken = default)
{
if (_z is null)
{
@@ -552,7 +552,7 @@ internal class ZlibBaseStream : Stream, IStreamStack
}
}
#if !LEGACY_DOTNET
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async ValueTask DisposeAsync()
{
if (isDisposed)
@@ -646,9 +646,7 @@ internal class ZlibBaseStream : Stream, IStreamStack
return _encoding.GetString(buffer, 0, buffer.Length);
}
private async ValueTask<string> ReadZeroTerminatedStringAsync(
CancellationToken cancellationToken
)
private async Task<string> ReadZeroTerminatedStringAsync(CancellationToken cancellationToken)
{
var list = new List<byte>();
var done = false;
@@ -731,9 +729,7 @@ internal class ZlibBaseStream : Stream, IStreamStack
return totalBytesRead;
}
private async ValueTask<int> _ReadAndValidateGzipHeaderAsync(
CancellationToken cancellationToken
)
private async Task<int> _ReadAndValidateGzipHeaderAsync(CancellationToken cancellationToken)
{
var totalBytesRead = 0;
@@ -1171,7 +1167,7 @@ internal class ZlibBaseStream : Stream, IStreamStack
return rc;
}
#if !LEGACY_DOTNET
#if !NETFRAMEWORK && !NETSTANDARD2_0
public override async ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default

View File

@@ -108,9 +108,13 @@ internal static class ZlibConstants
public const int Z_BUF_ERROR = -5;
/// <summary>
/// The size of the working buffer used in the ZlibCodec class. Defaults to 16384 bytes.
/// The size of the working buffer used in the ZlibCodec class. Defaults to 8192 bytes.
/// </summary>
#if NETCF
public const int WorkingBufferSizeDefault = 8192;
#else
public const int WorkingBufferSizeDefault = 16384;
#endif
/// <summary>
/// The minimum size of the working buffer used in the ZlibCodec class. Currently it is 128 bytes.

Some files were not shown because too many files have changed in this diff Show More