Integrate async/await support from PR #976 as baseline

Co-authored-by: adamhathcock <527620+adamhathcock@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-10-27 09:00:38 +00:00
parent 1b2ba921bb
commit 43aa2bad22
21 changed files with 1135 additions and 48 deletions

View file

@ -1,45 +1,64 @@
# Agent usage and commands
---
description: 'Guidelines for building C# applications'
applyTo: '**/*.cs'
---
This document explains how maintainers and contributors can instruct the GitHub Copilot coding agent in this repository.
# C# Development
Supported instruction channels
- PR front-matter (YAML at top of PR body) — preferred for reproducibility.
- PR comment using slash-style commands (e.g. `/copilot run apply-fixes`).
- Add a label that triggers a run (e.g. `copilot: run`).
## C# Instructions
- Always use the latest version C#, currently C# 13 features.
- Write clear and concise comments for each function.
Example PR front-matter (place at the top of the PR body):
## 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.
```yaml
copilot:
run: "apply-fixes"
target_branch: "master"
auto_merge: false
run_tests: true
required_approvals: 1
```
## Naming Conventions
Example slash command via PR comment:
- `/copilot run apply-fixes --target=master --run-tests`
- Follow PascalCase for component names, method names, and public members.
- Use camelCase for private fields and local variables.
- Prefix interface names with "I" (e.g., IUserService).
Recommended labels
- `copilot: run` -> instructs agent to run its default task on the PR
- `copilot: approve` -> if allowed by policy, agent may merge once checks pass
## Code Formatting
How to enable and grant permissions
1. Merge `.github/agents/copilot-agent.yml` into master.
2. As a repository administrator, install/authorize the GitHub Copilot coding agent app and grant it repository permissions that match the manifest (Contents: write, Pull requests: write, Checks: write, Actions: write/read, Issues: write).
3. Ensure Actions is enabled for the repository and branch protection rules are compatible with the manifest (or allow the agent to have the bypass when appropriate).
- 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.
Safety & governance
- Keep allow paths narrow — only grant the agent write access where it needs it.
- Prefer `require_review_before_merge: true` during initial rollout.
- Use audit logs to review agent activity and require a human reviewer until you trust the automation.
## Project Setup and Structure
PR details
- Branch name: copilot-agent-config-and-docs
- Changes: add/modify .github/agents/copilot-agent.yml and add AGENTS.md at repo root
- This PR is intentionally limited to configuration and documentation; it does not add any workflows that push changes or perform merges.
- 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.
If the repository settings or installed apps block the agent from running, include a clear note in the PR description describing actions an admin must take: enable Actions, install Copilot coding agent app, grant repo write permissions to agent, or run onboarding steps.
## Nullable Reference Types
Author: GitHub Copilot (@copilot) acting on behalf of adamhathcock.
- Declare variables non-nullable, and check for `null` at entry points.
- 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.
## Testing
- Always include test cases for critical paths of the application.
- Guide users through creating unit tests.
- 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.

View file

@ -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 -
[![SharpCompress](https://github.com/adamhathcock/sharpcompress/actions/workflows/dotnetcore.yml/badge.svg)](https://github.com/adamhathcock/sharpcompress/actions/workflows/dotnetcore.yml)
[![Static Badge](https://img.shields.io/badge/API%20Docs-DNDocs-190088?logo=readme&logoColor=white)](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!

143
USAGE.md
View file

@ -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

View file

@ -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;
@ -20,6 +22,12 @@ public class GZipArchiveEntry : GZipEntry, IArchiveEntry
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; }

View file

@ -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>

View file

@ -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

View file

@ -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;

View file

@ -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,9 @@ 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; }

View file

@ -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,9 @@ 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; }

View file

@ -1,5 +1,7 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace SharpCompress.Common;
@ -116,4 +118,115 @@ 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, StringComparison.Ordinal))
{
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,
StringComparison.Ordinal
)
)
{
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);
}
}
}

View file

@ -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
}

View file

@ -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,106 @@ 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

View file

@ -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;
@ -171,6 +173,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 +207,15 @@ 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;
using Stream s = OpenEntryStream();
await s
.TransferToAsync(writeStream, Entry, streamListener, cancellationToken)
.ConfigureAwait(false);
}
public EntryStream OpenEntryStream()
{
if (_wroteCurrentEntry)

View file

@ -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();

View file

@ -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);
}
}
}

View file

@ -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;
@ -217,6 +219,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 +313,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)

View file

@ -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,19 @@ 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);
}
protected virtual void Dispose(bool isDisposing)
{
if (isDisposing)

View file

@ -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,10 @@ 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
);
}

View file

@ -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,71 @@ public static class IWriterExtensions
writer.Write(file.Substring(directory.Length), file);
}
}
// 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);
}
}
}

View file

@ -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.20, )",
"resolved": "8.0.20",
"contentHash": "Rhcto2AjGvTO62+/VTmBpumBOmqIGp7nYEbTbmEXkCq4yPGxV8whju3/HsIA/bKyo2+DggaYk5+/8sxb1AbPTw=="
},
"Microsoft.SourceLink.GitHub": {
"type": "Direct",

View file

@ -0,0 +1,133 @@
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.IO;
using SharpCompress.Readers;
using SharpCompress.Writers;
using Xunit;
namespace SharpCompress.Test;
public class AsyncTests : TestBase
{
[Fact]
public async Task Reader_Async_Extract_All()
{
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 }
);
// 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");
using var stream = File.OpenRead(testArchive);
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)!);
using var outputStream = File.Create(outputPath);
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))
{
using var entryStream = await entry.OpenEntryStreamAsync();
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);
}
}