Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit d70da0b

Browse files
committed
Merge pull request #2247 from stephentoub/enumerate_symlink
Fix FileSystem enumeration with broken symlinks
2 parents 81ab210 + 5428c98 commit d70da0b

File tree

3 files changed

+90
-29
lines changed

3 files changed

+90
-29
lines changed

src/System.IO.FileSystem/src/System/IO/UnixFileSystem.cs

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ public override bool DirectoryExists(string fullPath)
318318
return DirectoryExists(fullPath, out errno);
319319
}
320320

321-
private bool DirectoryExists(string fullPath, out int errno)
321+
private static bool DirectoryExists(string fullPath, out int errno)
322322
{
323323
return FileExists(fullPath, Interop.libcoreclr.FileTypes.S_IFDIR, out errno);
324324
}
@@ -329,7 +329,7 @@ public override bool FileExists(string fullPath)
329329
return FileExists(fullPath, Interop.libcoreclr.FileTypes.S_IFREG, out errno);
330330
}
331331

332-
private bool FileExists(string fullPath, int fileType, out int errno)
332+
private static bool FileExists(string fullPath, int fileType, out int errno)
333333
{
334334
Interop.libcoreclr.fileinfo fileinfo;
335335
while (true)
@@ -475,48 +475,53 @@ private IEnumerator<T> Enumerate(Interop.libc.SafeDirHandle dirHandle)
475475
string name = Interop.libc.GetDirEntName(curEntry);
476476

477477
// Get from the dir entry whether the entry is a file or directory.
478-
// If we're not sure from the dir entry itself, stat to the entry.
479-
bool isDir = false, isFile = false;
478+
// We classify everything as a file unless we know it to be a directory,
479+
// e.g. a FIFO will be classified as a file.
480+
bool isDir;
480481
switch (Interop.libc.GetDirEntType(curEntry))
481482
{
482483
case Interop.libc.DType.DT_DIR:
484+
// We know it's a directory.
483485
isDir = true;
484486
break;
485-
case Interop.libc.DType.DT_REG:
486-
isFile = true;
487-
break;
488487
case Interop.libc.DType.DT_LNK:
489488
case Interop.libc.DType.DT_UNKNOWN:
490-
string fullPath = Path.Combine(dirPath.FullPath, name);
491-
Interop.libcoreclr.fileinfo fileinfo;
492-
while (Interop.CheckIo(Interop.libcoreclr.GetFileInformationFromPath(fullPath, out fileinfo), fullPath)) ;
493-
isDir = (fileinfo.mode & Interop.libcoreclr.FileTypes.S_IFMT) == Interop.libcoreclr.FileTypes.S_IFDIR;
494-
isFile = (fileinfo.mode & Interop.libcoreclr.FileTypes.S_IFMT) == Interop.libcoreclr.FileTypes.S_IFREG;
489+
// It's a symlink or unknown: stat to it to see if we can resolve it to a directory.
490+
// If we can't (e.g.symlink to a file, broken symlink, etc.), we'll just treat it as a file.
491+
int errnoIgnored;
492+
isDir = DirectoryExists(Path.Combine(dirPath.FullPath, name), out errnoIgnored);
493+
break;
494+
default:
495+
// Otherwise, treat it as a file. This includes regular files,
496+
// FIFOs, etc.
497+
isDir = false;
495498
break;
496499
}
497-
bool matchesSearchPattern =
498-
(isFile || isDir) &&
499-
Interop.libc.fnmatch(_searchPattern, name, Interop.libc.FnmatchFlags.None) == 0;
500500

501501
// Yield the result if the user has asked for it. In the case of directories,
502502
// always explore it by pushing it onto the stack, regardless of whether
503503
// we're returning directories.
504-
if (isDir && !ShouldIgnoreDirectory(name))
504+
if (isDir)
505505
{
506-
if (_includeDirectories && matchesSearchPattern)
507-
{
508-
yield return _translateResult(Path.Combine(dirPath.UserPath, name), /*isDirectory*/true);
509-
}
510-
if (_searchOption == SearchOption.AllDirectories)
506+
if (!ShouldIgnoreDirectory(name))
511507
{
512-
if (toExplore == null)
508+
if (_includeDirectories &&
509+
Interop.libc.fnmatch(_searchPattern, name, Interop.libc.FnmatchFlags.None) == 0)
510+
{
511+
yield return _translateResult(Path.Combine(dirPath.UserPath, name), /*isDirectory*/true);
512+
}
513+
if (_searchOption == SearchOption.AllDirectories)
513514
{
514-
toExplore = new Stack<PathPair>();
515+
if (toExplore == null)
516+
{
517+
toExplore = new Stack<PathPair>();
518+
}
519+
toExplore.Push(new PathPair(Path.Combine(dirPath.UserPath, name), Path.Combine(dirPath.FullPath, name)));
515520
}
516-
toExplore.Push(new PathPair(Path.Combine(dirPath.UserPath, name), Path.Combine(dirPath.FullPath, name)));
517521
}
518522
}
519-
else if (isFile && _includeFiles && matchesSearchPattern)
523+
else if (_includeFiles &&
524+
Interop.libc.fnmatch(_searchPattern, name, Interop.libc.FnmatchFlags.None) == 0)
520525
{
521526
yield return _translateResult(Path.Combine(dirPath.UserPath, name), /*isDirectory*/false);
522527
}

src/System.IO.FileSystem/tests/Directory/GetDirectories_str.cs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@
33

44
using System;
55
using System.IO;
6-
using System.Collections;
7-
using System.Globalization;
8-
using System.Text;
9-
using System.Threading;
6+
using System.Linq;
107
using System.Runtime.CompilerServices;
8+
using System.Runtime.InteropServices;
119
using Xunit;
1210

1311
public class Directory_GetDirectories_str
@@ -347,6 +345,33 @@ public static void printerr(String err, [CallerMemberName] string memberName = "
347345
{
348346
Console.WriteLine("ERROR: ({0}, {1}, {2}) {3}", memberName, filePath, lineNumber, err);
349347
}
348+
349+
[Fact]
350+
[PlatformSpecific(PlatformID.AnyUnix)]
351+
public void EnumerateWithSymLinkToDirectory()
352+
{
353+
using (var containingFolder = new TemporaryDirectory())
354+
{
355+
// Test a symlink to a directory that does and then doesn't exist
356+
using (var targetDir = new TemporaryDirectory())
357+
{
358+
// Create a symlink to a folder that exists
359+
string linkPath = Path.Combine(containingFolder.Path, Path.GetRandomFileName());
360+
Assert.Equal(0, symlink(targetDir.Path, linkPath));
361+
Assert.True(Directory.Exists(linkPath));
362+
Assert.Equal(1, Directory.GetDirectories(containingFolder.Path).Count());
363+
}
364+
365+
// The target file is gone and the symlink still exists; since it can't be resolved,
366+
// it's treated as a file rather than as a directory.
367+
Assert.Equal(0, Directory.GetDirectories(containingFolder.Path).Count());
368+
Assert.Equal(1, Directory.GetFiles(containingFolder.Path).Count());
369+
}
370+
}
371+
372+
[DllImport("libc", SetLastError = true)]
373+
private static extern int symlink(string path1, string path2);
374+
350375
}
351376

352377

src/System.IO.FileSystem/tests/Directory/GetFiles_str.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
using System;
55
using System.IO;
66
using System.IO.FileSystem.Tests;
7+
using System.Linq;
8+
using System.Runtime.InteropServices;
79
using Xunit;
810

911
public class Directory_GetFiles_str : FileSystemTest
@@ -156,5 +158,34 @@ public void UnixFilePathWithSpaces()
156158
}
157159
}
158160

161+
[Fact]
162+
[PlatformSpecific(PlatformID.AnyUnix)]
163+
public void EnumerateWithSymLinkToFile()
164+
{
165+
using (var containingFolder = new TemporaryDirectory())
166+
{
167+
string linkPath;
168+
169+
// Test a symlink to a file that does and then doesn't exist
170+
using (var targetFile = new TemporaryFile())
171+
{
172+
linkPath = Path.Combine(containingFolder.Path, Path.GetRandomFileName());
173+
Assert.Equal(0, symlink(targetFile.Path, linkPath));
174+
Assert.True(File.Exists(linkPath));
175+
Assert.Equal(1, GetFiles(containingFolder.Path).Count());
176+
}
177+
178+
// The symlink still exists even though the target file is gone.
179+
Assert.Equal(1, GetFiles(containingFolder.Path).Count());
180+
181+
// The symlink is gone
182+
File.Delete(linkPath);
183+
Assert.Equal(0, GetFiles(containingFolder.Path).Count());
184+
}
185+
}
186+
187+
[DllImport("libc", SetLastError = true)]
188+
private static extern int symlink(string path1, string path2);
189+
159190
#endregion
160191
}

0 commit comments

Comments
 (0)