Skip to content

Commit 9f3c9c9

Browse files
authored
Safe File Traversing (#380)
* Safe File Traversing * Formatting Markdown --------- Co-authored-by: Tom Longhurst <thomhurst@users.noreply.github.com>
1 parent 247b849 commit 9f3c9c9

File tree

7 files changed

+112
-14
lines changed

7 files changed

+112
-14
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ Define your pipeline in .NET! Strong types, intellisense, parallelisation, and t
6464
| ModularPipelines.WinGet | Helpers for interacting with the Windows Package Manager. | [![nuget](https://img.shields.io/nuget/v/ModularPipelines.WinGet.svg)](https://www.nuget.org/packages/ModularPipelines.WinGet/) |
6565
| ModularPipelines.Yarn | Helpers for interacting with Yarn CLI. | [![nuget](https://img.shields.io/nuget/v/ModularPipelines.Yarn.svg)](https://www.nuget.org/packages/ModularPipelines.Yarn/) |
6666

67-
6867
## Getting Started
6968

7069
If you want to see how to get started, or want to know more about ModularPipelines, [read the Documentation here](https://thomhurst.github.io/ModularPipelines)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* Improved the stability of searching nested directories for files and folders so UnauthorizedAccessException's don't occur and only return files and folders that are accessible, and doesn't break the entire enumerator

src/ModularPipelines/Engine/RequirementChecker.cs

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Collections.Concurrent;
12
using EnumerableAsyncProcessor.Extensions;
23
using ModularPipelines.Exceptions;
34
using ModularPipelines.Requirements;
@@ -17,18 +18,23 @@ public RequirementChecker(IEnumerable<IPipelineRequirement> requirements, IPipel
1718

1819
public async Task CheckRequirementsAsync()
1920
{
20-
var failedRequirementsNames = new List<string>();
21+
var failedRequirementsNames = new ConcurrentBag<string>();
2122

22-
await _requirements.ToAsyncProcessorBuilder()
23-
.ForEachAsync(async requirement =>
23+
var groupedRequirements = _requirements.GroupBy(x => x.Order);
24+
25+
foreach (var pipelineRequirements in groupedRequirements)
2426
{
25-
var requirementDecision = await requirement.MustAsync(await _moduleContextProvider.GetModuleContext());
27+
await pipelineRequirements.ToAsyncProcessorBuilder()
28+
.ForEachAsync(async requirement =>
29+
{
30+
var requirementDecision = await requirement.MustAsync(await _moduleContextProvider.GetModuleContext());
2631

27-
if (!requirementDecision.Success)
28-
{
29-
failedRequirementsNames.Add(requirementDecision.Reason ?? requirement.GetType().Name);
30-
}
31-
}).ProcessInParallel();
32+
if (!requirementDecision.Success)
33+
{
34+
failedRequirementsNames.Add(requirementDecision.Reason ?? requirement.GetType().Name);
35+
}
36+
}).ProcessInParallel();
37+
}
3238

3339
if (failedRequirementsNames.Any())
3440
{

src/ModularPipelines/FileSystem/Folder.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,16 @@ public Folder MoveTo(string path)
106106

107107
public File CreateFile(string name) => GetFile(name).Create();
108108

109-
public IEnumerable<Folder> GetFolders(Func<Folder, bool> predicate) => DirectoryInfo.EnumerateDirectories("*", SearchOption.AllDirectories)
109+
public IEnumerable<Folder> GetFolders(Func<Folder, bool> predicate) => GetFolders(predicate, _ => false);
110+
111+
public IEnumerable<File> GetFiles(Func<File, bool> predicate) => GetFiles(predicate, _ => false);
112+
113+
public IEnumerable<Folder> GetFolders(Func<Folder, bool> predicate, Func<Folder, bool> exclusionFilters) => SafeWalk.EnumerateFolders(this, exclusionFilters)
110114
.Select(x => new Folder(x))
111115
.Distinct()
112116
.Where(predicate);
113117

114-
public IEnumerable<File> GetFiles(Func<File, bool> predicate) => DirectoryInfo.EnumerateFiles("*", SearchOption.AllDirectories)
118+
public IEnumerable<File> GetFiles(Func<File, bool> predicate, Func<Folder, bool> directoryExclusionFilters) => SafeWalk.EnumerateFiles(this, directoryExclusionFilters)
115119
.Select(x => new File(x))
116120
.Distinct()
117121
.Where(predicate);
@@ -126,9 +130,13 @@ public IEnumerable<File> GetFiles(string globPattern)
126130
.Distinct();
127131
}
128132

129-
public File? FindFile(Func<File, bool> predicate) => GetFiles(predicate).FirstOrDefault();
133+
public File? FindFile(Func<File, bool> predicate) => FindFile(predicate, _ => false);
134+
135+
public Folder? FindFolder(Func<Folder, bool> predicate) => FindFolder(predicate, _ => false);
136+
137+
public File? FindFile(Func<File, bool> predicate, Func<Folder, bool> directoryExclusionFilters) => GetFiles(predicate, directoryExclusionFilters).FirstOrDefault();
130138

131-
public Folder? FindFolder(Func<Folder, bool> predicate) => GetFolders(predicate).FirstOrDefault();
139+
public Folder? FindFolder(Func<Folder, bool> predicate, Func<Folder, bool> directoryExclusionFilters) => GetFolders(predicate, directoryExclusionFilters).FirstOrDefault();
132140

133141
public IEnumerable<File> ListFiles()
134142
{
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
namespace ModularPipelines.FileSystem;
2+
3+
internal static class SafeWalk
4+
{
5+
public static IEnumerable<string> EnumerateFiles(Folder path, Func<Folder, bool> directoryExclusionFilters)
6+
{
7+
foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.TopDirectoryOnly))
8+
{
9+
yield return file;
10+
}
11+
12+
var innerFiles = new List<string>();
13+
foreach (var folder in Directory.EnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly)
14+
.Where(x => !directoryExclusionFilters(x!)))
15+
{
16+
try
17+
{
18+
innerFiles.AddRange(EnumerateFiles(folder!, directoryExclusionFilters));
19+
}
20+
catch (UnauthorizedAccessException)
21+
{
22+
continue;
23+
}
24+
25+
foreach (var innerFile in innerFiles)
26+
{
27+
yield return innerFile;
28+
}
29+
30+
innerFiles.Clear();
31+
}
32+
}
33+
34+
public static IEnumerable<string> EnumerateFolders(Folder path, Func<Folder, bool> exclusionFilters)
35+
{
36+
var innerFolders = new List<string>();
37+
foreach (var folder in Directory.EnumerateDirectories(path, "*", SearchOption.TopDirectoryOnly)
38+
.Where(x => !exclusionFilters(x!)))
39+
{
40+
yield return folder;
41+
42+
try
43+
{
44+
innerFolders.AddRange(EnumerateFolders(folder!, exclusionFilters));
45+
}
46+
catch (UnauthorizedAccessException)
47+
{
48+
continue;
49+
}
50+
51+
foreach (var innerFolder in innerFolders)
52+
{
53+
yield return innerFolder;
54+
}
55+
56+
innerFolders.Clear();
57+
}
58+
}
59+
}

src/ModularPipelines/Requirements/IPipelineRequirement.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ namespace ModularPipelines.Requirements;
66
public interface IPipelineRequirement
77
{
88
Task<RequirementDecision> MustAsync(IPipelineHookContext context);
9+
10+
int Order => 0;
911
}

test/ModularPipelines.UnitTests/FolderTests.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,29 @@ public void AssertExists_ThrowsWhenNull()
316316
Assert.Throws<DirectoryNotFoundException>(() => folder.AssertExists());
317317
}
318318

319+
[Test, WindowsOnlyTest]
320+
public void Searching_Local_Files_User_Does_Not_Throw_Unauth_Exception()
321+
{
322+
Assert.DoesNotThrow(() => new Folder(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile))
323+
.GetFolder("AppData")
324+
?.FindFile(x => x.Name.Contains(Guid.NewGuid().ToString()), exclude => exclude.Name.StartsWith('.')));
325+
}
326+
327+
[Test, WindowsOnlyTest]
328+
public void Searching_Local_Files_User_Does_Not_Throw_Unauth_Exception2()
329+
{
330+
Assert.DoesNotThrow(() => new Folder(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile))
331+
?.GetFiles(Guid.NewGuid().ToString()));
332+
}
333+
334+
[Test, WindowsOnlyTest]
335+
public void Searching_Local_Folders_User_Does_Not_Throw_Unauth_Exception()
336+
{
337+
Assert.DoesNotThrow(() => new Folder(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile))
338+
.GetFolder("AppData")
339+
?.FindFolder(x => x.Name.Contains(Guid.NewGuid().ToString()), exclude => exclude.Name.StartsWith('.')));
340+
}
341+
319342
private static Folder CreateRandomFolder()
320343
{
321344
var tempFolderPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));

0 commit comments

Comments
 (0)