From 7e2c066a3ab18f71592c4004d29c38d1063637a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Nov 2025 15:38:27 +0000 Subject: [PATCH 1/3] Initial plan From 5945f08bd5316684a622c1e2d94876d9197d97fc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Nov 2025 16:06:44 +0000 Subject: [PATCH 2/3] Add DisposeAsync to WrappedStream and test for async-only streams Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com> --- .../System/IO/Compression/ZipCustomStreams.cs | 14 +++ .../tests/ZipArchive/zip_netcoreappTests.cs | 114 ++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipCustomStreams.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipCustomStreams.cs index fbbecefe676373..0514bea7dac946 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipCustomStreams.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipCustomStreams.cs @@ -216,6 +216,20 @@ protected override void Dispose(bool disposing) } base.Dispose(disposing); } + + public override async ValueTask DisposeAsync() + { + if (!_isDisposed) + { + _onClosed?.Invoke(_zipArchiveEntry); + + if (_closeBaseStream) + await _baseStream.DisposeAsync().ConfigureAwait(false); + + _isDisposed = true; + } + await base.DisposeAsync().ConfigureAwait(false); + } } internal sealed class SubReadStream : Stream diff --git a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_netcoreappTests.cs b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_netcoreappTests.cs index e32ab1cb546aac..09ebe247d4163b 100644 --- a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_netcoreappTests.cs +++ b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_netcoreappTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using Xunit; @@ -51,5 +52,118 @@ public static async Task RoundTrips_UnixFilePermissions(int expectedAttr) } } } + + [Fact] + public static async Task AsyncOnlyStream_NoSynchronousCalls() + { + // This test verifies that async Zip methods don't make synchronous calls + // which would fail with async-only streams (e.g., Kestrel response streams) + var innerStream = new MemoryStream(); + var asyncOnlyStream = new AsyncOnlyStream(innerStream); + byte[] testData = new byte[1024]; + Random.Shared.NextBytes(testData); + + await using (var zipArchive = new ZipArchive(asyncOnlyStream, ZipArchiveMode.Create, leaveOpen: true)) + { + var entry = zipArchive.CreateEntry("TestEntry"); + await using (var entryStream = await entry.OpenAsync()) + { + await entryStream.WriteAsync(testData); + await entryStream.FlushAsync(); + } + } + + // Verify the archive was created successfully by reading from the inner stream + innerStream.Position = 0; + using (var zipArchive = new ZipArchive(innerStream, ZipArchiveMode.Read)) + { + Assert.Single(zipArchive.Entries); + var entry = zipArchive.Entries[0]; + Assert.Equal("TestEntry", entry.Name); + Assert.Equal(testData.Length, entry.Length); + + using (var entryStream = entry.Open()) + { + byte[] readData = new byte[testData.Length]; + int bytesRead = entryStream.Read(readData); + Assert.Equal(testData.Length, bytesRead); + Assert.Equal(testData, readData); + } + } + } + + private sealed class AsyncOnlyStream : Stream + { + private readonly MemoryStream _innerStream; + + public AsyncOnlyStream(MemoryStream innerStream) + { + _innerStream = innerStream; + } + + public override void Flush() + { + throw new NotSupportedException("Synchronous operations not supported"); + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException("Synchronous operations not supported"); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return _innerStream.Seek(offset, origin); + } + + public override void SetLength(long value) + { + _innerStream.SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException("Synchronous operations not supported"); + } + + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + return _innerStream.CopyToAsync(destination, bufferSize, cancellationToken); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + return _innerStream.FlushAsync(cancellationToken); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _innerStream.WriteAsync(buffer, offset, count, cancellationToken); + } + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + return _innerStream.WriteAsync(buffer, cancellationToken); + } + + public override ValueTask DisposeAsync() + { + return _innerStream.DisposeAsync(); + } + + public override bool CanRead => _innerStream.CanRead; + + public override bool CanSeek => _innerStream.CanSeek; + + public override bool CanWrite => _innerStream.CanWrite; + + public override long Length => _innerStream.Length; + + public override long Position + { + get => _innerStream.Position; + set => _innerStream.Position = value; + } + } } } From 2634862f30d5497a58f1ff81c6a116d22d1c9dff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 24 Nov 2025 13:53:28 +0000 Subject: [PATCH 3/3] Remove new test and fix existing NoSyncCallsWhenUsingAsync test per review feedback Co-authored-by: rzikm <32671551+rzikm@users.noreply.github.com> --- .../IO/Compression/NoSyncCallsStream.cs | 16 +-- .../zip_InvalidParametersAndStrangeFiles.cs | 6 +- .../tests/ZipArchive/zip_netcoreappTests.cs | 114 ------------------ 3 files changed, 5 insertions(+), 131 deletions(-) diff --git a/src/libraries/Common/tests/System/IO/Compression/NoSyncCallsStream.cs b/src/libraries/Common/tests/System/IO/Compression/NoSyncCallsStream.cs index e19760eaeb9ade..44c9e3276752e3 100644 --- a/src/libraries/Common/tests/System/IO/Compression/NoSyncCallsStream.cs +++ b/src/libraries/Common/tests/System/IO/Compression/NoSyncCallsStream.cs @@ -89,21 +89,9 @@ public override int Read(Span buffer) => IsRestrictionEnabled ? throw new InvalidOperationException() : _s.Read(buffer); public override void Write(byte[] buffer, int offset, int count) { - bool isDeflateStream = false; - - // Get the stack trace to determine the calling method - var stackTrace = new System.Diagnostics.StackTrace(); - var callingMethod = stackTrace.GetFrame(1)?.GetMethod(); - - // Check if the calling method belongs to the DeflateStream class - if (callingMethod?.DeclaringType == typeof(System.IO.Compression.DeflateStream)) - { - isDeflateStream = true; - } - - if (!isDeflateStream && IsRestrictionEnabled) + if (IsRestrictionEnabled) { - throw new InvalidOperationException($"Parent class is {callingMethod.DeclaringType}"); + throw new InvalidOperationException(); } else { diff --git a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs index 2adee42cf659fd..bd8ae669765ada 100644 --- a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs +++ b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_InvalidParametersAndStrangeFiles.cs @@ -1572,7 +1572,7 @@ public static async Task NoAsyncCallsWhenUsingSync() public static async Task NoSyncCallsWhenUsingAsync() { using MemoryStream ms = new(); - using NoSyncCallsStream s = new(ms); // Only allows async calls + using NoSyncCallsStream s = new(ms) { IsRestrictionEnabled = true }; // Only allows async calls // Create mode await using (ZipArchive archive = await ZipArchive.CreateAsync(s, ZipArchiveMode.Create, leaveOpen: true, entryNameEncoding: Encoding.UTF8)) @@ -1593,8 +1593,8 @@ public static async Task NoSyncCallsWhenUsingAsync() // Note the parent archive is not using NoSyncCallsStream, so it can be opened in sync mode using (Stream normalEntryStream = normalEntry.Open()) { - // Note the parent archive is not using NoSyncCallsStream, so it can be copied in sync mode - normalEntryStream.CopyTo(newEntryStream); + // Use async CopyTo when writing to NoSyncCallsStream + await normalEntryStream.CopyToAsync(newEntryStream); } } } diff --git a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_netcoreappTests.cs b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_netcoreappTests.cs index 09ebe247d4163b..e32ab1cb546aac 100644 --- a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_netcoreappTests.cs +++ b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_netcoreappTests.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Runtime.InteropServices; -using System.Threading; using System.Threading.Tasks; using Xunit; @@ -52,118 +51,5 @@ public static async Task RoundTrips_UnixFilePermissions(int expectedAttr) } } } - - [Fact] - public static async Task AsyncOnlyStream_NoSynchronousCalls() - { - // This test verifies that async Zip methods don't make synchronous calls - // which would fail with async-only streams (e.g., Kestrel response streams) - var innerStream = new MemoryStream(); - var asyncOnlyStream = new AsyncOnlyStream(innerStream); - byte[] testData = new byte[1024]; - Random.Shared.NextBytes(testData); - - await using (var zipArchive = new ZipArchive(asyncOnlyStream, ZipArchiveMode.Create, leaveOpen: true)) - { - var entry = zipArchive.CreateEntry("TestEntry"); - await using (var entryStream = await entry.OpenAsync()) - { - await entryStream.WriteAsync(testData); - await entryStream.FlushAsync(); - } - } - - // Verify the archive was created successfully by reading from the inner stream - innerStream.Position = 0; - using (var zipArchive = new ZipArchive(innerStream, ZipArchiveMode.Read)) - { - Assert.Single(zipArchive.Entries); - var entry = zipArchive.Entries[0]; - Assert.Equal("TestEntry", entry.Name); - Assert.Equal(testData.Length, entry.Length); - - using (var entryStream = entry.Open()) - { - byte[] readData = new byte[testData.Length]; - int bytesRead = entryStream.Read(readData); - Assert.Equal(testData.Length, bytesRead); - Assert.Equal(testData, readData); - } - } - } - - private sealed class AsyncOnlyStream : Stream - { - private readonly MemoryStream _innerStream; - - public AsyncOnlyStream(MemoryStream innerStream) - { - _innerStream = innerStream; - } - - public override void Flush() - { - throw new NotSupportedException("Synchronous operations not supported"); - } - - public override int Read(byte[] buffer, int offset, int count) - { - throw new NotSupportedException("Synchronous operations not supported"); - } - - public override long Seek(long offset, SeekOrigin origin) - { - return _innerStream.Seek(offset, origin); - } - - public override void SetLength(long value) - { - _innerStream.SetLength(value); - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException("Synchronous operations not supported"); - } - - public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) - { - return _innerStream.CopyToAsync(destination, bufferSize, cancellationToken); - } - - public override Task FlushAsync(CancellationToken cancellationToken) - { - return _innerStream.FlushAsync(cancellationToken); - } - - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return _innerStream.WriteAsync(buffer, offset, count, cancellationToken); - } - - public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) - { - return _innerStream.WriteAsync(buffer, cancellationToken); - } - - public override ValueTask DisposeAsync() - { - return _innerStream.DisposeAsync(); - } - - public override bool CanRead => _innerStream.CanRead; - - public override bool CanSeek => _innerStream.CanSeek; - - public override bool CanWrite => _innerStream.CanWrite; - - public override long Length => _innerStream.Length; - - public override long Position - { - get => _innerStream.Position; - set => _innerStream.Position = value; - } - } } }