Skip to content

Commit

Permalink
Zip.Unix: don't hang when creating zip file from directory with named…
Browse files Browse the repository at this point in the history
… pipe. (#85301)

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

When we open the named pipe for reading, that call will block indefinitely
when there is no writer to provide data.

And if we manage to read data from a pipe, that data was probably meant
for someone else.

Rather than opening named pipes, throw an IOException indicating they
are not supported.

And like the named pipes, this also treat character devices,
block devices, and sockets as unsupported types.

* Skip test on browser.

* Tar,Zip: update unsupported file type error message.

* Skip tests on tvOS/iOS.
  • Loading branch information
tmds authored May 18, 2023
1 parent 5fb5a6f commit b86722c
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 44 deletions.
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())
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);
}
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))
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()
{
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

0 comments on commit b86722c

Please sign in to comment.