diff --git a/src/Cellm.Tests/Unit/Tools/IgnoreInaccessibleDirectoryInfoWrapperTests.cs b/src/Cellm.Tests/Unit/Tools/IgnoreInaccessibleDirectoryInfoWrapperTests.cs new file mode 100644 index 00000000..89d92f59 --- /dev/null +++ b/src/Cellm.Tests/Unit/Tools/IgnoreInaccessibleDirectoryInfoWrapperTests.cs @@ -0,0 +1,84 @@ +using System.IO; +using System.Linq; +using Cellm.Tools.FileSearch; +using Microsoft.Extensions.FileSystemGlobbing; +using Microsoft.Extensions.FileSystemGlobbing.Abstractions; +using Xunit; + +namespace Cellm.Tests.Unit.Tools; + +public class IgnoreInaccessibleDirectoryInfoWrapperTests +{ + [Fact] + public void EnumerateFileSystemInfos_ReturnsFilesInAccessibleDirectory() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + + try + { + File.WriteAllText(Path.Combine(tempDir, "test.txt"), "hello"); + + var wrapper = new IgnoreInaccessibleDirectoryInfoWrapper(new DirectoryInfo(tempDir)); + var entries = wrapper.EnumerateFileSystemInfos().ToList(); + + Assert.Single(entries); + Assert.Equal("test.txt", entries[0].Name); + } + finally + { + Directory.Delete(tempDir, true); + } + } + + [Fact] + public void EnumerateFileSystemInfos_SkipsInaccessibleDirectories() + { + // C:\ProgramData contains junction points (Application Data, Desktop, etc.) + // that throw when enumerated with the stock DirectoryInfoWrapper. + var programData = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData); + + // Stock wrapper throws on problematic junction points: + // - IOException when recursive junctions cause path-too-long + // - UnauthorizedAccessException when junctions deny access + var stockWrapper = new DirectoryInfoWrapper(new DirectoryInfo(programData)); + var ex = Record.Exception(() => + new Matcher().AddInclude("**/*").Execute(stockWrapper).Files.ToList()); + Assert.True(ex is IOException or UnauthorizedAccessException, + $"Expected IOException or UnauthorizedAccessException, got {ex?.GetType().Name}: {ex?.Message}"); + + // Our wrapper skips them + var safeWrapper = new IgnoreInaccessibleDirectoryInfoWrapper(new DirectoryInfo(programData)); + var result = new Matcher().AddInclude("**/*").Execute(safeWrapper); + + // ProgramData has files, so we should get matches without throwing + Assert.True(result.HasMatches); + } + + [Fact] + public void Execute_WithMatcher_ReturnsMatchingFiles() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + var subDir = Path.Combine(tempDir, "sub"); + Directory.CreateDirectory(subDir); + + try + { + File.WriteAllText(Path.Combine(subDir, "match.cs"), "code"); + File.WriteAllText(Path.Combine(subDir, "skip.txt"), "text"); + + var matcher = new Matcher(); + matcher.AddInclude("**/*.cs"); + + var result = matcher.Execute(new IgnoreInaccessibleDirectoryInfoWrapper(new DirectoryInfo(tempDir))); + + Assert.True(result.HasMatches); + Assert.Single(result.Files); + Assert.EndsWith("match.cs", result.Files.First().Path); + } + finally + { + Directory.Delete(tempDir, true); + } + } +} diff --git a/src/Cellm/Tools/FileSearch/FileSearchRequestHandler.cs b/src/Cellm/Tools/FileSearch/FileSearchRequestHandler.cs index 900d2715..d5bd43cf 100644 --- a/src/Cellm/Tools/FileSearch/FileSearchRequestHandler.cs +++ b/src/Cellm/Tools/FileSearch/FileSearchRequestHandler.cs @@ -10,7 +10,8 @@ public Task Handle(FileSearchRequest request, CancellationTo var matcher = new Matcher(); matcher.AddIncludePatterns(request.IncludePatterns); matcher.AddExcludePatterns(request.ExcludePatterns ?? []); - var fileNames = matcher.GetResultsInFullPath(request.RootPath); + var result = matcher.Execute(new IgnoreInaccessibleDirectoryInfoWrapper(new DirectoryInfo(request.RootPath))); + var fileNames = result.Files.Select(x => Path.GetFullPath(Path.Combine(request.RootPath, x.Path))); return Task.FromResult(new FileSearchResponse(fileNames.ToList())); } diff --git a/src/Cellm/Tools/FileSearch/IgnoreInaccessibleDirectoryInfoWrapper.cs b/src/Cellm/Tools/FileSearch/IgnoreInaccessibleDirectoryInfoWrapper.cs new file mode 100644 index 00000000..ab35086d --- /dev/null +++ b/src/Cellm/Tools/FileSearch/IgnoreInaccessibleDirectoryInfoWrapper.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.FileSystemGlobbing.Abstractions; + +namespace Cellm.Tools.FileSearch; + +internal class IgnoreInaccessibleDirectoryInfoWrapper(DirectoryInfo directoryInfo) : DirectoryInfoBase +{ + public override string Name => directoryInfo.Name; + + public override string FullName => directoryInfo.FullName; + + public override DirectoryInfoBase? ParentDirectory => + directoryInfo.Parent is not null ? new IgnoreInaccessibleDirectoryInfoWrapper(directoryInfo.Parent) : null; + + public override IEnumerable EnumerateFileSystemInfos() + { + var options = new EnumerationOptions { IgnoreInaccessible = true }; + + foreach (var info in directoryInfo.EnumerateFileSystemInfos("*", options)) + { + if (info is DirectoryInfo dir) + yield return new IgnoreInaccessibleDirectoryInfoWrapper(dir); + else if (info is FileInfo file) + yield return new FileInfoWrapper(file); + } + } + + public override DirectoryInfoBase GetDirectory(string path) => + new IgnoreInaccessibleDirectoryInfoWrapper(new DirectoryInfo(Path.Combine(directoryInfo.FullName, path))); + + public override FileInfoBase GetFile(string path) => + new FileInfoWrapper(new FileInfo(Path.Combine(directoryInfo.FullName, path))); +}