mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-02-06 13:34:58 +00:00
Compare commits
84 Commits
adam/enabl
...
copilot/se
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
238ed748fc | ||
|
|
be6aefc8c4 | ||
|
|
b8867e7e54 | ||
|
|
8a108b590d | ||
|
|
bca0f67344 | ||
|
|
f3dad51134 | ||
|
|
f51840829c | ||
|
|
aa1c0d0870 | ||
|
|
dee5ee6589 | ||
|
|
b799f479c4 | ||
|
|
b4352fefa5 | ||
|
|
77d06fb60e | ||
|
|
00b647457c | ||
|
|
153d10a35c | ||
|
|
06713c641e | ||
|
|
210978ec2d | ||
|
|
42f7d43139 | ||
|
|
19967f5ad7 | ||
|
|
a1de3eb47d | ||
|
|
e88841bdec | ||
|
|
c8e4915f8e | ||
|
|
a93a3f0598 | ||
|
|
084f81fc8d | ||
|
|
d148f36e87 | ||
|
|
150d9c35b7 | ||
|
|
e11198616e | ||
|
|
2f27f1e6f9 | ||
|
|
5392ca9794 | ||
|
|
46672eb583 | ||
|
|
79653eee80 | ||
|
|
16ad86c52a | ||
|
|
6b7c6be5f5 | ||
|
|
fda1c2cc79 | ||
|
|
ef2fee0ee3 | ||
|
|
e287d0811d | ||
|
|
a7164f3c9f | ||
|
|
c55060039a | ||
|
|
c68d8deddd | ||
|
|
f6eabc5db1 | ||
|
|
72d5884db6 | ||
|
|
3595c89c79 | ||
|
|
9ebbc718c5 | ||
|
|
e862480b86 | ||
|
|
1f3d8fe6f1 | ||
|
|
41ae036ab4 | ||
|
|
588d176b96 | ||
|
|
f8697120a0 | ||
|
|
1a767105e6 | ||
|
|
4067b6ed2c | ||
|
|
b272dbfd1f | ||
|
|
48be7bbf86 | ||
|
|
51e22cea71 | ||
|
|
2241e27e68 | ||
|
|
11c90ae879 | ||
|
|
cf55125202 | ||
|
|
9cefb85905 | ||
|
|
fc672da0e0 | ||
|
|
25b297b142 | ||
|
|
ab03c12fa8 | ||
|
|
3095c805ad | ||
|
|
9c18daafb8 | ||
|
|
16182417fb | ||
|
|
9af35201e4 | ||
|
|
f21b982955 | ||
|
|
b3a20d05c5 | ||
|
|
4cd024a2b2 | ||
|
|
63d08ebfd2 | ||
|
|
c696197b03 | ||
|
|
738a72228b | ||
|
|
90641f4488 | ||
|
|
a4cc7eaf9b | ||
|
|
fdca728fdc | ||
|
|
d2c4ae8cdf | ||
|
|
f3d3ac30a6 | ||
|
|
f8cc4ade8a | ||
|
|
b3975b7bbd | ||
|
|
4f1b61f5bc | ||
|
|
beeb37b4fd | ||
|
|
43aa2bad22 | ||
|
|
1b2ba921bb | ||
|
|
f543da0ea8 | ||
|
|
e60c9efa84 | ||
|
|
c52fc6f240 | ||
|
|
ee136b024a |
8
.github/COPILOT_AGENT_README.md
vendored
8
.github/COPILOT_AGENT_README.md
vendored
@@ -3,11 +3,13 @@
|
||||
This repository includes a minimal opt-in configuration and CI workflow to allow the GitHub Copilot coding agent to open and validate PRs.
|
||||
|
||||
- .copilot-agent.yml: opt-in config for automated agents
|
||||
- .github/agents/copilot-agent.yml: detailed agent policy configuration
|
||||
- .github/workflows/dotnetcore.yml: CI runs on PRs touching the solution, source, or tests to validate changes
|
||||
- AGENTS.yml: general information for this project
|
||||
- AGENTS.md: general instructions for Copilot coding agent with project-specific guidelines
|
||||
|
||||
Maintainers can adjust the allowed paths or disable the agent by editing or removing .copilot-agent.yml.
|
||||
|
||||
Notes:
|
||||
- Do not change any other files in the repository.
|
||||
- If build/test paths are different, update the workflow accordingly; this workflow targets SharpCompress.sln and the SharpCompress.Tests test project.
|
||||
- The agent can create, modify, and delete files within the allowed paths (src, tests, README.md, AGENTS.md)
|
||||
- All changes require review before merge
|
||||
- If build/test paths are different, update the workflow accordingly; this workflow targets SharpCompress.sln and the SharpCompress.Test test project.
|
||||
|
||||
17
.github/agents/copilot-agent.yml
vendored
Normal file
17
.github/agents/copilot-agent.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
enabled: true
|
||||
agent:
|
||||
name: copilot-coding-agent
|
||||
allow:
|
||||
- paths: ["src/**/*", "tests/**/*", "README.md", "AGENTS.md"]
|
||||
actions: ["create", "modify", "delete"]
|
||||
require_review_before_merge: true
|
||||
required_approvals: 1
|
||||
allowed_merge_strategies:
|
||||
- squash
|
||||
- merge
|
||||
auto_merge_on_green: false
|
||||
run_workflows: true
|
||||
notes: |
|
||||
- This manifest expresses the policy for the Copilot coding agent in this repository.
|
||||
- It does NOT install or authorize the agent; a repository admin must install the Copilot coding agent app and grant the repository the necessary permissions (contents: write, pull_requests: write, checks: write, actions: write/read, issues: write) to allow the agent to act.
|
||||
- Keep allow paths narrow and prefer require_review_before_merge during initial rollout.
|
||||
2
.github/workflows/dotnetcore.yml
vendored
2
.github/workflows/dotnetcore.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
- run: dotnet run --project build/build.csproj
|
||||
- uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: ${{ matrix.os }}-sharpcompress.nupkg
|
||||
path: artifacts/*
|
||||
|
||||
106
AGENTS.md
106
AGENTS.md
@@ -1,19 +1,24 @@
|
||||
---
|
||||
description: 'Guidelines for building C# applications'
|
||||
description: 'Guidelines for building SharpCompress - A C# compression library'
|
||||
applyTo: '**/*.cs'
|
||||
---
|
||||
|
||||
# C# Development
|
||||
# SharpCompress Development
|
||||
|
||||
## About SharpCompress
|
||||
SharpCompress is a pure C# compression library supporting multiple archive formats (Zip, Tar, GZip, BZip2, 7Zip, Rar, LZip, XZ, ZStandard) for .NET Framework 4.62, .NET Standard 2.1, .NET 6.0, and .NET 8.0. The library provides both seekable Archive APIs and forward-only Reader/Writer APIs for streaming scenarios.
|
||||
|
||||
## C# Instructions
|
||||
- Always use the latest version C#, currently C# 13 features.
|
||||
- Write clear and concise comments for each function.
|
||||
- Follow the existing code style and patterns in the codebase.
|
||||
|
||||
## General Instructions
|
||||
- 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.
|
||||
- For libraries or external dependencies, mention their usage and purpose in comments.
|
||||
- Preserve backward compatibility when making changes to public APIs.
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
@@ -23,20 +28,26 @@ applyTo: '**/*.cs'
|
||||
|
||||
## Code Formatting
|
||||
|
||||
- Use CSharpier for all code formatting to ensure consistent style across the project.
|
||||
- Install CSharpier globally: `dotnet tool install -g csharpier`
|
||||
- Format files with: `dotnet csharpier format .`
|
||||
- Configure your IDE to format on save using CSharpier.
|
||||
- CSharpier configuration can be customized via `.csharpierrc` file in the project root.
|
||||
- Trust CSharpier's opinionated formatting decisions to maintain consistency.
|
||||
- Use CSharpier for code formatting to ensure consistent style across the project
|
||||
- CSharpier is configured as a local tool in `.config/dotnet-tools.json`
|
||||
- Restore tools with: `dotnet tool restore`
|
||||
- Format files from the project root with: `dotnet csharpier .`
|
||||
- **Run `dotnet csharpier .` from the project root after making code changes before committing**
|
||||
- Configure your IDE to format on save using CSharpier for the best experience
|
||||
- The project also uses `.editorconfig` for editor settings (indentation, encoding, etc.)
|
||||
- Let CSharpier handle code style while `.editorconfig` handles editor behavior
|
||||
|
||||
## Project Setup and Structure
|
||||
|
||||
- Guide users through creating a new .NET project with the appropriate templates.
|
||||
- Explain the purpose of each generated file and folder to build understanding of the project structure.
|
||||
- Demonstrate how to organize code using feature folders or domain-driven design principles.
|
||||
- Show proper separation of concerns with models, services, and data access layers.
|
||||
- Explain the Program.cs and configuration system in ASP.NET Core 9 including environment-specific settings.
|
||||
- The project targets multiple frameworks: .NET Framework 4.62, .NET Standard 2.1, .NET 6.0, and .NET 8.0
|
||||
- Main library is in `src/SharpCompress/`
|
||||
- Tests are in `tests/SharpCompress.Test/`
|
||||
- Performance tests are in `tests/SharpCompress.Performance/`
|
||||
- Test archives are in `tests/TestArchives/`
|
||||
- Build project is in `build/`
|
||||
- Use `dotnet build` to build the solution
|
||||
- Use `dotnet test` to run tests
|
||||
- Solution file: `SharpCompress.sln`
|
||||
|
||||
## Nullable Reference Types
|
||||
|
||||
@@ -44,21 +55,64 @@ applyTo: '**/*.cs'
|
||||
- Always use `is null` or `is not null` instead of `== null` or `!= null`.
|
||||
- Trust the C# null annotations and don't add null checks when the type system says a value cannot be null.
|
||||
|
||||
## SharpCompress-Specific Guidelines
|
||||
|
||||
### Supported Formats
|
||||
SharpCompress supports multiple archive and compression formats:
|
||||
- **Archive Formats**: Zip, Tar, 7Zip, Rar (read-only)
|
||||
- **Compression**: DEFLATE, BZip2, LZMA/LZMA2, PPMd, ZStandard (decompress only), Deflate64 (decompress only)
|
||||
- **Combined Formats**: Tar.GZip, Tar.BZip2, Tar.LZip, Tar.XZ, Tar.ZStandard
|
||||
- See FORMATS.md for complete format support matrix
|
||||
|
||||
### Stream Handling Rules
|
||||
- **Disposal**: As of version 0.21, SharpCompress closes wrapped streams by default
|
||||
- Use `ReaderOptions` or `WriterOptions` with `LeaveStreamOpen = true` to control stream disposal
|
||||
- Use `NonDisposingStream` wrapper when working with compression streams directly to prevent disposal
|
||||
- Always dispose of readers, writers, and archives in `using` blocks
|
||||
- For forward-only operations, use Reader/Writer APIs; for random access, use Archive APIs
|
||||
|
||||
### Async/Await Patterns
|
||||
- All I/O operations support async/await with `CancellationToken`
|
||||
- Async methods follow the naming convention: `MethodNameAsync`
|
||||
- Key async methods:
|
||||
- `WriteEntryToAsync` - Extract entry asynchronously
|
||||
- `WriteAllToDirectoryAsync` - Extract all entries asynchronously
|
||||
- `WriteAsync` - Write entry asynchronously
|
||||
- `WriteAllAsync` - Write directory asynchronously
|
||||
- `OpenEntryStreamAsync` - Open entry stream asynchronously
|
||||
- Always provide `CancellationToken` parameter in async methods
|
||||
|
||||
### Archive APIs vs Reader/Writer APIs
|
||||
- **Archive API**: Use for random access with seekable streams (e.g., `ZipArchive`, `TarArchive`)
|
||||
- **Reader API**: Use for forward-only reading on non-seekable streams (e.g., `ZipReader`, `TarReader`)
|
||||
- **Writer API**: Use for forward-only writing on streams (e.g., `ZipWriter`, `TarWriter`)
|
||||
- 7Zip only supports Archive API due to format limitations
|
||||
|
||||
### Tar-Specific Considerations
|
||||
- Tar format requires file size in the header
|
||||
- If no size is specified to TarWriter and the stream is not seekable, an exception will be thrown
|
||||
- Tar combined with compression (GZip, BZip2, LZip, XZ) is supported
|
||||
|
||||
### Zip-Specific Considerations
|
||||
- Supports Zip64 for large files (seekable streams only)
|
||||
- Supports PKWare and WinZip AES encryption
|
||||
- Multiple compression methods: None, Shrink, Reduce, Implode, DEFLATE, Deflate64, BZip2, LZMA, PPMd
|
||||
- Encrypted LZMA is not supported
|
||||
|
||||
### Performance Considerations
|
||||
- For large files, use Reader/Writer APIs with non-seekable streams to avoid loading entire file in memory
|
||||
- Leverage async I/O for better scalability
|
||||
- Consider compression level trade-offs (speed vs. size)
|
||||
- Use appropriate buffer sizes for stream operations
|
||||
|
||||
## Testing
|
||||
|
||||
- Always include test cases for critical paths of the application.
|
||||
- Guide users through creating unit tests.
|
||||
- Test with multiple archive formats when making changes to core functionality.
|
||||
- Include tests for both Archive and Reader/Writer APIs when applicable.
|
||||
- Test async operations with cancellation tokens.
|
||||
- Do not emit "Act", "Arrange" or "Assert" comments.
|
||||
- Copy existing style in nearby files for test method names and capitalization.
|
||||
- Explain integration testing approaches for API endpoints.
|
||||
- Demonstrate how to mock dependencies for effective testing.
|
||||
- Show how to test authentication and authorization logic.
|
||||
- Explain test-driven development principles as applied to API development.
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
- Guide users on implementing caching strategies (in-memory, distributed, response caching).
|
||||
- Explain asynchronous programming patterns and why they matter for API performance.
|
||||
- Demonstrate pagination, filtering, and sorting for large data sets.
|
||||
- Show how to implement compression and other performance optimizations.
|
||||
- Explain how to measure and benchmark API performance.
|
||||
- Use test archives from `tests/TestArchives` directory for consistency.
|
||||
- Test stream disposal and `LeaveStreamOpen` behavior.
|
||||
- Test edge cases: empty archives, large files, corrupted archives, encrypted archives.
|
||||
|
||||
78
README.md
78
README.md
@@ -4,6 +4,8 @@ SharpCompress is a compression library in pure C# for .NET Framework 4.62, .NET
|
||||
|
||||
The major feature is support for non-seekable streams so large files can be processed on the fly (i.e. download stream).
|
||||
|
||||
**NEW:** All I/O operations now support async/await for improved performance and scalability. See the [Async Usage](#async-usage) section below.
|
||||
|
||||
GitHub Actions Build -
|
||||
[](https://github.com/adamhathcock/sharpcompress/actions/workflows/dotnetcore.yml)
|
||||
[](https://dndocs.com/d/sharpcompress/api/index.html)
|
||||
@@ -32,6 +34,82 @@ Hi everyone. I hope you're using SharpCompress and finding it useful. Please giv
|
||||
|
||||
Please do not email me directly to ask for help. If you think there is a real issue, please report it here.
|
||||
|
||||
## Async Usage
|
||||
|
||||
SharpCompress now provides full async/await support for all I/O operations, allowing for better performance and scalability in modern applications.
|
||||
|
||||
### Async Reading Examples
|
||||
|
||||
Extract entries asynchronously:
|
||||
```csharp
|
||||
using (Stream stream = File.OpenRead("archive.zip"))
|
||||
using (var reader = ReaderFactory.Open(stream))
|
||||
{
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
// Async extraction
|
||||
await reader.WriteEntryToDirectoryAsync(
|
||||
@"C:\temp",
|
||||
new ExtractionOptions() { ExtractFullPath = true, Overwrite = true },
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Extract all entries to directory asynchronously:
|
||||
```csharp
|
||||
using (Stream stream = File.OpenRead("archive.tar.gz"))
|
||||
using (var reader = ReaderFactory.Open(stream))
|
||||
{
|
||||
await reader.WriteAllToDirectoryAsync(
|
||||
@"C:\temp",
|
||||
new ExtractionOptions() { ExtractFullPath = true, Overwrite = true },
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Open entry stream asynchronously:
|
||||
```csharp
|
||||
using (var archive = ZipArchive.Open("archive.zip"))
|
||||
{
|
||||
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
|
||||
{
|
||||
using (var entryStream = await entry.OpenEntryStreamAsync(cancellationToken))
|
||||
{
|
||||
// Process stream asynchronously
|
||||
await entryStream.CopyToAsync(outputStream, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Async Writing Examples
|
||||
|
||||
Write files asynchronously:
|
||||
```csharp
|
||||
using (Stream stream = File.OpenWrite("output.zip"))
|
||||
using (var writer = WriterFactory.Open(stream, ArchiveType.Zip, CompressionType.Deflate))
|
||||
{
|
||||
await writer.WriteAsync("file1.txt", fileStream, DateTime.Now, cancellationToken);
|
||||
}
|
||||
```
|
||||
|
||||
Write all files from directory asynchronously:
|
||||
```csharp
|
||||
using (Stream stream = File.OpenWrite("output.tar.gz"))
|
||||
using (var writer = WriterFactory.Open(stream, ArchiveType.Tar, new WriterOptions(CompressionType.GZip)))
|
||||
{
|
||||
await writer.WriteAllAsync(@"D:\files", "*", SearchOption.AllDirectories, cancellationToken);
|
||||
}
|
||||
```
|
||||
|
||||
All async methods support `CancellationToken` for graceful cancellation of long-running operations.
|
||||
|
||||
## Want to contribute?
|
||||
|
||||
I'm always looking for help or ideas. Please submit code or email with ideas. Unfortunately, just letting me know you'd like to help is not enough because I really have no overall plan of what needs to be done. I'll definitely accept code submissions and add you as a member of the project!
|
||||
|
||||
@@ -21,6 +21,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Config", "Config", "{CDB425
|
||||
Directory.Packages.props = Directory.Packages.props
|
||||
NuGet.config = NuGet.config
|
||||
.github\workflows\dotnetcore.yml = .github\workflows\dotnetcore.yml
|
||||
USAGE.md = USAGE.md
|
||||
README.md = README.md
|
||||
FORMATS.md = FORMATS.md
|
||||
AGENTS.md = AGENTS.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharpCompress.Performance", "tests\SharpCompress.Performance\SharpCompress.Performance.csproj", "{5BDE6DBC-9E5F-4E21-AB71-F138A3E72B17}"
|
||||
|
||||
143
USAGE.md
143
USAGE.md
@@ -1,5 +1,18 @@
|
||||
# SharpCompress Usage
|
||||
|
||||
## 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.
|
||||
|
||||
**Key Async Methods:**
|
||||
- `reader.WriteEntryToAsync(stream, cancellationToken)` - Extract entry asynchronously
|
||||
- `reader.WriteAllToDirectoryAsync(path, options, cancellationToken)` - Extract all asynchronously
|
||||
- `writer.WriteAsync(filename, stream, modTime, cancellationToken)` - Write entry asynchronously
|
||||
- `writer.WriteAllAsync(directory, pattern, searchOption, cancellationToken)` - Write directory asynchronously
|
||||
- `entry.OpenEntryStreamAsync(cancellationToken)` - Open entry stream asynchronously
|
||||
|
||||
See [Async Examples](#async-examples) section below for usage patterns.
|
||||
|
||||
## 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.
|
||||
@@ -172,3 +185,133 @@ foreach(var entry in tr.Entries)
|
||||
Console.WriteLine($"{entry.Key}");
|
||||
}
|
||||
```
|
||||
|
||||
## Async Examples
|
||||
|
||||
### Async Reader Examples
|
||||
|
||||
**Extract single entry asynchronously:**
|
||||
```C#
|
||||
using (Stream stream = File.OpenRead("archive.zip"))
|
||||
using (var reader = ReaderFactory.Open(stream))
|
||||
{
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
using (var entryStream = reader.OpenEntryStream())
|
||||
{
|
||||
using (var outputStream = File.Create("output.bin"))
|
||||
{
|
||||
await reader.WriteEntryToAsync(outputStream, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Extract all entries asynchronously:**
|
||||
```C#
|
||||
using (Stream stream = File.OpenRead("archive.tar.gz"))
|
||||
using (var reader = ReaderFactory.Open(stream))
|
||||
{
|
||||
await reader.WriteAllToDirectoryAsync(
|
||||
@"D:\temp",
|
||||
new ExtractionOptions()
|
||||
{
|
||||
ExtractFullPath = true,
|
||||
Overwrite = true
|
||||
},
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Open and process entry stream asynchronously:**
|
||||
```C#
|
||||
using (var archive = ZipArchive.Open("archive.zip"))
|
||||
{
|
||||
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
|
||||
{
|
||||
using (var entryStream = await entry.OpenEntryStreamAsync(cancellationToken))
|
||||
{
|
||||
// Process the decompressed stream asynchronously
|
||||
await ProcessStreamAsync(entryStream, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Async Writer Examples
|
||||
|
||||
**Write single file asynchronously:**
|
||||
```C#
|
||||
using (Stream archiveStream = File.OpenWrite("output.zip"))
|
||||
using (var writer = WriterFactory.Open(archiveStream, ArchiveType.Zip, CompressionType.Deflate))
|
||||
{
|
||||
using (Stream fileStream = File.OpenRead("input.txt"))
|
||||
{
|
||||
await writer.WriteAsync("entry.txt", fileStream, DateTime.Now, cancellationToken);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Write entire directory asynchronously:**
|
||||
```C#
|
||||
using (Stream stream = File.OpenWrite("backup.tar.gz"))
|
||||
using (var writer = WriterFactory.Open(stream, ArchiveType.Tar, new WriterOptions(CompressionType.GZip)))
|
||||
{
|
||||
await writer.WriteAllAsync(
|
||||
@"D:\files",
|
||||
"*",
|
||||
SearchOption.AllDirectories,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Write with progress tracking and cancellation:**
|
||||
```C#
|
||||
var cts = new CancellationTokenSource();
|
||||
|
||||
// Set timeout or cancel from UI
|
||||
cts.CancelAfter(TimeSpan.FromMinutes(5));
|
||||
|
||||
using (Stream stream = File.OpenWrite("archive.zip"))
|
||||
using (var writer = WriterFactory.Open(stream, ArchiveType.Zip, CompressionType.Deflate))
|
||||
{
|
||||
try
|
||||
{
|
||||
await writer.WriteAllAsync(@"D:\data", "*", SearchOption.AllDirectories, cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Console.WriteLine("Operation was cancelled");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Archive Async Examples
|
||||
|
||||
**Extract from archive asynchronously:**
|
||||
```C#
|
||||
using (var archive = ZipArchive.Open("archive.zip"))
|
||||
{
|
||||
using (var reader = archive.ExtractAllEntries())
|
||||
{
|
||||
await reader.WriteAllToDirectoryAsync(
|
||||
@"C:\output",
|
||||
new ExtractionOptions() { ExtractFullPath = true, Overwrite = true },
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Benefits of Async Operations:**
|
||||
- Non-blocking I/O for better application responsiveness
|
||||
- Improved scalability for server applications
|
||||
- Support for cancellation via CancellationToken
|
||||
- Better resource utilization in async/await contexts
|
||||
- Compatible with modern .NET async patterns
|
||||
|
||||
@@ -2,6 +2,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Writers;
|
||||
@@ -94,6 +96,9 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
|
||||
DateTime? modified
|
||||
) => AddEntry(key, source, closeStream, size, modified);
|
||||
|
||||
IArchiveEntry IWritableArchive.AddDirectoryEntry(string key, DateTime? modified) =>
|
||||
AddDirectoryEntry(key, modified);
|
||||
|
||||
public TEntry AddEntry(
|
||||
string key,
|
||||
Stream source,
|
||||
@@ -134,6 +139,22 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
|
||||
return false;
|
||||
}
|
||||
|
||||
public TEntry AddDirectoryEntry(string key, DateTime? modified = null)
|
||||
{
|
||||
if (key.Length > 0 && key[0] is '/' or '\\')
|
||||
{
|
||||
key = key.Substring(1);
|
||||
}
|
||||
if (DoesKeyMatchExisting(key))
|
||||
{
|
||||
throw new ArchiveException("Cannot add entry with duplicate key: " + key);
|
||||
}
|
||||
var entry = CreateDirectoryEntry(key, modified);
|
||||
newEntries.Add(entry);
|
||||
RebuildModifiedCollection();
|
||||
return entry;
|
||||
}
|
||||
|
||||
public void SaveTo(Stream stream, WriterOptions options)
|
||||
{
|
||||
//reset streams of new entries
|
||||
@@ -141,6 +162,18 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
|
||||
SaveTo(stream, options, OldEntries, newEntries);
|
||||
}
|
||||
|
||||
public async Task SaveToAsync(
|
||||
Stream stream,
|
||||
WriterOptions options,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
//reset streams of new entries
|
||||
newEntries.Cast<IWritableArchiveEntry>().ForEach(x => x.Stream.Seek(0, SeekOrigin.Begin));
|
||||
await SaveToAsync(stream, options, OldEntries, newEntries, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected TEntry CreateEntry(
|
||||
string key,
|
||||
Stream source,
|
||||
@@ -166,6 +199,8 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
|
||||
bool closeStream
|
||||
);
|
||||
|
||||
protected abstract TEntry CreateDirectoryEntry(string key, DateTime? modified);
|
||||
|
||||
protected abstract void SaveTo(
|
||||
Stream stream,
|
||||
WriterOptions options,
|
||||
@@ -173,6 +208,14 @@ public abstract class AbstractWritableArchive<TEntry, TVolume>
|
||||
IEnumerable<TEntry> newEntries
|
||||
);
|
||||
|
||||
protected abstract Task SaveToAsync(
|
||||
Stream stream,
|
||||
WriterOptions options,
|
||||
IEnumerable<TEntry> oldEntries,
|
||||
IEnumerable<TEntry> newEntries,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
base.Dispose();
|
||||
|
||||
@@ -2,6 +2,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.GZip;
|
||||
using SharpCompress.IO;
|
||||
@@ -136,6 +138,16 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
|
||||
SaveTo(stream, new WriterOptions(CompressionType.GZip));
|
||||
}
|
||||
|
||||
public Task SaveToAsync(string filePath, CancellationToken cancellationToken = default) =>
|
||||
SaveToAsync(new FileInfo(filePath), cancellationToken);
|
||||
|
||||
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
|
||||
@@ -173,6 +185,11 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
|
||||
return new GZipWritableArchiveEntry(this, source, filePath, size, modified, closeStream);
|
||||
}
|
||||
|
||||
protected override GZipArchiveEntry CreateDirectoryEntry(
|
||||
string directoryPath,
|
||||
DateTime? modified
|
||||
) => throw new NotSupportedException("GZip archives do not support directory entries.");
|
||||
|
||||
protected override void SaveTo(
|
||||
Stream stream,
|
||||
WriterOptions options,
|
||||
@@ -196,6 +213,28 @@ public class GZipArchive : AbstractWritableArchive<GZipArchiveEntry, GZipVolume>
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task SaveToAsync(
|
||||
Stream stream,
|
||||
WriterOptions options,
|
||||
IEnumerable<GZipArchiveEntry> oldEntries,
|
||||
IEnumerable<GZipArchiveEntry> newEntries,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (Entries.Count > 1)
|
||||
{
|
||||
throw new InvalidFormatException("Only one entry is allowed in a GZip Archive");
|
||||
}
|
||||
using var writer = new GZipWriter(stream, new GZipWriterOptions(options));
|
||||
foreach (var entry in oldEntries.Concat(newEntries).Where(x => !x.IsDirectory))
|
||||
{
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
await writer
|
||||
.WriteAsync(entry.Key.NotNull("Entry Key is null"), entryStream, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
protected override IEnumerable<GZipArchiveEntry> LoadEntries(IEnumerable<GZipVolume> volumes)
|
||||
{
|
||||
var stream = volumes.Single().Stream;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common.GZip;
|
||||
|
||||
namespace SharpCompress.Archives.GZip;
|
||||
@@ -13,13 +15,20 @@ public class GZipArchiveEntry : GZipEntry, IArchiveEntry
|
||||
{
|
||||
//this is to reset the stream to be read multiple times
|
||||
var part = (GZipFilePart)Parts.Single();
|
||||
if (part.GetRawStream().Position != part.EntryStartPosition)
|
||||
var rawStream = part.GetRawStream();
|
||||
if (rawStream.CanSeek && rawStream.Position != part.EntryStartPosition)
|
||||
{
|
||||
part.GetRawStream().Position = part.EntryStartPosition;
|
||||
rawStream.Position = part.EntryStartPosition;
|
||||
}
|
||||
return Parts.Single().GetCompressedStream().NotNull();
|
||||
}
|
||||
|
||||
public virtual Task<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// GZip synchronous implementation is fast enough, just wrap it
|
||||
return Task.FromResult(OpenEntryStream());
|
||||
}
|
||||
|
||||
#region IArchiveEntry Members
|
||||
|
||||
public IArchive Archive { get; }
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
|
||||
namespace SharpCompress.Archives;
|
||||
@@ -11,6 +13,12 @@ public interface IArchiveEntry : IEntry
|
||||
/// </summary>
|
||||
Stream OpenEntryStream();
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
Task<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// The archive can find all the parts of the archive needed to extract this entry.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.IO;
|
||||
|
||||
@@ -30,6 +32,34 @@ public static class IArchiveEntryExtensions
|
||||
streamListener.FireEntryExtractionEnd(archiveEntry);
|
||||
}
|
||||
|
||||
public static async Task WriteToAsync(
|
||||
this IArchiveEntry archiveEntry,
|
||||
Stream streamToWriteTo,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (archiveEntry.IsDirectory)
|
||||
{
|
||||
throw new ExtractionException("Entry is a file directory and cannot be extracted.");
|
||||
}
|
||||
|
||||
var streamListener = (IArchiveExtractionListener)archiveEntry.Archive;
|
||||
streamListener.EnsureEntriesLoaded();
|
||||
streamListener.FireEntryExtractionBegin(archiveEntry);
|
||||
streamListener.FireFilePartExtractionBegin(
|
||||
archiveEntry.Key ?? "Key",
|
||||
archiveEntry.Size,
|
||||
archiveEntry.CompressedSize
|
||||
);
|
||||
var entryStream = archiveEntry.OpenEntryStream();
|
||||
using (entryStream)
|
||||
{
|
||||
using Stream s = new ListeningStream(streamListener, entryStream);
|
||||
await s.CopyToAsync(streamToWriteTo, 81920, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
streamListener.FireEntryExtractionEnd(archiveEntry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract to specific directory, retaining filename
|
||||
/// </summary>
|
||||
@@ -45,6 +75,23 @@ public static class IArchiveEntryExtensions
|
||||
entry.WriteToFile
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Extract to specific directory asynchronously, retaining filename
|
||||
/// </summary>
|
||||
public static Task WriteToDirectoryAsync(
|
||||
this IArchiveEntry entry,
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options = null,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
ExtractionMethods.WriteEntryToDirectoryAsync(
|
||||
entry,
|
||||
destinationDirectory,
|
||||
options,
|
||||
(x, opt) => entry.WriteToFileAsync(x, opt, cancellationToken),
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Extract to specific file
|
||||
/// </summary>
|
||||
@@ -63,4 +110,24 @@ public static class IArchiveEntryExtensions
|
||||
entry.WriteTo(fs);
|
||||
}
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Extract to specific file asynchronously
|
||||
/// </summary>
|
||||
public static Task WriteToFileAsync(
|
||||
this IArchiveEntry entry,
|
||||
string destinationFileName,
|
||||
ExtractionOptions? options = null,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
ExtractionMethods.WriteEntryToFileAsync(
|
||||
entry,
|
||||
destinationFileName,
|
||||
options,
|
||||
async (x, fm) =>
|
||||
{
|
||||
using var fs = File.Open(destinationFileName, fm);
|
||||
await entry.WriteToAsync(fs, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Writers;
|
||||
|
||||
namespace SharpCompress.Archives;
|
||||
@@ -16,8 +18,16 @@ public interface IWritableArchive : IArchive
|
||||
DateTime? modified = null
|
||||
);
|
||||
|
||||
IArchiveEntry AddDirectoryEntry(string key, DateTime? modified = null);
|
||||
|
||||
void SaveTo(Stream stream, WriterOptions options);
|
||||
|
||||
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>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Writers;
|
||||
|
||||
namespace SharpCompress.Archives;
|
||||
@@ -42,6 +44,24 @@ public static class IWritableArchiveExtensions
|
||||
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,
|
||||
|
||||
@@ -2,6 +2,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Rar;
|
||||
using SharpCompress.Common.Rar.Headers;
|
||||
@@ -84,6 +86,9 @@ public class RarArchiveEntry : RarEntry, IArchiveEntry
|
||||
);
|
||||
}
|
||||
|
||||
public Task<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(OpenEntryStream());
|
||||
|
||||
public bool IsComplete
|
||||
{
|
||||
get
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common.SevenZip;
|
||||
|
||||
namespace SharpCompress.Archives.SevenZip;
|
||||
@@ -10,6 +12,9 @@ public class SevenZipArchiveEntry : SevenZipEntry, IArchiveEntry
|
||||
|
||||
public Stream OpenEntryStream() => FilePart.GetCompressedStream();
|
||||
|
||||
public Task<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default) =>
|
||||
Task.FromResult(OpenEntryStream());
|
||||
|
||||
public IArchive Archive { get; }
|
||||
|
||||
public bool IsComplete => true;
|
||||
|
||||
@@ -2,6 +2,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Tar;
|
||||
using SharpCompress.Common.Tar.Headers;
|
||||
@@ -222,6 +224,11 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
|
||||
closeStream
|
||||
);
|
||||
|
||||
protected override TarArchiveEntry CreateDirectoryEntry(
|
||||
string directoryPath,
|
||||
DateTime? modified
|
||||
) => new TarWritableArchiveEntry(this, directoryPath, modified);
|
||||
|
||||
protected override void SaveTo(
|
||||
Stream stream,
|
||||
WriterOptions options,
|
||||
@@ -230,15 +237,62 @@ public class TarArchive : AbstractWritableArchive<TarArchiveEntry, TarVolume>
|
||||
)
|
||||
{
|
||||
using var writer = new TarWriter(stream, new TarWriterOptions(options));
|
||||
foreach (var entry in oldEntries.Concat(newEntries).Where(x => !x.IsDirectory))
|
||||
foreach (var entry in oldEntries.Concat(newEntries))
|
||||
{
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
writer.Write(
|
||||
entry.Key.NotNull("Entry Key is null"),
|
||||
entryStream,
|
||||
entry.LastModifiedTime,
|
||||
entry.Size
|
||||
);
|
||||
if (entry.IsDirectory)
|
||||
{
|
||||
writer.WriteDirectory(
|
||||
entry.Key.NotNull("Entry Key is null"),
|
||||
entry.LastModifiedTime
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
writer.Write(
|
||||
entry.Key.NotNull("Entry Key is null"),
|
||||
entryStream,
|
||||
entry.LastModifiedTime,
|
||||
entry.Size
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task SaveToAsync(
|
||||
Stream stream,
|
||||
WriterOptions options,
|
||||
IEnumerable<TarArchiveEntry> oldEntries,
|
||||
IEnumerable<TarArchiveEntry> newEntries,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
using var writer = new TarWriter(stream, new TarWriterOptions(options));
|
||||
foreach (var entry in oldEntries.Concat(newEntries))
|
||||
{
|
||||
if (entry.IsDirectory)
|
||||
{
|
||||
await writer
|
||||
.WriteDirectoryAsync(
|
||||
entry.Key.NotNull("Entry Key is null"),
|
||||
entry.LastModifiedTime,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
await writer
|
||||
.WriteAsync(
|
||||
entry.Key.NotNull("Entry Key is null"),
|
||||
entryStream,
|
||||
entry.LastModifiedTime,
|
||||
entry.Size,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Tar;
|
||||
|
||||
@@ -12,6 +14,10 @@ public class TarArchiveEntry : TarEntry, IArchiveEntry
|
||||
|
||||
public virtual Stream OpenEntryStream() => Parts.Single().GetCompressedStream().NotNull();
|
||||
|
||||
public virtual Task<Stream> OpenEntryStreamAsync(
|
||||
CancellationToken cancellationToken = default
|
||||
) => Task.FromResult(OpenEntryStream());
|
||||
|
||||
#region IArchiveEntry Members
|
||||
|
||||
public IArchive Archive { get; }
|
||||
|
||||
@@ -9,7 +9,8 @@ namespace SharpCompress.Archives.Tar;
|
||||
internal sealed class TarWritableArchiveEntry : TarArchiveEntry, IWritableArchiveEntry
|
||||
{
|
||||
private readonly bool closeStream;
|
||||
private readonly Stream stream;
|
||||
private readonly Stream? stream;
|
||||
private readonly bool isDirectory;
|
||||
|
||||
internal TarWritableArchiveEntry(
|
||||
TarArchive archive,
|
||||
@@ -27,6 +28,22 @@ internal sealed class TarWritableArchiveEntry : TarArchiveEntry, IWritableArchiv
|
||||
Size = size;
|
||||
LastModifiedTime = lastModified;
|
||||
this.closeStream = closeStream;
|
||||
isDirectory = false;
|
||||
}
|
||||
|
||||
internal TarWritableArchiveEntry(
|
||||
TarArchive archive,
|
||||
string directoryPath,
|
||||
DateTime? lastModified
|
||||
)
|
||||
: base(archive, null, CompressionType.None)
|
||||
{
|
||||
stream = null;
|
||||
Key = directoryPath;
|
||||
Size = 0;
|
||||
LastModifiedTime = lastModified;
|
||||
closeStream = false;
|
||||
isDirectory = true;
|
||||
}
|
||||
|
||||
public override long Crc => 0;
|
||||
@@ -47,15 +64,19 @@ internal sealed class TarWritableArchiveEntry : TarArchiveEntry, IWritableArchiv
|
||||
|
||||
public override bool IsEncrypted => false;
|
||||
|
||||
public override bool IsDirectory => false;
|
||||
public override bool IsDirectory => isDirectory;
|
||||
|
||||
public override bool IsSplitAfter => false;
|
||||
|
||||
internal override IEnumerable<FilePart> Parts => throw new NotImplementedException();
|
||||
Stream IWritableArchiveEntry.Stream => stream;
|
||||
Stream IWritableArchiveEntry.Stream => stream ?? Stream.Null;
|
||||
|
||||
public override Stream OpenEntryStream()
|
||||
{
|
||||
if (stream is null)
|
||||
{
|
||||
return Stream.Null;
|
||||
}
|
||||
//ensure new stream is at the start, this could be reset
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
return SharpCompressStream.Create(stream, leaveOpen: true);
|
||||
@@ -63,7 +84,7 @@ internal sealed class TarWritableArchiveEntry : TarArchiveEntry, IWritableArchiv
|
||||
|
||||
internal override void Close()
|
||||
{
|
||||
if (closeStream)
|
||||
if (closeStream && stream is not null)
|
||||
{
|
||||
stream.Dispose();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Zip;
|
||||
using SharpCompress.Common.Zip.Headers;
|
||||
@@ -306,14 +308,59 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
|
||||
)
|
||||
{
|
||||
using var writer = new ZipWriter(stream, new ZipWriterOptions(options));
|
||||
foreach (var entry in oldEntries.Concat(newEntries).Where(x => !x.IsDirectory))
|
||||
foreach (var entry in oldEntries.Concat(newEntries))
|
||||
{
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
writer.Write(
|
||||
entry.Key.NotNull("Entry Key is null"),
|
||||
entryStream,
|
||||
entry.LastModifiedTime
|
||||
);
|
||||
if (entry.IsDirectory)
|
||||
{
|
||||
writer.WriteDirectory(
|
||||
entry.Key.NotNull("Entry Key is null"),
|
||||
entry.LastModifiedTime
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
writer.Write(
|
||||
entry.Key.NotNull("Entry Key is null"),
|
||||
entryStream,
|
||||
entry.LastModifiedTime
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task SaveToAsync(
|
||||
Stream stream,
|
||||
WriterOptions options,
|
||||
IEnumerable<ZipArchiveEntry> oldEntries,
|
||||
IEnumerable<ZipArchiveEntry> newEntries,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
using var writer = new ZipWriter(stream, new ZipWriterOptions(options));
|
||||
foreach (var entry in oldEntries.Concat(newEntries))
|
||||
{
|
||||
if (entry.IsDirectory)
|
||||
{
|
||||
await writer
|
||||
.WriteDirectoryAsync(
|
||||
entry.Key.NotNull("Entry Key is null"),
|
||||
entry.LastModifiedTime,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
await writer
|
||||
.WriteAsync(
|
||||
entry.Key.NotNull("Entry Key is null"),
|
||||
entryStream,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,6 +372,11 @@ public class ZipArchive : AbstractWritableArchive<ZipArchiveEntry, ZipVolume>
|
||||
bool closeStream
|
||||
) => new ZipWritableArchiveEntry(this, source, filePath, size, modified, closeStream);
|
||||
|
||||
protected override ZipArchiveEntry CreateDirectoryEntry(
|
||||
string directoryPath,
|
||||
DateTime? modified
|
||||
) => new ZipWritableArchiveEntry(this, directoryPath, modified);
|
||||
|
||||
public static ZipArchive Create() => new();
|
||||
|
||||
protected override IReader CreateReaderForSolidExtraction()
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common.Zip;
|
||||
|
||||
namespace SharpCompress.Archives.Zip;
|
||||
@@ -11,6 +13,10 @@ public class ZipArchiveEntry : ZipEntry, IArchiveEntry
|
||||
|
||||
public virtual Stream OpenEntryStream() => Parts.Single().GetCompressedStream().NotNull();
|
||||
|
||||
public virtual Task<Stream> OpenEntryStreamAsync(
|
||||
CancellationToken cancellationToken = default
|
||||
) => Task.FromResult(OpenEntryStream());
|
||||
|
||||
#region IArchiveEntry Members
|
||||
|
||||
public IArchive Archive { get; }
|
||||
|
||||
@@ -9,7 +9,8 @@ namespace SharpCompress.Archives.Zip;
|
||||
internal class ZipWritableArchiveEntry : ZipArchiveEntry, IWritableArchiveEntry
|
||||
{
|
||||
private readonly bool closeStream;
|
||||
private readonly Stream stream;
|
||||
private readonly Stream? stream;
|
||||
private readonly bool isDirectory;
|
||||
private bool isDisposed;
|
||||
|
||||
internal ZipWritableArchiveEntry(
|
||||
@@ -27,6 +28,22 @@ internal class ZipWritableArchiveEntry : ZipArchiveEntry, IWritableArchiveEntry
|
||||
Size = size;
|
||||
LastModifiedTime = lastModified;
|
||||
this.closeStream = closeStream;
|
||||
isDirectory = false;
|
||||
}
|
||||
|
||||
internal ZipWritableArchiveEntry(
|
||||
ZipArchive archive,
|
||||
string directoryPath,
|
||||
DateTime? lastModified
|
||||
)
|
||||
: base(archive, null)
|
||||
{
|
||||
stream = null;
|
||||
Key = directoryPath;
|
||||
Size = 0;
|
||||
LastModifiedTime = lastModified;
|
||||
closeStream = false;
|
||||
isDirectory = true;
|
||||
}
|
||||
|
||||
public override long Crc => 0;
|
||||
@@ -47,16 +64,20 @@ internal class ZipWritableArchiveEntry : ZipArchiveEntry, IWritableArchiveEntry
|
||||
|
||||
public override bool IsEncrypted => false;
|
||||
|
||||
public override bool IsDirectory => false;
|
||||
public override bool IsDirectory => isDirectory;
|
||||
|
||||
public override bool IsSplitAfter => false;
|
||||
|
||||
internal override IEnumerable<FilePart> Parts => throw new NotImplementedException();
|
||||
|
||||
Stream IWritableArchiveEntry.Stream => stream;
|
||||
Stream IWritableArchiveEntry.Stream => stream ?? Stream.Null;
|
||||
|
||||
public override Stream OpenEntryStream()
|
||||
{
|
||||
if (stream is null)
|
||||
{
|
||||
return Stream.Null;
|
||||
}
|
||||
//ensure new stream is at the start, this could be reset
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
return SharpCompressStream.Create(stream, leaveOpen: true);
|
||||
@@ -64,7 +85,7 @@ internal class ZipWritableArchiveEntry : ZipArchiveEntry, IWritableArchiveEntry
|
||||
|
||||
internal override void Close()
|
||||
{
|
||||
if (closeStream && !isDisposed)
|
||||
if (closeStream && !isDisposed && stream is not null)
|
||||
{
|
||||
stream.Dispose();
|
||||
isDisposed = true;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
@@ -51,6 +53,15 @@ public class EntryStream : Stream, IStreamStack
|
||||
_completed = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously skip the rest of the entry stream.
|
||||
/// </summary>
|
||||
public async Task SkipEntryAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await this.SkipAsync(cancellationToken).ConfigureAwait(false);
|
||||
_completed = true;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (!(_completed || _reader.Cancelled))
|
||||
@@ -83,6 +94,40 @@ public class EntryStream : Stream, IStreamStack
|
||||
_stream.Dispose();
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
if (!(_completed || _reader.Cancelled))
|
||||
{
|
||||
await SkipEntryAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
//Need a safe standard approach to this - it's okay for compression to overreads. Handling needs to be standardised
|
||||
if (_stream is IStreamStack ss)
|
||||
{
|
||||
if (ss.BaseStream() is SharpCompress.Compressors.Deflate.DeflateStream deflateStream)
|
||||
{
|
||||
await deflateStream.FlushAsync().ConfigureAwait(false);
|
||||
}
|
||||
else if (ss.BaseStream() is SharpCompress.Compressors.LZMA.LzmaStream lzmaStream)
|
||||
{
|
||||
await lzmaStream.FlushAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_isDisposed = true;
|
||||
#if DEBUG_STREAMS
|
||||
this.DebugDispose(typeof(EntryStream));
|
||||
#endif
|
||||
await base.DisposeAsync().ConfigureAwait(false);
|
||||
await _stream.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
#endif
|
||||
|
||||
public override bool CanRead => true;
|
||||
|
||||
public override bool CanSeek => false;
|
||||
@@ -91,6 +136,8 @@ public class EntryStream : Stream, IStreamStack
|
||||
|
||||
public override void Flush() { }
|
||||
|
||||
public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
public override long Length => _stream.Length;
|
||||
|
||||
public override long Position
|
||||
@@ -109,6 +156,38 @@ public class EntryStream : Stream, IStreamStack
|
||||
return read;
|
||||
}
|
||||
|
||||
public override async Task<int> ReadAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var read = await _stream
|
||||
.ReadAsync(buffer, offset, count, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (read <= 0)
|
||||
{
|
||||
_completed = true;
|
||||
}
|
||||
return read;
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override async ValueTask<int> ReadAsync(
|
||||
Memory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var read = await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
if (read <= 0)
|
||||
{
|
||||
_completed = true;
|
||||
}
|
||||
return read;
|
||||
}
|
||||
#endif
|
||||
|
||||
public override int ReadByte()
|
||||
{
|
||||
var value = _stream.ReadByte();
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharpCompress.Common;
|
||||
|
||||
internal static class ExtractionMethods
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the appropriate StringComparison for path checks based on the file system.
|
||||
/// Windows uses case-insensitive file systems, while Unix-like systems use case-sensitive file systems.
|
||||
/// </summary>
|
||||
private static StringComparison PathComparison =>
|
||||
RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||
? StringComparison.OrdinalIgnoreCase
|
||||
: StringComparison.Ordinal;
|
||||
|
||||
/// <summary>
|
||||
/// Extract to specific directory, retaining filename
|
||||
/// </summary>
|
||||
@@ -46,7 +58,7 @@ internal static class ExtractionMethods
|
||||
|
||||
if (!Directory.Exists(destdir))
|
||||
{
|
||||
if (!destdir.StartsWith(fullDestinationDirectoryPath, StringComparison.Ordinal))
|
||||
if (!destdir.StartsWith(fullDestinationDirectoryPath, PathComparison))
|
||||
{
|
||||
throw new ExtractionException(
|
||||
"Entry is trying to create a directory outside of the destination directory."
|
||||
@@ -66,12 +78,7 @@ internal static class ExtractionMethods
|
||||
{
|
||||
destinationFileName = Path.GetFullPath(destinationFileName);
|
||||
|
||||
if (
|
||||
!destinationFileName.StartsWith(
|
||||
fullDestinationDirectoryPath,
|
||||
StringComparison.Ordinal
|
||||
)
|
||||
)
|
||||
if (!destinationFileName.StartsWith(fullDestinationDirectoryPath, PathComparison))
|
||||
{
|
||||
throw new ExtractionException(
|
||||
"Entry is trying to write a file outside of the destination directory."
|
||||
@@ -116,4 +123,110 @@ internal static class ExtractionMethods
|
||||
entry.PreserveExtractionOptions(destinationFileName, options);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task WriteEntryToDirectoryAsync(
|
||||
IEntry entry,
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options,
|
||||
Func<string, ExtractionOptions?, Task> writeAsync,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
string destinationFileName;
|
||||
var fullDestinationDirectoryPath = Path.GetFullPath(destinationDirectory);
|
||||
|
||||
//check for trailing slash.
|
||||
if (
|
||||
fullDestinationDirectoryPath[fullDestinationDirectoryPath.Length - 1]
|
||||
!= Path.DirectorySeparatorChar
|
||||
)
|
||||
{
|
||||
fullDestinationDirectoryPath += Path.DirectorySeparatorChar;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(fullDestinationDirectoryPath))
|
||||
{
|
||||
throw new ExtractionException(
|
||||
$"Directory does not exist to extract to: {fullDestinationDirectoryPath}"
|
||||
);
|
||||
}
|
||||
|
||||
options ??= new ExtractionOptions() { Overwrite = true };
|
||||
|
||||
var file = Path.GetFileName(entry.Key.NotNull("Entry Key is null")).NotNull("File is null");
|
||||
file = Utility.ReplaceInvalidFileNameChars(file);
|
||||
if (options.ExtractFullPath)
|
||||
{
|
||||
var folder = Path.GetDirectoryName(entry.Key.NotNull("Entry Key is null"))
|
||||
.NotNull("Directory is null");
|
||||
var destdir = Path.GetFullPath(Path.Combine(fullDestinationDirectoryPath, folder));
|
||||
|
||||
if (!Directory.Exists(destdir))
|
||||
{
|
||||
if (!destdir.StartsWith(fullDestinationDirectoryPath, PathComparison))
|
||||
{
|
||||
throw new ExtractionException(
|
||||
"Entry is trying to create a directory outside of the destination directory."
|
||||
);
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(destdir);
|
||||
}
|
||||
destinationFileName = Path.Combine(destdir, file);
|
||||
}
|
||||
else
|
||||
{
|
||||
destinationFileName = Path.Combine(fullDestinationDirectoryPath, file);
|
||||
}
|
||||
|
||||
if (!entry.IsDirectory)
|
||||
{
|
||||
destinationFileName = Path.GetFullPath(destinationFileName);
|
||||
|
||||
if (!destinationFileName.StartsWith(fullDestinationDirectoryPath, PathComparison))
|
||||
{
|
||||
throw new ExtractionException(
|
||||
"Entry is trying to write a file outside of the destination directory."
|
||||
);
|
||||
}
|
||||
await writeAsync(destinationFileName, options).ConfigureAwait(false);
|
||||
}
|
||||
else if (options.ExtractFullPath && !Directory.Exists(destinationFileName))
|
||||
{
|
||||
Directory.CreateDirectory(destinationFileName);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task WriteEntryToFileAsync(
|
||||
IEntry entry,
|
||||
string destinationFileName,
|
||||
ExtractionOptions? options,
|
||||
Func<string, FileMode, Task> openAndWriteAsync,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (entry.LinkTarget != null)
|
||||
{
|
||||
if (options?.WriteSymbolicLink is null)
|
||||
{
|
||||
throw new ExtractionException(
|
||||
"Entry is a symbolic link but ExtractionOptions.WriteSymbolicLink delegate is null"
|
||||
);
|
||||
}
|
||||
options.WriteSymbolicLink(destinationFileName, entry.LinkTarget);
|
||||
}
|
||||
else
|
||||
{
|
||||
var fm = FileMode.Create;
|
||||
options ??= new ExtractionOptions() { Overwrite = true };
|
||||
|
||||
if (!options.Overwrite)
|
||||
{
|
||||
fm = FileMode.CreateNew;
|
||||
}
|
||||
|
||||
await openAndWriteAsync(destinationFileName, fm).ConfigureAwait(false);
|
||||
entry.PreserveExtractionOptions(destinationFileName, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,8 +24,14 @@ internal sealed class GZipFilePart : FilePart
|
||||
stream.Position = stream.Length - 8;
|
||||
ReadTrailer();
|
||||
stream.Position = 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.
|
||||
EntryStartPosition = 0;
|
||||
}
|
||||
EntryStartPosition = stream.Position;
|
||||
}
|
||||
|
||||
internal long EntryStartPosition { get; }
|
||||
|
||||
@@ -67,6 +67,7 @@ internal class SevenZipFilePart : FilePart
|
||||
}
|
||||
}
|
||||
|
||||
private const uint K_COPY = 0x0;
|
||||
private const uint K_LZMA2 = 0x21;
|
||||
private const uint K_LZMA = 0x030101;
|
||||
private const uint K_PPMD = 0x030401;
|
||||
@@ -82,6 +83,7 @@ internal class SevenZipFilePart : FilePart
|
||||
var coder = Folder.NotNull()._coders.First();
|
||||
return coder._methodId._id switch
|
||||
{
|
||||
K_COPY => CompressionType.None,
|
||||
K_LZMA or K_LZMA2 => CompressionType.LZMA,
|
||||
K_PPMD => CompressionType.PPMd,
|
||||
K_B_ZIP2 => CompressionType.BZip2,
|
||||
|
||||
@@ -66,6 +66,36 @@ internal class TarReadOnlySubStream : SharpCompressStream, IStreamStack
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override async System.Threading.Tasks.ValueTask DisposeAsync()
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isDisposed = true;
|
||||
#if DEBUG_STREAMS
|
||||
this.DebugDispose(typeof(TarReadOnlySubStream));
|
||||
#endif
|
||||
// Ensure we read all remaining blocks for this entry.
|
||||
await Stream.SkipAsync(BytesLeftToRead).ConfigureAwait(false);
|
||||
_amountRead += BytesLeftToRead;
|
||||
|
||||
// If the last block wasn't a full 512 bytes, skip the remaining padding bytes.
|
||||
var bytesInLastBlock = _amountRead % 512;
|
||||
|
||||
if (bytesInLastBlock != 0)
|
||||
{
|
||||
await Stream.SkipAsync(512 - bytesInLastBlock).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Call base Dispose instead of base DisposeAsync to avoid double disposal
|
||||
base.Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
#endif
|
||||
|
||||
private long BytesLeftToRead { get; set; }
|
||||
|
||||
public override bool CanRead => true;
|
||||
@@ -76,6 +106,10 @@ internal class TarReadOnlySubStream : SharpCompressStream, IStreamStack
|
||||
|
||||
public override void Flush() { }
|
||||
|
||||
public override System.Threading.Tasks.Task FlushAsync(
|
||||
System.Threading.CancellationToken cancellationToken
|
||||
) => System.Threading.Tasks.Task.CompletedTask;
|
||||
|
||||
public override long Length => throw new NotSupportedException();
|
||||
|
||||
public override long Position
|
||||
@@ -114,6 +148,48 @@ internal class TarReadOnlySubStream : SharpCompressStream, IStreamStack
|
||||
return value;
|
||||
}
|
||||
|
||||
public override async System.Threading.Tasks.Task<int> ReadAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
System.Threading.CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (BytesLeftToRead < count)
|
||||
{
|
||||
count = (int)BytesLeftToRead;
|
||||
}
|
||||
var read = await Stream
|
||||
.ReadAsync(buffer, offset, count, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (read > 0)
|
||||
{
|
||||
BytesLeftToRead -= read;
|
||||
_amountRead += read;
|
||||
}
|
||||
return read;
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override async System.Threading.Tasks.ValueTask<int> ReadAsync(
|
||||
System.Memory<byte> buffer,
|
||||
System.Threading.CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (BytesLeftToRead < buffer.Length)
|
||||
{
|
||||
buffer = buffer.Slice(0, (int)BytesLeftToRead);
|
||||
}
|
||||
var read = await Stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
if (read > 0)
|
||||
{
|
||||
BytesLeftToRead -= read;
|
||||
_amountRead += read;
|
||||
}
|
||||
return read;
|
||||
}
|
||||
#endif
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
|
||||
|
||||
public override void SetLength(long value) => throw new NotSupportedException();
|
||||
|
||||
@@ -36,11 +36,10 @@ internal class StreamingZipHeaderFactory : ZipHeaderFactory
|
||||
throw new ArgumentException("Stream must be a SharpCompressStream", nameof(stream));
|
||||
}
|
||||
}
|
||||
SharpCompressStream rewindableStream = (SharpCompressStream)stream;
|
||||
var rewindableStream = (SharpCompressStream)stream;
|
||||
|
||||
while (true)
|
||||
{
|
||||
ZipHeader? header;
|
||||
var reader = new BinaryReader(rewindableStream);
|
||||
uint headerBytes = 0;
|
||||
if (
|
||||
@@ -155,7 +154,7 @@ internal class StreamingZipHeaderFactory : ZipHeaderFactory
|
||||
}
|
||||
|
||||
_lastEntryHeader = null;
|
||||
header = ReadHeader(headerBytes, reader);
|
||||
var header = ReadHeader(headerBytes, reader);
|
||||
if (header is null)
|
||||
{
|
||||
yield break;
|
||||
|
||||
@@ -28,6 +28,7 @@ using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.IO;
|
||||
|
||||
namespace SharpCompress.Compressors.Deflate;
|
||||
@@ -289,6 +290,34 @@ public class DeflateStream : Stream, IStreamStack
|
||||
_baseStream.Flush();
|
||||
}
|
||||
|
||||
public override async Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException("DeflateStream");
|
||||
}
|
||||
await _baseStream.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_disposed = true;
|
||||
if (_baseStream != null)
|
||||
{
|
||||
await _baseStream.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
#if DEBUG_STREAMS
|
||||
this.DebugDispose(typeof(DeflateStream));
|
||||
#endif
|
||||
await base.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Read data from the stream.
|
||||
/// </summary>
|
||||
@@ -325,6 +354,36 @@ public class DeflateStream : Stream, IStreamStack
|
||||
return _baseStream.Read(buffer, offset, count);
|
||||
}
|
||||
|
||||
public override async Task<int> ReadAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException("DeflateStream");
|
||||
}
|
||||
return await _baseStream
|
||||
.ReadAsync(buffer, offset, count, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override async ValueTask<int> ReadAsync(
|
||||
Memory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException("DeflateStream");
|
||||
}
|
||||
return await _baseStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
#endif
|
||||
|
||||
public override int ReadByte()
|
||||
{
|
||||
if (_disposed)
|
||||
@@ -386,6 +445,36 @@ public class DeflateStream : Stream, IStreamStack
|
||||
_baseStream.Write(buffer, offset, count);
|
||||
}
|
||||
|
||||
public override async Task WriteAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException("DeflateStream");
|
||||
}
|
||||
await _baseStream
|
||||
.WriteAsync(buffer, offset, count, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override async ValueTask WriteAsync(
|
||||
ReadOnlyMemory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException("DeflateStream");
|
||||
}
|
||||
await _baseStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
#endif
|
||||
|
||||
public override void WriteByte(byte value)
|
||||
{
|
||||
if (_disposed)
|
||||
|
||||
@@ -30,6 +30,8 @@ using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.IO;
|
||||
|
||||
namespace SharpCompress.Compressors.Deflate;
|
||||
@@ -257,6 +259,15 @@ public class GZipStream : Stream, IStreamStack
|
||||
BaseStream.Flush();
|
||||
}
|
||||
|
||||
public override async Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException("GZipStream");
|
||||
}
|
||||
await BaseStream.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read and decompress data from the source stream.
|
||||
/// </summary>
|
||||
@@ -309,6 +320,54 @@ public class GZipStream : Stream, IStreamStack
|
||||
return n;
|
||||
}
|
||||
|
||||
public override async Task<int> ReadAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException("GZipStream");
|
||||
}
|
||||
var n = await BaseStream
|
||||
.ReadAsync(buffer, offset, count, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (!_firstReadDone)
|
||||
{
|
||||
_firstReadDone = true;
|
||||
FileName = BaseStream._GzipFileName;
|
||||
Comment = BaseStream._GzipComment;
|
||||
LastModified = BaseStream._GzipMtime;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override async ValueTask<int> ReadAsync(
|
||||
Memory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException("GZipStream");
|
||||
}
|
||||
var n = await BaseStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!_firstReadDone)
|
||||
{
|
||||
_firstReadDone = true;
|
||||
FileName = BaseStream._GzipFileName;
|
||||
Comment = BaseStream._GzipComment;
|
||||
LastModified = BaseStream._GzipMtime;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Calling this method always throws a <see cref="NotImplementedException"/>.
|
||||
/// </summary>
|
||||
@@ -368,6 +427,77 @@ public class GZipStream : Stream, IStreamStack
|
||||
BaseStream.Write(buffer, offset, count);
|
||||
}
|
||||
|
||||
public override async Task WriteAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException("GZipStream");
|
||||
}
|
||||
if (BaseStream._streamMode == ZlibBaseStream.StreamMode.Undefined)
|
||||
{
|
||||
if (BaseStream._wantCompress)
|
||||
{
|
||||
// first write in compression, therefore, emit the GZIP header
|
||||
_headerByteCount = EmitHeader();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
await BaseStream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override async ValueTask WriteAsync(
|
||||
ReadOnlyMemory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException("GZipStream");
|
||||
}
|
||||
if (BaseStream._streamMode == ZlibBaseStream.StreamMode.Undefined)
|
||||
{
|
||||
if (BaseStream._wantCompress)
|
||||
{
|
||||
// first write in compression, therefore, emit the GZIP header
|
||||
_headerByteCount = EmitHeader();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
await BaseStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_disposed = true;
|
||||
if (BaseStream != null)
|
||||
{
|
||||
await BaseStream.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
#if DEBUG_STREAMS
|
||||
this.DebugDispose(typeof(GZipStream));
|
||||
#endif
|
||||
await base.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
#endif
|
||||
|
||||
#endregion Stream methods
|
||||
|
||||
public string? Comment
|
||||
|
||||
@@ -31,6 +31,8 @@ using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common.Tar.Headers;
|
||||
using SharpCompress.IO;
|
||||
|
||||
@@ -197,6 +199,69 @@ internal class ZlibBaseStream : Stream, IStreamStack
|
||||
} while (!done);
|
||||
}
|
||||
|
||||
public override async Task WriteAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
// workitem 7159
|
||||
// calculate the CRC on the unccompressed data (before writing)
|
||||
if (crc != null)
|
||||
{
|
||||
crc.SlurpBlock(buffer, offset, count);
|
||||
}
|
||||
|
||||
if (_streamMode == StreamMode.Undefined)
|
||||
{
|
||||
_streamMode = StreamMode.Writer;
|
||||
}
|
||||
else if (_streamMode != StreamMode.Writer)
|
||||
{
|
||||
throw new ZlibException("Cannot Write after Reading.");
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// first reference of z property will initialize the private var _z
|
||||
z.InputBuffer = buffer;
|
||||
_z.NextIn = offset;
|
||||
_z.AvailableBytesIn = count;
|
||||
var done = false;
|
||||
do
|
||||
{
|
||||
_z.OutputBuffer = workingBuffer;
|
||||
_z.NextOut = 0;
|
||||
_z.AvailableBytesOut = _workingBuffer.Length;
|
||||
var rc = (_wantCompress) ? _z.Deflate(_flushMode) : _z.Inflate(_flushMode);
|
||||
if (rc != ZlibConstants.Z_OK && rc != ZlibConstants.Z_STREAM_END)
|
||||
{
|
||||
throw new ZlibException((_wantCompress ? "de" : "in") + "flating: " + _z.Message);
|
||||
}
|
||||
|
||||
await _stream
|
||||
.WriteAsync(
|
||||
_workingBuffer,
|
||||
0,
|
||||
_workingBuffer.Length - _z.AvailableBytesOut,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
done = _z.AvailableBytesIn == 0 && _z.AvailableBytesOut != 0;
|
||||
|
||||
// If GZIP and de-compress, we're done when 8 bytes remain.
|
||||
if (_flavor == ZlibStreamFlavor.GZIP && !_wantCompress)
|
||||
{
|
||||
done = (_z.AvailableBytesIn == 8 && _z.AvailableBytesOut != 0);
|
||||
}
|
||||
} while (!done);
|
||||
}
|
||||
|
||||
private void finish()
|
||||
{
|
||||
if (_z is null)
|
||||
@@ -335,6 +400,111 @@ internal class ZlibBaseStream : Stream, IStreamStack
|
||||
}
|
||||
}
|
||||
|
||||
private async Task finishAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_z is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_streamMode == StreamMode.Writer)
|
||||
{
|
||||
var done = false;
|
||||
do
|
||||
{
|
||||
_z.OutputBuffer = workingBuffer;
|
||||
_z.NextOut = 0;
|
||||
_z.AvailableBytesOut = _workingBuffer.Length;
|
||||
var rc =
|
||||
(_wantCompress) ? _z.Deflate(FlushType.Finish) : _z.Inflate(FlushType.Finish);
|
||||
|
||||
if (rc != ZlibConstants.Z_STREAM_END && rc != ZlibConstants.Z_OK)
|
||||
{
|
||||
var verb = (_wantCompress ? "de" : "in") + "flating";
|
||||
if (_z.Message is null)
|
||||
{
|
||||
throw new ZlibException(String.Format("{0}: (rc = {1})", verb, rc));
|
||||
}
|
||||
throw new ZlibException(verb + ": " + _z.Message);
|
||||
}
|
||||
|
||||
if (_workingBuffer.Length - _z.AvailableBytesOut > 0)
|
||||
{
|
||||
await _stream
|
||||
.WriteAsync(
|
||||
_workingBuffer,
|
||||
0,
|
||||
_workingBuffer.Length - _z.AvailableBytesOut,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
done = _z.AvailableBytesIn == 0 && _z.AvailableBytesOut != 0;
|
||||
|
||||
// If GZIP and de-compress, we're done when 8 bytes remain.
|
||||
if (_flavor == ZlibStreamFlavor.GZIP && !_wantCompress)
|
||||
{
|
||||
done = (_z.AvailableBytesIn == 8 && _z.AvailableBytesOut != 0);
|
||||
}
|
||||
} while (!done);
|
||||
|
||||
await FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// workitem 7159
|
||||
if (_flavor == ZlibStreamFlavor.GZIP)
|
||||
{
|
||||
if (_wantCompress)
|
||||
{
|
||||
// Emit the GZIP trailer: CRC32 and size mod 2^32
|
||||
byte[] intBuf = new byte[4];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(intBuf, crc.Crc32Result);
|
||||
await _stream.WriteAsync(intBuf, 0, 4, cancellationToken).ConfigureAwait(false);
|
||||
var c2 = (int)(crc.TotalBytesRead & 0x00000000FFFFFFFF);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(intBuf, c2);
|
||||
await _stream.WriteAsync(intBuf, 0, 4, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ZlibException("Writing with decompression is not supported.");
|
||||
}
|
||||
}
|
||||
}
|
||||
// workitem 7159
|
||||
else if (_streamMode == StreamMode.Reader)
|
||||
{
|
||||
if (_flavor == ZlibStreamFlavor.GZIP)
|
||||
{
|
||||
if (!_wantCompress)
|
||||
{
|
||||
// workitem 8501: handle edge case (decompress empty stream)
|
||||
if (_z.TotalBytesOut == 0L)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Read and potentially verify the GZIP trailer: CRC32 and size mod 2^32
|
||||
byte[] trailer = new byte[8];
|
||||
|
||||
// workitem 8679
|
||||
if (_z.AvailableBytesIn != 8)
|
||||
{
|
||||
// Make sure we have read to the end of the stream
|
||||
_z.InputBuffer.AsSpan(_z.NextIn, _z.AvailableBytesIn).CopyTo(trailer);
|
||||
var bytesNeeded = 8 - _z.AvailableBytesIn;
|
||||
var bytesRead = await _stream
|
||||
.ReadAsync(trailer, _z.AvailableBytesIn, bytesNeeded, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ZlibException("Reading with compression is not supported.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void end()
|
||||
{
|
||||
if (z is null)
|
||||
@@ -382,6 +552,38 @@ internal class ZlibBaseStream : Stream, IStreamStack
|
||||
}
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
if (isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
isDisposed = true;
|
||||
#if DEBUG_STREAMS
|
||||
this.DebugDispose(typeof(ZlibBaseStream));
|
||||
#endif
|
||||
await base.DisposeAsync().ConfigureAwait(false);
|
||||
if (_stream is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
await finishAsync().ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
end();
|
||||
if (_stream != null)
|
||||
{
|
||||
await _stream.DisposeAsync().ConfigureAwait(false);
|
||||
_stream = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
_stream.Flush();
|
||||
@@ -390,6 +592,14 @@ internal class ZlibBaseStream : Stream, IStreamStack
|
||||
z.AvailableBytesIn = 0;
|
||||
}
|
||||
|
||||
public override async Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _stream.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
//rewind the buffer
|
||||
((IStreamStack)this).Rewind(z.AvailableBytesIn); //unused
|
||||
z.AvailableBytesIn = 0;
|
||||
}
|
||||
|
||||
public override Int64 Seek(Int64 offset, SeekOrigin origin) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
@@ -436,6 +646,31 @@ internal class ZlibBaseStream : Stream, IStreamStack
|
||||
return _encoding.GetString(buffer, 0, buffer.Length);
|
||||
}
|
||||
|
||||
private async Task<string> ReadZeroTerminatedStringAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var list = new List<byte>();
|
||||
var done = false;
|
||||
do
|
||||
{
|
||||
// workitem 7740
|
||||
var n = await _stream.ReadAsync(_buf1, 0, 1, cancellationToken).ConfigureAwait(false);
|
||||
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 _encoding.GetString(buffer, 0, buffer.Length);
|
||||
}
|
||||
|
||||
private int _ReadAndValidateGzipHeader()
|
||||
{
|
||||
var totalBytesRead = 0;
|
||||
@@ -494,6 +729,68 @@ internal class ZlibBaseStream : Stream, IStreamStack
|
||||
return totalBytesRead;
|
||||
}
|
||||
|
||||
private async Task<int> _ReadAndValidateGzipHeaderAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var totalBytesRead = 0;
|
||||
|
||||
// read the header on the first read
|
||||
byte[] header = new byte[10];
|
||||
var n = await _stream.ReadAsync(header, 0, 10, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// workitem 8501: handle edge case (decompress empty stream)
|
||||
if (n == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
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(4));
|
||||
_GzipMtime = TarHeader.EPOCH.AddSeconds(timet);
|
||||
totalBytesRead += n;
|
||||
if ((header[3] & 0x04) == 0x04)
|
||||
{
|
||||
// read and discard extra field
|
||||
n = await _stream.ReadAsync(header, 0, 2, cancellationToken).ConfigureAwait(false); // 2-byte length field
|
||||
totalBytesRead += n;
|
||||
|
||||
var extraLength = (short)(header[0] + header[1] * 256);
|
||||
var extra = new byte[extraLength];
|
||||
n = await _stream
|
||||
.ReadAsync(extra, 0, extra.Length, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (n != extraLength)
|
||||
{
|
||||
throw new ZlibException("Unexpected end-of-file reading GZIP header.");
|
||||
}
|
||||
totalBytesRead += n;
|
||||
}
|
||||
if ((header[3] & 0x08) == 0x08)
|
||||
{
|
||||
_GzipFileName = await ReadZeroTerminatedStringAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
if ((header[3] & 0x10) == 0x010)
|
||||
{
|
||||
_GzipComment = await ReadZeroTerminatedStringAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
if ((header[3] & 0x02) == 0x02)
|
||||
{
|
||||
await _stream.ReadAsync(_buf1, 0, 1, cancellationToken).ConfigureAwait(false); // CRC16, ignore
|
||||
}
|
||||
|
||||
return totalBytesRead;
|
||||
}
|
||||
|
||||
public override Int32 Read(Byte[] buffer, Int32 offset, Int32 count)
|
||||
{
|
||||
// According to MS documentation, any implementation of the IO.Stream.Read function must:
|
||||
@@ -678,6 +975,220 @@ internal class ZlibBaseStream : Stream, IStreamStack
|
||||
return rc;
|
||||
}
|
||||
|
||||
public override async Task<int> ReadAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
// According to MS documentation, any implementation of the IO.Stream.Read function must:
|
||||
// (a) throw an exception if offset & count reference an invalid part of the buffer,
|
||||
// or if count < 0, or if buffer is null
|
||||
// (b) return 0 only upon EOF, or if count = 0
|
||||
// (c) if not EOF, then return at least 1 byte, up to <count> bytes
|
||||
|
||||
if (_streamMode == StreamMode.Undefined)
|
||||
{
|
||||
if (!_stream.CanRead)
|
||||
{
|
||||
throw new ZlibException("The stream is not readable.");
|
||||
}
|
||||
|
||||
// for the first read, set up some controls.
|
||||
_streamMode = StreamMode.Reader;
|
||||
|
||||
// (The first reference to _z goes through the private accessor which
|
||||
// may initialize it.)
|
||||
z.AvailableBytesIn = 0;
|
||||
if (_flavor == ZlibStreamFlavor.GZIP)
|
||||
{
|
||||
_gzipHeaderByteCount = await _ReadAndValidateGzipHeaderAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// workitem 8501: handle edge case (decompress empty stream)
|
||||
if (_gzipHeaderByteCount == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_streamMode != StreamMode.Reader)
|
||||
{
|
||||
throw new ZlibException("Cannot Read after Writing.");
|
||||
}
|
||||
|
||||
var rc = 0;
|
||||
|
||||
// set up the output of the deflate/inflate codec:
|
||||
_z.OutputBuffer = buffer;
|
||||
_z.NextOut = offset;
|
||||
_z.AvailableBytesOut = count;
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
if (nomoreinput && _wantCompress)
|
||||
{
|
||||
// no more input data available; therefore we flush to
|
||||
// try to complete the read
|
||||
rc = _z.Deflate(FlushType.Finish);
|
||||
|
||||
if (rc != ZlibConstants.Z_OK && rc != ZlibConstants.Z_STREAM_END)
|
||||
{
|
||||
throw new ZlibException(
|
||||
String.Format("Deflating: rc={0} msg={1}", rc, _z.Message)
|
||||
);
|
||||
}
|
||||
|
||||
rc = (count - _z.AvailableBytesOut);
|
||||
|
||||
// calculate CRC after reading
|
||||
if (crc != null)
|
||||
{
|
||||
crc.SlurpBlock(buffer, offset, rc);
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
if (buffer is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(buffer));
|
||||
}
|
||||
if (count < 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
}
|
||||
if (offset < buffer.GetLowerBound(0))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(offset));
|
||||
}
|
||||
if ((offset + count) > buffer.GetLength(0))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(count));
|
||||
}
|
||||
|
||||
// This is necessary in case _workingBuffer has been resized. (new byte[])
|
||||
// (The first reference to _workingBuffer goes through the private accessor which
|
||||
// may initialize it.)
|
||||
_z.InputBuffer = workingBuffer;
|
||||
|
||||
do
|
||||
{
|
||||
// need data in _workingBuffer in order to deflate/inflate. Here, we check if we have any.
|
||||
if ((_z.AvailableBytesIn == 0) && (!nomoreinput))
|
||||
{
|
||||
// No data available, so try to Read data from the captive stream.
|
||||
_z.NextIn = 0;
|
||||
_z.AvailableBytesIn = await _stream
|
||||
.ReadAsync(_workingBuffer, 0, _workingBuffer.Length, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (_z.AvailableBytesIn == 0)
|
||||
{
|
||||
nomoreinput = true;
|
||||
}
|
||||
}
|
||||
|
||||
// we have data in InputBuffer; now compress or decompress as appropriate
|
||||
rc = (_wantCompress) ? _z.Deflate(_flushMode) : _z.Inflate(_flushMode);
|
||||
|
||||
if (nomoreinput && (rc == ZlibConstants.Z_BUF_ERROR))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (rc != ZlibConstants.Z_OK && rc != ZlibConstants.Z_STREAM_END)
|
||||
{
|
||||
throw new ZlibException(
|
||||
String.Format(
|
||||
"{0}flating: rc={1} msg={2}",
|
||||
(_wantCompress ? "de" : "in"),
|
||||
rc,
|
||||
_z.Message
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(nomoreinput || rc == ZlibConstants.Z_STREAM_END) && (_z.AvailableBytesOut == count)
|
||||
)
|
||||
{
|
||||
break; // nothing more to read
|
||||
}
|
||||
} //while (_z.AvailableBytesOut == count && rc == ZlibConstants.Z_OK);
|
||||
while (_z.AvailableBytesOut > 0 && !nomoreinput && rc == ZlibConstants.Z_OK);
|
||||
|
||||
// workitem 8557
|
||||
// is there more room in output?
|
||||
if (_z.AvailableBytesOut > 0)
|
||||
{
|
||||
if (rc == ZlibConstants.Z_OK && _z.AvailableBytesIn == 0)
|
||||
{
|
||||
// deferred
|
||||
}
|
||||
|
||||
// are we completely done reading?
|
||||
if (nomoreinput)
|
||||
{
|
||||
// and in compression?
|
||||
if (_wantCompress)
|
||||
{
|
||||
// no more input data available; therefore we flush to
|
||||
// try to complete the read
|
||||
rc = _z.Deflate(FlushType.Finish);
|
||||
|
||||
if (rc != ZlibConstants.Z_OK && rc != ZlibConstants.Z_STREAM_END)
|
||||
{
|
||||
throw new ZlibException(
|
||||
String.Format("Deflating: rc={0} msg={1}", rc, _z.Message)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rc = (count - _z.AvailableBytesOut);
|
||||
|
||||
// calculate CRC after reading
|
||||
if (crc != null)
|
||||
{
|
||||
crc.SlurpBlock(buffer, offset, rc);
|
||||
}
|
||||
|
||||
if (rc == ZlibConstants.Z_STREAM_END && z.AvailableBytesIn != 0 && !_wantCompress)
|
||||
{
|
||||
//rewind the buffer
|
||||
((IStreamStack)this).Rewind(z.AvailableBytesIn); //unused
|
||||
z.AvailableBytesIn = 0;
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override async ValueTask<int> ReadAsync(
|
||||
Memory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
// Use ArrayPool to rent a buffer and delegate to byte[] ReadAsync
|
||||
byte[] array = System.Buffers.ArrayPool<byte>.Shared.Rent(buffer.Length);
|
||||
try
|
||||
{
|
||||
int read = await ReadAsync(array, 0, buffer.Length, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
array.AsSpan(0, read).CopyTo(buffer.Span);
|
||||
return read;
|
||||
}
|
||||
finally
|
||||
{
|
||||
System.Buffers.ArrayPool<byte>.Shared.Return(array);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
public override Boolean CanRead => _stream.CanRead;
|
||||
|
||||
public override Boolean CanSeek => _stream.CanSeek;
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.IO;
|
||||
|
||||
namespace SharpCompress.Compressors.Deflate;
|
||||
@@ -266,6 +268,34 @@ public class ZlibStream : Stream, IStreamStack
|
||||
_baseStream.Flush();
|
||||
}
|
||||
|
||||
public override async Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException("ZlibStream");
|
||||
}
|
||||
await _baseStream.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_disposed = true;
|
||||
if (_baseStream != null)
|
||||
{
|
||||
await _baseStream.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
#if DEBUG_STREAMS
|
||||
this.DebugDispose(typeof(ZlibStream));
|
||||
#endif
|
||||
await base.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Read data from the stream.
|
||||
/// </summary>
|
||||
@@ -301,6 +331,36 @@ public class ZlibStream : Stream, IStreamStack
|
||||
return _baseStream.Read(buffer, offset, count);
|
||||
}
|
||||
|
||||
public override async Task<int> ReadAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException("ZlibStream");
|
||||
}
|
||||
return await _baseStream
|
||||
.ReadAsync(buffer, offset, count, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override async ValueTask<int> ReadAsync(
|
||||
Memory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException("ZlibStream");
|
||||
}
|
||||
return await _baseStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
#endif
|
||||
|
||||
public override int ReadByte()
|
||||
{
|
||||
if (_disposed)
|
||||
@@ -355,6 +415,36 @@ public class ZlibStream : Stream, IStreamStack
|
||||
_baseStream.Write(buffer, offset, count);
|
||||
}
|
||||
|
||||
public override async Task WriteAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException("ZlibStream");
|
||||
}
|
||||
await _baseStream
|
||||
.WriteAsync(buffer, offset, count, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override async ValueTask WriteAsync(
|
||||
ReadOnlyMemory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException("ZlibStream");
|
||||
}
|
||||
await _baseStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
#endif
|
||||
|
||||
public override void WriteByte(byte value)
|
||||
{
|
||||
if (_disposed)
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Zip;
|
||||
using SharpCompress.IO;
|
||||
@@ -39,7 +39,6 @@ public sealed class Deflate64Stream : Stream, IStreamStack
|
||||
private const int DEFAULT_BUFFER_SIZE = 8192;
|
||||
|
||||
private Stream _stream;
|
||||
private CompressionMode _mode;
|
||||
private InflaterManaged _inflater;
|
||||
private byte[] _buffer;
|
||||
|
||||
@@ -62,61 +61,23 @@ public sealed class Deflate64Stream : Stream, IStreamStack
|
||||
throw new ArgumentException("Deflate64: input stream is not readable", nameof(stream));
|
||||
}
|
||||
|
||||
InitializeInflater(stream, ZipCompressionMethod.Deflate64);
|
||||
#if DEBUG_STREAMS
|
||||
this.DebugConstruct(typeof(Deflate64Stream));
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up this DeflateManagedStream to be used for Inflation/Decompression
|
||||
/// </summary>
|
||||
private void InitializeInflater(
|
||||
Stream stream,
|
||||
ZipCompressionMethod method = ZipCompressionMethod.Deflate
|
||||
)
|
||||
{
|
||||
Debug.Assert(stream != null);
|
||||
Debug.Assert(
|
||||
method == ZipCompressionMethod.Deflate || method == ZipCompressionMethod.Deflate64
|
||||
);
|
||||
if (!stream.CanRead)
|
||||
{
|
||||
throw new ArgumentException("Deflate64: input stream is not readable", nameof(stream));
|
||||
}
|
||||
|
||||
_inflater = new InflaterManaged(method == ZipCompressionMethod.Deflate64);
|
||||
_inflater = new InflaterManaged(true);
|
||||
|
||||
_stream = stream;
|
||||
_mode = CompressionMode.Decompress;
|
||||
_buffer = new byte[DEFAULT_BUFFER_SIZE];
|
||||
#if DEBUG_STREAMS
|
||||
this.DebugConstruct(typeof(Deflate64Stream));
|
||||
#endif
|
||||
}
|
||||
|
||||
public override bool CanRead
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_stream is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
public override bool CanRead => _stream.CanRead;
|
||||
|
||||
return (_mode == CompressionMode.Decompress && _stream.CanRead);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanWrite
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_stream is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return (_mode == CompressionMode.Compress && _stream.CanWrite);
|
||||
}
|
||||
}
|
||||
public override bool CanWrite => false;
|
||||
|
||||
public override bool CanSeek => false;
|
||||
|
||||
@@ -138,7 +99,6 @@ public sealed class Deflate64Stream : Stream, IStreamStack
|
||||
|
||||
public override int Read(byte[] array, int offset, int count)
|
||||
{
|
||||
EnsureDecompressionMode();
|
||||
ValidateParameters(array, offset, count);
|
||||
EnsureNotDisposed();
|
||||
|
||||
@@ -185,6 +145,106 @@ public sealed class Deflate64Stream : Stream, IStreamStack
|
||||
return count - remainingCount;
|
||||
}
|
||||
|
||||
public override async Task<int> ReadAsync(
|
||||
byte[] array,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
ValidateParameters(array, offset, count);
|
||||
EnsureNotDisposed();
|
||||
|
||||
int bytesRead;
|
||||
var currentOffset = offset;
|
||||
var remainingCount = count;
|
||||
|
||||
while (true)
|
||||
{
|
||||
bytesRead = _inflater.Inflate(array, currentOffset, remainingCount);
|
||||
currentOffset += bytesRead;
|
||||
remainingCount -= bytesRead;
|
||||
|
||||
if (remainingCount == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (_inflater.Finished())
|
||||
{
|
||||
// if we finished decompressing, we can't have anything left in the outputwindow.
|
||||
Debug.Assert(
|
||||
_inflater.AvailableOutput == 0,
|
||||
"We should have copied all stuff out!"
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
var bytes = await _stream
|
||||
.ReadAsync(_buffer, 0, _buffer.Length, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (bytes <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
else if (bytes > _buffer.Length)
|
||||
{
|
||||
// The stream is either malicious or poorly implemented and returned a number of
|
||||
// bytes larger than the buffer supplied to it.
|
||||
throw new InvalidFormatException("Deflate64: invalid data");
|
||||
}
|
||||
|
||||
_inflater.SetInput(_buffer, 0, bytes);
|
||||
}
|
||||
|
||||
return count - remainingCount;
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override async ValueTask<int> ReadAsync(
|
||||
Memory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
|
||||
// InflaterManaged doesn't have a Span-based Inflate method, so we need to work with arrays
|
||||
// For large buffers, we could rent from ArrayPool, but for simplicity we'll use the buffer's array if available
|
||||
if (
|
||||
System.Runtime.InteropServices.MemoryMarshal.TryGetArray<byte>(
|
||||
buffer,
|
||||
out var arraySegment
|
||||
)
|
||||
)
|
||||
{
|
||||
// Fast path: the Memory<byte> is backed by an array
|
||||
return await ReadAsync(
|
||||
arraySegment.Array!,
|
||||
arraySegment.Offset,
|
||||
arraySegment.Count,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Slow path: rent a temporary array
|
||||
var tempBuffer = System.Buffers.ArrayPool<byte>.Shared.Rent(buffer.Length);
|
||||
try
|
||||
{
|
||||
var bytesRead = await ReadAsync(tempBuffer, 0, buffer.Length, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
tempBuffer.AsMemory(0, bytesRead).CopyTo(buffer);
|
||||
return bytesRead;
|
||||
}
|
||||
finally
|
||||
{
|
||||
System.Buffers.ArrayPool<byte>.Shared.Return(tempBuffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private void ValidateParameters(byte[] array, int offset, int count)
|
||||
{
|
||||
if (array is null)
|
||||
@@ -220,26 +280,6 @@ public sealed class Deflate64Stream : Stream, IStreamStack
|
||||
private static void ThrowStreamClosedException() =>
|
||||
throw new ObjectDisposedException(null, "Deflate64: stream has been disposed");
|
||||
|
||||
private void EnsureDecompressionMode()
|
||||
{
|
||||
if (_mode != CompressionMode.Decompress)
|
||||
{
|
||||
ThrowCannotReadFromDeflateManagedStreamException();
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowCannotReadFromDeflateManagedStreamException() =>
|
||||
throw new InvalidOperationException("Deflate64: cannot read from this stream");
|
||||
|
||||
private void EnsureCompressionMode()
|
||||
{
|
||||
if (_mode != CompressionMode.Compress)
|
||||
{
|
||||
ThrowCannotWriteToDeflateManagedStreamException();
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
private static void ThrowCannotWriteToDeflateManagedStreamException() =>
|
||||
throw new InvalidOperationException("Deflate64: cannot write to this stream");
|
||||
@@ -281,20 +321,17 @@ public sealed class Deflate64Stream : Stream, IStreamStack
|
||||
#endif
|
||||
if (disposing)
|
||||
{
|
||||
_stream?.Dispose();
|
||||
_stream.Dispose();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_stream = null;
|
||||
|
||||
try
|
||||
{
|
||||
_inflater?.Dispose();
|
||||
_inflater.Dispose();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_inflater = null;
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using SharpCompress.Common;
|
||||
|
||||
namespace SharpCompress.Compressors.LZMA;
|
||||
|
||||
/// <summary>
|
||||
/// The exception that is thrown when an error in input stream occurs during decoding.
|
||||
/// </summary>
|
||||
internal class DataErrorException : Exception
|
||||
internal class DataErrorException : SharpCompressException
|
||||
{
|
||||
public DataErrorException()
|
||||
: base("Data Error") { }
|
||||
@@ -15,7 +16,7 @@ internal class DataErrorException : Exception
|
||||
/// <summary>
|
||||
/// The exception that is thrown when the value of an argument is outside the allowable range.
|
||||
/// </summary>
|
||||
internal class InvalidParamException : Exception
|
||||
internal class InvalidParamException : SharpCompressException
|
||||
{
|
||||
public InvalidParamException()
|
||||
: base("Invalid Parameter") { }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System;
|
||||
using SharpCompress.Common;
|
||||
|
||||
namespace SharpCompress.Compressors.Xz;
|
||||
|
||||
public class XZIndexMarkerReachedException : Exception { }
|
||||
public class XZIndexMarkerReachedException : SharpCompressException { }
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharpCompress.IO;
|
||||
|
||||
@@ -93,6 +95,47 @@ internal class ReadOnlySubStream : SharpCompressStream, IStreamStack
|
||||
}
|
||||
#endif
|
||||
|
||||
public override async Task<int> ReadAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (BytesLeftToRead < count)
|
||||
{
|
||||
count = (int)BytesLeftToRead;
|
||||
}
|
||||
var read = await Stream
|
||||
.ReadAsync(buffer, offset, count, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (read > 0)
|
||||
{
|
||||
BytesLeftToRead -= read;
|
||||
_position += read;
|
||||
}
|
||||
return read;
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override async ValueTask<int> ReadAsync(
|
||||
Memory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var sliceLen = BytesLeftToRead < buffer.Length ? BytesLeftToRead : buffer.Length;
|
||||
var read = await Stream
|
||||
.ReadAsync(buffer.Slice(0, (int)sliceLen), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (read > 0)
|
||||
{
|
||||
BytesLeftToRead -= read;
|
||||
_position += read;
|
||||
}
|
||||
return read;
|
||||
}
|
||||
#endif
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
|
||||
|
||||
public override void SetLength(long value) => throw new NotSupportedException();
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharpCompress.IO;
|
||||
|
||||
@@ -326,20 +327,146 @@ public class SharpCompressStream : Stream, IStreamStack
|
||||
_internalPosition += count;
|
||||
}
|
||||
|
||||
public override async Task<int> ReadAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (count == 0)
|
||||
return 0;
|
||||
|
||||
if (_bufferingEnabled)
|
||||
{
|
||||
ValidateBufferState();
|
||||
|
||||
// Fill buffer if needed
|
||||
if (_bufferedLength == 0)
|
||||
{
|
||||
_bufferedLength = await Stream
|
||||
.ReadAsync(_buffer!, 0, _bufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
_bufferPosition = 0;
|
||||
}
|
||||
int available = _bufferedLength - _bufferPosition;
|
||||
int toRead = Math.Min(count, available);
|
||||
if (toRead > 0)
|
||||
{
|
||||
Array.Copy(_buffer!, _bufferPosition, buffer, offset, toRead);
|
||||
_bufferPosition += toRead;
|
||||
_internalPosition += toRead;
|
||||
return toRead;
|
||||
}
|
||||
// If buffer exhausted, refill
|
||||
int r = await Stream
|
||||
.ReadAsync(_buffer!, 0, _bufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (r == 0)
|
||||
return 0;
|
||||
_bufferedLength = r;
|
||||
_bufferPosition = 0;
|
||||
if (_bufferedLength == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
toRead = Math.Min(count, _bufferedLength);
|
||||
Array.Copy(_buffer!, 0, buffer, offset, toRead);
|
||||
_bufferPosition = toRead;
|
||||
_internalPosition += toRead;
|
||||
return toRead;
|
||||
}
|
||||
else
|
||||
{
|
||||
int read = await Stream
|
||||
.ReadAsync(buffer, offset, count, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
_internalPosition += read;
|
||||
return read;
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task WriteAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
await Stream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false);
|
||||
_internalPosition += count;
|
||||
}
|
||||
|
||||
public override async Task FlushAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await Stream.FlushAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
|
||||
//public override int Read(Span<byte> buffer)
|
||||
//{
|
||||
// int bytesRead = Stream.Read(buffer);
|
||||
// _internalPosition += bytesRead;
|
||||
// return bytesRead;
|
||||
//}
|
||||
public override async ValueTask<int> ReadAsync(
|
||||
Memory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (buffer.Length == 0)
|
||||
return 0;
|
||||
|
||||
// public override void Write(ReadOnlySpan<byte> buffer)
|
||||
// {
|
||||
// Stream.Write(buffer);
|
||||
// _internalPosition += buffer.Length;
|
||||
// }
|
||||
if (_bufferingEnabled)
|
||||
{
|
||||
ValidateBufferState();
|
||||
|
||||
// Fill buffer if needed
|
||||
if (_bufferedLength == 0)
|
||||
{
|
||||
_bufferedLength = await Stream
|
||||
.ReadAsync(_buffer.AsMemory(0, _bufferSize), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
_bufferPosition = 0;
|
||||
}
|
||||
int available = _bufferedLength - _bufferPosition;
|
||||
int toRead = Math.Min(buffer.Length, available);
|
||||
if (toRead > 0)
|
||||
{
|
||||
_buffer.AsSpan(_bufferPosition, toRead).CopyTo(buffer.Span);
|
||||
_bufferPosition += toRead;
|
||||
_internalPosition += toRead;
|
||||
return toRead;
|
||||
}
|
||||
// If buffer exhausted, refill
|
||||
int r = await Stream
|
||||
.ReadAsync(_buffer.AsMemory(0, _bufferSize), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (r == 0)
|
||||
return 0;
|
||||
_bufferedLength = r;
|
||||
_bufferPosition = 0;
|
||||
if (_bufferedLength == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
toRead = Math.Min(buffer.Length, _bufferedLength);
|
||||
_buffer.AsSpan(0, toRead).CopyTo(buffer.Span);
|
||||
_bufferPosition = toRead;
|
||||
_internalPosition += toRead;
|
||||
return toRead;
|
||||
}
|
||||
else
|
||||
{
|
||||
int read = await Stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
_internalPosition += read;
|
||||
return read;
|
||||
}
|
||||
}
|
||||
|
||||
public override async ValueTask WriteAsync(
|
||||
ReadOnlyMemory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
await Stream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
_internalPosition += buffer.Length;
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.IO;
|
||||
@@ -238,6 +240,105 @@ public class SourceStream : Stream, IStreamStack
|
||||
public override void Write(byte[] buffer, int offset, int count) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public override async Task<int> ReadAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (count <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var total = count;
|
||||
var r = -1;
|
||||
|
||||
while (count != 0 && r != 0)
|
||||
{
|
||||
r = await Current
|
||||
.ReadAsync(
|
||||
buffer,
|
||||
offset,
|
||||
(int)Math.Min(count, Current.Length - Current.Position),
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
count -= r;
|
||||
offset += r;
|
||||
|
||||
if (!IsVolumes && count != 0 && Current.Position == Current.Length)
|
||||
{
|
||||
var length = Current.Length;
|
||||
|
||||
// Load next file if present
|
||||
if (!SetStream(_stream + 1))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Current stream switched
|
||||
// Add length of previous stream
|
||||
_prevSize += length;
|
||||
Current.Seek(0, SeekOrigin.Begin);
|
||||
r = -1; //BugFix: reset to allow loop if count is still not 0 - was breaking split zipx (lzma xz etc)
|
||||
}
|
||||
}
|
||||
|
||||
return total - count;
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
|
||||
public override async ValueTask<int> ReadAsync(
|
||||
Memory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (buffer.Length <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var total = buffer.Length;
|
||||
var count = buffer.Length;
|
||||
var offset = 0;
|
||||
var r = -1;
|
||||
|
||||
while (count != 0 && r != 0)
|
||||
{
|
||||
r = await Current
|
||||
.ReadAsync(
|
||||
buffer.Slice(offset, (int)Math.Min(count, Current.Length - Current.Position)),
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
count -= r;
|
||||
offset += r;
|
||||
|
||||
if (!IsVolumes && count != 0 && Current.Position == Current.Length)
|
||||
{
|
||||
var length = Current.Length;
|
||||
|
||||
// Load next file if present
|
||||
if (!SetStream(_stream + 1))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Current stream switched
|
||||
// Add length of previous stream
|
||||
_prevSize += length;
|
||||
Current.Seek(0, SeekOrigin.Begin);
|
||||
r = -1;
|
||||
}
|
||||
}
|
||||
|
||||
return total - count;
|
||||
}
|
||||
#endif
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
if (IsFileMode || !ReaderOptions.LeaveStreamOpen) //close if file mode or options specify it
|
||||
|
||||
@@ -2,6 +2,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
|
||||
namespace SharpCompress.Readers;
|
||||
@@ -94,6 +96,33 @@ public abstract class AbstractReader<TEntry, TVolume> : IReader, IReaderExtracti
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<bool> MoveToNextEntryAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_completed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (Cancelled)
|
||||
{
|
||||
throw new ReaderCancelledException("Reader has been cancelled.");
|
||||
}
|
||||
if (_entriesForCurrentReadStream is null)
|
||||
{
|
||||
return LoadStreamForReading(RequestInitialStream());
|
||||
}
|
||||
if (!_wroteCurrentEntry)
|
||||
{
|
||||
await SkipEntryAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
_wroteCurrentEntry = false;
|
||||
if (NextEntryForCurrentStream())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
_completed = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
protected bool LoadStreamForReading(Stream stream)
|
||||
{
|
||||
_entriesForCurrentReadStream?.Dispose();
|
||||
@@ -127,6 +156,14 @@ public abstract class AbstractReader<TEntry, TVolume> : IReader, IReaderExtracti
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SkipEntryAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!Entry.IsDirectory)
|
||||
{
|
||||
await SkipAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void Skip()
|
||||
{
|
||||
var part = Entry.Parts.First();
|
||||
@@ -149,6 +186,33 @@ public abstract class AbstractReader<TEntry, TVolume> : IReader, IReaderExtracti
|
||||
s.SkipEntry();
|
||||
}
|
||||
|
||||
private async Task SkipAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var part = Entry.Parts.First();
|
||||
|
||||
if (!Entry.IsSplitAfter && !Entry.IsSolid && Entry.CompressedSize > 0)
|
||||
{
|
||||
//not solid and has a known compressed size then we can skip raw bytes.
|
||||
var rawStream = part.GetRawStream();
|
||||
|
||||
if (rawStream != null)
|
||||
{
|
||||
var bytesToAdvance = Entry.CompressedSize;
|
||||
await rawStream.SkipAsync(bytesToAdvance, cancellationToken).ConfigureAwait(false);
|
||||
part.Skipped = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
//don't know the size so we have to try to decompress to skip
|
||||
#if NETFRAMEWORK || NETSTANDARD2_0
|
||||
using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
await s.SkipEntryAsync(cancellationToken).ConfigureAwait(false);
|
||||
#else
|
||||
await using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
await s.SkipEntryAsync(cancellationToken).ConfigureAwait(false);
|
||||
#endif
|
||||
}
|
||||
|
||||
public void WriteEntryTo(Stream writableStream)
|
||||
{
|
||||
if (_wroteCurrentEntry)
|
||||
@@ -171,6 +235,33 @@ public abstract class AbstractReader<TEntry, TVolume> : IReader, IReaderExtracti
|
||||
_wroteCurrentEntry = true;
|
||||
}
|
||||
|
||||
public async Task WriteEntryToAsync(
|
||||
Stream writableStream,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (_wroteCurrentEntry)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"WriteEntryToAsync or OpenEntryStream can only be called once."
|
||||
);
|
||||
}
|
||||
|
||||
if (writableStream is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(writableStream));
|
||||
}
|
||||
if (!writableStream.CanWrite)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"A writable Stream was required. Use Cancel if that was intended."
|
||||
);
|
||||
}
|
||||
|
||||
await WriteAsync(writableStream, cancellationToken).ConfigureAwait(false);
|
||||
_wroteCurrentEntry = true;
|
||||
}
|
||||
|
||||
internal void Write(Stream writeStream)
|
||||
{
|
||||
var streamListener = this as IReaderExtractionListener;
|
||||
@@ -178,6 +269,20 @@ public abstract class AbstractReader<TEntry, TVolume> : IReader, IReaderExtracti
|
||||
s.TransferTo(writeStream, Entry, streamListener);
|
||||
}
|
||||
|
||||
internal async Task WriteAsync(Stream writeStream, CancellationToken cancellationToken)
|
||||
{
|
||||
var streamListener = this as IReaderExtractionListener;
|
||||
#if NETFRAMEWORK || NETSTANDARD2_0
|
||||
using Stream s = OpenEntryStream();
|
||||
await s.TransferToAsync(writeStream, Entry, streamListener, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
#else
|
||||
await using Stream s = OpenEntryStream();
|
||||
await s.TransferToAsync(writeStream, Entry, streamListener, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
#endif
|
||||
}
|
||||
|
||||
public EntryStream OpenEntryStream()
|
||||
{
|
||||
if (_wroteCurrentEntry)
|
||||
@@ -189,6 +294,19 @@ public abstract class AbstractReader<TEntry, TVolume> : IReader, IReaderExtracti
|
||||
return stream;
|
||||
}
|
||||
|
||||
public Task<EntryStream> OpenEntryStreamAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_wroteCurrentEntry)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"WriteEntryToAsync or OpenEntryStreamAsync can only be called once."
|
||||
);
|
||||
}
|
||||
var stream = GetEntryStream();
|
||||
_wroteCurrentEntry = true;
|
||||
return Task.FromResult(stream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retains a reference to the entry stream, so we can check whether it completed later.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
|
||||
namespace SharpCompress.Readers;
|
||||
@@ -21,6 +23,13 @@ public interface IReader : IDisposable
|
||||
/// <param name="writableStream"></param>
|
||||
void WriteEntryTo(Stream writableStream);
|
||||
|
||||
/// <summary>
|
||||
/// Decompresses the current entry to the stream asynchronously. This cannot be called twice for the current entry.
|
||||
/// </summary>
|
||||
/// <param name="writableStream"></param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
Task WriteEntryToAsync(Stream writableStream, CancellationToken cancellationToken = default);
|
||||
|
||||
bool Cancelled { get; }
|
||||
void Cancel();
|
||||
|
||||
@@ -30,9 +39,23 @@ public interface IReader : IDisposable
|
||||
/// <returns></returns>
|
||||
bool MoveToNextEntry();
|
||||
|
||||
/// <summary>
|
||||
/// Moves to the next entry asynchronously by reading more data from the underlying stream. This skips if data has not been read.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
Task<bool> MoveToNextEntryAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Opens the current entry as a stream that will decompress as it is read.
|
||||
/// Read the entire stream or use SkipEntry on EntryStream.
|
||||
/// </summary>
|
||||
EntryStream OpenEntryStream();
|
||||
|
||||
/// <summary>
|
||||
/// Opens the current entry asynchronously as a stream that will decompress as it is read.
|
||||
/// Read the entire stream or use SkipEntry on EntryStream.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
Task<EntryStream> OpenEntryStreamAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
|
||||
namespace SharpCompress.Readers;
|
||||
@@ -65,4 +67,64 @@ public static class IReaderExtensions
|
||||
reader.WriteEntryTo(fs);
|
||||
}
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Extract to specific directory asynchronously, retaining filename
|
||||
/// </summary>
|
||||
public static async Task WriteEntryToDirectoryAsync(
|
||||
this IReader reader,
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options = null,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
await ExtractionMethods
|
||||
.WriteEntryToDirectoryAsync(
|
||||
reader.Entry,
|
||||
destinationDirectory,
|
||||
options,
|
||||
(fileName, opts) => reader.WriteEntryToFileAsync(fileName, opts, cancellationToken),
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
/// <summary>
|
||||
/// Extract to specific file asynchronously
|
||||
/// </summary>
|
||||
public static async Task WriteEntryToFileAsync(
|
||||
this IReader reader,
|
||||
string destinationFileName,
|
||||
ExtractionOptions? options = null,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
await ExtractionMethods
|
||||
.WriteEntryToFileAsync(
|
||||
reader.Entry,
|
||||
destinationFileName,
|
||||
options,
|
||||
async (x, fm) =>
|
||||
{
|
||||
using var fs = File.Open(destinationFileName, fm);
|
||||
await reader.WriteEntryToAsync(fs, cancellationToken).ConfigureAwait(false);
|
||||
},
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
/// <summary>
|
||||
/// Extract all remaining unread entries to specific directory asynchronously, retaining filename
|
||||
/// </summary>
|
||||
public static async Task WriteAllToDirectoryAsync(
|
||||
this IReader reader,
|
||||
string destinationDirectory,
|
||||
ExtractionOptions? options = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
await reader
|
||||
.WriteEntryToDirectoryAsync(destinationDirectory, options, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress;
|
||||
@@ -89,6 +91,31 @@ internal static class Utility
|
||||
while (source.Read(buffer.Memory.Span) > 0) { }
|
||||
}
|
||||
|
||||
public static async Task SkipAsync(
|
||||
this Stream source,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var array = ArrayPool<byte>.Shared.Rent(TEMP_BUFFER_SIZE);
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var read = await source
|
||||
.ReadAsync(array, 0, array.Length, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (read <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(array);
|
||||
}
|
||||
}
|
||||
|
||||
public static DateTime DosDateToDateTime(ushort iDate, ushort iTime)
|
||||
{
|
||||
var year = (iDate / 512) + 1980;
|
||||
@@ -217,6 +244,89 @@ internal static class Utility
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<long> TransferToAsync(
|
||||
this Stream source,
|
||||
Stream destination,
|
||||
long maxLength,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var array = ArrayPool<byte>.Shared.Rent(TEMP_BUFFER_SIZE);
|
||||
try
|
||||
{
|
||||
var maxReadSize = array.Length;
|
||||
long total = 0;
|
||||
var remaining = maxLength;
|
||||
if (remaining < maxReadSize)
|
||||
{
|
||||
maxReadSize = (int)remaining;
|
||||
}
|
||||
while (
|
||||
await ReadTransferBlockAsync(source, array, maxReadSize, cancellationToken)
|
||||
.ConfigureAwait(false)
|
||||
is var (success, count)
|
||||
&& success
|
||||
)
|
||||
{
|
||||
await destination
|
||||
.WriteAsync(array, 0, count, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
total += count;
|
||||
if (remaining - count < 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
remaining -= count;
|
||||
if (remaining < maxReadSize)
|
||||
{
|
||||
maxReadSize = (int)remaining;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(array);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<long> TransferToAsync(
|
||||
this Stream source,
|
||||
Stream destination,
|
||||
Common.Entry entry,
|
||||
IReaderExtractionListener readerExtractionListener,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var array = ArrayPool<byte>.Shared.Rent(TEMP_BUFFER_SIZE);
|
||||
try
|
||||
{
|
||||
var iterations = 0;
|
||||
long total = 0;
|
||||
int count;
|
||||
while (
|
||||
(
|
||||
count = await source
|
||||
.ReadAsync(array, 0, array.Length, cancellationToken)
|
||||
.ConfigureAwait(false)
|
||||
) != 0
|
||||
)
|
||||
{
|
||||
total += count;
|
||||
await destination
|
||||
.WriteAsync(array, 0, count, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
iterations++;
|
||||
readerExtractionListener.FireEntryExtractionProgress(entry, total, iterations);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(array);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ReadTransferBlock(Stream source, byte[] array, int maxSize, out int count)
|
||||
{
|
||||
var size = maxSize;
|
||||
@@ -228,6 +338,56 @@ internal static class Utility
|
||||
return count != 0;
|
||||
}
|
||||
|
||||
private static async Task<(bool success, int count)> ReadTransferBlockAsync(
|
||||
Stream source,
|
||||
byte[] array,
|
||||
int maxSize,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var size = maxSize;
|
||||
if (maxSize > array.Length)
|
||||
{
|
||||
size = array.Length;
|
||||
}
|
||||
var count = await source.ReadAsync(array, 0, size, cancellationToken).ConfigureAwait(false);
|
||||
return (count != 0, count);
|
||||
}
|
||||
|
||||
public static async Task SkipAsync(
|
||||
this Stream source,
|
||||
long advanceAmount,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (source.CanSeek)
|
||||
{
|
||||
source.Position += advanceAmount;
|
||||
return;
|
||||
}
|
||||
|
||||
var array = ArrayPool<byte>.Shared.Rent(TEMP_BUFFER_SIZE);
|
||||
try
|
||||
{
|
||||
while (advanceAmount > 0)
|
||||
{
|
||||
var toRead = (int)Math.Min(array.Length, advanceAmount);
|
||||
var read = await source
|
||||
.ReadAsync(array, 0, toRead, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (read <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
advanceAmount -= read;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(array);
|
||||
}
|
||||
}
|
||||
|
||||
#if NET60_OR_GREATER
|
||||
|
||||
public static bool ReadFully(this Stream stream, byte[] buffer)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
|
||||
namespace SharpCompress.Writers;
|
||||
@@ -22,6 +24,33 @@ public abstract class AbstractWriter(ArchiveType type, WriterOptions writerOptio
|
||||
|
||||
public abstract void Write(string filename, Stream source, DateTime? modificationTime);
|
||||
|
||||
public virtual async Task WriteAsync(
|
||||
string filename,
|
||||
Stream source,
|
||||
DateTime? modificationTime,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
// Default implementation calls synchronous version
|
||||
// Derived classes should override for true async behavior
|
||||
Write(filename, source, modificationTime);
|
||||
await Task.CompletedTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public abstract void WriteDirectory(string directoryName, DateTime? modificationTime);
|
||||
|
||||
public virtual async Task WriteDirectoryAsync(
|
||||
string directoryName,
|
||||
DateTime? modificationTime,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
// Default implementation calls synchronous version
|
||||
// Derived classes should override for true async behavior
|
||||
WriteDirectory(directoryName, modificationTime);
|
||||
await Task.CompletedTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool isDisposing)
|
||||
{
|
||||
if (isDisposing)
|
||||
|
||||
@@ -50,4 +50,7 @@ public sealed class GZipWriter : AbstractWriter
|
||||
source.CopyTo(stream);
|
||||
_wroteToStream = true;
|
||||
}
|
||||
|
||||
public override void WriteDirectory(string directoryName, DateTime? modificationTime) =>
|
||||
throw new NotSupportedException("GZip archives do not support directory entries.");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
|
||||
namespace SharpCompress.Writers;
|
||||
@@ -8,4 +10,16 @@ public interface IWriter : IDisposable
|
||||
{
|
||||
ArchiveType WriterType { get; }
|
||||
void Write(string filename, Stream source, DateTime? modificationTime);
|
||||
Task WriteAsync(
|
||||
string filename,
|
||||
Stream source,
|
||||
DateTime? modificationTime,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
void WriteDirectory(string directoryName, DateTime? modificationTime);
|
||||
Task WriteDirectoryAsync(
|
||||
string directoryName,
|
||||
DateTime? modificationTime,
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharpCompress.Writers;
|
||||
|
||||
@@ -52,4 +54,80 @@ public static class IWriterExtensions
|
||||
writer.Write(file.Substring(directory.Length), file);
|
||||
}
|
||||
}
|
||||
|
||||
public static void WriteDirectory(this IWriter writer, string directoryName) =>
|
||||
writer.WriteDirectory(directoryName, null);
|
||||
|
||||
// Async extensions
|
||||
public static Task WriteAsync(
|
||||
this IWriter writer,
|
||||
string entryPath,
|
||||
Stream source,
|
||||
CancellationToken cancellationToken = default
|
||||
) => writer.WriteAsync(entryPath, source, null, cancellationToken);
|
||||
|
||||
public static async Task WriteAsync(
|
||||
this IWriter writer,
|
||||
string entryPath,
|
||||
FileInfo source,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (!source.Exists)
|
||||
{
|
||||
throw new ArgumentException("Source does not exist: " + source.FullName);
|
||||
}
|
||||
using var stream = source.OpenRead();
|
||||
await writer
|
||||
.WriteAsync(entryPath, stream, source.LastWriteTime, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static Task WriteAsync(
|
||||
this IWriter writer,
|
||||
string entryPath,
|
||||
string source,
|
||||
CancellationToken cancellationToken = default
|
||||
) => writer.WriteAsync(entryPath, new FileInfo(source), cancellationToken);
|
||||
|
||||
public static Task WriteAllAsync(
|
||||
this IWriter writer,
|
||||
string directory,
|
||||
string searchPattern = "*",
|
||||
SearchOption option = SearchOption.TopDirectoryOnly,
|
||||
CancellationToken cancellationToken = default
|
||||
) => writer.WriteAllAsync(directory, searchPattern, null, option, cancellationToken);
|
||||
|
||||
public static async Task WriteAllAsync(
|
||||
this IWriter writer,
|
||||
string directory,
|
||||
string searchPattern = "*",
|
||||
Func<string, bool>? fileSearchFunc = null,
|
||||
SearchOption option = SearchOption.TopDirectoryOnly,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
throw new ArgumentException("Directory does not exist: " + directory);
|
||||
}
|
||||
|
||||
fileSearchFunc ??= n => true;
|
||||
foreach (
|
||||
var file in Directory
|
||||
.EnumerateFiles(directory, searchPattern, option)
|
||||
.Where(fileSearchFunc)
|
||||
)
|
||||
{
|
||||
await writer
|
||||
.WriteAsync(file.Substring(directory.Length), file, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public static Task WriteDirectoryAsync(
|
||||
this IWriter writer,
|
||||
string directoryName,
|
||||
CancellationToken cancellationToken = default
|
||||
) => writer.WriteDirectoryAsync(directoryName, null, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Tar.Headers;
|
||||
using SharpCompress.Compressors;
|
||||
@@ -72,6 +74,44 @@ public class TarWriter : AbstractWriter
|
||||
return filename.Trim('/');
|
||||
}
|
||||
|
||||
private string NormalizeDirectoryName(string directoryName)
|
||||
{
|
||||
directoryName = NormalizeFilename(directoryName);
|
||||
// Ensure directory name ends with '/' for tar format
|
||||
if (!string.IsNullOrEmpty(directoryName) && !directoryName.EndsWith('/'))
|
||||
{
|
||||
directoryName += '/';
|
||||
}
|
||||
return directoryName;
|
||||
}
|
||||
|
||||
public override void WriteDirectory(string directoryName, DateTime? modificationTime)
|
||||
{
|
||||
var normalizedName = NormalizeDirectoryName(directoryName);
|
||||
if (string.IsNullOrEmpty(normalizedName))
|
||||
{
|
||||
return; // Skip empty or root directory
|
||||
}
|
||||
|
||||
var header = new TarHeader(WriterOptions.ArchiveEncoding);
|
||||
header.LastModifiedTime = modificationTime ?? TarHeader.EPOCH;
|
||||
header.Name = normalizedName;
|
||||
header.Size = 0;
|
||||
header.EntryType = EntryType.Directory;
|
||||
header.Write(OutputStream);
|
||||
}
|
||||
|
||||
public override async Task WriteDirectoryAsync(
|
||||
string directoryName,
|
||||
DateTime? modificationTime,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
// Synchronous implementation is sufficient for header-only write
|
||||
WriteDirectory(directoryName, modificationTime);
|
||||
await Task.CompletedTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void Write(string filename, Stream source, DateTime? modificationTime, long? size)
|
||||
{
|
||||
if (!source.CanSeek && size is null)
|
||||
@@ -91,6 +131,40 @@ public class TarWriter : AbstractWriter
|
||||
PadTo512(size.Value);
|
||||
}
|
||||
|
||||
public override async Task WriteAsync(
|
||||
string filename,
|
||||
Stream source,
|
||||
DateTime? modificationTime,
|
||||
CancellationToken cancellationToken = default
|
||||
) => await WriteAsync(filename, source, modificationTime, null, cancellationToken);
|
||||
|
||||
public async Task WriteAsync(
|
||||
string filename,
|
||||
Stream source,
|
||||
DateTime? modificationTime,
|
||||
long? size,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (!source.CanSeek && size is null)
|
||||
{
|
||||
throw new ArgumentException("Seekable stream is required if no size is given.");
|
||||
}
|
||||
|
||||
var realSize = size ?? source.Length;
|
||||
|
||||
var header = new TarHeader(WriterOptions.ArchiveEncoding);
|
||||
|
||||
header.LastModifiedTime = modificationTime ?? TarHeader.EPOCH;
|
||||
header.Name = NormalizeFilename(filename);
|
||||
header.Size = realSize;
|
||||
header.Write(OutputStream);
|
||||
var written = await source
|
||||
.TransferToAsync(OutputStream, realSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
PadTo512(written);
|
||||
}
|
||||
|
||||
private void PadTo512(long size)
|
||||
{
|
||||
var zeros = unchecked((int)(((size + 511L) & ~511L) - size));
|
||||
|
||||
@@ -3,6 +3,8 @@ using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Zip;
|
||||
using SharpCompress.Common.Zip.Headers;
|
||||
@@ -135,6 +137,73 @@ public class ZipWriter : AbstractWriter
|
||||
return filename.Trim('/');
|
||||
}
|
||||
|
||||
private string NormalizeDirectoryName(string directoryName)
|
||||
{
|
||||
directoryName = NormalizeFilename(directoryName);
|
||||
// Ensure directory name ends with '/' for zip format
|
||||
if (!string.IsNullOrEmpty(directoryName) && !directoryName.EndsWith('/'))
|
||||
{
|
||||
directoryName += '/';
|
||||
}
|
||||
return directoryName;
|
||||
}
|
||||
|
||||
public override void WriteDirectory(string directoryName, DateTime? modificationTime)
|
||||
{
|
||||
var normalizedName = NormalizeDirectoryName(directoryName);
|
||||
if (string.IsNullOrEmpty(normalizedName))
|
||||
{
|
||||
return; // Skip empty or root directory
|
||||
}
|
||||
|
||||
var options = new ZipWriterEntryOptions { ModificationDateTime = modificationTime };
|
||||
WriteDirectoryEntry(normalizedName, options);
|
||||
}
|
||||
|
||||
public override async Task WriteDirectoryAsync(
|
||||
string directoryName,
|
||||
DateTime? modificationTime,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
// Synchronous implementation is sufficient for directory entries
|
||||
WriteDirectory(directoryName, modificationTime);
|
||||
await Task.CompletedTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private void WriteDirectoryEntry(string directoryPath, ZipWriterEntryOptions options)
|
||||
{
|
||||
var compression = ZipCompressionMethod.None;
|
||||
|
||||
options.ModificationDateTime ??= DateTime.Now;
|
||||
options.EntryComment ??= string.Empty;
|
||||
|
||||
var entry = new ZipCentralDirectoryEntry(
|
||||
compression,
|
||||
directoryPath,
|
||||
(ulong)streamPosition,
|
||||
WriterOptions.ArchiveEncoding
|
||||
)
|
||||
{
|
||||
Comment = options.EntryComment,
|
||||
ModificationTime = options.ModificationDateTime,
|
||||
Crc = 0,
|
||||
Compressed = 0,
|
||||
Decompressed = 0,
|
||||
};
|
||||
|
||||
// Use the archive default setting for zip64 and allow overrides
|
||||
var useZip64 = isZip64;
|
||||
if (options.EnableZip64.HasValue)
|
||||
{
|
||||
useZip64 = options.EnableZip64.Value;
|
||||
}
|
||||
|
||||
var headersize = (uint)WriteHeader(directoryPath, options, entry, useZip64);
|
||||
streamPosition += headersize;
|
||||
entries.Add(entry);
|
||||
}
|
||||
|
||||
private int WriteHeader(
|
||||
string filename,
|
||||
ZipWriterEntryOptions zipWriterEntryOptions,
|
||||
|
||||
@@ -335,9 +335,9 @@
|
||||
"net8.0": {
|
||||
"Microsoft.NET.ILLink.Tasks": {
|
||||
"type": "Direct",
|
||||
"requested": "[8.0.21, )",
|
||||
"resolved": "8.0.21",
|
||||
"contentHash": "s8H5PZQs50OcNkaB6Si54+v3GWM7vzs6vxFRMlD3aXsbM+aPCtod62gmK0BYWou9diGzmo56j8cIf/PziijDqQ=="
|
||||
"requested": "[8.0.17, )",
|
||||
"resolved": "8.0.17",
|
||||
"contentHash": "x5/y4l8AtshpBOrCZdlE4txw8K3e3s9meBFeZeR3l8hbbku2V7kK6ojhXvrbjg1rk3G+JqL1BI26gtgc1ZrdUw=="
|
||||
},
|
||||
"Microsoft.SourceLink.GitHub": {
|
||||
"type": "Direct",
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Compressors.Xz;
|
||||
@@ -569,4 +570,56 @@ public class ArchiveTests : ReaderTests
|
||||
|
||||
return (extractedData, crc);
|
||||
}
|
||||
|
||||
protected async Task ArchiveStreamReadAsync(
|
||||
string testArchive,
|
||||
ReaderOptions? readerOptions = null
|
||||
)
|
||||
{
|
||||
testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive);
|
||||
await ArchiveStreamReadAsync(
|
||||
ArchiveFactory.AutoFactory,
|
||||
readerOptions,
|
||||
new[] { testArchive }
|
||||
);
|
||||
}
|
||||
|
||||
protected async Task ArchiveStreamReadAsync(
|
||||
IArchiveFactory archiveFactory,
|
||||
ReaderOptions? readerOptions,
|
||||
IEnumerable<string> testArchives
|
||||
)
|
||||
{
|
||||
foreach (var path in testArchives)
|
||||
{
|
||||
using (
|
||||
var stream = SharpCompressStream.Create(
|
||||
File.OpenRead(path),
|
||||
leaveOpen: true,
|
||||
throwOnDispose: true
|
||||
)
|
||||
)
|
||||
using (var archive = archiveFactory.Open(stream, readerOptions))
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
|
||||
{
|
||||
await entry.WriteToDirectoryAsync(
|
||||
SCRATCH_FILES_PATH,
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (IndexOutOfRangeException)
|
||||
{
|
||||
//SevenZipArchive_BZip2_Split test needs this
|
||||
stream.ThrowOnDispose = false;
|
||||
throw;
|
||||
}
|
||||
stream.ThrowOnDispose = false;
|
||||
}
|
||||
VerifyFiles();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
116
tests/SharpCompress.Test/ExceptionHierarchyTests.cs
Normal file
116
tests/SharpCompress.Test/ExceptionHierarchyTests.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Compressors.Deflate;
|
||||
using SharpCompress.Compressors.LZMA;
|
||||
using SharpCompress.Compressors.Xz;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test;
|
||||
|
||||
public class ExceptionHierarchyTests
|
||||
{
|
||||
[Fact]
|
||||
public void AllSharpCompressExceptions_InheritFromSharpCompressException()
|
||||
{
|
||||
// Verify that ArchiveException inherits from SharpCompressException
|
||||
Assert.True(typeof(SharpCompressException).IsAssignableFrom(typeof(ArchiveException)));
|
||||
|
||||
// Verify that ExtractionException inherits from SharpCompressException
|
||||
Assert.True(typeof(SharpCompressException).IsAssignableFrom(typeof(ExtractionException)));
|
||||
|
||||
// Verify that InvalidFormatException inherits from SharpCompressException (through ExtractionException)
|
||||
Assert.True(
|
||||
typeof(SharpCompressException).IsAssignableFrom(typeof(InvalidFormatException))
|
||||
);
|
||||
|
||||
// Verify that CryptographicException inherits from SharpCompressException
|
||||
Assert.True(
|
||||
typeof(SharpCompressException).IsAssignableFrom(typeof(CryptographicException))
|
||||
);
|
||||
|
||||
// Verify that IncompleteArchiveException inherits from SharpCompressException (through ArchiveException)
|
||||
Assert.True(
|
||||
typeof(SharpCompressException).IsAssignableFrom(typeof(IncompleteArchiveException))
|
||||
);
|
||||
|
||||
// Verify that ReaderCancelledException inherits from SharpCompressException
|
||||
Assert.True(
|
||||
typeof(SharpCompressException).IsAssignableFrom(typeof(ReaderCancelledException))
|
||||
);
|
||||
|
||||
// Verify that MultipartStreamRequiredException inherits from SharpCompressException (through ExtractionException)
|
||||
Assert.True(
|
||||
typeof(SharpCompressException).IsAssignableFrom(
|
||||
typeof(MultipartStreamRequiredException)
|
||||
)
|
||||
);
|
||||
|
||||
// Verify that MultiVolumeExtractionException inherits from SharpCompressException (through ExtractionException)
|
||||
Assert.True(
|
||||
typeof(SharpCompressException).IsAssignableFrom(typeof(MultiVolumeExtractionException))
|
||||
);
|
||||
|
||||
// Verify that ZlibException inherits from SharpCompressException
|
||||
Assert.True(typeof(SharpCompressException).IsAssignableFrom(typeof(ZlibException)));
|
||||
|
||||
// Verify that XZIndexMarkerReachedException inherits from SharpCompressException
|
||||
Assert.True(
|
||||
typeof(SharpCompressException).IsAssignableFrom(typeof(XZIndexMarkerReachedException))
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SharpCompressException_CanBeCaughtByBaseType()
|
||||
{
|
||||
// Test that a derived exception can be caught as SharpCompressException
|
||||
var exception = new InvalidFormatException("Test message");
|
||||
var caughtException = false;
|
||||
|
||||
try
|
||||
{
|
||||
throw exception;
|
||||
}
|
||||
catch (SharpCompressException ex)
|
||||
{
|
||||
caughtException = true;
|
||||
Assert.Same(exception, ex);
|
||||
}
|
||||
|
||||
Assert.True(caughtException, "Exception should have been caught as SharpCompressException");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InternalLzmaExceptions_InheritFromSharpCompressException()
|
||||
{
|
||||
// Use reflection to verify internal exception types
|
||||
var dataErrorExceptionType = Type.GetType(
|
||||
"SharpCompress.Compressors.LZMA.DataErrorException, SharpCompress"
|
||||
);
|
||||
Assert.NotNull(dataErrorExceptionType);
|
||||
Assert.True(typeof(SharpCompressException).IsAssignableFrom(dataErrorExceptionType));
|
||||
|
||||
var invalidParamExceptionType = Type.GetType(
|
||||
"SharpCompress.Compressors.LZMA.InvalidParamException, SharpCompress"
|
||||
);
|
||||
Assert.NotNull(invalidParamExceptionType);
|
||||
Assert.True(typeof(SharpCompressException).IsAssignableFrom(invalidParamExceptionType));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExceptionConstructors_WorkCorrectly()
|
||||
{
|
||||
// Test parameterless constructor
|
||||
var ex1 = new SharpCompressException();
|
||||
Assert.NotNull(ex1);
|
||||
|
||||
// Test message constructor
|
||||
var ex2 = new SharpCompressException("Test message");
|
||||
Assert.Equal("Test message", ex2.Message);
|
||||
|
||||
// Test message and inner exception constructor
|
||||
var inner = new InvalidOperationException("Inner");
|
||||
var ex3 = new SharpCompressException("Test message", inner);
|
||||
Assert.Equal("Test message", ex3.Message);
|
||||
Assert.Same(inner, ex3.InnerException);
|
||||
}
|
||||
}
|
||||
99
tests/SharpCompress.Test/ExtractionTests.cs
Normal file
99
tests/SharpCompress.Test/ExtractionTests.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Writers;
|
||||
using SharpCompress.Writers.Zip;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test;
|
||||
|
||||
public class ExtractionTests : TestBase
|
||||
{
|
||||
[Fact]
|
||||
public void Extraction_ShouldHandleCaseInsensitivePathsOnWindows()
|
||||
{
|
||||
// This test validates that extraction succeeds when Path.GetFullPath returns paths
|
||||
// with casing that matches the platform's file system behavior. On Windows,
|
||||
// Path.GetFullPath can return different casing than the actual directory on disk
|
||||
// (e.g., "system32" vs "System32"), and the extraction should succeed because
|
||||
// Windows file systems are case-insensitive. On Unix-like systems, this test
|
||||
// verifies that the case-sensitive comparison is used correctly.
|
||||
|
||||
var testArchive = Path.Combine(SCRATCH2_FILES_PATH, "test-extraction.zip");
|
||||
var extractPath = SCRATCH_FILES_PATH;
|
||||
|
||||
// Create a simple test archive with a single file
|
||||
using (var stream = File.Create(testArchive))
|
||||
{
|
||||
using var writer = (ZipWriter)
|
||||
WriterFactory.Open(stream, ArchiveType.Zip, CompressionType.Deflate);
|
||||
|
||||
// Create a test file to add to the archive
|
||||
var testFilePath = Path.Combine(SCRATCH2_FILES_PATH, "testfile.txt");
|
||||
File.WriteAllText(testFilePath, "Test content");
|
||||
|
||||
writer.Write("testfile.txt", testFilePath);
|
||||
}
|
||||
|
||||
// Extract the archive - this should succeed regardless of path casing
|
||||
using (var stream = File.OpenRead(testArchive))
|
||||
{
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
|
||||
// This should not throw an exception even if Path.GetFullPath returns
|
||||
// a path with different casing than the actual directory
|
||||
var exception = Record.Exception(() =>
|
||||
reader.WriteAllToDirectory(
|
||||
extractPath,
|
||||
new ExtractionOptions { ExtractFullPath = false, Overwrite = true }
|
||||
)
|
||||
);
|
||||
|
||||
Assert.Null(exception);
|
||||
}
|
||||
|
||||
// Verify the file was extracted successfully
|
||||
var extractedFile = Path.Combine(extractPath, "testfile.txt");
|
||||
Assert.True(File.Exists(extractedFile));
|
||||
Assert.Equal("Test content", File.ReadAllText(extractedFile));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Extraction_ShouldPreventPathTraversalAttacks()
|
||||
{
|
||||
// This test ensures that the security check still works to prevent
|
||||
// path traversal attacks (e.g., using "../" to escape the destination directory)
|
||||
|
||||
var testArchive = Path.Combine(SCRATCH2_FILES_PATH, "test-traversal.zip");
|
||||
var extractPath = SCRATCH_FILES_PATH;
|
||||
|
||||
// Create a test archive with a path traversal attempt
|
||||
using (var stream = File.Create(testArchive))
|
||||
{
|
||||
using var writer = (ZipWriter)
|
||||
WriterFactory.Open(stream, ArchiveType.Zip, CompressionType.Deflate);
|
||||
|
||||
var testFilePath = Path.Combine(SCRATCH2_FILES_PATH, "testfile2.txt");
|
||||
File.WriteAllText(testFilePath, "Test content");
|
||||
|
||||
// Try to write with a path that attempts to escape the destination directory
|
||||
writer.Write("../../evil.txt", testFilePath);
|
||||
}
|
||||
|
||||
// Extract the archive - this should throw an exception for path traversal
|
||||
using (var stream = File.OpenRead(testArchive))
|
||||
{
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
|
||||
var exception = Assert.Throws<ExtractionException>(() =>
|
||||
reader.WriteAllToDirectory(
|
||||
extractPath,
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
|
||||
)
|
||||
);
|
||||
|
||||
Assert.Contains("outside of the destination", exception.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
238
tests/SharpCompress.Test/GZip/AsyncTests.cs
Normal file
238
tests/SharpCompress.Test/GZip/AsyncTests.cs
Normal file
@@ -0,0 +1,238 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Archives.Zip;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Writers;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.GZip;
|
||||
|
||||
public class AsyncTests : TestBase
|
||||
{
|
||||
[Fact]
|
||||
public async Task Reader_Async_Extract_All()
|
||||
{
|
||||
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz");
|
||||
#if NETFRAMEWORK
|
||||
using var stream = File.OpenRead(testArchive);
|
||||
#else
|
||||
await using var stream = File.OpenRead(testArchive);
|
||||
#endif
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
|
||||
await reader.WriteAllToDirectoryAsync(
|
||||
SCRATCH_FILES_PATH,
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
|
||||
);
|
||||
|
||||
// Just verify some files were extracted
|
||||
var extractedFiles = Directory.GetFiles(
|
||||
SCRATCH_FILES_PATH,
|
||||
"*",
|
||||
SearchOption.AllDirectories
|
||||
);
|
||||
Assert.True(extractedFiles.Length > 0, "No files were extracted");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reader_Async_Extract_Single_Entry()
|
||||
{
|
||||
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz");
|
||||
#if NETFRAMEWORK
|
||||
using var stream = File.OpenRead(testArchive);
|
||||
#else
|
||||
await using var stream = File.OpenRead(testArchive);
|
||||
#endif
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
var outputPath = Path.Combine(SCRATCH_FILES_PATH, reader.Entry.Key!);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);
|
||||
#if NETFRAMEWORK
|
||||
using var outputStream = File.Create(outputPath);
|
||||
#else
|
||||
await using var outputStream = File.Create(outputPath);
|
||||
#endif
|
||||
await reader.WriteEntryToAsync(outputStream);
|
||||
break; // Just test one entry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Archive_Entry_Async_Open_Stream()
|
||||
{
|
||||
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz");
|
||||
using var archive = ArchiveFactory.Open(testArchive);
|
||||
|
||||
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory).Take(1))
|
||||
{
|
||||
#if NETFRAMEWORK
|
||||
using var entryStream = await entry.OpenEntryStreamAsync();
|
||||
#else
|
||||
await using var entryStream = await entry.OpenEntryStreamAsync();
|
||||
#endif
|
||||
Assert.NotNull(entryStream);
|
||||
Assert.True(entryStream.CanRead);
|
||||
|
||||
// Read some data to verify it works
|
||||
var buffer = new byte[1024];
|
||||
var read = await entryStream.ReadAsync(buffer, 0, buffer.Length);
|
||||
Assert.True(read > 0);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Writer_Async_Write_Single_File()
|
||||
{
|
||||
var outputPath = Path.Combine(SCRATCH_FILES_PATH, "async_test.zip");
|
||||
using (var stream = File.Create(outputPath))
|
||||
using (var writer = WriterFactory.Open(stream, ArchiveType.Zip, CompressionType.Deflate))
|
||||
{
|
||||
var testFile = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz");
|
||||
using var fileStream = File.OpenRead(testFile);
|
||||
await writer.WriteAsync("test_entry.bin", fileStream, new DateTime(2023, 1, 1));
|
||||
}
|
||||
|
||||
// Verify the archive was created and contains the entry
|
||||
Assert.True(File.Exists(outputPath));
|
||||
using var archive = ZipArchive.Open(outputPath);
|
||||
Assert.Single(archive.Entries.Where(e => !e.IsDirectory));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Async_With_Cancellation_Token()
|
||||
{
|
||||
using var cts = new CancellationTokenSource();
|
||||
cts.CancelAfter(10000); // 10 seconds should be plenty
|
||||
|
||||
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz");
|
||||
using var stream = File.OpenRead(testArchive);
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
|
||||
await reader.WriteAllToDirectoryAsync(
|
||||
SCRATCH_FILES_PATH,
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true },
|
||||
cts.Token
|
||||
);
|
||||
|
||||
// Just verify some files were extracted
|
||||
var extractedFiles = Directory.GetFiles(
|
||||
SCRATCH_FILES_PATH,
|
||||
"*",
|
||||
SearchOption.AllDirectories
|
||||
);
|
||||
Assert.True(extractedFiles.Length > 0, "No files were extracted");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Stream_Extensions_Async()
|
||||
{
|
||||
var testFile = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz");
|
||||
using var inputStream = File.OpenRead(testFile);
|
||||
var outputPath = Path.Combine(SCRATCH_FILES_PATH, "async_copy.bin");
|
||||
using var outputStream = File.Create(outputPath);
|
||||
|
||||
// Test the async extension method
|
||||
var buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
while ((bytesRead = await inputStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
await outputStream.WriteAsync(buffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
Assert.True(File.Exists(outputPath));
|
||||
Assert.True(new FileInfo(outputPath).Length > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EntryStream_ReadAsync_Works()
|
||||
{
|
||||
var testArchive = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz");
|
||||
using var stream = File.OpenRead(testArchive);
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
using var entryStream = reader.OpenEntryStream();
|
||||
var buffer = new byte[4096];
|
||||
var totalRead = 0;
|
||||
int bytesRead;
|
||||
|
||||
// Test ReadAsync on EntryStream
|
||||
while ((bytesRead = await entryStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
totalRead += bytesRead;
|
||||
}
|
||||
|
||||
Assert.True(totalRead > 0, "Should have read some data from entry stream");
|
||||
break; // Test just one entry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CompressionStream_Async_ReadWrite()
|
||||
{
|
||||
var testData = new byte[1024];
|
||||
new Random(42).NextBytes(testData);
|
||||
|
||||
var compressedPath = Path.Combine(SCRATCH_FILES_PATH, "async_compressed.gz");
|
||||
|
||||
// Test async write with GZipStream
|
||||
using (var fileStream = File.Create(compressedPath))
|
||||
using (
|
||||
var gzipStream = new Compressors.Deflate.GZipStream(
|
||||
fileStream,
|
||||
Compressors.CompressionMode.Compress
|
||||
)
|
||||
)
|
||||
{
|
||||
await gzipStream.WriteAsync(testData, 0, testData.Length);
|
||||
await gzipStream.FlushAsync();
|
||||
}
|
||||
|
||||
Assert.True(File.Exists(compressedPath));
|
||||
Assert.True(new FileInfo(compressedPath).Length > 0);
|
||||
|
||||
// Test async read with GZipStream
|
||||
using (var fileStream = File.OpenRead(compressedPath))
|
||||
using (
|
||||
var gzipStream = new Compressors.Deflate.GZipStream(
|
||||
fileStream,
|
||||
Compressors.CompressionMode.Decompress
|
||||
)
|
||||
)
|
||||
{
|
||||
var decompressed = new byte[testData.Length];
|
||||
var totalRead = 0;
|
||||
int bytesRead;
|
||||
while (
|
||||
totalRead < decompressed.Length
|
||||
&& (
|
||||
bytesRead = await gzipStream.ReadAsync(
|
||||
decompressed,
|
||||
totalRead,
|
||||
decompressed.Length - totalRead
|
||||
)
|
||||
) > 0
|
||||
)
|
||||
{
|
||||
totalRead += bytesRead;
|
||||
}
|
||||
|
||||
Assert.Equal(testData.Length, totalRead);
|
||||
Assert.Equal(testData, decompressed);
|
||||
}
|
||||
}
|
||||
}
|
||||
127
tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs
Normal file
127
tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Archives.GZip;
|
||||
using SharpCompress.Archives.Tar;
|
||||
using SharpCompress.Common;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.GZip;
|
||||
|
||||
public class GZipArchiveAsyncTests : ArchiveTests
|
||||
{
|
||||
public GZipArchiveAsyncTests() => UseExtensionInsteadOfNameToVerify = true;
|
||||
|
||||
[Fact]
|
||||
public async Task GZip_Archive_Generic_Async()
|
||||
{
|
||||
using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")))
|
||||
using (var archive = ArchiveFactory.Open(stream))
|
||||
{
|
||||
var entry = archive.Entries.First();
|
||||
await entry.WriteToFileAsync(Path.Combine(SCRATCH_FILES_PATH, entry.Key.NotNull()));
|
||||
|
||||
var size = entry.Size;
|
||||
var scratch = new FileInfo(Path.Combine(SCRATCH_FILES_PATH, "Tar.tar"));
|
||||
var test = new FileInfo(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar"));
|
||||
|
||||
Assert.Equal(size, scratch.Length);
|
||||
Assert.Equal(size, test.Length);
|
||||
}
|
||||
CompareArchivesByPath(
|
||||
Path.Combine(SCRATCH_FILES_PATH, "Tar.tar"),
|
||||
Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar")
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GZip_Archive_Async()
|
||||
{
|
||||
using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")))
|
||||
using (var archive = GZipArchive.Open(stream))
|
||||
{
|
||||
var entry = archive.Entries.First();
|
||||
await entry.WriteToFileAsync(Path.Combine(SCRATCH_FILES_PATH, entry.Key.NotNull()));
|
||||
|
||||
var size = entry.Size;
|
||||
var scratch = new FileInfo(Path.Combine(SCRATCH_FILES_PATH, "Tar.tar"));
|
||||
var test = new FileInfo(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar"));
|
||||
|
||||
Assert.Equal(size, scratch.Length);
|
||||
Assert.Equal(size, test.Length);
|
||||
}
|
||||
CompareArchivesByPath(
|
||||
Path.Combine(SCRATCH_FILES_PATH, "Tar.tar"),
|
||||
Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar")
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GZip_Archive_NoAdd_Async()
|
||||
{
|
||||
var jpg = Path.Combine(ORIGINAL_FILES_PATH, "jpg", "test.jpg");
|
||||
using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"));
|
||||
using var archive = GZipArchive.Open(stream);
|
||||
Assert.Throws<InvalidFormatException>(() => archive.AddEntry("jpg\\test.jpg", jpg));
|
||||
await archive.SaveToAsync(Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GZip_Archive_Multiple_Reads_Async()
|
||||
{
|
||||
var inputStream = new MemoryStream();
|
||||
using (var fileStream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")))
|
||||
{
|
||||
await fileStream.CopyToAsync(inputStream);
|
||||
inputStream.Position = 0;
|
||||
}
|
||||
using var archive = GZipArchive.Open(inputStream);
|
||||
var archiveEntry = archive.Entries.First();
|
||||
|
||||
MemoryStream tarStream;
|
||||
using (var entryStream = archiveEntry.OpenEntryStream())
|
||||
{
|
||||
tarStream = new MemoryStream();
|
||||
await entryStream.CopyToAsync(tarStream);
|
||||
}
|
||||
var size = tarStream.Length;
|
||||
using (var entryStream = archiveEntry.OpenEntryStream())
|
||||
{
|
||||
tarStream = new MemoryStream();
|
||||
await entryStream.CopyToAsync(tarStream);
|
||||
}
|
||||
Assert.Equal(size, tarStream.Length);
|
||||
using (var entryStream = archiveEntry.OpenEntryStream())
|
||||
{
|
||||
var result = TarArchive.IsTarFile(entryStream);
|
||||
Assert.True(result);
|
||||
}
|
||||
Assert.Equal(size, tarStream.Length);
|
||||
using (var entryStream = archiveEntry.OpenEntryStream())
|
||||
{
|
||||
tarStream = new MemoryStream();
|
||||
await entryStream.CopyToAsync(tarStream);
|
||||
}
|
||||
Assert.Equal(size, tarStream.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestGzCrcWithMostSignificantBitNotNegative_Async()
|
||||
{
|
||||
using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"));
|
||||
using var archive = GZipArchive.Open(stream);
|
||||
foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
|
||||
{
|
||||
Assert.InRange(entry.Crc, 0L, 0xFFFFFFFFL);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestGzArchiveTypeGzip_Async()
|
||||
{
|
||||
using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"));
|
||||
using var archive = GZipArchive.Open(stream);
|
||||
Assert.Equal(archive.Type, ArchiveType.GZip);
|
||||
}
|
||||
}
|
||||
19
tests/SharpCompress.Test/GZip/GZipArchiveDirectoryTests.cs
Normal file
19
tests/SharpCompress.Test/GZip/GZipArchiveDirectoryTests.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using SharpCompress.Archives.GZip;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.GZip;
|
||||
|
||||
public class GZipArchiveDirectoryTests : TestBase
|
||||
{
|
||||
[Fact]
|
||||
public void GZipArchive_AddDirectoryEntry_ThrowsNotSupportedException()
|
||||
{
|
||||
using var archive = GZipArchive.Create();
|
||||
|
||||
Assert.Throws<NotSupportedException>(() =>
|
||||
archive.AddDirectoryEntry("test-dir", DateTime.Now)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using SharpCompress.Archives;
|
||||
@@ -124,4 +125,60 @@ public class GZipArchiveTests : ArchiveTests
|
||||
using var archive = GZipArchive.Open(stream);
|
||||
Assert.Equal(archive.Type, ArchiveType.GZip);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GZip_Archive_NonSeekableStream()
|
||||
{
|
||||
// Test that GZip extraction works with non-seekable streams (like HttpBaseStream)
|
||||
using var fileStream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"));
|
||||
var buffer = new MemoryStream();
|
||||
fileStream.CopyTo(buffer);
|
||||
buffer.Position = 0;
|
||||
|
||||
// Create a non-seekable wrapper around the MemoryStream
|
||||
using var nonSeekableStream = new NonSeekableStream(buffer);
|
||||
using var reader = SharpCompress.Readers.GZip.GZipReader.Open(nonSeekableStream);
|
||||
|
||||
// Verify we can move to the first entry and read it without exceptions
|
||||
Assert.True(reader.MoveToNextEntry());
|
||||
Assert.NotNull(reader.Entry);
|
||||
|
||||
// Extract and verify the entry can be read
|
||||
using var outputStream = new MemoryStream();
|
||||
reader.WriteEntryTo(outputStream);
|
||||
|
||||
Assert.True(outputStream.Length > 0);
|
||||
}
|
||||
|
||||
// Helper class to simulate a non-seekable stream like HttpBaseStream
|
||||
private class NonSeekableStream : Stream
|
||||
{
|
||||
private readonly Stream _baseStream;
|
||||
|
||||
public NonSeekableStream(Stream baseStream) => _baseStream = baseStream;
|
||||
|
||||
public override bool CanRead => _baseStream.CanRead;
|
||||
public override bool CanSeek => false; // Simulate non-seekable stream
|
||||
public override bool CanWrite => false;
|
||||
public override long Length => throw new NotSupportedException();
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get => throw new NotSupportedException();
|
||||
set => throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void Flush() => _baseStream.Flush();
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count) =>
|
||||
_baseStream.Read(buffer, offset, count);
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public override void SetLength(long value) => throw new NotSupportedException();
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
99
tests/SharpCompress.Test/GZip/GZipReaderAsyncTests.cs
Normal file
99
tests/SharpCompress.Test/GZip/GZipReaderAsyncTests.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Readers.GZip;
|
||||
using SharpCompress.Test.Mocks;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.GZip;
|
||||
|
||||
public class GZipReaderAsyncTests : ReaderTests
|
||||
{
|
||||
public GZipReaderAsyncTests() => UseExtensionInsteadOfNameToVerify = true;
|
||||
|
||||
[Fact]
|
||||
public async Task GZip_Reader_Generic_Async() =>
|
||||
await ReadAsync("Tar.tar.gz", CompressionType.GZip);
|
||||
|
||||
[Fact]
|
||||
public async Task GZip_Reader_Generic2_Async()
|
||||
{
|
||||
//read only as GZip item
|
||||
using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"));
|
||||
using var reader = GZipReader.Open(new SharpCompressStream(stream));
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
Assert.NotEqual(0, reader.Entry.Size);
|
||||
Assert.NotEqual(0, reader.Entry.Crc);
|
||||
|
||||
// Use async overload for reading the entry
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
using var entryStream = reader.OpenEntryStream();
|
||||
using var ms = new MemoryStream();
|
||||
await entryStream.CopyToAsync(ms);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task ReadAsync(
|
||||
string testArchive,
|
||||
CompressionType expectedCompression,
|
||||
ReaderOptions? options = null
|
||||
)
|
||||
{
|
||||
testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive);
|
||||
|
||||
options ??= new ReaderOptions() { BufferSize = 0x20000 };
|
||||
|
||||
options.LeaveStreamOpen = true;
|
||||
await ReadImplAsync(testArchive, expectedCompression, options);
|
||||
|
||||
options.LeaveStreamOpen = false;
|
||||
await ReadImplAsync(testArchive, expectedCompression, options);
|
||||
VerifyFiles();
|
||||
}
|
||||
|
||||
private async Task ReadImplAsync(
|
||||
string testArchive,
|
||||
CompressionType expectedCompression,
|
||||
ReaderOptions options
|
||||
)
|
||||
{
|
||||
using var file = File.OpenRead(testArchive);
|
||||
using var protectedStream = SharpCompressStream.Create(
|
||||
new ForwardOnlyStream(file, options.BufferSize),
|
||||
leaveOpen: true,
|
||||
throwOnDispose: true,
|
||||
bufferSize: options.BufferSize
|
||||
);
|
||||
using var testStream = new TestStream(protectedStream);
|
||||
using (var reader = ReaderFactory.Open(testStream, options))
|
||||
{
|
||||
await UseReaderAsync(reader, expectedCompression);
|
||||
protectedStream.ThrowOnDispose = false;
|
||||
Assert.False(testStream.IsDisposed, $"{nameof(testStream)} prematurely closed");
|
||||
}
|
||||
|
||||
var message =
|
||||
$"{nameof(options.LeaveStreamOpen)} is set to '{options.LeaveStreamOpen}', so {nameof(testStream.IsDisposed)} should be set to '{!testStream.IsDisposed}', but is set to {testStream.IsDisposed}";
|
||||
Assert.True(options.LeaveStreamOpen != testStream.IsDisposed, message);
|
||||
}
|
||||
|
||||
private async Task UseReaderAsync(IReader reader, CompressionType expectedCompression)
|
||||
{
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
Assert.Equal(expectedCompression, reader.Entry.CompressionType);
|
||||
await reader.WriteEntryToDirectoryAsync(
|
||||
SCRATCH_FILES_PATH,
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
83
tests/SharpCompress.Test/GZip/GZipWriterAsyncTests.cs
Normal file
83
tests/SharpCompress.Test/GZip/GZipWriterAsyncTests.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Writers;
|
||||
using SharpCompress.Writers.GZip;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.GZip;
|
||||
|
||||
public class GZipWriterAsyncTests : WriterTests
|
||||
{
|
||||
public GZipWriterAsyncTests()
|
||||
: base(ArchiveType.GZip) => UseExtensionInsteadOfNameToVerify = true;
|
||||
|
||||
[Fact]
|
||||
public async Task GZip_Writer_Generic_Async()
|
||||
{
|
||||
using (
|
||||
Stream stream = File.Open(
|
||||
Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"),
|
||||
FileMode.OpenOrCreate,
|
||||
FileAccess.Write
|
||||
)
|
||||
)
|
||||
using (var writer = WriterFactory.Open(stream, ArchiveType.GZip, CompressionType.GZip))
|
||||
{
|
||||
await writer.WriteAsync("Tar.tar", Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar"));
|
||||
}
|
||||
CompareArchivesByPath(
|
||||
Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"),
|
||||
Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GZip_Writer_Async()
|
||||
{
|
||||
using (
|
||||
Stream stream = File.Open(
|
||||
Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"),
|
||||
FileMode.OpenOrCreate,
|
||||
FileAccess.Write
|
||||
)
|
||||
)
|
||||
using (var writer = new GZipWriter(stream))
|
||||
{
|
||||
await writer.WriteAsync("Tar.tar", Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar"));
|
||||
}
|
||||
CompareArchivesByPath(
|
||||
Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"),
|
||||
Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GZip_Writer_Generic_Bad_Compression_Async() =>
|
||||
Assert.Throws<InvalidFormatException>(() =>
|
||||
{
|
||||
using Stream stream = File.OpenWrite(Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"));
|
||||
using var writer = WriterFactory.Open(stream, ArchiveType.GZip, CompressionType.BZip2);
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public async Task GZip_Writer_Entry_Path_With_Dir_Async()
|
||||
{
|
||||
using (
|
||||
Stream stream = File.Open(
|
||||
Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"),
|
||||
FileMode.OpenOrCreate,
|
||||
FileAccess.Write
|
||||
)
|
||||
)
|
||||
using (var writer = new GZipWriter(stream))
|
||||
{
|
||||
var path = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar");
|
||||
await writer.WriteAsync(path, path);
|
||||
}
|
||||
CompareArchivesByPath(
|
||||
Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"),
|
||||
Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")
|
||||
);
|
||||
}
|
||||
}
|
||||
19
tests/SharpCompress.Test/GZip/GZipWriterDirectoryTests.cs
Normal file
19
tests/SharpCompress.Test/GZip/GZipWriterDirectoryTests.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Writers.GZip;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.GZip;
|
||||
|
||||
public class GZipWriterDirectoryTests : TestBase
|
||||
{
|
||||
[Fact]
|
||||
public void GZipWriter_WriteDirectory_ThrowsNotSupportedException()
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
using var writer = new GZipWriter(memoryStream, new GZipWriterOptions());
|
||||
|
||||
Assert.Throws<NotSupportedException>(() => writer.WriteDirectory("test-dir", DateTime.Now));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Readers;
|
||||
|
||||
@@ -57,7 +59,7 @@ public class ForwardOnlyStream : SharpCompressStream, IStreamStack
|
||||
public override bool CanRead => true;
|
||||
|
||||
public override bool CanSeek => false;
|
||||
public override bool CanWrite => false;
|
||||
public override bool CanWrite => true;
|
||||
|
||||
public override void Flush() { }
|
||||
|
||||
@@ -72,10 +74,41 @@ public class ForwardOnlyStream : SharpCompressStream, IStreamStack
|
||||
public override int Read(byte[] buffer, int offset, int count) =>
|
||||
stream.Read(buffer, offset, count);
|
||||
|
||||
public override Task<int> ReadAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
) => stream.ReadAsync(buffer, offset, count, cancellationToken);
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override ValueTask<int> ReadAsync(
|
||||
Memory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
) => stream.ReadAsync(buffer, cancellationToken);
|
||||
#endif
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
|
||||
|
||||
public override void SetLength(long value) => throw new NotSupportedException();
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count) =>
|
||||
throw new NotSupportedException();
|
||||
stream.Write(buffer, offset, count);
|
||||
|
||||
public override Task WriteAsync(
|
||||
byte[] buffer,
|
||||
int offset,
|
||||
int count,
|
||||
CancellationToken cancellationToken
|
||||
) => stream.WriteAsync(buffer, offset, count, cancellationToken);
|
||||
|
||||
#if !NETFRAMEWORK && !NETSTANDARD2_0
|
||||
public override ValueTask WriteAsync(
|
||||
ReadOnlyMemory<byte> buffer,
|
||||
CancellationToken cancellationToken = default
|
||||
) => stream.WriteAsync(buffer, cancellationToken);
|
||||
#endif
|
||||
|
||||
public override Task FlushAsync(CancellationToken cancellationToken) =>
|
||||
stream.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Readers;
|
||||
@@ -72,6 +74,72 @@ public abstract class ReaderTests : TestBase
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task ReadAsync(
|
||||
string testArchive,
|
||||
CompressionType expectedCompression,
|
||||
ReaderOptions? options = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
testArchive = Path.Combine(TEST_ARCHIVES_PATH, testArchive);
|
||||
|
||||
options ??= new ReaderOptions() { BufferSize = 0x20000 };
|
||||
|
||||
options.LeaveStreamOpen = true;
|
||||
await ReadImplAsync(testArchive, expectedCompression, options, cancellationToken);
|
||||
|
||||
options.LeaveStreamOpen = false;
|
||||
await ReadImplAsync(testArchive, expectedCompression, options, cancellationToken);
|
||||
VerifyFiles();
|
||||
}
|
||||
|
||||
private async Task ReadImplAsync(
|
||||
string testArchive,
|
||||
CompressionType expectedCompression,
|
||||
ReaderOptions options,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
using var file = File.OpenRead(testArchive);
|
||||
using var protectedStream = SharpCompressStream.Create(
|
||||
new ForwardOnlyStream(file, options.BufferSize),
|
||||
leaveOpen: true,
|
||||
throwOnDispose: true,
|
||||
bufferSize: options.BufferSize
|
||||
);
|
||||
using var testStream = new TestStream(protectedStream);
|
||||
using (var reader = ReaderFactory.Open(testStream, options))
|
||||
{
|
||||
await UseReaderAsync(reader, expectedCompression, cancellationToken);
|
||||
protectedStream.ThrowOnDispose = false;
|
||||
Assert.False(testStream.IsDisposed, $"{nameof(testStream)} prematurely closed");
|
||||
}
|
||||
|
||||
var message =
|
||||
$"{nameof(options.LeaveStreamOpen)} is set to '{options.LeaveStreamOpen}', so {nameof(testStream.IsDisposed)} should be set to '{!testStream.IsDisposed}', but is set to {testStream.IsDisposed}";
|
||||
Assert.True(options.LeaveStreamOpen != testStream.IsDisposed, message);
|
||||
}
|
||||
|
||||
public async Task UseReaderAsync(
|
||||
IReader reader,
|
||||
CompressionType expectedCompression,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
while (await reader.MoveToNextEntryAsync(cancellationToken))
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
Assert.Equal(expectedCompression, reader.Entry.CompressionType);
|
||||
await reader.WriteEntryToDirectoryAsync(
|
||||
SCRATCH_FILES_PATH,
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true },
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void Iterate(
|
||||
string testArchive,
|
||||
string fileOrder,
|
||||
|
||||
@@ -122,6 +122,25 @@ public class SevenZipArchiveTests : ArchiveTests
|
||||
//"7Zip.BZip2.split.006",
|
||||
//"7Zip.BZip2.split.007"
|
||||
|
||||
[Fact]
|
||||
public void SevenZipArchive_Copy_StreamRead() => ArchiveStreamRead("7Zip.Copy.7z");
|
||||
|
||||
[Fact]
|
||||
public void SevenZipArchive_Copy_PathRead() => ArchiveFileRead("7Zip.Copy.7z");
|
||||
|
||||
[Fact]
|
||||
public void SevenZipArchive_Copy_CompressionType()
|
||||
{
|
||||
using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "7Zip.Copy.7z")))
|
||||
using (var archive = SevenZipArchive.Open(stream))
|
||||
{
|
||||
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
|
||||
{
|
||||
Assert.Equal(CompressionType.None, entry.CompressionType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SevenZipArchive_ZSTD_StreamRead() => ArchiveStreamRead("7Zip.ZSTD.7z");
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
<PropertyGroup Condition="$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))">
|
||||
<DefineConstants>$(DefineConstants);WINDOWS</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))">
|
||||
<DefineConstants>$(DefineConstants);LINUX</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\SharpCompress\SharpCompress.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
223
tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs
Normal file
223
tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs
Normal file
@@ -0,0 +1,223 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Archives.Tar;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Readers.Tar;
|
||||
using SharpCompress.Writers;
|
||||
using SharpCompress.Writers.Tar;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Tar;
|
||||
|
||||
public class TarArchiveAsyncTests : ArchiveTests
|
||||
{
|
||||
public TarArchiveAsyncTests() => UseExtensionInsteadOfNameToVerify = true;
|
||||
|
||||
[Fact]
|
||||
public async Task TarArchiveStreamRead_Async() => await ArchiveStreamReadAsync("Tar.tar");
|
||||
|
||||
[Fact]
|
||||
public async Task Tar_FileName_Exactly_100_Characters_Async()
|
||||
{
|
||||
var archive = "Tar_FileName_Exactly_100_Characters.tar";
|
||||
|
||||
// create the 100 char filename
|
||||
var filename =
|
||||
"filename_with_exactly_100_characters_______________________________________________________________X";
|
||||
|
||||
// Step 1: create a tar file containing a file with the test name
|
||||
using (Stream stream = File.OpenWrite(Path.Combine(SCRATCH2_FILES_PATH, archive)))
|
||||
using (var writer = WriterFactory.Open(stream, ArchiveType.Tar, CompressionType.None))
|
||||
using (Stream inputStream = new MemoryStream())
|
||||
{
|
||||
var sw = new StreamWriter(inputStream);
|
||||
await sw.WriteAsync("dummy filecontent");
|
||||
await sw.FlushAsync();
|
||||
|
||||
inputStream.Position = 0;
|
||||
await writer.WriteAsync(filename, inputStream, null);
|
||||
}
|
||||
|
||||
// Step 2: check if the written tar file can be read correctly
|
||||
var unmodified = Path.Combine(SCRATCH2_FILES_PATH, archive);
|
||||
using (var archive2 = TarArchive.Open(unmodified))
|
||||
{
|
||||
Assert.Equal(1, archive2.Entries.Count);
|
||||
Assert.Contains(filename, archive2.Entries.Select(entry => entry.Key));
|
||||
|
||||
foreach (var entry in archive2.Entries)
|
||||
{
|
||||
Assert.Equal(
|
||||
"dummy filecontent",
|
||||
await new StreamReader(entry.OpenEntryStream()).ReadLineAsync()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Tar_VeryLongFilepathReadback_Async()
|
||||
{
|
||||
var archive = "Tar_VeryLongFilepathReadback.tar";
|
||||
|
||||
// create a very long filename
|
||||
var longFilename = "";
|
||||
for (var i = 0; i < 600; i = longFilename.Length)
|
||||
{
|
||||
longFilename += i.ToString("D10") + "-";
|
||||
}
|
||||
|
||||
longFilename += ".txt";
|
||||
|
||||
// Step 1: create a tar file containing a file with a long name
|
||||
using (Stream stream = File.OpenWrite(Path.Combine(SCRATCH2_FILES_PATH, archive)))
|
||||
using (var writer = WriterFactory.Open(stream, ArchiveType.Tar, CompressionType.None))
|
||||
using (Stream inputStream = new MemoryStream())
|
||||
{
|
||||
var sw = new StreamWriter(inputStream);
|
||||
await sw.WriteAsync("dummy filecontent");
|
||||
await sw.FlushAsync();
|
||||
|
||||
inputStream.Position = 0;
|
||||
await writer.WriteAsync(longFilename, inputStream, null);
|
||||
}
|
||||
|
||||
// Step 2: check if the written tar file can be read correctly
|
||||
var unmodified = Path.Combine(SCRATCH2_FILES_PATH, archive);
|
||||
using (var archive2 = TarArchive.Open(unmodified))
|
||||
{
|
||||
Assert.Equal(1, archive2.Entries.Count);
|
||||
Assert.Contains(longFilename, archive2.Entries.Select(entry => entry.Key));
|
||||
|
||||
foreach (var entry in archive2.Entries)
|
||||
{
|
||||
Assert.Equal(
|
||||
"dummy filecontent",
|
||||
await new StreamReader(entry.OpenEntryStream()).ReadLineAsync()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Tar_Create_New_Async()
|
||||
{
|
||||
var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "Tar.tar");
|
||||
var unmodified = Path.Combine(TEST_ARCHIVES_PATH, "Tar.noEmptyDirs.tar");
|
||||
|
||||
using (var archive = TarArchive.Create())
|
||||
{
|
||||
archive.AddAllFromDirectory(ORIGINAL_FILES_PATH);
|
||||
var twopt = new TarWriterOptions(CompressionType.None, true);
|
||||
twopt.ArchiveEncoding = new ArchiveEncoding { Default = Encoding.GetEncoding(866) };
|
||||
await archive.SaveToAsync(scratchPath, twopt);
|
||||
}
|
||||
CompareArchivesByPath(unmodified, scratchPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Tar_Random_Write_Add_Async()
|
||||
{
|
||||
var jpg = Path.Combine(ORIGINAL_FILES_PATH, "jpg", "test.jpg");
|
||||
var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "Tar.mod.tar");
|
||||
var unmodified = Path.Combine(TEST_ARCHIVES_PATH, "Tar.mod.tar");
|
||||
var modified = Path.Combine(TEST_ARCHIVES_PATH, "Tar.noEmptyDirs.tar");
|
||||
|
||||
using (var archive = TarArchive.Open(unmodified))
|
||||
{
|
||||
archive.AddEntry("jpg\\test.jpg", jpg);
|
||||
await archive.SaveToAsync(scratchPath, new WriterOptions(CompressionType.None));
|
||||
}
|
||||
CompareArchivesByPath(modified, scratchPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Tar_Random_Write_Remove_Async()
|
||||
{
|
||||
var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "Tar.mod.tar");
|
||||
var modified = Path.Combine(TEST_ARCHIVES_PATH, "Tar.mod.tar");
|
||||
var unmodified = Path.Combine(TEST_ARCHIVES_PATH, "Tar.noEmptyDirs.tar");
|
||||
|
||||
using (var archive = TarArchive.Open(unmodified))
|
||||
{
|
||||
var entry = archive.Entries.Single(x =>
|
||||
x.Key.NotNull().EndsWith("jpg", StringComparison.OrdinalIgnoreCase)
|
||||
);
|
||||
archive.RemoveEntry(entry);
|
||||
await archive.SaveToAsync(scratchPath, new WriterOptions(CompressionType.None));
|
||||
}
|
||||
CompareArchivesByPath(modified, scratchPath);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(10)]
|
||||
[InlineData(128)]
|
||||
public async Task Tar_Japanese_Name_Async(int length)
|
||||
{
|
||||
using var mstm = new MemoryStream();
|
||||
var enc = new ArchiveEncoding { Default = Encoding.UTF8 };
|
||||
var twopt = new TarWriterOptions(CompressionType.None, true);
|
||||
twopt.ArchiveEncoding = enc;
|
||||
var fname = new string((char)0x3042, length);
|
||||
using (var tw = new TarWriter(mstm, twopt))
|
||||
using (var input = new MemoryStream(new byte[32]))
|
||||
{
|
||||
await tw.WriteAsync(fname, input, null);
|
||||
}
|
||||
using (var inputMemory = new MemoryStream(mstm.ToArray()))
|
||||
{
|
||||
var tropt = new ReaderOptions { ArchiveEncoding = enc };
|
||||
using (var tr = TarReader.Open(inputMemory, tropt))
|
||||
{
|
||||
while (tr.MoveToNextEntry())
|
||||
{
|
||||
Assert.Equal(fname, tr.Entry.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Tar_Read_One_At_A_Time_Async()
|
||||
{
|
||||
var archiveEncoding = new ArchiveEncoding { Default = Encoding.UTF8 };
|
||||
var tarWriterOptions = new TarWriterOptions(CompressionType.None, true)
|
||||
{
|
||||
ArchiveEncoding = archiveEncoding,
|
||||
};
|
||||
var testBytes = Encoding.UTF8.GetBytes("This is a test.");
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
using (var tarWriter = new TarWriter(memoryStream, tarWriterOptions))
|
||||
using (var testFileStream = new MemoryStream(testBytes))
|
||||
{
|
||||
await tarWriter.WriteAsync("test1.txt", testFileStream, null);
|
||||
testFileStream.Position = 0;
|
||||
await tarWriter.WriteAsync("test2.txt", testFileStream, null);
|
||||
}
|
||||
|
||||
memoryStream.Position = 0;
|
||||
|
||||
var numberOfEntries = 0;
|
||||
|
||||
using (var archiveFactory = TarArchive.Open(memoryStream))
|
||||
{
|
||||
foreach (var entry in archiveFactory.Entries)
|
||||
{
|
||||
++numberOfEntries;
|
||||
|
||||
using var tarEntryStream = entry.OpenEntryStream();
|
||||
using var testFileStream = new MemoryStream();
|
||||
await tarEntryStream.CopyToAsync(testFileStream);
|
||||
Assert.Equal(testBytes.Length, testFileStream.Length);
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Equal(2, numberOfEntries);
|
||||
}
|
||||
}
|
||||
112
tests/SharpCompress.Test/Tar/TarArchiveDirectoryTests.cs
Normal file
112
tests/SharpCompress.Test/Tar/TarArchiveDirectoryTests.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using SharpCompress.Archives.Tar;
|
||||
using SharpCompress.Common;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Tar;
|
||||
|
||||
public class TarArchiveDirectoryTests : TestBase
|
||||
{
|
||||
[Fact]
|
||||
public void TarArchive_AddDirectoryEntry_CreatesDirectoryEntry()
|
||||
{
|
||||
using var archive = TarArchive.Create();
|
||||
|
||||
archive.AddDirectoryEntry("test-dir", DateTime.Now);
|
||||
|
||||
var entries = archive.Entries.ToList();
|
||||
Assert.Single(entries);
|
||||
Assert.Equal("test-dir", entries[0].Key);
|
||||
Assert.True(entries[0].IsDirectory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TarArchive_AddDirectoryEntry_MultipleDirectories()
|
||||
{
|
||||
using var archive = TarArchive.Create();
|
||||
|
||||
archive.AddDirectoryEntry("dir1", DateTime.Now);
|
||||
archive.AddDirectoryEntry("dir2", DateTime.Now);
|
||||
archive.AddDirectoryEntry("dir1/subdir", DateTime.Now);
|
||||
|
||||
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
|
||||
Assert.Equal(3, entries.Count);
|
||||
Assert.True(entries.All(e => e.IsDirectory));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TarArchive_AddDirectoryEntry_MixedWithFiles()
|
||||
{
|
||||
using var archive = TarArchive.Create();
|
||||
|
||||
archive.AddDirectoryEntry("dir1", DateTime.Now);
|
||||
|
||||
using var contentStream = new MemoryStream(
|
||||
System.Text.Encoding.UTF8.GetBytes("test content")
|
||||
);
|
||||
archive.AddEntry("dir1/file.txt", contentStream, false, contentStream.Length, DateTime.Now);
|
||||
|
||||
archive.AddDirectoryEntry("dir2", DateTime.Now);
|
||||
|
||||
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
|
||||
Assert.Equal(3, entries.Count);
|
||||
Assert.True(entries[0].IsDirectory);
|
||||
Assert.False(entries[1].IsDirectory);
|
||||
Assert.True(entries[2].IsDirectory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TarArchive_AddDirectoryEntry_SaveAndReload()
|
||||
{
|
||||
var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "tar-directory-test.tar");
|
||||
|
||||
using (var archive = TarArchive.Create())
|
||||
{
|
||||
archive.AddDirectoryEntry("dir1", DateTime.Now);
|
||||
archive.AddDirectoryEntry("dir2", DateTime.Now);
|
||||
|
||||
using var contentStream = new MemoryStream(
|
||||
System.Text.Encoding.UTF8.GetBytes("test content")
|
||||
);
|
||||
archive.AddEntry(
|
||||
"dir1/file.txt",
|
||||
contentStream,
|
||||
false,
|
||||
contentStream.Length,
|
||||
DateTime.Now
|
||||
);
|
||||
|
||||
using (var fileStream = File.Create(scratchPath))
|
||||
{
|
||||
archive.SaveTo(fileStream, CompressionType.None);
|
||||
}
|
||||
}
|
||||
|
||||
using (var archive = TarArchive.Open(scratchPath))
|
||||
{
|
||||
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
|
||||
Assert.Equal(3, entries.Count);
|
||||
|
||||
Assert.Equal("dir1/", entries[0].Key);
|
||||
Assert.True(entries[0].IsDirectory);
|
||||
|
||||
Assert.Equal("dir1/file.txt", entries[1].Key);
|
||||
Assert.False(entries[1].IsDirectory);
|
||||
|
||||
Assert.Equal("dir2/", entries[2].Key);
|
||||
Assert.True(entries[2].IsDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TarArchive_AddDirectoryEntry_DuplicateKey_ThrowsException()
|
||||
{
|
||||
using var archive = TarArchive.Create();
|
||||
|
||||
archive.AddDirectoryEntry("test-dir", DateTime.Now);
|
||||
|
||||
Assert.Throws<ArchiveException>(() => archive.AddDirectoryEntry("test-dir", DateTime.Now));
|
||||
}
|
||||
}
|
||||
272
tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs
Normal file
272
tests/SharpCompress.Test/Tar/TarReaderAsyncTests.cs
Normal file
@@ -0,0 +1,272 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Readers.Tar;
|
||||
using SharpCompress.Test.Mocks;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Tar;
|
||||
|
||||
public class TarReaderAsyncTests : ReaderTests
|
||||
{
|
||||
public TarReaderAsyncTests() => UseExtensionInsteadOfNameToVerify = true;
|
||||
|
||||
[Fact]
|
||||
public async Task Tar_Reader_Async() => await ReadAsync("Tar.tar", CompressionType.None);
|
||||
|
||||
[Fact]
|
||||
public async Task Tar_Skip_Async()
|
||||
{
|
||||
using Stream stream = new ForwardOnlyStream(
|
||||
File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar"))
|
||||
);
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
var x = 0;
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
x++;
|
||||
if (x % 2 == 0)
|
||||
{
|
||||
await reader.WriteEntryToDirectoryAsync(
|
||||
SCRATCH_FILES_PATH,
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Tar_Z_Reader_Async() => await ReadAsync("Tar.tar.Z", CompressionType.Lzw);
|
||||
|
||||
[Fact]
|
||||
public async Task Tar_BZip2_Reader_Async() =>
|
||||
await ReadAsync("Tar.tar.bz2", CompressionType.BZip2);
|
||||
|
||||
[Fact]
|
||||
public async Task Tar_GZip_Reader_Async() =>
|
||||
await ReadAsync("Tar.tar.gz", CompressionType.GZip);
|
||||
|
||||
[Fact]
|
||||
public async Task Tar_ZStandard_Reader_Async() =>
|
||||
await ReadAsync("Tar.tar.zst", CompressionType.ZStandard);
|
||||
|
||||
[Fact]
|
||||
public async Task Tar_LZip_Reader_Async() =>
|
||||
await ReadAsync("Tar.tar.lz", CompressionType.LZip);
|
||||
|
||||
[Fact]
|
||||
public async Task Tar_Xz_Reader_Async() => await ReadAsync("Tar.tar.xz", CompressionType.Xz);
|
||||
|
||||
[Fact]
|
||||
public async Task Tar_GZip_OldGnu_Reader_Async() =>
|
||||
await ReadAsync("Tar.oldgnu.tar.gz", CompressionType.GZip);
|
||||
|
||||
[Fact]
|
||||
public async Task Tar_BZip2_Entry_Stream_Async()
|
||||
{
|
||||
using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.bz2")))
|
||||
using (var reader = TarReader.Open(stream))
|
||||
{
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
Assert.Equal(CompressionType.BZip2, reader.Entry.CompressionType);
|
||||
using var entryStream = reader.OpenEntryStream();
|
||||
var file = Path.GetFileName(reader.Entry.Key);
|
||||
var folder =
|
||||
Path.GetDirectoryName(reader.Entry.Key)
|
||||
?? throw new ArgumentNullException();
|
||||
var destdir = Path.Combine(SCRATCH_FILES_PATH, folder);
|
||||
if (!Directory.Exists(destdir))
|
||||
{
|
||||
Directory.CreateDirectory(destdir);
|
||||
}
|
||||
var destinationFileName = Path.Combine(destdir, file.NotNull());
|
||||
|
||||
using var fs = File.OpenWrite(destinationFileName);
|
||||
await entryStream.CopyToAsync(fs);
|
||||
}
|
||||
}
|
||||
}
|
||||
VerifyFiles();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tar_LongNamesWithLongNameExtension_Async()
|
||||
{
|
||||
var filePaths = new List<string>();
|
||||
|
||||
using (
|
||||
Stream stream = File.OpenRead(
|
||||
Path.Combine(TEST_ARCHIVES_PATH, "Tar.LongPathsWithLongNameExtension.tar")
|
||||
)
|
||||
)
|
||||
using (var reader = TarReader.Open(stream))
|
||||
{
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
filePaths.Add(reader.Entry.Key.NotNull("Entry Key is null"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Equal(3, filePaths.Count);
|
||||
Assert.Contains("a.txt", filePaths);
|
||||
Assert.Contains(
|
||||
"wp-content/plugins/gravityformsextend/lib/Aws/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/beta/Apc/ApcPrefixCollision/A/B/Bar.php",
|
||||
filePaths
|
||||
);
|
||||
Assert.Contains(
|
||||
"wp-content/plugins/gravityformsextend/lib/Aws/Symfony/Component/ClassLoader/Tests/Fixtures/Apc/beta/Apc/ApcPrefixCollision/A/B/Foo.php",
|
||||
filePaths
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tar_BZip2_Skip_Entry_Stream_Async()
|
||||
{
|
||||
using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.bz2"));
|
||||
using var reader = TarReader.Open(stream);
|
||||
var names = new List<string>();
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
Assert.Equal(CompressionType.BZip2, reader.Entry.CompressionType);
|
||||
using var entryStream = reader.OpenEntryStream();
|
||||
entryStream.SkipEntry();
|
||||
names.Add(reader.Entry.Key.NotNull());
|
||||
}
|
||||
}
|
||||
Assert.Equal(3, names.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tar_Containing_Rar_Reader_Async()
|
||||
{
|
||||
var archiveFullPath = Path.Combine(TEST_ARCHIVES_PATH, "Tar.ContainsRar.tar");
|
||||
using Stream stream = File.OpenRead(archiveFullPath);
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
Assert.True(reader.ArchiveType == ArchiveType.Tar);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tar_With_TarGz_With_Flushed_EntryStream_Async()
|
||||
{
|
||||
var archiveFullPath = Path.Combine(TEST_ARCHIVES_PATH, "Tar.ContainsTarGz.tar");
|
||||
using Stream stream = File.OpenRead(archiveFullPath);
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
Assert.True(reader.MoveToNextEntry());
|
||||
Assert.Equal("inner.tar.gz", reader.Entry.Key);
|
||||
|
||||
using var entryStream = reader.OpenEntryStream();
|
||||
using var flushingStream = new FlushOnDisposeStream(entryStream);
|
||||
|
||||
// Extract inner.tar.gz
|
||||
using var innerReader = ReaderFactory.Open(flushingStream);
|
||||
Assert.True(innerReader.MoveToNextEntry());
|
||||
Assert.Equal("test", innerReader.Entry.Key);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Tar_Broken_Stream_Async()
|
||||
{
|
||||
var archiveFullPath = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar");
|
||||
using Stream stream = File.OpenRead(archiveFullPath);
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
var memoryStream = new MemoryStream();
|
||||
|
||||
Assert.True(reader.MoveToNextEntry());
|
||||
Assert.True(reader.MoveToNextEntry());
|
||||
await reader.WriteEntryToAsync(memoryStream);
|
||||
stream.Close();
|
||||
Assert.Throws<IncompleteArchiveException>(() => reader.MoveToNextEntry());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Tar_Corrupted_Async()
|
||||
{
|
||||
var archiveFullPath = Path.Combine(TEST_ARCHIVES_PATH, "TarCorrupted.tar");
|
||||
using Stream stream = File.OpenRead(archiveFullPath);
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
var memoryStream = new MemoryStream();
|
||||
|
||||
Assert.True(reader.MoveToNextEntry());
|
||||
Assert.True(reader.MoveToNextEntry());
|
||||
await reader.WriteEntryToAsync(memoryStream);
|
||||
stream.Close();
|
||||
Assert.Throws<IncompleteArchiveException>(() => reader.MoveToNextEntry());
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK
|
||||
[Fact]
|
||||
public async Task Tar_GZip_With_Symlink_Entries_Async()
|
||||
{
|
||||
var isWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(
|
||||
System.Runtime.InteropServices.OSPlatform.Windows
|
||||
);
|
||||
using Stream stream = File.OpenRead(
|
||||
Path.Combine(TEST_ARCHIVES_PATH, "TarWithSymlink.tar.gz")
|
||||
);
|
||||
using var reader = TarReader.Open(stream);
|
||||
while (reader.MoveToNextEntry())
|
||||
{
|
||||
if (reader.Entry.IsDirectory)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
await reader.WriteEntryToDirectoryAsync(
|
||||
SCRATCH_FILES_PATH,
|
||||
new ExtractionOptions
|
||||
{
|
||||
ExtractFullPath = true,
|
||||
Overwrite = true,
|
||||
WriteSymbolicLink = (sourcePath, targetPath) =>
|
||||
{
|
||||
if (!isWindows)
|
||||
{
|
||||
var link = new Mono.Unix.UnixSymbolicLinkInfo(sourcePath);
|
||||
if (File.Exists(sourcePath))
|
||||
{
|
||||
link.Delete(); // equivalent to ln -s -f
|
||||
}
|
||||
link.CreateSymbolicLinkTo(targetPath);
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
if (!isWindows)
|
||||
{
|
||||
if (reader.Entry.LinkTarget != null)
|
||||
{
|
||||
var path = Path.Combine(SCRATCH_FILES_PATH, reader.Entry.Key.NotNull());
|
||||
var link = new Mono.Unix.UnixSymbolicLinkInfo(path);
|
||||
if (link.HasContents)
|
||||
{
|
||||
// need to convert the link to an absolute path for comparison
|
||||
var target = reader.Entry.LinkTarget;
|
||||
var realTarget = Path.GetFullPath(
|
||||
Path.Combine($"{Path.GetDirectoryName(path)}", target)
|
||||
);
|
||||
|
||||
Assert.Equal(realTarget, link.GetContents().ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.True(false, "Symlink has no target");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -201,13 +201,10 @@ public class TarReaderTests : ReaderTests
|
||||
Assert.Throws<IncompleteArchiveException>(() => reader.MoveToNextEntry());
|
||||
}
|
||||
|
||||
#if !NETFRAMEWORK
|
||||
#if LINUX
|
||||
[Fact]
|
||||
public void Tar_GZip_With_Symlink_Entries()
|
||||
{
|
||||
var isWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(
|
||||
System.Runtime.InteropServices.OSPlatform.Windows
|
||||
);
|
||||
using Stream stream = File.OpenRead(
|
||||
Path.Combine(TEST_ARCHIVES_PATH, "TarWithSymlink.tar.gz")
|
||||
);
|
||||
@@ -226,38 +223,32 @@ public class TarReaderTests : ReaderTests
|
||||
Overwrite = true,
|
||||
WriteSymbolicLink = (sourcePath, targetPath) =>
|
||||
{
|
||||
if (!isWindows)
|
||||
var link = new Mono.Unix.UnixSymbolicLinkInfo(sourcePath);
|
||||
if (File.Exists(sourcePath))
|
||||
{
|
||||
var link = new Mono.Unix.UnixSymbolicLinkInfo(sourcePath);
|
||||
if (File.Exists(sourcePath))
|
||||
{
|
||||
link.Delete(); // equivalent to ln -s -f
|
||||
}
|
||||
link.CreateSymbolicLinkTo(targetPath);
|
||||
link.Delete(); // equivalent to ln -s -f
|
||||
}
|
||||
link.CreateSymbolicLinkTo(targetPath);
|
||||
},
|
||||
}
|
||||
);
|
||||
if (!isWindows)
|
||||
if (reader.Entry.LinkTarget != null)
|
||||
{
|
||||
if (reader.Entry.LinkTarget != null)
|
||||
var path = Path.Combine(SCRATCH_FILES_PATH, reader.Entry.Key.NotNull());
|
||||
var link = new Mono.Unix.UnixSymbolicLinkInfo(path);
|
||||
if (link.HasContents)
|
||||
{
|
||||
var path = Path.Combine(SCRATCH_FILES_PATH, reader.Entry.Key.NotNull());
|
||||
var link = new Mono.Unix.UnixSymbolicLinkInfo(path);
|
||||
if (link.HasContents)
|
||||
{
|
||||
// need to convert the link to an absolute path for comparison
|
||||
var target = reader.Entry.LinkTarget;
|
||||
var realTarget = Path.GetFullPath(
|
||||
Path.Combine($"{Path.GetDirectoryName(path)}", target)
|
||||
);
|
||||
// need to convert the link to an absolute path for comparison
|
||||
var target = reader.Entry.LinkTarget;
|
||||
var realTarget = Path.GetFullPath(
|
||||
Path.Combine($"{Path.GetDirectoryName(path)}", target)
|
||||
);
|
||||
|
||||
Assert.Equal(realTarget, link.GetContents().ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.True(false, "Symlink has no target");
|
||||
}
|
||||
Assert.Equal(realTarget, link.GetContents().ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.True(false, "Symlink has no target");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
83
tests/SharpCompress.Test/Tar/TarWriterAsyncTests.cs
Normal file
83
tests/SharpCompress.Test/Tar/TarWriterAsyncTests.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Writers.Tar;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Tar;
|
||||
|
||||
public class TarWriterAsyncTests : WriterTests
|
||||
{
|
||||
static TarWriterAsyncTests()
|
||||
{
|
||||
#if !NETFRAMEWORK
|
||||
//fix issue where these tests could not be ran in isolation
|
||||
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
|
||||
#endif
|
||||
}
|
||||
|
||||
public TarWriterAsyncTests()
|
||||
: base(ArchiveType.Tar) => UseExtensionInsteadOfNameToVerify = true;
|
||||
|
||||
[Fact]
|
||||
public async Task Tar_Writer_Async() =>
|
||||
await WriteAsync(
|
||||
CompressionType.None,
|
||||
"Tar.noEmptyDirs.tar",
|
||||
"Tar.noEmptyDirs.tar",
|
||||
Encoding.GetEncoding(866)
|
||||
);
|
||||
|
||||
[Fact]
|
||||
public async Task Tar_BZip2_Writer_Async() =>
|
||||
await WriteAsync(
|
||||
CompressionType.BZip2,
|
||||
"Tar.noEmptyDirs.tar.bz2",
|
||||
"Tar.noEmptyDirs.tar.bz2",
|
||||
Encoding.GetEncoding(866)
|
||||
);
|
||||
|
||||
[Fact]
|
||||
public async Task Tar_LZip_Writer_Async() =>
|
||||
await WriteAsync(
|
||||
CompressionType.LZip,
|
||||
"Tar.noEmptyDirs.tar.lz",
|
||||
"Tar.noEmptyDirs.tar.lz",
|
||||
Encoding.GetEncoding(866)
|
||||
);
|
||||
|
||||
[Fact]
|
||||
public async Task Tar_Rar_Write_Async() =>
|
||||
await Assert.ThrowsAsync<InvalidFormatException>(async () =>
|
||||
await WriteAsync(
|
||||
CompressionType.Rar,
|
||||
"Zip.ppmd.noEmptyDirs.zip",
|
||||
"Zip.ppmd.noEmptyDirs.zip"
|
||||
)
|
||||
);
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public async Task Tar_Finalize_Archive_Async(bool finalizeArchive)
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
using Stream content = File.OpenRead(Path.Combine(ORIGINAL_FILES_PATH, "jpg", "test.jpg"));
|
||||
using (
|
||||
var writer = new TarWriter(
|
||||
stream,
|
||||
new TarWriterOptions(CompressionType.None, finalizeArchive)
|
||||
)
|
||||
)
|
||||
{
|
||||
await writer.WriteAsync("doesn't matter", content, null);
|
||||
}
|
||||
|
||||
var paddedContentWithHeader = (content.Length / 512 * 512) + 512 + 512;
|
||||
var expectedStreamLength = finalizeArchive
|
||||
? paddedContentWithHeader + (512 * 2)
|
||||
: paddedContentWithHeader;
|
||||
Assert.Equal(expectedStreamLength, stream.Length);
|
||||
}
|
||||
}
|
||||
164
tests/SharpCompress.Test/Tar/TarWriterDirectoryTests.cs
Normal file
164
tests/SharpCompress.Test/Tar/TarWriterDirectoryTests.cs
Normal file
@@ -0,0 +1,164 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using SharpCompress.Archives.Tar;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Writers.Tar;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Tar;
|
||||
|
||||
public class TarWriterDirectoryTests : TestBase
|
||||
{
|
||||
[Fact]
|
||||
public void TarWriter_WriteDirectory_CreatesDirectoryEntry()
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
using (
|
||||
var writer = new TarWriter(
|
||||
memoryStream,
|
||||
new TarWriterOptions(CompressionType.None, true)
|
||||
)
|
||||
)
|
||||
{
|
||||
writer.WriteDirectory("test-dir", DateTime.Now);
|
||||
}
|
||||
|
||||
memoryStream.Position = 0;
|
||||
using var archive = TarArchive.Open(memoryStream);
|
||||
var entries = archive.Entries.ToList();
|
||||
|
||||
Assert.Single(entries);
|
||||
Assert.Equal("test-dir/", entries[0].Key);
|
||||
Assert.True(entries[0].IsDirectory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TarWriter_WriteDirectory_WithTrailingSlash()
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
using (
|
||||
var writer = new TarWriter(
|
||||
memoryStream,
|
||||
new TarWriterOptions(CompressionType.None, true)
|
||||
)
|
||||
)
|
||||
{
|
||||
writer.WriteDirectory("test-dir/", DateTime.Now);
|
||||
}
|
||||
|
||||
memoryStream.Position = 0;
|
||||
using var archive = TarArchive.Open(memoryStream);
|
||||
var entries = archive.Entries.ToList();
|
||||
|
||||
Assert.Single(entries);
|
||||
Assert.Equal("test-dir/", entries[0].Key);
|
||||
Assert.True(entries[0].IsDirectory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TarWriter_WriteDirectory_WithBackslash()
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
using (
|
||||
var writer = new TarWriter(
|
||||
memoryStream,
|
||||
new TarWriterOptions(CompressionType.None, true)
|
||||
)
|
||||
)
|
||||
{
|
||||
writer.WriteDirectory("test-dir\\subdir", DateTime.Now);
|
||||
}
|
||||
|
||||
memoryStream.Position = 0;
|
||||
using var archive = TarArchive.Open(memoryStream);
|
||||
var entries = archive.Entries.ToList();
|
||||
|
||||
Assert.Single(entries);
|
||||
Assert.Equal("test-dir/subdir/", entries[0].Key);
|
||||
Assert.True(entries[0].IsDirectory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TarWriter_WriteDirectory_EmptyString_IsSkipped()
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
using (
|
||||
var writer = new TarWriter(
|
||||
memoryStream,
|
||||
new TarWriterOptions(CompressionType.None, true)
|
||||
)
|
||||
)
|
||||
{
|
||||
writer.WriteDirectory("", DateTime.Now);
|
||||
}
|
||||
|
||||
memoryStream.Position = 0;
|
||||
using var archive = TarArchive.Open(memoryStream);
|
||||
|
||||
Assert.Empty(archive.Entries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TarWriter_WriteDirectory_MultipleDirectories()
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
using (
|
||||
var writer = new TarWriter(
|
||||
memoryStream,
|
||||
new TarWriterOptions(CompressionType.None, true)
|
||||
)
|
||||
)
|
||||
{
|
||||
writer.WriteDirectory("dir1", DateTime.Now);
|
||||
writer.WriteDirectory("dir2", DateTime.Now);
|
||||
writer.WriteDirectory("dir1/subdir", DateTime.Now);
|
||||
}
|
||||
|
||||
memoryStream.Position = 0;
|
||||
using var archive = TarArchive.Open(memoryStream);
|
||||
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
|
||||
|
||||
Assert.Equal(3, entries.Count);
|
||||
Assert.Equal("dir1/", entries[0].Key);
|
||||
Assert.True(entries[0].IsDirectory);
|
||||
Assert.Equal("dir1/subdir/", entries[1].Key);
|
||||
Assert.True(entries[1].IsDirectory);
|
||||
Assert.Equal("dir2/", entries[2].Key);
|
||||
Assert.True(entries[2].IsDirectory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TarWriter_WriteDirectory_MixedWithFiles()
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
using (
|
||||
var writer = new TarWriter(
|
||||
memoryStream,
|
||||
new TarWriterOptions(CompressionType.None, true)
|
||||
)
|
||||
)
|
||||
{
|
||||
writer.WriteDirectory("dir1", DateTime.Now);
|
||||
|
||||
using var contentStream = new MemoryStream(
|
||||
System.Text.Encoding.UTF8.GetBytes("test content")
|
||||
);
|
||||
writer.Write("dir1/file.txt", contentStream, DateTime.Now);
|
||||
|
||||
writer.WriteDirectory("dir2", DateTime.Now);
|
||||
}
|
||||
|
||||
memoryStream.Position = 0;
|
||||
using var archive = TarArchive.Open(memoryStream);
|
||||
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
|
||||
|
||||
Assert.Equal(3, entries.Count);
|
||||
Assert.Equal("dir1/", entries[0].Key);
|
||||
Assert.True(entries[0].IsDirectory);
|
||||
Assert.Equal("dir1/file.txt", entries[1].Key);
|
||||
Assert.False(entries[1].IsDirectory);
|
||||
Assert.Equal("dir2/", entries[2].Key);
|
||||
Assert.True(entries[2].IsDirectory);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Readers;
|
||||
@@ -51,4 +53,49 @@ public class WriterTests : TestBase
|
||||
}
|
||||
VerifyFiles();
|
||||
}
|
||||
|
||||
protected async Task WriteAsync(
|
||||
CompressionType compressionType,
|
||||
string archive,
|
||||
string archiveToVerifyAgainst,
|
||||
Encoding? encoding = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
using (Stream stream = File.OpenWrite(Path.Combine(SCRATCH2_FILES_PATH, archive)))
|
||||
{
|
||||
var writerOptions = new WriterOptions(compressionType) { LeaveStreamOpen = true };
|
||||
|
||||
writerOptions.ArchiveEncoding.Default = encoding ?? Encoding.Default;
|
||||
|
||||
using var writer = WriterFactory.Open(stream, _type, writerOptions);
|
||||
await writer.WriteAllAsync(
|
||||
ORIGINAL_FILES_PATH,
|
||||
"*",
|
||||
SearchOption.AllDirectories,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
CompareArchivesByPath(
|
||||
Path.Combine(SCRATCH2_FILES_PATH, archive),
|
||||
Path.Combine(TEST_ARCHIVES_PATH, archiveToVerifyAgainst)
|
||||
);
|
||||
|
||||
using (Stream stream = File.OpenRead(Path.Combine(SCRATCH2_FILES_PATH, archive)))
|
||||
{
|
||||
var readerOptions = new ReaderOptions();
|
||||
|
||||
readerOptions.ArchiveEncoding.Default = encoding ?? Encoding.Default;
|
||||
|
||||
using var reader = ReaderFactory.Open(
|
||||
SharpCompressStream.Create(stream, leaveOpen: true),
|
||||
readerOptions
|
||||
);
|
||||
reader.WriteAllToDirectory(
|
||||
SCRATCH_FILES_PATH,
|
||||
new ExtractionOptions { ExtractFullPath = true }
|
||||
);
|
||||
}
|
||||
VerifyFiles();
|
||||
}
|
||||
}
|
||||
|
||||
242
tests/SharpCompress.Test/Zip/Zip64AsyncTests.cs
Normal file
242
tests/SharpCompress.Test/Zip/Zip64AsyncTests.cs
Normal file
@@ -0,0 +1,242 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Common.Zip;
|
||||
using SharpCompress.Compressors.Deflate;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Readers.Zip;
|
||||
using SharpCompress.Test.Mocks;
|
||||
using SharpCompress.Writers;
|
||||
using SharpCompress.Writers.Zip;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Zip;
|
||||
|
||||
public class Zip64AsyncTests : WriterTests
|
||||
{
|
||||
public Zip64AsyncTests()
|
||||
: base(ArchiveType.Zip) { }
|
||||
|
||||
// 4GiB + 1
|
||||
private const long FOUR_GB_LIMIT = ((long)uint.MaxValue) + 1;
|
||||
|
||||
//[Fact]
|
||||
[Trait("format", "zip64")]
|
||||
public async Task Zip64_Single_Large_File_Async() =>
|
||||
await RunSingleTestAsync(1, FOUR_GB_LIMIT, setZip64: true, forwardOnly: false);
|
||||
|
||||
//[Fact]
|
||||
[Trait("format", "zip64")]
|
||||
public async Task Zip64_Two_Large_Files_Async() =>
|
||||
await RunSingleTestAsync(2, FOUR_GB_LIMIT, setZip64: true, forwardOnly: false);
|
||||
|
||||
[Fact]
|
||||
[Trait("format", "zip64")]
|
||||
public async Task Zip64_Two_Small_files_Async() =>
|
||||
// Multiple files, does not require zip64
|
||||
await RunSingleTestAsync(2, FOUR_GB_LIMIT / 2, setZip64: false, forwardOnly: false);
|
||||
|
||||
[Fact]
|
||||
[Trait("format", "zip64")]
|
||||
public async Task Zip64_Two_Small_files_stream_Async() =>
|
||||
await RunSingleTestAsync(2, FOUR_GB_LIMIT / 2, setZip64: false, forwardOnly: true);
|
||||
|
||||
[Fact]
|
||||
[Trait("format", "zip64")]
|
||||
public async Task Zip64_Two_Small_Files_Zip64_Async() =>
|
||||
// Multiple files, use zip64 even though it is not required
|
||||
await RunSingleTestAsync(2, FOUR_GB_LIMIT / 2, setZip64: true, forwardOnly: false);
|
||||
|
||||
[Fact]
|
||||
[Trait("format", "zip64")]
|
||||
public async Task Zip64_Single_Large_File_Fail_Async()
|
||||
{
|
||||
try
|
||||
{
|
||||
// One single file, should fail
|
||||
await RunSingleTestAsync(1, FOUR_GB_LIMIT, setZip64: false, forwardOnly: false);
|
||||
throw new InvalidOperationException("Test did not fail?");
|
||||
}
|
||||
catch (NotSupportedException) { }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("zip64", "true")]
|
||||
public async Task Zip64_Single_Large_File_Zip64_Streaming_Fail_Async()
|
||||
{
|
||||
try
|
||||
{
|
||||
// One single file, should fail (fast) with zip64
|
||||
await RunSingleTestAsync(1, FOUR_GB_LIMIT, setZip64: true, forwardOnly: true);
|
||||
throw new InvalidOperationException("Test did not fail?");
|
||||
}
|
||||
catch (NotSupportedException) { }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("zip64", "true")]
|
||||
public async Task Zip64_Single_Large_File_Streaming_Fail_Async()
|
||||
{
|
||||
try
|
||||
{
|
||||
// One single file, should fail once the write discovers the problem
|
||||
await RunSingleTestAsync(1, FOUR_GB_LIMIT, setZip64: false, forwardOnly: true);
|
||||
throw new InvalidOperationException("Test did not fail?");
|
||||
}
|
||||
catch (NotSupportedException) { }
|
||||
}
|
||||
|
||||
public async Task RunSingleTestAsync(
|
||||
long files,
|
||||
long filesize,
|
||||
bool setZip64,
|
||||
bool forwardOnly,
|
||||
long writeChunkSize = 1024 * 1024,
|
||||
string filename = "zip64-test-async.zip"
|
||||
)
|
||||
{
|
||||
filename = Path.Combine(SCRATCH2_FILES_PATH, filename);
|
||||
|
||||
try
|
||||
{
|
||||
if (File.Exists(filename))
|
||||
{
|
||||
File.Delete(filename);
|
||||
}
|
||||
|
||||
if (!File.Exists(filename))
|
||||
{
|
||||
await CreateZipArchiveAsync(
|
||||
filename,
|
||||
files,
|
||||
filesize,
|
||||
writeChunkSize,
|
||||
setZip64,
|
||||
forwardOnly
|
||||
);
|
||||
}
|
||||
|
||||
var resForward = await ReadForwardOnlyAsync(filename);
|
||||
if (resForward.Item1 != files)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Incorrect number of items reported: {resForward.Item1}, should have been {files}"
|
||||
);
|
||||
}
|
||||
|
||||
if (resForward.Item2 != files * filesize)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Incorrect combined size reported: {resForward.Item2}, should have been {files * filesize}"
|
||||
);
|
||||
}
|
||||
|
||||
var resArchive = ReadArchive(filename);
|
||||
if (resArchive.Item1 != files)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Incorrect number of items reported: {resArchive.Item1}, should have been {files}"
|
||||
);
|
||||
}
|
||||
|
||||
if (resArchive.Item2 != files * filesize)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Incorrect number of items reported: {resArchive.Item2}, should have been {files * filesize}"
|
||||
);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(filename))
|
||||
{
|
||||
File.Delete(filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateZipArchiveAsync(
|
||||
string filename,
|
||||
long files,
|
||||
long filesize,
|
||||
long chunksize,
|
||||
bool setZip64,
|
||||
bool forwardOnly
|
||||
)
|
||||
{
|
||||
var data = new byte[chunksize];
|
||||
|
||||
// Use deflate for speed
|
||||
var opts = new ZipWriterOptions(CompressionType.Deflate) { UseZip64 = setZip64 };
|
||||
|
||||
// Use no compression to ensure we hit the limits (actually inflates a bit, but seems better than using method==Store)
|
||||
var eo = new ZipWriterEntryOptions { DeflateCompressionLevel = CompressionLevel.None };
|
||||
|
||||
using var zip = File.OpenWrite(filename);
|
||||
using var st = forwardOnly ? (Stream)new ForwardOnlyStream(zip) : zip;
|
||||
using var zipWriter = (ZipWriter)WriterFactory.Open(st, ArchiveType.Zip, opts);
|
||||
for (var i = 0; i < files; i++)
|
||||
{
|
||||
using var str = zipWriter.WriteToStream(i.ToString(), eo);
|
||||
var left = filesize;
|
||||
while (left > 0)
|
||||
{
|
||||
var b = (int)Math.Min(left, data.Length);
|
||||
// Use synchronous Write to match the sync version and avoid ForwardOnlyStream issues
|
||||
await str.WriteAsync(data, 0, b);
|
||||
left -= b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Tuple<long, long>> ReadForwardOnlyAsync(string filename)
|
||||
{
|
||||
long count = 0;
|
||||
long size = 0;
|
||||
ZipEntry? prev = null;
|
||||
using (var fs = File.OpenRead(filename))
|
||||
using (var rd = ZipReader.Open(fs, new ReaderOptions { LookForHeader = false }))
|
||||
{
|
||||
while (await rd.MoveToNextEntryAsync())
|
||||
{
|
||||
#if NETFRAMEWORK || NETSTANDARD2_0
|
||||
using (var entryStream = await rd.OpenEntryStreamAsync())
|
||||
{
|
||||
await entryStream.SkipEntryAsync();
|
||||
}
|
||||
#else
|
||||
await using (var entryStream = await rd.OpenEntryStreamAsync())
|
||||
{
|
||||
await entryStream.SkipEntryAsync();
|
||||
}
|
||||
#endif
|
||||
count++;
|
||||
if (prev != null)
|
||||
{
|
||||
size += prev.Size;
|
||||
}
|
||||
|
||||
prev = rd.Entry;
|
||||
}
|
||||
}
|
||||
|
||||
if (prev != null)
|
||||
{
|
||||
size += prev.Size;
|
||||
}
|
||||
|
||||
return new Tuple<long, long>(count, size);
|
||||
}
|
||||
|
||||
public Tuple<long, long> ReadArchive(string filename)
|
||||
{
|
||||
using var archive = ArchiveFactory.Open(filename);
|
||||
return new Tuple<long, long>(
|
||||
archive.Entries.Count(),
|
||||
archive.Entries.Select(x => x.Size).Sum()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -22,31 +22,37 @@ public class Zip64Tests : WriterTests
|
||||
// 4GiB + 1
|
||||
private const long FOUR_GB_LIMIT = ((long)uint.MaxValue) + 1;
|
||||
|
||||
//[Fact]
|
||||
[Trait("format", "zip64")]
|
||||
public void Zip64_Single_Large_File() =>
|
||||
// One single file, requires zip64
|
||||
RunSingleTest(1, FOUR_GB_LIMIT, setZip64: true, forwardOnly: false);
|
||||
|
||||
//[Fact]
|
||||
[Trait("format", "zip64")]
|
||||
public void Zip64_Two_Large_Files() =>
|
||||
// One single file, requires zip64
|
||||
RunSingleTest(2, FOUR_GB_LIMIT, setZip64: true, forwardOnly: false);
|
||||
|
||||
[Fact]
|
||||
[Trait("format", "zip64")]
|
||||
public void Zip64_Two_Small_files() =>
|
||||
// Multiple files, does not require zip64
|
||||
RunSingleTest(2, FOUR_GB_LIMIT / 2, setZip64: false, forwardOnly: false);
|
||||
|
||||
[Fact]
|
||||
[Trait("format", "zip64")]
|
||||
public void Zip64_Two_Small_files_stream() =>
|
||||
// Multiple files, does not require zip64, and works with streams
|
||||
RunSingleTest(2, FOUR_GB_LIMIT / 2, setZip64: false, forwardOnly: true);
|
||||
|
||||
[Fact]
|
||||
[Trait("format", "zip64")]
|
||||
public void Zip64_Two_Small_Files_Zip64() =>
|
||||
// Multiple files, use zip64 even though it is not required
|
||||
RunSingleTest(2, FOUR_GB_LIMIT / 2, setZip64: true, forwardOnly: false);
|
||||
|
||||
[Fact]
|
||||
[Trait("format", "zip64")]
|
||||
public void Zip64_Single_Large_File_Fail()
|
||||
{
|
||||
@@ -59,6 +65,7 @@ public class Zip64Tests : WriterTests
|
||||
catch (NotSupportedException) { }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("zip64", "true")]
|
||||
public void Zip64_Single_Large_File_Zip64_Streaming_Fail()
|
||||
{
|
||||
@@ -71,6 +78,7 @@ public class Zip64Tests : WriterTests
|
||||
catch (NotSupportedException) { }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("zip64", "true")]
|
||||
public void Zip64_Single_Large_File_Streaming_Fail()
|
||||
{
|
||||
|
||||
196
tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs
Normal file
196
tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs
Normal file
@@ -0,0 +1,196 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Archives.Zip;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Writers;
|
||||
using SharpCompress.Writers.Zip;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Zip;
|
||||
|
||||
public class ZipArchiveAsyncTests : ArchiveTests
|
||||
{
|
||||
public ZipArchiveAsyncTests() => UseExtensionInsteadOfNameToVerify = true;
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_ZipX_ArchiveStreamRead_Async() =>
|
||||
await ArchiveStreamReadAsync("Zip.zipx");
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_BZip2_Streamed_ArchiveStreamRead_Async() =>
|
||||
await ArchiveStreamReadAsync("Zip.bzip2.dd.zip");
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_BZip2_ArchiveStreamRead_Async() =>
|
||||
await ArchiveStreamReadAsync("Zip.bzip2.zip");
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Deflate_Streamed2_ArchiveStreamRead_Async() =>
|
||||
await ArchiveStreamReadAsync("Zip.deflate.dd-.zip");
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Deflate_Streamed_ArchiveStreamRead_Async() =>
|
||||
await ArchiveStreamReadAsync("Zip.deflate.dd.zip");
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Deflate_ArchiveStreamRead_Async() =>
|
||||
await ArchiveStreamReadAsync("Zip.deflate.zip");
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Deflate64_ArchiveStreamRead_Async() =>
|
||||
await ArchiveStreamReadAsync("Zip.deflate64.zip");
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_LZMA_Streamed_ArchiveStreamRead_Async() =>
|
||||
await ArchiveStreamReadAsync("Zip.lzma.dd.zip");
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_LZMA_ArchiveStreamRead_Async() =>
|
||||
await ArchiveStreamReadAsync("Zip.lzma.zip");
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_PPMd_Streamed_ArchiveStreamRead_Async() =>
|
||||
await ArchiveStreamReadAsync("Zip.ppmd.dd.zip");
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_PPMd_ArchiveStreamRead_Async() =>
|
||||
await ArchiveStreamReadAsync("Zip.ppmd.zip");
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_None_ArchiveStreamRead_Async() =>
|
||||
await ArchiveStreamReadAsync("Zip.none.zip");
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Zip64_ArchiveStreamRead_Async() =>
|
||||
await ArchiveStreamReadAsync("Zip.zip64.zip");
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Shrink_ArchiveStreamRead_Async()
|
||||
{
|
||||
UseExtensionInsteadOfNameToVerify = true;
|
||||
UseCaseInsensitiveToVerify = true;
|
||||
await ArchiveStreamReadAsync("Zip.shrink.zip");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Implode_ArchiveStreamRead_Async()
|
||||
{
|
||||
UseExtensionInsteadOfNameToVerify = true;
|
||||
UseCaseInsensitiveToVerify = true;
|
||||
await ArchiveStreamReadAsync("Zip.implode.zip");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Reduce1_ArchiveStreamRead_Async()
|
||||
{
|
||||
UseExtensionInsteadOfNameToVerify = true;
|
||||
UseCaseInsensitiveToVerify = true;
|
||||
await ArchiveStreamReadAsync("Zip.reduce1.zip");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Reduce2_ArchiveStreamRead_Async()
|
||||
{
|
||||
UseExtensionInsteadOfNameToVerify = true;
|
||||
UseCaseInsensitiveToVerify = true;
|
||||
await ArchiveStreamReadAsync("Zip.reduce2.zip");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Reduce3_ArchiveStreamRead_Async()
|
||||
{
|
||||
UseExtensionInsteadOfNameToVerify = true;
|
||||
UseCaseInsensitiveToVerify = true;
|
||||
await ArchiveStreamReadAsync("Zip.reduce3.zip");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Reduce4_ArchiveStreamRead_Async()
|
||||
{
|
||||
UseExtensionInsteadOfNameToVerify = true;
|
||||
UseCaseInsensitiveToVerify = true;
|
||||
await ArchiveStreamReadAsync("Zip.reduce4.zip");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Random_Write_Remove_Async()
|
||||
{
|
||||
var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "Zip.deflate.mod.zip");
|
||||
var unmodified = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.noEmptyDirs.zip");
|
||||
var modified = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.mod.zip");
|
||||
|
||||
using (var archive = ZipArchive.Open(unmodified))
|
||||
{
|
||||
var entry = archive.Entries.Single(x =>
|
||||
x.Key.NotNull().EndsWith("jpg", StringComparison.OrdinalIgnoreCase)
|
||||
);
|
||||
archive.RemoveEntry(entry);
|
||||
|
||||
WriterOptions writerOptions = new ZipWriterOptions(CompressionType.Deflate);
|
||||
writerOptions.ArchiveEncoding.Default = Encoding.GetEncoding(866);
|
||||
|
||||
await archive.SaveToAsync(scratchPath, writerOptions);
|
||||
}
|
||||
CompareArchivesByPath(modified, scratchPath, Encoding.GetEncoding(866));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Random_Write_Add_Async()
|
||||
{
|
||||
var jpg = Path.Combine(ORIGINAL_FILES_PATH, "jpg", "test.jpg");
|
||||
var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "Zip.deflate.mod.zip");
|
||||
var unmodified = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.mod.zip");
|
||||
var modified = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.noEmptyDirs.zip");
|
||||
|
||||
using (var archive = ZipArchive.Open(unmodified))
|
||||
{
|
||||
archive.AddEntry("jpg\\test.jpg", jpg);
|
||||
|
||||
WriterOptions writerOptions = new ZipWriterOptions(CompressionType.Deflate);
|
||||
writerOptions.ArchiveEncoding.Default = Encoding.GetEncoding(866);
|
||||
|
||||
await archive.SaveToAsync(scratchPath, writerOptions);
|
||||
}
|
||||
CompareArchivesByPath(modified, scratchPath, Encoding.GetEncoding(866));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Create_New_Async()
|
||||
{
|
||||
var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "Zip.deflate.noEmptyDirs.zip");
|
||||
var unmodified = Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.noEmptyDirs.zip");
|
||||
|
||||
using (var archive = ZipArchive.Create())
|
||||
{
|
||||
archive.DeflateCompressionLevel = Compressors.Deflate.CompressionLevel.BestSpeed;
|
||||
archive.AddAllFromDirectory(ORIGINAL_FILES_PATH);
|
||||
|
||||
WriterOptions writerOptions = new ZipWriterOptions(CompressionType.Deflate);
|
||||
writerOptions.ArchiveEncoding.Default = Encoding.UTF8;
|
||||
|
||||
await archive.SaveToAsync(scratchPath, writerOptions);
|
||||
}
|
||||
CompareArchivesByPath(unmodified, scratchPath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Deflate_Entry_Stream_Async()
|
||||
{
|
||||
using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip")))
|
||||
using (var archive = ZipArchive.Open(stream))
|
||||
{
|
||||
foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
|
||||
{
|
||||
await entry.WriteToDirectoryAsync(
|
||||
SCRATCH_FILES_PATH,
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
|
||||
);
|
||||
}
|
||||
}
|
||||
VerifyFiles();
|
||||
}
|
||||
}
|
||||
112
tests/SharpCompress.Test/Zip/ZipArchiveDirectoryTests.cs
Normal file
112
tests/SharpCompress.Test/Zip/ZipArchiveDirectoryTests.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using SharpCompress.Archives.Zip;
|
||||
using SharpCompress.Common;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Zip;
|
||||
|
||||
public class ZipArchiveDirectoryTests : TestBase
|
||||
{
|
||||
[Fact]
|
||||
public void ZipArchive_AddDirectoryEntry_CreatesDirectoryEntry()
|
||||
{
|
||||
using var archive = ZipArchive.Create();
|
||||
|
||||
archive.AddDirectoryEntry("test-dir", DateTime.Now);
|
||||
|
||||
var entries = archive.Entries.ToList();
|
||||
Assert.Single(entries);
|
||||
Assert.Equal("test-dir", entries[0].Key);
|
||||
Assert.True(entries[0].IsDirectory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZipArchive_AddDirectoryEntry_MultipleDirectories()
|
||||
{
|
||||
using var archive = ZipArchive.Create();
|
||||
|
||||
archive.AddDirectoryEntry("dir1", DateTime.Now);
|
||||
archive.AddDirectoryEntry("dir2", DateTime.Now);
|
||||
archive.AddDirectoryEntry("dir1/subdir", DateTime.Now);
|
||||
|
||||
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
|
||||
Assert.Equal(3, entries.Count);
|
||||
Assert.True(entries.All(e => e.IsDirectory));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZipArchive_AddDirectoryEntry_MixedWithFiles()
|
||||
{
|
||||
using var archive = ZipArchive.Create();
|
||||
|
||||
archive.AddDirectoryEntry("dir1", DateTime.Now);
|
||||
|
||||
using var contentStream = new MemoryStream(
|
||||
System.Text.Encoding.UTF8.GetBytes("test content")
|
||||
);
|
||||
archive.AddEntry("dir1/file.txt", contentStream, false, contentStream.Length, DateTime.Now);
|
||||
|
||||
archive.AddDirectoryEntry("dir2", DateTime.Now);
|
||||
|
||||
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
|
||||
Assert.Equal(3, entries.Count);
|
||||
Assert.True(entries[0].IsDirectory);
|
||||
Assert.False(entries[1].IsDirectory);
|
||||
Assert.True(entries[2].IsDirectory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZipArchive_AddDirectoryEntry_SaveAndReload()
|
||||
{
|
||||
var scratchPath = Path.Combine(SCRATCH_FILES_PATH, "zip-directory-test.zip");
|
||||
|
||||
using (var archive = ZipArchive.Create())
|
||||
{
|
||||
archive.AddDirectoryEntry("dir1", DateTime.Now);
|
||||
archive.AddDirectoryEntry("dir2", DateTime.Now);
|
||||
|
||||
using var contentStream = new MemoryStream(
|
||||
System.Text.Encoding.UTF8.GetBytes("test content")
|
||||
);
|
||||
archive.AddEntry(
|
||||
"dir1/file.txt",
|
||||
contentStream,
|
||||
false,
|
||||
contentStream.Length,
|
||||
DateTime.Now
|
||||
);
|
||||
|
||||
using (var fileStream = File.Create(scratchPath))
|
||||
{
|
||||
archive.SaveTo(fileStream, CompressionType.Deflate);
|
||||
}
|
||||
}
|
||||
|
||||
using (var archive = ZipArchive.Open(scratchPath))
|
||||
{
|
||||
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
|
||||
Assert.Equal(3, entries.Count);
|
||||
|
||||
Assert.Equal("dir1/", entries[0].Key);
|
||||
Assert.True(entries[0].IsDirectory);
|
||||
|
||||
Assert.Equal("dir1/file.txt", entries[1].Key);
|
||||
Assert.False(entries[1].IsDirectory);
|
||||
|
||||
Assert.Equal("dir2/", entries[2].Key);
|
||||
Assert.True(entries[2].IsDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZipArchive_AddDirectoryEntry_DuplicateKey_ThrowsException()
|
||||
{
|
||||
using var archive = ZipArchive.Create();
|
||||
|
||||
archive.AddDirectoryEntry("test-dir", DateTime.Now);
|
||||
|
||||
Assert.Throws<ArchiveException>(() => archive.AddDirectoryEntry("test-dir", DateTime.Now));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Archives.Zip;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Compressors.Deflate;
|
||||
using SharpCompress.Compressors.Xz;
|
||||
using SharpCompress.Crypto;
|
||||
using SharpCompress.Writers;
|
||||
using SharpCompress.Writers.Zip;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Zip;
|
||||
|
||||
public class ZipTypesLevelsWithCrcRatioAsyncTests : ArchiveTests
|
||||
{
|
||||
public ZipTypesLevelsWithCrcRatioAsyncTests() => UseExtensionInsteadOfNameToVerify = true;
|
||||
|
||||
[Theory]
|
||||
[InlineData(CompressionType.Deflate, 1, 1, 0.11f)] // was 0.8f, actual 0.104
|
||||
[InlineData(CompressionType.Deflate, 3, 1, 0.08f)] // was 0.8f, actual 0.078
|
||||
[InlineData(CompressionType.Deflate, 6, 1, 0.05f)] // was 0.8f, actual ~0.042
|
||||
[InlineData(CompressionType.Deflate, 9, 1, 0.04f)] // was 0.7f, actual 0.038
|
||||
[InlineData(CompressionType.ZStandard, 1, 1, 0.025f)] // was 0.8f, actual 0.023
|
||||
[InlineData(CompressionType.ZStandard, 3, 1, 0.015f)] // was 0.7f, actual 0.013
|
||||
[InlineData(CompressionType.ZStandard, 9, 1, 0.006f)] // was 0.7f, actual 0.005
|
||||
[InlineData(CompressionType.ZStandard, 22, 1, 0.005f)] // was 0.7f, actual 0.004
|
||||
[InlineData(CompressionType.BZip2, 0, 1, 0.035f)] // was 0.8f, actual 0.033
|
||||
[InlineData(CompressionType.LZMA, 0, 1, 0.005f)] // was 0.8f, actual 0.004
|
||||
[InlineData(CompressionType.None, 0, 1, 1.001f)] // was 1.1f, actual 1.000
|
||||
[InlineData(CompressionType.Deflate, 6, 2, 0.045f)] // was 0.8f, actual 0.042
|
||||
[InlineData(CompressionType.ZStandard, 3, 2, 0.012f)] // was 0.7f, actual 0.010
|
||||
[InlineData(CompressionType.BZip2, 0, 2, 0.035f)] // was 0.8f, actual 0.032
|
||||
[InlineData(CompressionType.Deflate, 9, 3, 0.04f)] // was 0.7f, actual 0.038
|
||||
[InlineData(CompressionType.ZStandard, 9, 3, 0.003f)] // was 0.7f, actual 0.002
|
||||
public async Task Zip_Create_Archive_With_3_Files_Crc32_Test_Async(
|
||||
CompressionType compressionType,
|
||||
int compressionLevel,
|
||||
int sizeMb,
|
||||
float expectedRatio
|
||||
)
|
||||
{
|
||||
const int OneMiB = 1024 * 1024;
|
||||
var baseSize = sizeMb * OneMiB;
|
||||
|
||||
// Generate test content for files with sizes based on the sizeMb parameter
|
||||
var file1Data = TestPseudoTextStream.Create(baseSize);
|
||||
var file2Data = TestPseudoTextStream.Create(baseSize * 2);
|
||||
var file3Data = TestPseudoTextStream.Create(baseSize * 3);
|
||||
|
||||
var expectedFiles = new Dictionary<string, (byte[] data, uint crc)>
|
||||
{
|
||||
[$"file1_{sizeMb}MiB.txt"] = (file1Data, CalculateCrc32(file1Data)),
|
||||
[$"data/file2_{sizeMb * 2}MiB.txt"] = (file2Data, CalculateCrc32(file2Data)),
|
||||
[$"deep/nested/file3_{sizeMb * 3}MiB.txt"] = (file3Data, CalculateCrc32(file3Data)),
|
||||
};
|
||||
|
||||
// Create zip archive in memory
|
||||
using var zipStream = new MemoryStream();
|
||||
using (var writer = CreateWriterWithLevel(zipStream, compressionType, compressionLevel))
|
||||
{
|
||||
await writer.WriteAsync($"file1_{sizeMb}MiB.txt", new MemoryStream(file1Data));
|
||||
await writer.WriteAsync($"data/file2_{sizeMb * 2}MiB.txt", new MemoryStream(file2Data));
|
||||
await writer.WriteAsync(
|
||||
$"deep/nested/file3_{sizeMb * 3}MiB.txt",
|
||||
new MemoryStream(file3Data)
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate and output actual compression ratio
|
||||
var originalSize = file1Data.Length + file2Data.Length + file3Data.Length;
|
||||
var actualRatio = (double)zipStream.Length / originalSize;
|
||||
//Debug.WriteLine($"Zip_Create_Archive_With_3_Files_Crc32_Test_Async: {compressionType} Level={compressionLevel} Size={sizeMb}MB Expected={expectedRatio:F3} Actual={actualRatio:F3}");
|
||||
|
||||
// Verify compression occurred (except for None compression type)
|
||||
if (compressionType != CompressionType.None)
|
||||
{
|
||||
Assert.True(
|
||||
zipStream.Length < originalSize,
|
||||
$"Compression failed: compressed={zipStream.Length}, original={originalSize}"
|
||||
);
|
||||
}
|
||||
|
||||
// Verify compression ratio
|
||||
VerifyCompressionRatio(
|
||||
originalSize,
|
||||
zipStream.Length,
|
||||
expectedRatio,
|
||||
$"{compressionType} level {compressionLevel}"
|
||||
);
|
||||
|
||||
// Verify archive content and CRC32
|
||||
await VerifyArchiveContentAsync(zipStream, expectedFiles);
|
||||
|
||||
// Verify compression type is correctly set
|
||||
VerifyCompressionType(zipStream, compressionType);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(CompressionType.Deflate, 1, 4, 0.11f)] // was 0.8, actual 0.105
|
||||
[InlineData(CompressionType.Deflate, 3, 4, 0.08f)] // was 0.8, actual 0.077
|
||||
[InlineData(CompressionType.Deflate, 6, 4, 0.045f)] // was 0.8, actual 0.042
|
||||
[InlineData(CompressionType.Deflate, 9, 4, 0.04f)] // was 0.8, actual 0.037
|
||||
[InlineData(CompressionType.ZStandard, 1, 4, 0.025f)] // was 0.8, actual 0.022
|
||||
[InlineData(CompressionType.ZStandard, 3, 4, 0.012f)] // was 0.8, actual 0.010
|
||||
[InlineData(CompressionType.ZStandard, 9, 4, 0.003f)] // was 0.8, actual 0.002
|
||||
[InlineData(CompressionType.ZStandard, 22, 4, 0.003f)] // was 0.8, actual 0.002
|
||||
[InlineData(CompressionType.BZip2, 0, 4, 0.035f)] // was 0.8, actual 0.032
|
||||
[InlineData(CompressionType.LZMA, 0, 4, 0.003f)] // was 0.8, actual 0.002
|
||||
public async Task Zip_WriterFactory_Crc32_Test_Async(
|
||||
CompressionType compressionType,
|
||||
int compressionLevel,
|
||||
int sizeMb,
|
||||
float expectedRatio
|
||||
)
|
||||
{
|
||||
var fileSize = sizeMb * 1024 * 1024;
|
||||
|
||||
var testData = TestPseudoTextStream.Create(fileSize);
|
||||
var expectedCrc = CalculateCrc32(testData);
|
||||
|
||||
// Create archive with specified compression level
|
||||
using var zipStream = new MemoryStream();
|
||||
var writerOptions = new ZipWriterOptions(compressionType)
|
||||
{
|
||||
CompressionLevel = compressionLevel,
|
||||
};
|
||||
|
||||
using (var writer = WriterFactory.Open(zipStream, ArchiveType.Zip, writerOptions))
|
||||
{
|
||||
await writer.WriteAsync(
|
||||
$"{compressionType}_level_{compressionLevel}_{sizeMb}MiB.txt",
|
||||
new MemoryStream(testData)
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate and output actual compression ratio
|
||||
var actualRatio = (double)zipStream.Length / testData.Length;
|
||||
//Debug.WriteLine($"Zip_WriterFactory_Crc32_Test_Async: {compressionType} Level={compressionLevel} Size={sizeMb}MB Expected={expectedRatio:F3} Actual={actualRatio:F3}");
|
||||
|
||||
VerifyCompressionRatio(
|
||||
testData.Length,
|
||||
zipStream.Length,
|
||||
expectedRatio,
|
||||
$"{compressionType} level {compressionLevel}"
|
||||
);
|
||||
|
||||
// Verify the archive
|
||||
zipStream.Position = 0;
|
||||
using var archive = ZipArchive.Open(zipStream);
|
||||
|
||||
var entry = archive.Entries.Single(e => !e.IsDirectory);
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
using var extractedStream = new MemoryStream();
|
||||
await entryStream.CopyToAsync(extractedStream);
|
||||
|
||||
var extractedData = extractedStream.ToArray();
|
||||
var actualCrc = CalculateCrc32(extractedData);
|
||||
|
||||
Assert.Equal(compressionType, entry.CompressionType);
|
||||
Assert.Equal(expectedCrc, actualCrc);
|
||||
Assert.Equal(testData.Length, extractedData.Length);
|
||||
Assert.Equal(testData, extractedData);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(CompressionType.Deflate, 1, 2, 0.11f)] // was 0.8, actual 0.104
|
||||
[InlineData(CompressionType.Deflate, 3, 2, 0.08f)] // was 0.8, actual 0.077
|
||||
[InlineData(CompressionType.Deflate, 6, 2, 0.045f)] // was 0.8, actual 0.042
|
||||
[InlineData(CompressionType.Deflate, 9, 2, 0.04f)] // was 0.7, actual 0.038
|
||||
[InlineData(CompressionType.ZStandard, 1, 2, 0.025f)] // was 0.8, actual 0.023
|
||||
[InlineData(CompressionType.ZStandard, 3, 2, 0.015f)] // was 0.7, actual 0.012
|
||||
[InlineData(CompressionType.ZStandard, 9, 2, 0.006f)] // was 0.7, actual 0.005
|
||||
[InlineData(CompressionType.ZStandard, 22, 2, 0.005f)] // was 0.7, actual 0.004
|
||||
[InlineData(CompressionType.BZip2, 0, 2, 0.035f)] // was 0.8, actual 0.032
|
||||
[InlineData(CompressionType.LZMA, 0, 2, 0.005f)] // was 0.8, actual 0.004
|
||||
public async Task Zip_ZipArchiveOpen_Crc32_Test_Async(
|
||||
CompressionType compressionType,
|
||||
int compressionLevel,
|
||||
int sizeMb,
|
||||
float expectedRatio
|
||||
)
|
||||
{
|
||||
var fileSize = sizeMb * 1024 * 1024;
|
||||
|
||||
var testData = TestPseudoTextStream.Create(fileSize);
|
||||
var expectedCrc = CalculateCrc32(testData);
|
||||
|
||||
// Create archive with specified compression and level
|
||||
using var zipStream = new MemoryStream();
|
||||
using (var writer = CreateWriterWithLevel(zipStream, compressionType, compressionLevel))
|
||||
{
|
||||
await writer.WriteAsync(
|
||||
$"{compressionType}_{compressionLevel}_{sizeMb}MiB.txt",
|
||||
new MemoryStream(testData)
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate and output actual compression ratio
|
||||
var actualRatio = (double)zipStream.Length / testData.Length;
|
||||
//Debug.WriteLine($"Zip_ZipArchiveOpen_Crc32_Test_Async: {compressionType} Level={compressionLevel} Size={sizeMb}MB Expected={expectedRatio:F3} Actual={actualRatio:F3}");
|
||||
|
||||
// Verify the archive
|
||||
zipStream.Position = 0;
|
||||
using var archive = ZipArchive.Open(zipStream);
|
||||
|
||||
var entry = archive.Entries.Single(e => !e.IsDirectory);
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
using var extractedStream = new MemoryStream();
|
||||
await entryStream.CopyToAsync(extractedStream);
|
||||
|
||||
var extractedData = extractedStream.ToArray();
|
||||
var actualCrc = CalculateCrc32(extractedData);
|
||||
|
||||
Assert.Equal(compressionType, entry.CompressionType);
|
||||
Assert.Equal(expectedCrc, actualCrc);
|
||||
Assert.Equal(testData.Length, extractedData.Length);
|
||||
|
||||
// For smaller files, verify full content; for larger, spot check
|
||||
if (testData.Length <= sizeMb * 2)
|
||||
{
|
||||
Assert.Equal(testData, extractedData);
|
||||
}
|
||||
else
|
||||
{
|
||||
VerifyDataSpotCheck(testData, extractedData);
|
||||
}
|
||||
|
||||
VerifyCompressionRatio(
|
||||
testData.Length,
|
||||
zipStream.Length,
|
||||
expectedRatio,
|
||||
$"{compressionType} Level {compressionLevel}"
|
||||
);
|
||||
}
|
||||
|
||||
// Helper method for async archive content verification
|
||||
private async Task VerifyArchiveContentAsync(
|
||||
MemoryStream zipStream,
|
||||
Dictionary<string, (byte[] data, uint crc)> expectedFiles
|
||||
)
|
||||
{
|
||||
zipStream.Position = 0;
|
||||
using var archive = ZipArchive.Open(zipStream);
|
||||
|
||||
foreach (var entry in archive.Entries.Where(e => !e.IsDirectory))
|
||||
{
|
||||
Assert.True(
|
||||
expectedFiles.ContainsKey(entry.Key!),
|
||||
$"Unexpected file in archive: {entry.Key}"
|
||||
);
|
||||
|
||||
var expected = expectedFiles[entry.Key!];
|
||||
using var entryStream = entry.OpenEntryStream();
|
||||
using var extractedStream = new MemoryStream();
|
||||
await entryStream.CopyToAsync(extractedStream);
|
||||
|
||||
var extractedData = extractedStream.ToArray();
|
||||
var actualCrc = CalculateCrc32(extractedData);
|
||||
|
||||
Assert.Equal(expected.crc, actualCrc);
|
||||
Assert.Equal(expected.data.Length, extractedData.Length);
|
||||
|
||||
// For larger files, just spot check, for smaller verify full content
|
||||
var expectedData = expected.data;
|
||||
if (expectedData.Length <= 2 * 1024 * 1024)
|
||||
{
|
||||
Assert.Equal(expectedData, extractedData);
|
||||
}
|
||||
else
|
||||
{
|
||||
VerifyDataSpotCheck(expectedData, extractedData);
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Equal(expectedFiles.Count, archive.Entries.Count(e => !e.IsDirectory));
|
||||
}
|
||||
}
|
||||
254
tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs
Normal file
254
tests/SharpCompress.Test/Zip/ZipReaderAsyncTests.cs
Normal file
@@ -0,0 +1,254 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Readers;
|
||||
using SharpCompress.Readers.Zip;
|
||||
using SharpCompress.Test.Mocks;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Zip;
|
||||
|
||||
public class ZipReaderAsyncTests : ReaderTests
|
||||
{
|
||||
public ZipReaderAsyncTests() => UseExtensionInsteadOfNameToVerify = true;
|
||||
|
||||
[Fact]
|
||||
public async Task Issue_269_Double_Skip_Async()
|
||||
{
|
||||
var path = Path.Combine(TEST_ARCHIVES_PATH, "PrePostHeaders.zip");
|
||||
using Stream stream = new ForwardOnlyStream(File.OpenRead(path));
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
var count = 0;
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
count++;
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
if (count % 2 != 0)
|
||||
{
|
||||
await reader.WriteEntryToAsync(Stream.Null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Zip64_Streamed_Read_Async() =>
|
||||
await ReadAsync("Zip.zip64.zip", CompressionType.Deflate);
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_ZipX_Streamed_Read_Async() =>
|
||||
await ReadAsync("Zip.zipx", CompressionType.LZMA);
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_BZip2_Streamed_Read_Async() =>
|
||||
await ReadAsync("Zip.bzip2.dd.zip", CompressionType.BZip2);
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_BZip2_Read_Async() =>
|
||||
await ReadAsync("Zip.bzip2.zip", CompressionType.BZip2);
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Deflate_Streamed2_Read_Async() =>
|
||||
await ReadAsync("Zip.deflate.dd-.zip", CompressionType.Deflate);
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Deflate_Streamed_Read_Async() =>
|
||||
await ReadAsync("Zip.deflate.dd.zip", CompressionType.Deflate);
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Deflate_Streamed_Skip_Async()
|
||||
{
|
||||
using Stream stream = new ForwardOnlyStream(
|
||||
File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip"))
|
||||
);
|
||||
using var reader = ReaderFactory.Open(stream);
|
||||
var x = 0;
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
x++;
|
||||
if (x % 2 == 0)
|
||||
{
|
||||
await reader.WriteEntryToDirectoryAsync(
|
||||
SCRATCH_FILES_PATH,
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Deflate_Read_Async() =>
|
||||
await ReadAsync("Zip.deflate.zip", CompressionType.Deflate);
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Deflate64_Read_Async() =>
|
||||
await ReadAsync("Zip.deflate64.zip", CompressionType.Deflate64);
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_LZMA_Streamed_Read_Async() =>
|
||||
await ReadAsync("Zip.lzma.dd.zip", CompressionType.LZMA);
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_LZMA_Read_Async() =>
|
||||
await ReadAsync("Zip.lzma.zip", CompressionType.LZMA);
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_PPMd_Streamed_Read_Async() =>
|
||||
await ReadAsync("Zip.ppmd.dd.zip", CompressionType.PPMd);
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_PPMd_Read_Async() =>
|
||||
await ReadAsync("Zip.ppmd.zip", CompressionType.PPMd);
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_None_Read_Async() =>
|
||||
await ReadAsync("Zip.none.zip", CompressionType.None);
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Deflate_NoEmptyDirs_Read_Async() =>
|
||||
await ReadAsync("Zip.deflate.noEmptyDirs.zip", CompressionType.Deflate);
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_BZip2_PkwareEncryption_Read_Async()
|
||||
{
|
||||
using (
|
||||
Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.bzip2.pkware.zip"))
|
||||
)
|
||||
using (var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" }))
|
||||
{
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
Assert.Equal(CompressionType.BZip2, reader.Entry.CompressionType);
|
||||
await reader.WriteEntryToDirectoryAsync(
|
||||
SCRATCH_FILES_PATH,
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
VerifyFiles();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Reader_Disposal_Test_Async()
|
||||
{
|
||||
using var stream = new TestStream(
|
||||
File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip"))
|
||||
);
|
||||
using (var reader = ReaderFactory.Open(stream))
|
||||
{
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
await reader.WriteEntryToDirectoryAsync(
|
||||
SCRATCH_FILES_PATH,
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Assert.True(stream.IsDisposed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Reader_Disposal_Test2_Async()
|
||||
{
|
||||
using var stream = new TestStream(
|
||||
File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.dd.zip"))
|
||||
);
|
||||
var reader = ReaderFactory.Open(stream);
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
await reader.WriteEntryToDirectoryAsync(
|
||||
SCRATCH_FILES_PATH,
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
|
||||
);
|
||||
}
|
||||
}
|
||||
Assert.False(stream.IsDisposed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_LZMA_WinzipAES_Read_Async() =>
|
||||
await Assert.ThrowsAsync<NotSupportedException>(async () =>
|
||||
{
|
||||
using (
|
||||
Stream stream = File.OpenRead(
|
||||
Path.Combine(TEST_ARCHIVES_PATH, "Zip.lzma.WinzipAES.zip")
|
||||
)
|
||||
)
|
||||
using (var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" }))
|
||||
{
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
Assert.Equal(CompressionType.Unknown, reader.Entry.CompressionType);
|
||||
await reader.WriteEntryToDirectoryAsync(
|
||||
SCRATCH_FILES_PATH,
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
VerifyFiles();
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Deflate_WinzipAES_Read_Async()
|
||||
{
|
||||
using (
|
||||
Stream stream = File.OpenRead(
|
||||
Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.WinzipAES.zip")
|
||||
)
|
||||
)
|
||||
using (var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" }))
|
||||
{
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
Assert.Equal(CompressionType.Unknown, reader.Entry.CompressionType);
|
||||
await reader.WriteEntryToDirectoryAsync(
|
||||
SCRATCH_FILES_PATH,
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
VerifyFiles();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Deflate_ZipCrypto_Read_Async()
|
||||
{
|
||||
var count = 0;
|
||||
using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "zipcrypto.zip")))
|
||||
using (var reader = ZipReader.Open(stream, new ReaderOptions { Password = "test" }))
|
||||
{
|
||||
while (await reader.MoveToNextEntryAsync())
|
||||
{
|
||||
if (!reader.Entry.IsDirectory)
|
||||
{
|
||||
Assert.Equal(CompressionType.None, reader.Entry.CompressionType);
|
||||
await reader.WriteEntryToDirectoryAsync(
|
||||
SCRATCH_FILES_PATH,
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
|
||||
);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
Assert.Equal(8, count);
|
||||
}
|
||||
}
|
||||
67
tests/SharpCompress.Test/Zip/ZipWriterAsyncTests.cs
Normal file
67
tests/SharpCompress.Test/Zip/ZipWriterAsyncTests.cs
Normal file
@@ -0,0 +1,67 @@
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Zip;
|
||||
|
||||
public class ZipWriterAsyncTests : WriterTests
|
||||
{
|
||||
public ZipWriterAsyncTests()
|
||||
: base(ArchiveType.Zip) { }
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Deflate_Write_Async() =>
|
||||
await WriteAsync(
|
||||
CompressionType.Deflate,
|
||||
"Zip.deflate.noEmptyDirs.zip",
|
||||
"Zip.deflate.noEmptyDirs.zip",
|
||||
Encoding.UTF8
|
||||
);
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_BZip2_Write_Async() =>
|
||||
await WriteAsync(
|
||||
CompressionType.BZip2,
|
||||
"Zip.bzip2.noEmptyDirs.zip",
|
||||
"Zip.bzip2.noEmptyDirs.zip",
|
||||
Encoding.UTF8
|
||||
);
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_None_Write_Async() =>
|
||||
await WriteAsync(
|
||||
CompressionType.None,
|
||||
"Zip.none.noEmptyDirs.zip",
|
||||
"Zip.none.noEmptyDirs.zip",
|
||||
Encoding.UTF8
|
||||
);
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_LZMA_Write_Async() =>
|
||||
await WriteAsync(
|
||||
CompressionType.LZMA,
|
||||
"Zip.lzma.noEmptyDirs.zip",
|
||||
"Zip.lzma.noEmptyDirs.zip",
|
||||
Encoding.UTF8
|
||||
);
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_PPMd_Write_Async() =>
|
||||
await WriteAsync(
|
||||
CompressionType.PPMd,
|
||||
"Zip.ppmd.noEmptyDirs.zip",
|
||||
"Zip.ppmd.noEmptyDirs.zip",
|
||||
Encoding.UTF8
|
||||
);
|
||||
|
||||
[Fact]
|
||||
public async Task Zip_Rar_Write_Async() =>
|
||||
await Assert.ThrowsAsync<InvalidFormatException>(async () =>
|
||||
await WriteAsync(
|
||||
CompressionType.Rar,
|
||||
"Zip.ppmd.noEmptyDirs.zip",
|
||||
"Zip.ppmd.noEmptyDirs.zip"
|
||||
)
|
||||
);
|
||||
}
|
||||
146
tests/SharpCompress.Test/Zip/ZipWriterDirectoryTests.cs
Normal file
146
tests/SharpCompress.Test/Zip/ZipWriterDirectoryTests.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using SharpCompress.Archives.Zip;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.Writers.Zip;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpCompress.Test.Zip;
|
||||
|
||||
public class ZipWriterDirectoryTests : TestBase
|
||||
{
|
||||
[Fact]
|
||||
public void ZipWriter_WriteDirectory_CreatesDirectoryEntry()
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
using (
|
||||
var writer = new ZipWriter(memoryStream, new ZipWriterOptions(CompressionType.Deflate))
|
||||
)
|
||||
{
|
||||
writer.WriteDirectory("test-dir", DateTime.Now);
|
||||
}
|
||||
|
||||
memoryStream.Position = 0;
|
||||
using var archive = ZipArchive.Open(memoryStream);
|
||||
var entries = archive.Entries.ToList();
|
||||
|
||||
Assert.Single(entries);
|
||||
Assert.Equal("test-dir/", entries[0].Key);
|
||||
Assert.True(entries[0].IsDirectory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZipWriter_WriteDirectory_WithTrailingSlash()
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
using (
|
||||
var writer = new ZipWriter(memoryStream, new ZipWriterOptions(CompressionType.Deflate))
|
||||
)
|
||||
{
|
||||
writer.WriteDirectory("test-dir/", DateTime.Now);
|
||||
}
|
||||
|
||||
memoryStream.Position = 0;
|
||||
using var archive = ZipArchive.Open(memoryStream);
|
||||
var entries = archive.Entries.ToList();
|
||||
|
||||
Assert.Single(entries);
|
||||
Assert.Equal("test-dir/", entries[0].Key);
|
||||
Assert.True(entries[0].IsDirectory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZipWriter_WriteDirectory_WithBackslash()
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
using (
|
||||
var writer = new ZipWriter(memoryStream, new ZipWriterOptions(CompressionType.Deflate))
|
||||
)
|
||||
{
|
||||
writer.WriteDirectory("test-dir\\subdir", DateTime.Now);
|
||||
}
|
||||
|
||||
memoryStream.Position = 0;
|
||||
using var archive = ZipArchive.Open(memoryStream);
|
||||
var entries = archive.Entries.ToList();
|
||||
|
||||
Assert.Single(entries);
|
||||
Assert.Equal("test-dir/subdir/", entries[0].Key);
|
||||
Assert.True(entries[0].IsDirectory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZipWriter_WriteDirectory_EmptyString_IsSkipped()
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
using (
|
||||
var writer = new ZipWriter(memoryStream, new ZipWriterOptions(CompressionType.Deflate))
|
||||
)
|
||||
{
|
||||
writer.WriteDirectory("", DateTime.Now);
|
||||
}
|
||||
|
||||
memoryStream.Position = 0;
|
||||
using var archive = ZipArchive.Open(memoryStream);
|
||||
|
||||
Assert.Empty(archive.Entries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZipWriter_WriteDirectory_MultipleDirectories()
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
using (
|
||||
var writer = new ZipWriter(memoryStream, new ZipWriterOptions(CompressionType.Deflate))
|
||||
)
|
||||
{
|
||||
writer.WriteDirectory("dir1", DateTime.Now);
|
||||
writer.WriteDirectory("dir2", DateTime.Now);
|
||||
writer.WriteDirectory("dir1/subdir", DateTime.Now);
|
||||
}
|
||||
|
||||
memoryStream.Position = 0;
|
||||
using var archive = ZipArchive.Open(memoryStream);
|
||||
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
|
||||
|
||||
Assert.Equal(3, entries.Count);
|
||||
Assert.Equal("dir1/", entries[0].Key);
|
||||
Assert.True(entries[0].IsDirectory);
|
||||
Assert.Equal("dir1/subdir/", entries[1].Key);
|
||||
Assert.True(entries[1].IsDirectory);
|
||||
Assert.Equal("dir2/", entries[2].Key);
|
||||
Assert.True(entries[2].IsDirectory);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZipWriter_WriteDirectory_MixedWithFiles()
|
||||
{
|
||||
using var memoryStream = new MemoryStream();
|
||||
using (
|
||||
var writer = new ZipWriter(memoryStream, new ZipWriterOptions(CompressionType.Deflate))
|
||||
)
|
||||
{
|
||||
writer.WriteDirectory("dir1", DateTime.Now);
|
||||
|
||||
using var contentStream = new MemoryStream(
|
||||
System.Text.Encoding.UTF8.GetBytes("test content")
|
||||
);
|
||||
writer.Write("dir1/file.txt", contentStream, DateTime.Now);
|
||||
|
||||
writer.WriteDirectory("dir2", DateTime.Now);
|
||||
}
|
||||
|
||||
memoryStream.Position = 0;
|
||||
using var archive = ZipArchive.Open(memoryStream);
|
||||
var entries = archive.Entries.OrderBy(e => e.Key).ToList();
|
||||
|
||||
Assert.Equal(3, entries.Count);
|
||||
Assert.Equal("dir1/", entries[0].Key);
|
||||
Assert.True(entries[0].IsDirectory);
|
||||
Assert.Equal("dir1/file.txt", entries[1].Key);
|
||||
Assert.False(entries[1].IsDirectory);
|
||||
Assert.Equal("dir2/", entries[2].Key);
|
||||
Assert.True(entries[2].IsDirectory);
|
||||
}
|
||||
}
|
||||
BIN
tests/TestArchives/Archives/7Zip.Copy.7z
Normal file
BIN
tests/TestArchives/Archives/7Zip.Copy.7z
Normal file
Binary file not shown.
Reference in New Issue
Block a user