From 5953b89c6167a2dd79d3b7061b48510c4f8651f7 Mon Sep 17 00:00:00 2001 From: alinpahontu2912 Date: Mon, 8 Jun 2026 13:15:58 +0300 Subject: [PATCH] Use async ReadExactlyAsync when reading ZIP archive comment in async path ZipEndOfCentralDirectoryBlock.ReadBlockAsync called the synchronous Stream.ReadExactly when copying the archive comment after the EOCD header. This blocks the calling thread on the underlying stream and silently ignores the supplied CancellationToken. Switch to ReadExactlyAsync with ConfigureAwait(false), matching the rest of the async path. Adds a regression test using CallTrackingStream to verify the async open path never invokes the synchronous Stream.Read or ReadByte when reading an archive with a non-empty comment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../System/IO/Compression/ZipBlocks.Async.cs | 2 +- .../tests/ZipArchive/zip_ReadTests.cs | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipBlocks.Async.cs b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipBlocks.Async.cs index fea75ba16db328..90350295475338 100644 --- a/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipBlocks.Async.cs +++ b/src/libraries/System.IO.Compression/src/System/IO/Compression/ZipBlocks.Async.cs @@ -251,7 +251,7 @@ public static async Task ReadBlockAsync(Stream st } else if (readComment) { - stream.ReadExactly(eocdBlock._archiveComment); + await stream.ReadExactlyAsync(eocdBlock._archiveComment, cancellationToken).ConfigureAwait(false); } return eocdBlock; } diff --git a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_ReadTests.cs b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_ReadTests.cs index c2f856dd88bf96..f73c7c7d201812 100644 --- a/src/libraries/System.IO.Compression/tests/ZipArchive/zip_ReadTests.cs +++ b/src/libraries/System.IO.Compression/tests/ZipArchive/zip_ReadTests.cs @@ -919,5 +919,32 @@ public static async Task ReadAfterSeekingPastEnd_ReturnsZeroBytes(bool async) await DisposeStream(async, readStream); } + + [Fact] + public static async Task ReadArchiveCommentAsync_DoesNotCallSyncRead() + { + const string ExpectedComment = "this is the archive-level comment"; + + byte[] zipBytes; + using (MemoryStream buildStream = new MemoryStream()) + { + using (ZipArchive archive = new ZipArchive(buildStream, ZipArchiveMode.Create, leaveOpen: true)) + { + archive.CreateEntry("file.txt"); + archive.Comment = ExpectedComment; + } + zipBytes = buildStream.ToArray(); + } + + await using MemoryStream ms = new MemoryStream(zipBytes); + await using CallTrackingStream tracker = new CallTrackingStream(ms); + + ZipArchive readArchive = await ZipArchive.CreateAsync(tracker, ZipArchiveMode.Read, leaveOpen: true, entryNameEncoding: null); + Assert.Equal(ExpectedComment, readArchive.Comment); + await readArchive.DisposeAsync(); + + Assert.Equal(0, tracker.TimesCalled(nameof(Stream.Read))); + Assert.Equal(0, tracker.TimesCalled(nameof(Stream.ReadByte))); + } } }