diff --git a/.copilot-agent.yml b/.copilot-agent.yml deleted file mode 100644 index 5f7f7ea2..00000000 --- a/.copilot-agent.yml +++ /dev/null @@ -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 diff --git a/.github/agents/copilot-agent.yml b/.github/agents/copilot-agent.yml deleted file mode 100644 index 055f8e56..00000000 --- a/.github/agents/copilot-agent.yml +++ /dev/null @@ -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. diff --git a/src/SharpCompress/Common/Zip/PkwareTraditionalCryptoStream.Async.cs b/src/SharpCompress/Common/Zip/PkwareTraditionalCryptoStream.Async.cs new file mode 100644 index 00000000..a17b04d2 --- /dev/null +++ b/src/SharpCompress/Common/Zip/PkwareTraditionalCryptoStream.Async.cs @@ -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 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 ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default + ) + { + if (_mode == CryptoMode.Encrypt) + { + throw new NotSupportedException("This stream does not encrypt via Read()"); + } + + byte[] temp = ArrayPool.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.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 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 +} diff --git a/src/SharpCompress/Common/Zip/PkwareTraditionalCryptoStream.cs b/src/SharpCompress/Common/Zip/PkwareTraditionalCryptoStream.cs index c3c04a82..618f9175 100644 --- a/src/SharpCompress/Common/Zip/PkwareTraditionalCryptoStream.cs +++ b/src/SharpCompress/Common/Zip/PkwareTraditionalCryptoStream.cs @@ -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; diff --git a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs new file mode 100644 index 00000000..cffb8386 --- /dev/null +++ b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.Async.cs @@ -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.Shared.Rent(10); + try + { + await _stream.ReadFullyAsync(authBytes, 0, 10).ConfigureAwait(false); + } + finally + { + ArrayPool.Shared.Return(authBytes); + await _stream.DisposeAsync().ConfigureAwait(false); + } + } +#endif + + public override async Task 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 ReadAsync( + Memory 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 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 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 buffer, int offset, int count) + { + for (var i = 0; i < count; i++) + { + buffer[offset + i] = (byte)(_counterOut[i] ^ buffer[offset + i]); + } + } +#endif +} diff --git a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs index 666bf446..5ceba087 100644 --- a/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs +++ b/src/SharpCompress/Common/Zip/WinzipAesCryptoStream.cs @@ -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 ten = stackalloc byte[10]; - _stream.ReadFully(ten); + // Read out last 10 auth bytes - catch exceptions for async-only streams + try + { + Span 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(); } }