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..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,12 @@ 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 = null; foreach (ZipArchiveEntry entry in archive.Entries) { cancellationToken.ThrowIfCancellationRequested(); - await entry.ExtractRelativeToDirectoryAsync(destinationDirectoryName, overwriteFiles, password, cancellationToken).ConfigureAwait(false); + destinationDirectoryFullPath ??= ZipFileExtensions.GetDestinationDirectoryFullPath(destinationDirectoryName); + await entry.ExtractRelativeToDirectoryAsync(destinationDirectoryFullPath, overwriteFiles, password, cancellationToken).ConfigureAwait(false); } } } @@ -488,10 +490,12 @@ 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 = null; foreach (ZipArchiveEntry entry in archive.Entries) { cancellationToken.ThrowIfCancellationRequested(); - await entry.ExtractRelativeToDirectoryAsync(destinationDirectoryName, overwriteFiles, password, cancellationToken).ConfigureAwait(false); + 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 8328d119cc80ef..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,11 @@ private static void ExtractToDirectory(string sourceArchiveFileName, string dest ArgumentNullException.ThrowIfNull(sourceArchiveFileName); using ZipArchive archive = Open(sourceArchiveFileName, ZipArchiveMode.Read, entryNameEncoding); + string? destinationDirectoryFullPath = null; foreach (ZipArchiveEntry entry in archive.Entries) { - entry.ExtractRelativeToDirectory(destinationDirectoryName, overwriteFiles, password); + destinationDirectoryFullPath ??= ZipFileExtensions.GetDestinationDirectoryFullPath(destinationDirectoryName); + entry.ExtractRelativeToDirectory(destinationDirectoryFullPath, overwriteFiles, password); } } @@ -438,9 +440,11 @@ private static void ExtractToDirectory(Stream source, string destinationDirector } using ZipArchive archive = new ZipArchive(source, ZipArchiveMode.Read, leaveOpen: true, entryNameEncoding); + string? destinationDirectoryFullPath = null; foreach (ZipArchiveEntry entry in archive.Entries) { - entry.ExtractRelativeToDirectory(destinationDirectoryName, overwriteFiles, password); + 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 12a1788559f3d4..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,11 @@ public static async Task ExtractToDirectoryAsync(this ZipArchive source, string ArgumentNullException.ThrowIfNull(source); ArgumentNullException.ThrowIfNull(destinationDirectoryName); + string? destinationDirectoryFullPath = null; foreach (ZipArchiveEntry entry in source.Entries) { - await entry.ExtractRelativeToDirectoryAsync(destinationDirectoryName, overwriteFiles, cancellationToken: cancellationToken).ConfigureAwait(false); + destinationDirectoryFullPath ??= ZipFileExtensions.GetDestinationDirectoryFullPath(destinationDirectoryName); + await entry.ExtractRelativeToDirectoryAsync(destinationDirectoryFullPath, overwriteFiles, cancellationToken: cancellationToken).ConfigureAwait(false); } } @@ -94,10 +96,12 @@ public static async Task ExtractToDirectoryAsync(this ZipArchive source, string cancellationToken.ThrowIfCancellationRequested(); + string? destinationDirectoryFullPath = null; foreach (ZipArchiveEntry entry in source.Entries) { cancellationToken.ThrowIfCancellationRequested(); - await entry.ExtractRelativeToDirectoryAsync(destinationDirectoryName, options.OverwriteFiles, options.Password, cancellationToken).ConfigureAwait(false); + 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 2fd56fc2bfa60f..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,11 @@ public static void ExtractToDirectory(this ZipArchive source, string destination ArgumentNullException.ThrowIfNull(source); ArgumentNullException.ThrowIfNull(destinationDirectoryName); + string? destinationDirectoryFullPath = null; foreach (ZipArchiveEntry entry in source.Entries) { - entry.ExtractRelativeToDirectory(destinationDirectoryName, overwriteFiles); + destinationDirectoryFullPath ??= GetDestinationDirectoryFullPath(destinationDirectoryName); + entry.ExtractRelativeToDirectory(destinationDirectoryFullPath, overwriteFiles); } } @@ -87,9 +89,11 @@ public static void ExtractToDirectory(this ZipArchive source, string destination ArgumentNullException.ThrowIfNull(destinationDirectoryName); ArgumentNullException.ThrowIfNull(options); + string? destinationDirectoryFullPath = null; foreach (ZipArchiveEntry entry in source.Entries) { - entry.ExtractRelativeToDirectory(destinationDirectoryName, options.OverwriteFiles, options.Password.Span); + destinationDirectoryFullPath ??= GetDestinationDirectoryFullPath(destinationDirectoryName); + 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: 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)