Skip to content

Commit e3f94a2

Browse files
authored
Fixes #5544 #issuecomment-4836836706. FileDialog/OpenDialog: one unre… (#5569)
* Fixes #5544 #issuecomment-4836836706. FileDialog/OpenDialog: one unreadable entry makes the entire directory render empty (`FileDialogState.GetChildren` swallows the exception) * Fixes #5570. FileDialog/OpenDialog tree builder crash on unreadable entries
1 parent b968e15 commit e3f94a2

4 files changed

Lines changed: 132 additions & 14 deletions

File tree

Terminal.Gui/FileServices/FileSystemTreeBuilder.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,15 @@ private IEnumerable<IFileSystemInfo> TryGetChildren (IFileSystemInfo entry)
7272
}
7373
}
7474

75-
internal static bool IsReparsePoint (IFileSystemInfo entry) => (entry.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint;
75+
internal static bool IsReparsePoint (IFileSystemInfo entry)
76+
{
77+
try
78+
{
79+
return (entry.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint;
80+
}
81+
catch (Exception)
82+
{
83+
return false; // treat an unreadable entry as not a reparse point / not expandable
84+
}
85+
}
7686
}

Terminal.Gui/Views/FileDialogs/FileDialogState.cs

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ namespace Terminal.Gui.Views;
44

55
internal class FileDialogState
66
{
7+
private static readonly EnumerationOptions _ignoreInaccessibleEnumerationOptions = new () { IgnoreInaccessible = true };
8+
79
public FileDialogState (IDirectoryInfo dir, FileDialog parent)
810
{
911
Parent = parent;
@@ -60,19 +62,7 @@ private void AddReadableChildren (List<FileSystemInfoStats> children, IDirectory
6062
{
6163
try
6264
{
63-
IEnumerable<IFileSystemInfo> entries;
64-
65-
// if directories only
66-
if (Parent.OpenMode == OpenMode.Directory)
67-
{
68-
entries = dir.GetDirectories ();
69-
}
70-
else
71-
{
72-
entries = dir.GetFileSystemInfos ();
73-
}
74-
75-
foreach (IFileSystemInfo entry in entries)
65+
foreach (IFileSystemInfo entry in EnumerateReadableEntries (dir))
7666
{
7767
AddReadableChild (children, entry);
7868
}
@@ -83,6 +73,17 @@ private void AddReadableChildren (List<FileSystemInfoStats> children, IDirectory
8373
}
8474
}
8575

76+
private IEnumerable<IFileSystemInfo> EnumerateReadableEntries (IDirectoryInfo dir)
77+
{
78+
// if directories only
79+
if (Parent.OpenMode == OpenMode.Directory)
80+
{
81+
return dir.EnumerateDirectories ("*", _ignoreInaccessibleEnumerationOptions);
82+
}
83+
84+
return dir.EnumerateFileSystemInfos ("*", _ignoreInaccessibleEnumerationOptions);
85+
}
86+
8687
private void AddReadableChild (List<FileSystemInfoStats> children, IFileSystemInfo entry)
8788
{
8889
try
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System.IO.Abstractions;
2+
using Moq;
3+
4+
namespace FileServicesTests;
5+
6+
public class FileSystemTreeBuilderTests
7+
{
8+
[Fact]
9+
public void CanExpand_DirectoryWithUnreadableAttributes_DoesNotThrowAndReturnsFalse ()
10+
{
11+
Mock<IDirectoryInfo> directory = new ();
12+
directory.SetupGet (d => d.Attributes).Throws (new UnauthorizedAccessException ("Access denied"));
13+
directory.SetupGet (d => d.Exists).Returns (true);
14+
directory.Setup (d => d.GetFileSystemInfos ()).Returns ([]);
15+
16+
FileSystemTreeBuilder builder = new ();
17+
18+
bool canExpand = builder.CanExpand (directory.Object);
19+
20+
Assert.False (canExpand);
21+
}
22+
23+
[Fact]
24+
public void GetChildren_DirectoryWithUnreadableAttributes_DoesNotThrowAndReturnsEmpty ()
25+
{
26+
Mock<IDirectoryInfo> directory = new ();
27+
directory.SetupGet (d => d.Attributes).Throws (new UnauthorizedAccessException ("Access denied"));
28+
directory.SetupGet (d => d.Exists).Returns (true);
29+
directory.Setup (d => d.GetFileSystemInfos ()).Returns ([]);
30+
31+
FileSystemTreeBuilder builder = new ();
32+
33+
IEnumerable<IFileSystemInfo> children = builder.GetChildren (directory.Object);
34+
35+
Assert.Empty (children);
36+
}
37+
}

Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,10 @@ public void FileDialog_MixedMode_SkipsUnreadableEntry_AndKeepsReadableEntries ()
265265
.Setup (d => d.GetFileSystemInfos ())
266266
.Returns ([goodFile, badFile, goodDirectory]);
267267

268+
Mock.Get (directory)
269+
.Setup (d => d.EnumerateFileSystemInfos ("*", It.IsAny<EnumerationOptions> ()))
270+
.Returns ([goodFile, badFile, goodDirectory]);
271+
268272
using FileDialog fd = new TestableFileDialog (fileSystem);
269273
fd.OpenMode = OpenMode.Mixed;
270274

@@ -288,6 +292,10 @@ public void FileDialog_DirectoryMode_SkipsUnreadableDirectory_AndKeepsParentNavi
288292
.Setup (d => d.GetDirectories ())
289293
.Returns ([goodDirectory, badDirectory]);
290294

295+
Mock.Get (directory)
296+
.Setup (d => d.EnumerateDirectories ("*", It.IsAny<EnumerationOptions> ()))
297+
.Returns ([goodDirectory, badDirectory]);
298+
291299
using FileDialog fd = new TestableFileDialog (fileSystem);
292300
fd.OpenMode = OpenMode.Directory;
293301

@@ -299,6 +307,64 @@ public void FileDialog_DirectoryMode_SkipsUnreadableDirectory_AndKeepsParentNavi
299307
Assert.DoesNotContain (fd.State.Children, c => c.Name == "bad-dir");
300308
}
301309

310+
[Fact]
311+
public void FileDialog_MixedMode_WhenEagerListingThrows_StillKeepsReadableEntries ()
312+
{
313+
IFileSystem fileSystem = CreateFileSystemWithDirectory (out IDirectoryInfo directory);
314+
IFileInfo goodFile = CreateFile ("/testdir/good.txt", "good.txt");
315+
IDirectoryInfo goodDirectory = CreateDirectory ("/testdir/good-dir", "good-dir", directory);
316+
317+
Mock.Get (directory)
318+
.Setup (d => d.GetFileSystemInfos ())
319+
.Throws (new UnauthorizedAccessException ());
320+
321+
Mock.Get (directory)
322+
.Setup (d => d.EnumerateFileSystemInfos ())
323+
.Returns ([goodFile, goodDirectory]);
324+
325+
Mock.Get (directory)
326+
.Setup (d => d.EnumerateFileSystemInfos ("*", It.IsAny<EnumerationOptions> ()))
327+
.Returns ([goodFile, goodDirectory]);
328+
329+
using FileDialog fd = new TestableFileDialog (fileSystem);
330+
fd.OpenMode = OpenMode.Mixed;
331+
332+
fd.Path = "/testdir";
333+
334+
Assert.NotNull (fd.State);
335+
Assert.Contains (fd.State!.Children, c => c.Name == "good.txt");
336+
Assert.Contains (fd.State.Children, c => c.Name == "good-dir");
337+
Assert.Contains (fd.State.Children, c => c.IsParent && c.Name == "..");
338+
}
339+
340+
[Fact]
341+
public void FileDialog_DirectoryMode_WhenEagerListingThrows_StillKeepsReadableDirectories ()
342+
{
343+
IFileSystem fileSystem = CreateFileSystemWithDirectory (out IDirectoryInfo directory);
344+
IDirectoryInfo goodDirectory = CreateDirectory ("/testdir/good-dir", "good-dir", directory);
345+
346+
Mock.Get (directory)
347+
.Setup (d => d.GetDirectories ())
348+
.Throws (new UnauthorizedAccessException ());
349+
350+
Mock.Get (directory)
351+
.Setup (d => d.EnumerateDirectories ())
352+
.Returns ([goodDirectory]);
353+
354+
Mock.Get (directory)
355+
.Setup (d => d.EnumerateDirectories ("*", It.IsAny<EnumerationOptions> ()))
356+
.Returns ([goodDirectory]);
357+
358+
using FileDialog fd = new TestableFileDialog (fileSystem);
359+
fd.OpenMode = OpenMode.Directory;
360+
361+
fd.Path = "/testdir";
362+
363+
Assert.NotNull (fd.State);
364+
Assert.Contains (fd.State!.Children, c => c.Name == "good-dir");
365+
Assert.Contains (fd.State.Children, c => c.IsParent && c.Name == "..");
366+
}
367+
302368
[Fact]
303369
public void FileDialog_Accepting_Directory_From_Table_Keeps_Path_On_Opened_Directory ()
304370
{
@@ -418,6 +484,10 @@ private static Mock<IDirectoryInfo> CreateDirectoryMock (string fullName, string
418484
directory.SetupGet (d => d.LastWriteTime).Returns (new DateTime (2026, 1, 1));
419485
directory.Setup (d => d.GetFileSystemInfos ()).Returns ([]);
420486
directory.Setup (d => d.GetDirectories ()).Returns ([]);
487+
directory.Setup (d => d.EnumerateFileSystemInfos ()).Returns ([]);
488+
directory.Setup (d => d.EnumerateDirectories ()).Returns ([]);
489+
directory.Setup (d => d.EnumerateFileSystemInfos ("*", It.IsAny<EnumerationOptions> ())).Returns ([]);
490+
directory.Setup (d => d.EnumerateDirectories ("*", It.IsAny<EnumerationOptions> ())).Returns ([]);
421491

422492
return directory;
423493
}

0 commit comments

Comments
 (0)