diff --git a/Directory.Packages.props b/Directory.Packages.props
index d00e197c37..d67eca267d 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -17,7 +17,7 @@
-
+
@@ -52,7 +52,7 @@
-
+
diff --git a/Terminal.Gui/FileServices/FileSystemTreeBuilder.cs b/Terminal.Gui/FileServices/FileSystemTreeBuilder.cs
index 80e9a740d0..4b616a59e1 100644
--- a/Terminal.Gui/FileServices/FileSystemTreeBuilder.cs
+++ b/Terminal.Gui/FileServices/FileSystemTreeBuilder.cs
@@ -72,5 +72,15 @@ private IEnumerable TryGetChildren (IFileSystemInfo entry)
}
}
- internal static bool IsReparsePoint (IFileSystemInfo entry) => (entry.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint;
+ internal static bool IsReparsePoint (IFileSystemInfo entry)
+ {
+ try
+ {
+ return (entry.Attributes & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint;
+ }
+ catch (Exception)
+ {
+ return false; // treat an unreadable entry as not a reparse point / not expandable
+ }
+ }
}
diff --git a/Terminal.Gui/Views/FileDialogs/FileDialogState.cs b/Terminal.Gui/Views/FileDialogs/FileDialogState.cs
index 24340147f0..2842534052 100644
--- a/Terminal.Gui/Views/FileDialogs/FileDialogState.cs
+++ b/Terminal.Gui/Views/FileDialogs/FileDialogState.cs
@@ -4,6 +4,8 @@ namespace Terminal.Gui.Views;
internal class FileDialogState
{
+ private static readonly EnumerationOptions _ignoreInaccessibleEnumerationOptions = new () { IgnoreInaccessible = true };
+
public FileDialogState (IDirectoryInfo dir, FileDialog parent)
{
Parent = parent;
@@ -60,19 +62,7 @@ private void AddReadableChildren (List children, IDirectory
{
try
{
- IEnumerable entries;
-
- // if directories only
- if (Parent.OpenMode == OpenMode.Directory)
- {
- entries = dir.GetDirectories ();
- }
- else
- {
- entries = dir.GetFileSystemInfos ();
- }
-
- foreach (IFileSystemInfo entry in entries)
+ foreach (IFileSystemInfo entry in EnumerateReadableEntries (dir))
{
AddReadableChild (children, entry);
}
@@ -83,6 +73,17 @@ private void AddReadableChildren (List children, IDirectory
}
}
+ private IEnumerable EnumerateReadableEntries (IDirectoryInfo dir)
+ {
+ // if directories only
+ if (Parent.OpenMode == OpenMode.Directory)
+ {
+ return dir.EnumerateDirectories ("*", _ignoreInaccessibleEnumerationOptions);
+ }
+
+ return dir.EnumerateFileSystemInfos ("*", _ignoreInaccessibleEnumerationOptions);
+ }
+
private void AddReadableChild (List children, IFileSystemInfo entry)
{
try
diff --git a/Tests/UnitTestsParallelizable/FileServices/FileSystemTreeBuilderTests.cs b/Tests/UnitTestsParallelizable/FileServices/FileSystemTreeBuilderTests.cs
new file mode 100644
index 0000000000..3e916a86f4
--- /dev/null
+++ b/Tests/UnitTestsParallelizable/FileServices/FileSystemTreeBuilderTests.cs
@@ -0,0 +1,37 @@
+using System.IO.Abstractions;
+using Moq;
+
+namespace FileServicesTests;
+
+public class FileSystemTreeBuilderTests
+{
+ [Fact]
+ public void CanExpand_DirectoryWithUnreadableAttributes_DoesNotThrowAndReturnsFalse ()
+ {
+ Mock directory = new ();
+ directory.SetupGet (d => d.Attributes).Throws (new UnauthorizedAccessException ("Access denied"));
+ directory.SetupGet (d => d.Exists).Returns (true);
+ directory.Setup (d => d.GetFileSystemInfos ()).Returns ([]);
+
+ FileSystemTreeBuilder builder = new ();
+
+ bool canExpand = builder.CanExpand (directory.Object);
+
+ Assert.False (canExpand);
+ }
+
+ [Fact]
+ public void GetChildren_DirectoryWithUnreadableAttributes_DoesNotThrowAndReturnsEmpty ()
+ {
+ Mock directory = new ();
+ directory.SetupGet (d => d.Attributes).Throws (new UnauthorizedAccessException ("Access denied"));
+ directory.SetupGet (d => d.Exists).Returns (true);
+ directory.Setup (d => d.GetFileSystemInfos ()).Returns ([]);
+
+ FileSystemTreeBuilder builder = new ();
+
+ IEnumerable children = builder.GetChildren (directory.Object);
+
+ Assert.Empty (children);
+ }
+}
diff --git a/Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs b/Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs
index d679958e7c..74cdfa74a6 100644
--- a/Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs
+++ b/Tests/UnitTestsParallelizable/Views/FileDialogResultTests.cs
@@ -265,6 +265,10 @@ public void FileDialog_MixedMode_SkipsUnreadableEntry_AndKeepsReadableEntries ()
.Setup (d => d.GetFileSystemInfos ())
.Returns ([goodFile, badFile, goodDirectory]);
+ Mock.Get (directory)
+ .Setup (d => d.EnumerateFileSystemInfos ("*", It.IsAny ()))
+ .Returns ([goodFile, badFile, goodDirectory]);
+
using FileDialog fd = new TestableFileDialog (fileSystem);
fd.OpenMode = OpenMode.Mixed;
@@ -288,6 +292,10 @@ public void FileDialog_DirectoryMode_SkipsUnreadableDirectory_AndKeepsParentNavi
.Setup (d => d.GetDirectories ())
.Returns ([goodDirectory, badDirectory]);
+ Mock.Get (directory)
+ .Setup (d => d.EnumerateDirectories ("*", It.IsAny ()))
+ .Returns ([goodDirectory, badDirectory]);
+
using FileDialog fd = new TestableFileDialog (fileSystem);
fd.OpenMode = OpenMode.Directory;
@@ -299,6 +307,64 @@ public void FileDialog_DirectoryMode_SkipsUnreadableDirectory_AndKeepsParentNavi
Assert.DoesNotContain (fd.State.Children, c => c.Name == "bad-dir");
}
+ [Fact]
+ public void FileDialog_MixedMode_WhenEagerListingThrows_StillKeepsReadableEntries ()
+ {
+ IFileSystem fileSystem = CreateFileSystemWithDirectory (out IDirectoryInfo directory);
+ IFileInfo goodFile = CreateFile ("/testdir/good.txt", "good.txt");
+ IDirectoryInfo goodDirectory = CreateDirectory ("/testdir/good-dir", "good-dir", directory);
+
+ Mock.Get (directory)
+ .Setup (d => d.GetFileSystemInfos ())
+ .Throws (new UnauthorizedAccessException ());
+
+ Mock.Get (directory)
+ .Setup (d => d.EnumerateFileSystemInfos ())
+ .Returns ([goodFile, goodDirectory]);
+
+ Mock.Get (directory)
+ .Setup (d => d.EnumerateFileSystemInfos ("*", It.IsAny ()))
+ .Returns ([goodFile, goodDirectory]);
+
+ using FileDialog fd = new TestableFileDialog (fileSystem);
+ fd.OpenMode = OpenMode.Mixed;
+
+ fd.Path = "/testdir";
+
+ Assert.NotNull (fd.State);
+ Assert.Contains (fd.State!.Children, c => c.Name == "good.txt");
+ Assert.Contains (fd.State.Children, c => c.Name == "good-dir");
+ Assert.Contains (fd.State.Children, c => c.IsParent && c.Name == "..");
+ }
+
+ [Fact]
+ public void FileDialog_DirectoryMode_WhenEagerListingThrows_StillKeepsReadableDirectories ()
+ {
+ IFileSystem fileSystem = CreateFileSystemWithDirectory (out IDirectoryInfo directory);
+ IDirectoryInfo goodDirectory = CreateDirectory ("/testdir/good-dir", "good-dir", directory);
+
+ Mock.Get (directory)
+ .Setup (d => d.GetDirectories ())
+ .Throws (new UnauthorizedAccessException ());
+
+ Mock.Get (directory)
+ .Setup (d => d.EnumerateDirectories ())
+ .Returns ([goodDirectory]);
+
+ Mock.Get (directory)
+ .Setup (d => d.EnumerateDirectories ("*", It.IsAny ()))
+ .Returns ([goodDirectory]);
+
+ using FileDialog fd = new TestableFileDialog (fileSystem);
+ fd.OpenMode = OpenMode.Directory;
+
+ fd.Path = "/testdir";
+
+ Assert.NotNull (fd.State);
+ Assert.Contains (fd.State!.Children, c => c.Name == "good-dir");
+ Assert.Contains (fd.State.Children, c => c.IsParent && c.Name == "..");
+ }
+
[Fact]
public void FileDialog_Accepting_Directory_From_Table_Keeps_Path_On_Opened_Directory ()
{
@@ -418,6 +484,10 @@ private static Mock CreateDirectoryMock (string fullName, string
directory.SetupGet (d => d.LastWriteTime).Returns (new DateTime (2026, 1, 1));
directory.Setup (d => d.GetFileSystemInfos ()).Returns ([]);
directory.Setup (d => d.GetDirectories ()).Returns ([]);
+ directory.Setup (d => d.EnumerateFileSystemInfos ()).Returns ([]);
+ directory.Setup (d => d.EnumerateDirectories ()).Returns ([]);
+ directory.Setup (d => d.EnumerateFileSystemInfos ("*", It.IsAny ())).Returns ([]);
+ directory.Setup (d => d.EnumerateDirectories ("*", It.IsAny ())).Returns ([]);
return directory;
}