From 9f69c86af3cc20fdbdbfa4601aad222711d35d67 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Aug 2026 10:03:45 +0000 Subject: [PATCH 1/2] Cache normalized destination path during Zip extraction Co-authored-by: alinpahontu2912 <56953855+alinpahontu2912@users.noreply.github.com> --- .../IO/Compression/ZipFile.Extract.Async.cs | 6 ++++-- .../System/IO/Compression/ZipFile.Extract.cs | 6 ++++-- ...ipFileExtensions.ZipArchive.Extract.Async.cs | 6 ++++-- .../ZipFileExtensions.ZipArchive.Extract.cs | 6 ++++-- ...eExtensions.ZipArchiveEntry.Extract.Async.cs | 4 ++-- ...ZipFileExtensions.ZipArchiveEntry.Extract.cs | 17 +++++++++++++---- 6 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFile.Extract.Async.cs b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFile.Extract.Async.cs index f7b5e1b823a0c2..d34b65a1870c68 100644 --- a/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFile.Extract.Async.cs +++ b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFile.Extract.Async.cs @@ -270,10 +270,11 @@ private static async Task ExtractToDirectoryAsync(string sourceArchiveFileName, ZipArchive archive = await OpenAsync(sourceArchiveFileName, ZipArchiveMode.Read, entryNameEncoding, cancellationToken).ConfigureAwait(false); await using (archive.ConfigureAwait(false)) { + string destinationDirectoryFullPath = ZipFileExtensions.GetDestinationDirectoryFullPath(destinationDirectoryName); foreach (ZipArchiveEntry entry in archive.Entries) { cancellationToken.ThrowIfCancellationRequested(); - await entry.ExtractRelativeToDirectoryAsync(destinationDirectoryName, overwriteFiles, password, cancellationToken).ConfigureAwait(false); + await entry.ExtractRelativeToDirectoryAsync(destinationDirectoryFullPath, overwriteFiles, password, cancellationToken).ConfigureAwait(false); } } } @@ -488,10 +489,11 @@ private static async Task ExtractToDirectoryAsync(Stream source, string destinat ZipArchive archive = await ZipArchive.CreateAsync(source, ZipArchiveMode.Read, leaveOpen: true, entryNameEncoding, cancellationToken).ConfigureAwait(false); await using (archive.ConfigureAwait(false)) { + string destinationDirectoryFullPath = ZipFileExtensions.GetDestinationDirectoryFullPath(destinationDirectoryName); foreach (ZipArchiveEntry entry in archive.Entries) { cancellationToken.ThrowIfCancellationRequested(); - await entry.ExtractRelativeToDirectoryAsync(destinationDirectoryName, overwriteFiles, password, cancellationToken).ConfigureAwait(false); + await entry.ExtractRelativeToDirectoryAsync(destinationDirectoryFullPath, overwriteFiles, password, cancellationToken).ConfigureAwait(false); } } } diff --git a/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFile.Extract.cs b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFile.Extract.cs index 8328d119cc80ef..4c38b42886f0ee 100644 --- a/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFile.Extract.cs +++ b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFile.Extract.cs @@ -246,9 +246,10 @@ private static void ExtractToDirectory(string sourceArchiveFileName, string dest ArgumentNullException.ThrowIfNull(sourceArchiveFileName); using ZipArchive archive = Open(sourceArchiveFileName, ZipArchiveMode.Read, entryNameEncoding); + string destinationDirectoryFullPath = ZipFileExtensions.GetDestinationDirectoryFullPath(destinationDirectoryName); foreach (ZipArchiveEntry entry in archive.Entries) { - entry.ExtractRelativeToDirectory(destinationDirectoryName, overwriteFiles, password); + entry.ExtractRelativeToDirectory(destinationDirectoryFullPath, overwriteFiles, password); } } @@ -438,9 +439,10 @@ private static void ExtractToDirectory(Stream source, string destinationDirector } using ZipArchive archive = new ZipArchive(source, ZipArchiveMode.Read, leaveOpen: true, entryNameEncoding); + string destinationDirectoryFullPath = ZipFileExtensions.GetDestinationDirectoryFullPath(destinationDirectoryName); foreach (ZipArchiveEntry entry in archive.Entries) { - entry.ExtractRelativeToDirectory(destinationDirectoryName, overwriteFiles, password); + entry.ExtractRelativeToDirectory(destinationDirectoryFullPath, overwriteFiles, password); } } diff --git a/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchive.Extract.Async.cs b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchive.Extract.Async.cs index 12a1788559f3d4..6509491bbc9287 100644 --- a/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchive.Extract.Async.cs +++ b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchive.Extract.Async.cs @@ -77,9 +77,10 @@ public static async Task ExtractToDirectoryAsync(this ZipArchive source, string ArgumentNullException.ThrowIfNull(source); ArgumentNullException.ThrowIfNull(destinationDirectoryName); + string destinationDirectoryFullPath = ZipFileExtensions.GetDestinationDirectoryFullPath(destinationDirectoryName); foreach (ZipArchiveEntry entry in source.Entries) { - await entry.ExtractRelativeToDirectoryAsync(destinationDirectoryName, overwriteFiles, cancellationToken: cancellationToken).ConfigureAwait(false); + await entry.ExtractRelativeToDirectoryAsync(destinationDirectoryFullPath, overwriteFiles, cancellationToken: cancellationToken).ConfigureAwait(false); } } @@ -94,10 +95,11 @@ public static async Task ExtractToDirectoryAsync(this ZipArchive source, string cancellationToken.ThrowIfCancellationRequested(); + string destinationDirectoryFullPath = ZipFileExtensions.GetDestinationDirectoryFullPath(destinationDirectoryName); foreach (ZipArchiveEntry entry in source.Entries) { cancellationToken.ThrowIfCancellationRequested(); - await entry.ExtractRelativeToDirectoryAsync(destinationDirectoryName, options.OverwriteFiles, options.Password, cancellationToken).ConfigureAwait(false); + await entry.ExtractRelativeToDirectoryAsync(destinationDirectoryFullPath, options.OverwriteFiles, options.Password, cancellationToken).ConfigureAwait(false); } } } diff --git a/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchive.Extract.cs b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchive.Extract.cs index 2fd56fc2bfa60f..78539a53996fd4 100644 --- a/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchive.Extract.cs +++ b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchive.Extract.cs @@ -68,9 +68,10 @@ public static void ExtractToDirectory(this ZipArchive source, string destination ArgumentNullException.ThrowIfNull(source); ArgumentNullException.ThrowIfNull(destinationDirectoryName); + string destinationDirectoryFullPath = GetDestinationDirectoryFullPath(destinationDirectoryName); foreach (ZipArchiveEntry entry in source.Entries) { - entry.ExtractRelativeToDirectory(destinationDirectoryName, overwriteFiles); + entry.ExtractRelativeToDirectory(destinationDirectoryFullPath, overwriteFiles); } } @@ -87,9 +88,10 @@ public static void ExtractToDirectory(this ZipArchive source, string destination ArgumentNullException.ThrowIfNull(destinationDirectoryName); ArgumentNullException.ThrowIfNull(options); + string destinationDirectoryFullPath = GetDestinationDirectoryFullPath(destinationDirectoryName); foreach (ZipArchiveEntry entry in source.Entries) { - entry.ExtractRelativeToDirectory(destinationDirectoryName, options.OverwriteFiles, options.Password.Span); + entry.ExtractRelativeToDirectory(destinationDirectoryFullPath, options.OverwriteFiles, options.Password.Span); } } } diff --git a/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchiveEntry.Extract.Async.cs b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchiveEntry.Extract.Async.cs index 9fef23d09473d4..b8ff9cfe4f2379 100644 --- a/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchiveEntry.Extract.Async.cs +++ b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchiveEntry.Extract.Async.cs @@ -176,11 +176,11 @@ private static async Task ExtractToFileAsync(ZipArchiveEntry source, string dest } } - internal static async Task ExtractRelativeToDirectoryAsync(this ZipArchiveEntry source, string destinationDirectoryName, bool overwrite, ReadOnlyMemory password = default, CancellationToken cancellationToken = default) + internal static async Task ExtractRelativeToDirectoryAsync(this ZipArchiveEntry source, string destinationDirectoryFullPath, bool overwrite, ReadOnlyMemory password = default, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - if (ExtractRelativeToDirectoryCheckIfFile(source, destinationDirectoryName, out string fileDestinationPath)) + if (ExtractRelativeToDirectoryCheckIfFile(source, destinationDirectoryFullPath, out string fileDestinationPath)) { // If it is a file: // Create containing directory: diff --git a/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchiveEntry.Extract.cs b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchiveEntry.Extract.cs index b6934c5913c342..534767e9435d99 100644 --- a/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchiveEntry.Extract.cs +++ b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchiveEntry.Extract.cs @@ -212,9 +212,10 @@ private static void ExtractToFileInitialize(ZipArchiveEntry source, string desti private static void ExtractToFileFinalize(ZipArchiveEntry source, string destinationFileName) => ArchivingUtils.AttemptSetLastWriteTime(destinationFileName, source.LastWriteTime); - private static bool ExtractRelativeToDirectoryCheckIfFile(ZipArchiveEntry source, string destinationDirectoryName, out string fileDestinationPath) + // Computes the normalized destination directory root once per extraction call, so that it can be + // reused across all entries instead of being recomputed for every single one. + internal static string GetDestinationDirectoryFullPath(string destinationDirectoryName) { - ArgumentNullException.ThrowIfNull(source); ArgumentNullException.ThrowIfNull(destinationDirectoryName); // Note that this will give us a good DirectoryInfo even if destinationDirectoryName exists: @@ -226,6 +227,14 @@ private static bool ExtractRelativeToDirectoryCheckIfFile(ZipArchiveEntry source destinationDirectoryFullPath = string.Concat(destinationDirectoryFullPath, new ReadOnlySpan(in sep)); } + return destinationDirectoryFullPath; + } + + private static bool ExtractRelativeToDirectoryCheckIfFile(ZipArchiveEntry source, string destinationDirectoryFullPath, out string fileDestinationPath) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(destinationDirectoryFullPath); + fileDestinationPath = Path.GetFullPath(Path.Combine(destinationDirectoryFullPath, ArchivingUtils.SanitizeEntryFilePath(source.FullName))); if (!fileDestinationPath.StartsWith(destinationDirectoryFullPath, PathInternal.StringComparison)) @@ -248,9 +257,9 @@ private static bool ExtractRelativeToDirectoryCheckIfFile(ZipArchiveEntry source return true; // It is a file } - internal static void ExtractRelativeToDirectory(this ZipArchiveEntry source, string destinationDirectoryName, bool overwrite, ReadOnlySpan password = default) + internal static void ExtractRelativeToDirectory(this ZipArchiveEntry source, string destinationDirectoryFullPath, bool overwrite, ReadOnlySpan password = default) { - if (ExtractRelativeToDirectoryCheckIfFile(source, destinationDirectoryName, out string fileDestinationPath)) + if (ExtractRelativeToDirectoryCheckIfFile(source, destinationDirectoryFullPath, out string fileDestinationPath)) { // If it is a file: // Create containing directory: From 7c148afff3a78846951a886d119e89394af81287 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 4 Aug 2026 11:51:37 +0000 Subject: [PATCH 2/2] Fix eager destination directory creation on empty Zip extraction; add regression test Co-authored-by: alinpahontu2912 <56953855+alinpahontu2912@users.noreply.github.com> --- .../System/IO/Compression/ZipFile.Extract.Async.cs | 6 ++++-- .../src/System/IO/Compression/ZipFile.Extract.cs | 6 ++++-- .../ZipFileExtensions.ZipArchive.Extract.Async.cs | 6 ++++-- .../ZipFileExtensions.ZipArchive.Extract.cs | 6 ++++-- .../tests/ZipFile.Extract.cs | 12 ++++++++++++ 5 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFile.Extract.Async.cs b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFile.Extract.Async.cs index d34b65a1870c68..0ab0c7097709de 100644 --- a/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFile.Extract.Async.cs +++ b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFile.Extract.Async.cs @@ -270,10 +270,11 @@ private static async Task ExtractToDirectoryAsync(string sourceArchiveFileName, ZipArchive archive = await OpenAsync(sourceArchiveFileName, ZipArchiveMode.Read, entryNameEncoding, cancellationToken).ConfigureAwait(false); await using (archive.ConfigureAwait(false)) { - string destinationDirectoryFullPath = ZipFileExtensions.GetDestinationDirectoryFullPath(destinationDirectoryName); + string? destinationDirectoryFullPath = null; foreach (ZipArchiveEntry entry in archive.Entries) { cancellationToken.ThrowIfCancellationRequested(); + destinationDirectoryFullPath ??= ZipFileExtensions.GetDestinationDirectoryFullPath(destinationDirectoryName); await entry.ExtractRelativeToDirectoryAsync(destinationDirectoryFullPath, overwriteFiles, password, cancellationToken).ConfigureAwait(false); } } @@ -489,10 +490,11 @@ private static async Task ExtractToDirectoryAsync(Stream source, string destinat ZipArchive archive = await ZipArchive.CreateAsync(source, ZipArchiveMode.Read, leaveOpen: true, entryNameEncoding, cancellationToken).ConfigureAwait(false); await using (archive.ConfigureAwait(false)) { - string destinationDirectoryFullPath = ZipFileExtensions.GetDestinationDirectoryFullPath(destinationDirectoryName); + string? destinationDirectoryFullPath = null; foreach (ZipArchiveEntry entry in archive.Entries) { cancellationToken.ThrowIfCancellationRequested(); + destinationDirectoryFullPath ??= ZipFileExtensions.GetDestinationDirectoryFullPath(destinationDirectoryName); await entry.ExtractRelativeToDirectoryAsync(destinationDirectoryFullPath, overwriteFiles, password, cancellationToken).ConfigureAwait(false); } } diff --git a/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFile.Extract.cs b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFile.Extract.cs index 4c38b42886f0ee..c139357b3724ca 100644 --- a/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFile.Extract.cs +++ b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFile.Extract.cs @@ -246,9 +246,10 @@ private static void ExtractToDirectory(string sourceArchiveFileName, string dest ArgumentNullException.ThrowIfNull(sourceArchiveFileName); using ZipArchive archive = Open(sourceArchiveFileName, ZipArchiveMode.Read, entryNameEncoding); - string destinationDirectoryFullPath = ZipFileExtensions.GetDestinationDirectoryFullPath(destinationDirectoryName); + string? destinationDirectoryFullPath = null; foreach (ZipArchiveEntry entry in archive.Entries) { + destinationDirectoryFullPath ??= ZipFileExtensions.GetDestinationDirectoryFullPath(destinationDirectoryName); entry.ExtractRelativeToDirectory(destinationDirectoryFullPath, overwriteFiles, password); } } @@ -439,9 +440,10 @@ private static void ExtractToDirectory(Stream source, string destinationDirector } using ZipArchive archive = new ZipArchive(source, ZipArchiveMode.Read, leaveOpen: true, entryNameEncoding); - string destinationDirectoryFullPath = ZipFileExtensions.GetDestinationDirectoryFullPath(destinationDirectoryName); + string? destinationDirectoryFullPath = null; foreach (ZipArchiveEntry entry in archive.Entries) { + destinationDirectoryFullPath ??= ZipFileExtensions.GetDestinationDirectoryFullPath(destinationDirectoryName); entry.ExtractRelativeToDirectory(destinationDirectoryFullPath, overwriteFiles, password); } } diff --git a/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchive.Extract.Async.cs b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchive.Extract.Async.cs index 6509491bbc9287..e8213461477b76 100644 --- a/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchive.Extract.Async.cs +++ b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchive.Extract.Async.cs @@ -77,9 +77,10 @@ public static async Task ExtractToDirectoryAsync(this ZipArchive source, string ArgumentNullException.ThrowIfNull(source); ArgumentNullException.ThrowIfNull(destinationDirectoryName); - string destinationDirectoryFullPath = ZipFileExtensions.GetDestinationDirectoryFullPath(destinationDirectoryName); + string? destinationDirectoryFullPath = null; foreach (ZipArchiveEntry entry in source.Entries) { + destinationDirectoryFullPath ??= ZipFileExtensions.GetDestinationDirectoryFullPath(destinationDirectoryName); await entry.ExtractRelativeToDirectoryAsync(destinationDirectoryFullPath, overwriteFiles, cancellationToken: cancellationToken).ConfigureAwait(false); } } @@ -95,10 +96,11 @@ public static async Task ExtractToDirectoryAsync(this ZipArchive source, string cancellationToken.ThrowIfCancellationRequested(); - string destinationDirectoryFullPath = ZipFileExtensions.GetDestinationDirectoryFullPath(destinationDirectoryName); + string? destinationDirectoryFullPath = null; foreach (ZipArchiveEntry entry in source.Entries) { cancellationToken.ThrowIfCancellationRequested(); + destinationDirectoryFullPath ??= ZipFileExtensions.GetDestinationDirectoryFullPath(destinationDirectoryName); await entry.ExtractRelativeToDirectoryAsync(destinationDirectoryFullPath, options.OverwriteFiles, options.Password, cancellationToken).ConfigureAwait(false); } } diff --git a/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchive.Extract.cs b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchive.Extract.cs index 78539a53996fd4..211129eea910bb 100644 --- a/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchive.Extract.cs +++ b/src/libraries/System.IO.Compression.ZipFile/src/System/IO/Compression/ZipFileExtensions.ZipArchive.Extract.cs @@ -68,9 +68,10 @@ public static void ExtractToDirectory(this ZipArchive source, string destination ArgumentNullException.ThrowIfNull(source); ArgumentNullException.ThrowIfNull(destinationDirectoryName); - string destinationDirectoryFullPath = GetDestinationDirectoryFullPath(destinationDirectoryName); + string? destinationDirectoryFullPath = null; foreach (ZipArchiveEntry entry in source.Entries) { + destinationDirectoryFullPath ??= GetDestinationDirectoryFullPath(destinationDirectoryName); entry.ExtractRelativeToDirectory(destinationDirectoryFullPath, overwriteFiles); } } @@ -88,9 +89,10 @@ public static void ExtractToDirectory(this ZipArchive source, string destination ArgumentNullException.ThrowIfNull(destinationDirectoryName); ArgumentNullException.ThrowIfNull(options); - string destinationDirectoryFullPath = GetDestinationDirectoryFullPath(destinationDirectoryName); + string? destinationDirectoryFullPath = null; foreach (ZipArchiveEntry entry in source.Entries) { + destinationDirectoryFullPath ??= GetDestinationDirectoryFullPath(destinationDirectoryName); entry.ExtractRelativeToDirectory(destinationDirectoryFullPath, options.OverwriteFiles, options.Password.Span); } } diff --git a/src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Extract.cs b/src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Extract.cs index 516835a86b3579..0911f4a26f5c85 100644 --- a/src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Extract.cs +++ b/src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Extract.cs @@ -35,6 +35,18 @@ public async Task ExtractToDirectoryNormal(string file, string folder, bool asyn await DirsEqual(tempFolder.Path, folderName); } + [Theory] + [MemberData(nameof(Get_Booleans_Data))] + public async Task ExtractToDirectory_EmptyArchive_DoesNotCreateNonExistentDestinationDirectory(bool async) + { + string zipFileName = zfile("empty.zip"); + string destinationDirectoryName = GetTestFilePath(); + + Assert.False(Directory.Exists(destinationDirectoryName)); + await CallZipFileExtractToDirectory(async, zipFileName, destinationDirectoryName); + Assert.False(Directory.Exists(destinationDirectoryName)); + } + [Theory] [MemberData(nameof(Get_Booleans_Data))] public async Task ExtractToDirectoryNull(bool async)