Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
3 changes: 2 additions & 1 deletion src/Cellm/Tools/FileSearch/FileSearchRequestHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ public Task<FileSearchResponse> 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()));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<FileSystemInfoBase> 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)));
}
Loading