Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zip.Unix: don't hang when creating zip file from directory with named pipe. #85301

Merged
merged 4 commits into from
May 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/libraries/Common/src/System/IO/Archiving.Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ public static void EnsureCapacity(ref char[] buffer, int min)
}
}

public static bool IsDirEmpty(DirectoryInfo possiblyEmptyDir)
public static bool IsDirEmpty(string directoryFullName)
{
using (IEnumerator<string> enumerator = Directory.EnumerateFileSystemEntries(possiblyEmptyDir.FullName).GetEnumerator())
using (IEnumerator<string> enumerator = Directory.EnumerateFileSystemEntries(directoryFullName).GetEnumerator())
tmds marked this conversation as resolved.
Show resolved Hide resolved
return !enumerator.MoveNext();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@
<value>A metadata entry of type '{0}' was unexpectedly found after a metadata entry of type '{1}'.</value>
</data>
<data name="TarUnsupportedFile" xml:space="preserve">
<value>The file '{0}' is a type of file not supported for tar archiving.</value>
<value>The file type of '{0}' is not supported for tar archiving.</value>
</data>
<data name="UnauthorizedAccess_IODenied_NoPathName" xml:space="preserve">
<value>Access to the path is denied.</value>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,7 @@
<data name="UnauthorizedAccess_IODenied_Path" xml:space="preserve">
<value>Access to the path '{0}' is denied.</value>
</data>
<data name="ZipUnsupportedFile" xml:space="preserve">
<value>The file type of '{0}' is not supported for zip archiving.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@
<ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'windows'">
<Compile Include="$(CommonPath)System\IO\Archiving.Utils.Windows.cs"
Link="Common\System\IO\Archiving.Utils.Windows.cs" />
<Compile Include="System\IO\Compression\ZipFile.Create.Windows.cs" />
</ItemGroup>
<!-- Unix specific files -->
<ItemGroup Condition="'$(TargetPlatformIdentifier)' == ''">
<Compile Include="System\IO\Compression\ZipFile.Create.Unix.cs" />
<Compile Include="System\IO\Compression\ZipFileExtensions.ZipArchive.Create.Unix.cs" />
<Compile Include="$(CommonPath)System\IO\Compression\ZipArchiveEntryConstants.Unix.cs" />
<Compile Include="$(CommonPath)Interop\Unix\Interop.IOErrors.cs"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.IO.Enumeration;

namespace System.IO.Compression
{
public static partial class ZipFile
{
private static FileSystemEnumerable<(string, CreateEntryType)> CreateEnumerableForCreate(string directoryFullPath)
=> new FileSystemEnumerable<(string, CreateEntryType)>(directoryFullPath,
static (ref FileSystemEntry entry) =>
{
string fullPath = entry.ToFullPath();

int type;
if (entry.IsDirectory) // entry is a directory, or a link to a directory.
{
type = Interop.Sys.FileTypes.S_IFDIR;
}
else
{
// Use 'stat' to follow links.
Interop.CheckIo(Interop.Sys.Stat(fullPath, out Interop.Sys.FileStatus status), fullPath);
type = (status.Mode & Interop.Sys.FileTypes.S_IFMT);
tmds marked this conversation as resolved.
Show resolved Hide resolved
}

return type switch
{
Interop.Sys.FileTypes.S_IFREG => (fullPath, CreateEntryType.File),
Interop.Sys.FileTypes.S_IFDIR => (fullPath, CreateEntryType.Directory),
_ => (fullPath, CreateEntryType.Unsupported)
};
},
new EnumerationOptions { RecurseSubdirectories = true, AttributesToSkip = 0, IgnoreInaccessible = false });
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.IO.Enumeration;

namespace System.IO.Compression
{
public static partial class ZipFile
{
private static FileSystemEnumerable<(string, CreateEntryType)> CreateEnumerableForCreate(string directoryFullPath)
=> new FileSystemEnumerable<(string, CreateEntryType)>(directoryFullPath,
static (ref FileSystemEntry entry) => (entry.ToFullPath(), entry.IsDirectory ? CreateEntryType.Directory : CreateEntryType.File),
new EnumerationOptions { RecurseSubdirectories = true, AttributesToSkip = 0, IgnoreInaccessible = false });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.IO.Enumeration;

namespace System.IO.Compression
{
Expand Down Expand Up @@ -375,26 +376,34 @@ private static void DoCreateFromDirectory(string sourceDirectoryName, string des
if (includeBaseDirectory && di.Parent != null)
basePath = di.Parent.FullName;

foreach (FileSystemInfo file in di.EnumerateFileSystemInfos("*", SearchOption.AllDirectories))
tmds marked this conversation as resolved.
Show resolved Hide resolved
FileSystemEnumerable<(string, CreateEntryType)> fse = CreateEnumerableForCreate(di.FullName);

foreach ((string fullPath, CreateEntryType type) in fse)
{
directoryIsEmpty = false;

if (file is FileInfo)
{
// Create entry for file:
string entryName = ArchivingUtils.EntryFromPath(file.FullName.AsSpan(basePath.Length));
ZipFileExtensions.DoCreateEntryFromFile(archive, file.FullName, entryName, compressionLevel);
}
else
switch (type)
{
// Entry marking an empty dir:
if (file is DirectoryInfo possiblyEmpty && ArchivingUtils.IsDirEmpty(possiblyEmpty))
{
// FullName never returns a directory separator character on the end,
// but Zip archives require it to specify an explicit directory:
string entryName = ArchivingUtils.EntryFromPath(file.FullName.AsSpan(basePath.Length), appendPathSeparator: true);
archive.CreateEntry(entryName);
}
case CreateEntryType.File:
{
// Create entry for file:
string entryName = ArchivingUtils.EntryFromPath(fullPath.AsSpan(basePath.Length));
ZipFileExtensions.DoCreateEntryFromFile(archive, fullPath, entryName, compressionLevel);
}
break;
case CreateEntryType.Directory:
if (ArchivingUtils.IsDirEmpty(fullPath))
{
// Create entry marking an empty dir:
// FullName never returns a directory separator character on the end,
// but Zip archives require it to specify an explicit directory:
string entryName = ArchivingUtils.EntryFromPath(fullPath.AsSpan(basePath.Length), appendPathSeparator: true);
archive.CreateEntry(entryName);
}
break;
case CreateEntryType.Unsupported:
default:
throw new IOException(SR.Format(SR.ZipUnsupportedFile, fullPath));
}
}

Expand All @@ -403,5 +412,12 @@ private static void DoCreateFromDirectory(string sourceDirectoryName, string des
archive.CreateEntry(ArchivingUtils.EntryFromPath(di.Name, appendPathSeparator: true));
}
}

private enum CreateEntryType
{
File,
Directory,
Unsupported
}
}
}
28 changes: 3 additions & 25 deletions src/libraries/System.IO.Compression.ZipFile/tests/ZipFile.Unix.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,9 @@ public void UnixExtractFilePermissionsCompat(string zipName, string expectedPerm
}

[Fact]
[PlatformSpecific(TestPlatforms.AnyUnix & ~TestPlatforms.Browser & ~TestPlatforms.tvOS & ~TestPlatforms.iOS)]
[PlatformSpecific(TestPlatforms.AnyUnix & ~TestPlatforms.Browser & ~TestPlatforms.tvOS & ~TestPlatforms.iOS)] // browser doesn't have libc mkfifo. tvOS/iOS return an error for mkfifo.
[SkipOnPlatform(TestPlatforms.LinuxBionic, "Bionic is not normal Linux, has no normal file permissions")]
public async Task CanZipNamedPipe()
public void ZipNamedPipeIsNotSupported()
tmds marked this conversation as resolved.
Show resolved Hide resolved
{
string destPath = Path.Combine(TestDirectory, "dest.zip");

Expand All @@ -195,29 +195,7 @@ public async Task CanZipNamedPipe()
Directory.CreateDirectory(subFolderPath); // mandatory before calling mkfifo
Assert.Equal(0, mkfifo(fifoPath, 438 /* 666 in octal */));

byte[] contentBytes = { 1, 2, 3, 4, 5 };

await Task.WhenAll(
Task.Run(() =>
{
using FileStream fs = new (fifoPath, FileMode.Open, FileAccess.Write, FileShare.Read, bufferSize: 0);
foreach (byte content in contentBytes)
{
fs.WriteByte(content);
}
}),
Task.Run(() =>
{
ZipFile.CreateFromDirectory(subFolderPath, destPath);

using ZipArchive zippedFolder = ZipFile.OpenRead(destPath);
using Stream unzippedPipe = zippedFolder.Entries.Single().Open();

byte[] readBytes = new byte[contentBytes.Length];
Assert.Equal(contentBytes.Length, unzippedPipe.Read(readBytes));
Assert.Equal<byte>(contentBytes, readBytes);
Assert.Equal(0, unzippedPipe.Read(readBytes)); // EOF
}));
Assert.Throws<IOException>(() => ZipFile.CreateFromDirectory(subFolderPath, destPath));
}

private static string GetExpectedPermissions(string expectedPermissions)
Expand Down