more async streams

This commit is contained in:
Adam Hathcock
2026-02-02 12:39:36 +00:00
parent 9b8e3d8530
commit e6ad44def8
6 changed files with 288 additions and 29 deletions

View File

@@ -1,7 +0,0 @@
enabled: true
agent:
name: copilot-coding-agent
allow:
- paths: ["src/**/*", "tests/**/*", "README.md", "AGENTS.md"]
actions: ["create", "modify"]
require_review_before_merge: true

View File

@@ -1,17 +0,0 @@
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.

View File

@@ -0,0 +1,132 @@
using System;
using System.Buffers;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace SharpCompress.Common.Zip;
internal partial class PkwareTraditionalCryptoStream
{
public override async Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
if (_mode == CryptoMode.Encrypt)
{
throw new NotSupportedException("This stream does not encrypt via Read()");
}
if (buffer is null)
{
throw new ArgumentNullException(nameof(buffer));
}
var temp = new byte[count];
var readBytes = await _stream
.ReadAsync(temp, 0, count, cancellationToken)
.ConfigureAwait(false);
var decrypted = _encryptor.Decrypt(temp, readBytes);
Buffer.BlockCopy(decrypted, 0, buffer, offset, readBytes);
return readBytes;
}
#if !LEGACY_DOTNET
public override async ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
)
{
if (_mode == CryptoMode.Encrypt)
{
throw new NotSupportedException("This stream does not encrypt via Read()");
}
byte[] temp = ArrayPool<byte>.Shared.Rent(buffer.Length);
try
{
int readBytes = await _stream
.ReadAsync(temp.AsMemory(0, buffer.Length), cancellationToken)
.ConfigureAwait(false);
var decrypted = _encryptor.Decrypt(temp, readBytes);
decrypted.AsMemory(0, readBytes).CopyTo(buffer);
return readBytes;
}
finally
{
ArrayPool<byte>.Shared.Return(temp);
}
}
#endif
public override async Task WriteAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
if (_mode == CryptoMode.Decrypt)
{
throw new NotSupportedException("This stream does not Decrypt via Write()");
}
if (count == 0)
{
return;
}
byte[] plaintext;
if (offset != 0)
{
plaintext = new byte[count];
Buffer.BlockCopy(buffer, offset, plaintext, 0, count);
}
else
{
plaintext = buffer;
}
var encrypted = _encryptor.Encrypt(plaintext, count);
await _stream
.WriteAsync(encrypted, 0, encrypted.Length, cancellationToken)
.ConfigureAwait(false);
}
#if !LEGACY_DOTNET
public override async ValueTask WriteAsync(
ReadOnlyMemory<byte> buffer,
CancellationToken cancellationToken = default
)
{
if (_mode == CryptoMode.Decrypt)
{
throw new NotSupportedException("This stream does not Decrypt via Write()");
}
if (buffer.Length == 0)
{
return;
}
byte[] plaintext;
if (buffer.Span.Overlaps(buffer.Span))
{
plaintext = buffer.ToArray();
}
else
{
plaintext = new byte[buffer.Length];
buffer.CopyTo(plaintext);
}
var encrypted = _encryptor.Encrypt(plaintext, buffer.Length);
await _stream
.WriteAsync(encrypted.AsMemory(0, encrypted.Length), cancellationToken)
.ConfigureAwait(false);
}
#endif
}

View File

@@ -9,7 +9,7 @@ internal enum CryptoMode
Decrypt,
}
internal class PkwareTraditionalCryptoStream : Stream
internal partial class PkwareTraditionalCryptoStream : Stream
{
private readonly PkwareTraditionalEncryptionData _encryptor;
private readonly CryptoMode _mode;

View File

@@ -0,0 +1,142 @@
using System;
using System.Buffers;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace SharpCompress.Common.Zip;
internal partial class WinzipAesCryptoStream
{
#if !LEGACY_DOTNET
public override async ValueTask DisposeAsync()
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
#if DEBUG_STREAMS
this.DebugDispose(typeof(WinzipAesCryptoStream));
#endif
// Read out last 10 auth bytes asynchronously
byte[] authBytes = ArrayPool<byte>.Shared.Rent(10);
try
{
await _stream.ReadFullyAsync(authBytes, 0, 10).ConfigureAwait(false);
}
finally
{
ArrayPool<byte>.Shared.Return(authBytes);
await _stream.DisposeAsync().ConfigureAwait(false);
}
}
#endif
public override async Task<int> ReadAsync(
byte[] buffer,
int offset,
int count,
CancellationToken cancellationToken
)
{
if (_totalBytesLeftToRead == 0)
{
return 0;
}
var bytesToRead = count;
if (count > _totalBytesLeftToRead)
{
bytesToRead = (int)_totalBytesLeftToRead;
}
var read = await _stream
.ReadAsync(buffer, offset, bytesToRead, cancellationToken)
.ConfigureAwait(false);
_totalBytesLeftToRead -= read;
ReadTransformBlocks(buffer, offset, read);
return read;
}
#if !LEGACY_DOTNET
public override async ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default
)
{
if (_totalBytesLeftToRead == 0)
{
return 0;
}
var bytesToRead = buffer.Length;
if (buffer.Length > _totalBytesLeftToRead)
{
bytesToRead = (int)_totalBytesLeftToRead;
}
var read = await _stream
.ReadAsync(buffer.Slice(0, bytesToRead), cancellationToken)
.ConfigureAwait(false);
_totalBytesLeftToRead -= read;
ReadTransformBlocks(buffer.Span, read);
return read;
}
private void ReadTransformBlocks(Span<byte> buffer, int count)
{
var posn = 0;
var last = count;
while (posn < buffer.Length && posn < last)
{
var n = ReadTransformOneBlock(buffer, posn, last);
posn += n;
}
}
private int ReadTransformOneBlock(Span<byte> buffer, int offset, int last)
{
if (_isFinalBlock)
{
throw new InvalidOperationException();
}
var bytesRemaining = last - offset;
var bytesToRead =
(bytesRemaining > BLOCK_SIZE_IN_BYTES) ? BLOCK_SIZE_IN_BYTES : bytesRemaining;
// update the counter
System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(_counter, _nonce++);
// Determine if this is the final block
if ((bytesToRead == bytesRemaining) && (_totalBytesLeftToRead == 0))
{
_counterOut = _transform.TransformFinalBlock(_counter, 0, BLOCK_SIZE_IN_BYTES);
_isFinalBlock = true;
}
else
{
_transform.TransformBlock(
_counter,
0, // offset
BLOCK_SIZE_IN_BYTES,
_counterOut,
0
); // offset
}
XorInPlace(buffer, offset, bytesToRead);
return bytesToRead;
}
private void XorInPlace(Span<byte> buffer, int offset, int count)
{
for (var i = 0; i < count; i++)
{
buffer[offset + i] = (byte)(_counterOut[i] ^ buffer[offset + i]);
}
}
#endif
}

View File

@@ -2,10 +2,11 @@ using System;
using System.Buffers.Binary;
using System.IO;
using System.Security.Cryptography;
using System.Threading.Tasks;
namespace SharpCompress.Common.Zip;
internal class WinzipAesCryptoStream : Stream
internal partial class WinzipAesCryptoStream : Stream
{
private const int BLOCK_SIZE_IN_BYTES = 16;
private readonly SymmetricAlgorithm _cipher;
@@ -73,9 +74,17 @@ internal class WinzipAesCryptoStream : Stream
#endif
if (disposing)
{
//read out last 10 auth bytes
Span<byte> ten = stackalloc byte[10];
_stream.ReadFully(ten);
// Read out last 10 auth bytes - catch exceptions for async-only streams
try
{
Span<byte> ten = stackalloc byte[10];
_stream.ReadFully(ten);
}
catch (NotSupportedException)
{
// Stream may be async-only, auth bytes will be skipped
// This is acceptable when the entire stream has been read
}
_stream.Dispose();
}
}