Skip to content

Commit

Permalink
Implement stream-based ZipFile ExtractToDirectory and CreateFromDirec…
Browse files Browse the repository at this point in the history
…tory method overloads (#85491)

* ref: Add stream-based ZipFile.CreateFromDirectory and ZipFile.ExtractToDirectory methods.

* src: Add stream-based ZipFile.CreateFromDirectory ZipFile.ExtractToDirectory methods.

* tests: Move wrongly placed tests to the correct class.

* tests: Add stream-based tests for ZipFile.CreateFromDirectory and ZipFile.ExtractToDirectory.

* src: Documentation and resource strings.

* tests: More tests for unseekable/unreadable/unwritable streams.

* Address suggestions and include more exception validation tests.

* Fix braces after resolving conflict.

* Use AssertExtensions.SequenceEqual for clearer error message.

* Reset the resx file again.

* Order results of dir enumeration tests
  • Loading branch information
carlossanlop authored May 30, 2023
1 parent 7d50edd commit cfa28e3
Show file tree
Hide file tree
Showing 12 changed files with 1,051 additions and 361 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -329,9 +329,9 @@ public static void DirsEqual(string actual, string expected)

public static void DirFileNamesEqual(string actual, string expected)
{
IEnumerable<string> actualEntries = Directory.EnumerateFileSystemEntries(actual, "*", SearchOption.AllDirectories);
IEnumerable<string> expectedEntries = Directory.EnumerateFileSystemEntries(expected, "*", SearchOption.AllDirectories);
Assert.True(Enumerable.SequenceEqual(expectedEntries.Select(i => Path.GetFileName(i)), actualEntries.Select(i => Path.GetFileName(i))));
IOrderedEnumerable<string> actualEntries = Directory.EnumerateFileSystemEntries(actual, "*", SearchOption.AllDirectories).Order();
IOrderedEnumerable<string> expectedEntries = Directory.EnumerateFileSystemEntries(expected, "*", SearchOption.AllDirectories).Order();
AssertExtensions.SequenceEqual(expectedEntries.Select(Path.GetFileName).ToArray(), actualEntries.Select(Path.GetFileName).ToArray());
}

private static void ItemEqual(string[] actualList, List<FileData> expectedList, bool isFile)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,16 @@ namespace System.IO.Compression
{
public static partial class ZipFile
{
public static void CreateFromDirectory(string sourceDirectoryName, System.IO.Stream destination) { }
public static void CreateFromDirectory(string sourceDirectoryName, System.IO.Stream destination, System.IO.Compression.CompressionLevel compressionLevel, bool includeBaseDirectory) { }
public static void CreateFromDirectory(string sourceDirectoryName, System.IO.Stream destination, System.IO.Compression.CompressionLevel compressionLevel, bool includeBaseDirectory, System.Text.Encoding? entryNameEncoding) { }
public static void CreateFromDirectory(string sourceDirectoryName, string destinationArchiveFileName) { }
public static void CreateFromDirectory(string sourceDirectoryName, string destinationArchiveFileName, System.IO.Compression.CompressionLevel compressionLevel, bool includeBaseDirectory) { }
public static void CreateFromDirectory(string sourceDirectoryName, string destinationArchiveFileName, System.IO.Compression.CompressionLevel compressionLevel, bool includeBaseDirectory, System.Text.Encoding? entryNameEncoding) { }
public static void ExtractToDirectory(System.IO.Stream source, string destinationDirectoryName) { }
public static void ExtractToDirectory(System.IO.Stream source, string destinationDirectoryName, bool overwriteFiles) { }
public static void ExtractToDirectory(System.IO.Stream source, string destinationDirectoryName, System.Text.Encoding? entryNameEncoding) { }
public static void ExtractToDirectory(System.IO.Stream source, string destinationDirectoryName, System.Text.Encoding? entryNameEncoding, bool overwriteFiles) { }
public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName) { }
public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName, bool overwriteFiles) { }
public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName, System.Text.Encoding? entryNameEncoding) { }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<root>
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
Expand Down Expand Up @@ -102,4 +103,10 @@
<data name="ZipUnsupportedFile" xml:space="preserve">
<value>The file type of '{0}' is not supported for zip archiving.</value>
</data>
<data name="UnreadableStream" xml:space="preserve">
<value>The stream is unreadable.</value>
</data>
<data name="UnwritableStream" xml:space="preserve">
<value>The stream is unwritable.</value>
</data>
</root>

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<EnableLibraryImportGenerator>true</EnableLibraryImportGenerator>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
Expand All @@ -16,7 +16,10 @@

<ItemGroup>
<Compile Include="ZipFile.Create.cs" />
<Compile Include="ZipFile.Create.Stream.cs" />
<Compile Include="ZipFile.Extract.cs" />
<Compile Include="ZipFile.Extract.Stream.cs" />
<Compile Include="ZipFile.Open.cs" />
<Compile Include="ZipFileExtensions.ZipArchive.Create.cs" />
<Compile Include="ZipFileExtensions.ZipArchiveEntry.Extract.cs" />
<Compile Include="ZipFileExtensions.ZipArchive.Extract.cs" />
Expand All @@ -26,6 +29,8 @@
Link="Common\System\IO\TempFile.cs" />
<Compile Include="$(CommonTestPath)System\IO\TempDirectory.cs"
Link="Common\System\IO\TempDirectory.cs" />
<Compile Include="$(CommonTestPath)System\IO\WrappedStream.cs"
Link="Common\System\IO\WrappedStream.cs" />
<Compile Include="$(CommonTestPath)System\IO\Compression\CRC.cs"
Link="Common\System\IO\Compression\CRC.cs" />
<Compile Include="$(CommonTestPath)System\IO\Compression\FileData.cs"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,5 +91,25 @@ public static void Archive_ExtractToDirectory(string zipFile, string expectedOut
}
}
}

[Fact]
public void ExtractToDirectoryZipArchiveOverwrite()
{
string zipFileName = zfile("normal.zip");
string folderName = zfolder("normal");

using (var tempFolder = new TempDirectory(GetTestFilePath()))
{
using (ZipArchive archive = ZipFile.Open(zipFileName, ZipArchiveMode.Read))
{
archive.ExtractToDirectory(tempFolder.Path);
Assert.Throws<IOException>(() => archive.ExtractToDirectory(tempFolder.Path /* default false */));
Assert.Throws<IOException>(() => archive.ExtractToDirectory(tempFolder.Path, overwriteFiles: false));
archive.ExtractToDirectory(tempFolder.Path, overwriteFiles: true);

DirsEqual(tempFolder.Path, folderName);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xunit;

namespace System.IO.Compression.Tests;

public class ZipFile_Create_Stream : ZipFileTestBase
{
[Fact]
public void CreateFromDirectory_NullSourceDirectory_Throws()
{
using MemoryStream ms = new MemoryStream();
Assert.Throws<ArgumentNullException>(() => ZipFile.CreateFromDirectory(sourceDirectoryName: null, ms));
Assert.Throws<ArgumentNullException>(() => ZipFile.CreateFromDirectory(sourceDirectoryName: null, ms, CompressionLevel.NoCompression, includeBaseDirectory: false));
Assert.Throws<ArgumentNullException>(() => ZipFile.CreateFromDirectory(sourceDirectoryName: null, ms, CompressionLevel.NoCompression, includeBaseDirectory: false, Encoding.UTF8));
}

[Theory]
[InlineData((CompressionLevel)int.MinValue)]
[InlineData((CompressionLevel)(-1))]
[InlineData((CompressionLevel)4)]
[InlineData((CompressionLevel)int.MaxValue)]
public void CreateFromDirectory_CompressionLevel_OutOfRange_Throws(CompressionLevel invalidCompressionLevel)
{
using MemoryStream ms = new MemoryStream();
Assert.Throws<ArgumentOutOfRangeException>(() => ZipFile.CreateFromDirectory("sourceDirectory", ms, invalidCompressionLevel, includeBaseDirectory: false));
Assert.Throws<ArgumentOutOfRangeException>(() => ZipFile.CreateFromDirectory("sourceDirectory", ms, invalidCompressionLevel, includeBaseDirectory: false, Encoding.UTF8));
}

[Fact]
public void CreateFromDirectory_UnwritableStream_Throws()
{
using MemoryStream ms = new();
using WrappedStream destination = new(ms, canRead: true, canWrite: false, canSeek: true);
Assert.Throws<ArgumentException>("destination", () => ZipFile.CreateFromDirectory(GetTestFilePath(), destination));
}

[Fact]
public void CreateFromDirectoryNormal()
{
string folderName = zfolder("normal");
using MemoryStream destination = new();
ZipFile.CreateFromDirectory(folderName, destination);
destination.Position = 0;
IsZipSameAsDir(destination, folderName, ZipArchiveMode.Read, requireExplicit: false, checkTimes: false);
}

[Fact]
public void CreateFromDirectoryNormal_Unreadable_Unseekable()
{
string folderName = zfolder("normal");
using MemoryStream ms = new();
using WrappedStream destination = new(ms, canRead: false, canWrite: true, canSeek: false);
ZipFile.CreateFromDirectory(folderName, destination);
ms.Position = 0;
IsZipSameAsDir(ms, folderName, ZipArchiveMode.Read, requireExplicit: false, checkTimes: false);
}

[Fact]
public void CreateFromDirectory_IncludeBaseDirectory()
{
string folderName = zfolder("normal");
using MemoryStream destination = new();
ZipFile.CreateFromDirectory(folderName, destination, CompressionLevel.Optimal, true);

IEnumerable<string> expected = Directory.EnumerateFiles(zfolder("normal"), "*", SearchOption.AllDirectories);
destination.Position = 0;
using ZipArchive archive = new(destination);
foreach (ZipArchiveEntry actualEntry in archive.Entries)
{
string expectedFile = expected.Single(i => Path.GetFileName(i).Equals(actualEntry.Name));
Assert.StartsWith("normal", actualEntry.FullName);
Assert.Equal(new FileInfo(expectedFile).Length, actualEntry.Length);
using Stream expectedStream = File.OpenRead(expectedFile);
using Stream actualStream = actualEntry.Open();
StreamsEqual(expectedStream, actualStream);
}
}

[Fact]
public void CreateFromDirectoryUnicode()
{
string folderName = zfolder("unicode");
using MemoryStream destination = new();
ZipFile.CreateFromDirectory(folderName, destination);

using ZipArchive archive = new(destination);
IEnumerable<string> actual = archive.Entries.Select(entry => entry.Name);
IEnumerable<string> expected = Directory.EnumerateFileSystemEntries(zfolder("unicode"), "*", SearchOption.AllDirectories).ToList();
Assert.True(Enumerable.SequenceEqual(expected.Select(i => Path.GetFileName(i)), actual.Select(i => i)));
}

[Fact]
public void CreatedEmptyDirectoriesRoundtrip()
{
using TempDirectory tempFolder = new(GetTestFilePath());

DirectoryInfo rootDir = new(tempFolder.Path);
rootDir.CreateSubdirectory("empty1");

using MemoryStream destination = new();
ZipFile.CreateFromDirectory(
rootDir.FullName, destination,
CompressionLevel.Optimal, false, Encoding.UTF8);

using ZipArchive archive = new(destination);

Assert.Equal(1, archive.Entries.Count);
Assert.StartsWith("empty1", archive.Entries[0].FullName);
}

[Fact]
public void CreatedEmptyUtf32DirectoriesRoundtrip()
{
using TempDirectory tempFolder = new(GetTestFilePath());

Encoding entryEncoding = Encoding.UTF32;
DirectoryInfo rootDir = new(tempFolder.Path);
rootDir.CreateSubdirectory("empty1");

using MemoryStream destination = new();
ZipFile.CreateFromDirectory(
rootDir.FullName, destination,
CompressionLevel.Optimal, false, entryEncoding);

using ZipArchive archive = new(destination, ZipArchiveMode.Read, leaveOpen: false, entryEncoding);
Assert.Equal(1, archive.Entries.Count);
Assert.StartsWith("empty1", archive.Entries[0].FullName);
}

[Fact]
public void CreatedEmptyRootDirectoryRoundtrips()
{
using TempDirectory tempFolder = new(GetTestFilePath());

DirectoryInfo emptyRoot = new(tempFolder.Path);
using MemoryStream destination = new();
ZipFile.CreateFromDirectory(
emptyRoot.FullName, destination,
CompressionLevel.Optimal, true);

using ZipArchive archive = new(destination);
Assert.Equal(1, archive.Entries.Count);
}

[Fact]
public void CreateSetsExternalAttributesCorrectly()
{
string folderName = zfolder("normal");
using MemoryStream destination = new();
ZipFile.CreateFromDirectory(folderName, destination);
destination.Position = 0;
using ZipArchive archive = new(destination);

foreach (ZipArchiveEntry entry in archive.Entries)
{
if (OperatingSystem.IsWindows())
{
Assert.Equal(0, entry.ExternalAttributes);
}
else
{
Assert.NotEqual(0, entry.ExternalAttributes);
}
}
}
}
Loading

0 comments on commit cfa28e3

Please sign in to comment.