Skip to content

Commit bbca699

Browse files
Copilotpragnya17
andcommitted
Add RootPathPatterns configuration and implementation - fixing pattern matching logic
Co-authored-by: pragnya17 <59893188+pragnya17@users.noreply.github.com>
1 parent 448f77b commit bbca699

10 files changed

Lines changed: 483 additions & 3 deletions

File tree

debug_pattern.cs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System;
2+
using System.IO;
3+
using System.Text.RegularExpressions;
4+
5+
class DebugPattern
6+
{
7+
static void Main()
8+
{
9+
var basePath = @"C:\test";
10+
var pattern = @"src\*\file.txt";
11+
var filePath = @"C:\test\src\component\file.txt";
12+
13+
Console.WriteLine($"Pattern: {pattern}");
14+
Console.WriteLine($"FilePath: {filePath}");
15+
Console.WriteLine($"BasePath: {basePath}");
16+
17+
// Normalize
18+
var normalizedFilePath = Path.GetFullPath(filePath);
19+
var combinedPath = Path.Combine(basePath, pattern);
20+
var normalizedPattern = Path.GetFullPath(combinedPath);
21+
22+
Console.WriteLine($"Normalized FilePath: {normalizedFilePath}");
23+
Console.WriteLine($"Combined Pattern Path: {combinedPath}");
24+
Console.WriteLine($"Normalized Pattern: {normalizedPattern}");
25+
26+
// Convert to regex
27+
var regex = ConvertGlobToRegex(normalizedPattern);
28+
Console.WriteLine($"Regex: {regex}");
29+
30+
var match = Regex.IsMatch(normalizedFilePath, regex, RegexOptions.IgnoreCase);
31+
Console.WriteLine($"Match: {match}");
32+
}
33+
34+
static string ConvertGlobToRegex(string globPattern)
35+
{
36+
var regexPattern = new System.Text.StringBuilder();
37+
38+
for (var i = 0; i < globPattern.Length; i++)
39+
{
40+
var c = globPattern[i];
41+
42+
switch (c)
43+
{
44+
case '*':
45+
if (i + 1 < globPattern.Length && globPattern[i + 1] == '*')
46+
{
47+
regexPattern.Append(".*");
48+
i++;
49+
}
50+
else
51+
{
52+
regexPattern.Append(@"[^\\\/]*");
53+
}
54+
break;
55+
case '?':
56+
regexPattern.Append('.');
57+
break;
58+
case '\\':
59+
case '/':
60+
regexPattern.Append(@"[\\\/]");
61+
break;
62+
default:
63+
if ("()[]{}^$+.|".Contains(c))
64+
{
65+
regexPattern.Append('\\');
66+
}
67+
regexPattern.Append(c);
68+
break;
69+
}
70+
}
71+
72+
return "^" + regexPattern.ToString() + ".*$";
73+
}
74+
}

debug_test.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System;
2+
using Microsoft.Sbom.Api.Utils;
3+
4+
class Program
5+
{
6+
static void Main()
7+
{
8+
var basePath = @"C:\test";
9+
var pattern = @"src\*\file.txt";
10+
var filePath = @"C:\test\src\component\file.txt";
11+
12+
Console.WriteLine($"Pattern: {pattern}");
13+
Console.WriteLine($"FilePath: {filePath}");
14+
Console.WriteLine($"BasePath: {basePath}");
15+
Console.WriteLine($"Result: {PathPatternMatcher.IsMatch(filePath, pattern, basePath)}");
16+
17+
// Test simple pattern too
18+
var simplePattern = "*.txt";
19+
var simpleFile = @"C:\test\file.txt";
20+
Console.WriteLine($"Simple Pattern: {simplePattern}");
21+
Console.WriteLine($"Simple File: {simpleFile}");
22+
Console.WriteLine($"Simple Result: {PathPatternMatcher.IsMatch(simpleFile, simplePattern, basePath)}");
23+
}
24+
}

src/Microsoft.Sbom.Api/Filters/DownloadedRootPathFilter.cs

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.IO;
77
using System.Linq;
8+
using Microsoft.Sbom.Api.Utils;
89
using Microsoft.Sbom.Common;
910
using Microsoft.Sbom.Common.Config;
1011
using Serilog;
@@ -23,6 +24,7 @@ public class DownloadedRootPathFilter : IFilter<DownloadedRootPathFilter>
2324

2425
private bool skipValidation;
2526
private HashSet<string> validPaths;
27+
private List<string> patterns;
2628

2729
public DownloadedRootPathFilter(
2830
IConfiguration configuration,
@@ -37,12 +39,13 @@ public DownloadedRootPathFilter(
3739
}
3840

3941
/// <summary>
40-
/// Returns true if filePath is present in root path filters.
42+
/// Returns true if filePath is present in root path filters or matches root path patterns.
4143
///
4244
/// For example, say filePath is /root/parent1/parent2/child1/child2.txt, then if the root path
4345
/// filters contains /root/parent1/ or /root/parent1/parent2/ in it, this filePath with return true,
4446
/// but if the root path contains /root/parent3/, this filePath will return false.
4547
///
48+
/// If patterns are specified, the filePath will be matched against glob-style patterns instead.
4649
/// </summary>
4750
/// <param name="filePath">The file path to validate.</param>
4851
public bool IsValid(string filePath)
@@ -57,6 +60,48 @@ public bool IsValid(string filePath)
5760
return false;
5861
}
5962

63+
// If patterns are configured, use pattern matching
64+
if (patterns != null && patterns.Count > 0)
65+
{
66+
return IsValidWithPatterns(filePath);
67+
}
68+
69+
// Fall back to legacy path prefix matching
70+
return IsValidWithPathPrefix(filePath);
71+
}
72+
73+
/// <summary>
74+
/// Validates file path using glob-style patterns.
75+
/// </summary>
76+
/// <param name="filePath">The file path to validate.</param>
77+
/// <returns>True if the path matches any pattern, false otherwise.</returns>
78+
private bool IsValidWithPatterns(string filePath)
79+
{
80+
var buildDropPath = configuration.BuildDropPath?.Value;
81+
82+
foreach (var pattern in patterns)
83+
{
84+
if (PathPatternMatcher.IsMatch(filePath, pattern, buildDropPath))
85+
{
86+
return true;
87+
}
88+
}
89+
90+
return false;
91+
}
92+
93+
/// <summary>
94+
/// Validates file path using the legacy path prefix approach.
95+
/// </summary>
96+
/// <param name="filePath">The file path to validate.</param>
97+
/// <returns>True if the path starts with any valid path prefix, false otherwise.</returns>
98+
private bool IsValidWithPathPrefix(string filePath)
99+
{
100+
if (validPaths == null || validPaths.Count == 0)
101+
{
102+
return false;
103+
}
104+
60105
var isValid = false;
61106
var normalizedPath = new FileInfo(filePath).FullName;
62107

@@ -69,14 +114,33 @@ public bool IsValid(string filePath)
69114
}
70115

71116
/// <summary>
72-
/// Initializes the root path filters list.
117+
/// Initializes the root path filters list or patterns.
73118
/// </summary>
74119
public void Init()
75120
{
76121
logger.Verbose("Adding root path filter valid paths");
77122
skipValidation = true;
78123

79-
if (configuration.RootPathFilter != null && !string.IsNullOrWhiteSpace(configuration.RootPathFilter.Value))
124+
// Check for new pattern-based configuration first (takes precedence)
125+
if (configuration.RootPathPatterns != null && !string.IsNullOrWhiteSpace(configuration.RootPathPatterns.Value))
126+
{
127+
skipValidation = false;
128+
patterns = new List<string>();
129+
var patternStrings = configuration.RootPathPatterns.Value.Split(';');
130+
131+
foreach (var pattern in patternStrings)
132+
{
133+
var trimmedPattern = pattern.Trim();
134+
if (!string.IsNullOrEmpty(trimmedPattern))
135+
{
136+
patterns.Add(trimmedPattern);
137+
logger.Verbose($"Added pattern {trimmedPattern}");
138+
}
139+
}
140+
}
141+
142+
// Fall back to legacy path prefix configuration
143+
else if (configuration.RootPathFilter != null && !string.IsNullOrWhiteSpace(configuration.RootPathFilter.Value))
80144
{
81145
skipValidation = false;
82146
validPaths = new HashSet<string>();
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System.IO;
5+
using System.Text.RegularExpressions;
6+
7+
namespace Microsoft.Sbom.Api.Utils;
8+
9+
/// <summary>
10+
/// Provides pattern matching functionality for file paths using glob-style patterns.
11+
/// </summary>
12+
public static class PathPatternMatcher
13+
{
14+
/// <summary>
15+
/// Checks if a file path matches a glob-style pattern.
16+
/// Supports * (single level wildcard) and ** (recursive wildcard).
17+
/// </summary>
18+
/// <param name="filePath">The file path to check.</param>
19+
/// <param name="pattern">The glob pattern to match against.</param>
20+
/// <param name="basePath">The base path to resolve relative patterns.</param>
21+
/// <returns>True if the path matches the pattern, false otherwise.</returns>
22+
public static bool IsMatch(string filePath, string pattern, string basePath = null)
23+
{
24+
if (string.IsNullOrEmpty(filePath) || string.IsNullOrEmpty(pattern))
25+
{
26+
return false;
27+
}
28+
29+
// Normalize the file path
30+
var normalizedFilePath = Path.GetFullPath(filePath);
31+
32+
// If basePath is provided and pattern is relative, create the full pattern
33+
string normalizedPattern;
34+
if (!string.IsNullOrEmpty(basePath) && !Path.IsPathRooted(pattern))
35+
{
36+
var combinedPath = Path.Combine(basePath, pattern);
37+
normalizedPattern = Path.GetFullPath(combinedPath);
38+
}
39+
else
40+
{
41+
normalizedPattern = pattern;
42+
}
43+
44+
// Convert glob pattern to regex
45+
var regexPattern = ConvertGlobToRegex(normalizedPattern);
46+
47+
// Perform case-insensitive matching for cross-platform compatibility
48+
var regex = new Regex(regexPattern, RegexOptions.IgnoreCase);
49+
50+
return regex.IsMatch(normalizedFilePath);
51+
}
52+
53+
/// <summary>
54+
/// Converts a glob pattern to a regular expression.
55+
/// </summary>
56+
/// <param name="globPattern">The glob pattern to convert.</param>
57+
/// <returns>A regular expression string.</returns>
58+
private static string ConvertGlobToRegex(string globPattern)
59+
{
60+
// Start building the regex pattern
61+
var regexPattern = new System.Text.StringBuilder();
62+
63+
for (var i = 0; i < globPattern.Length; i++)
64+
{
65+
var c = globPattern[i];
66+
67+
switch (c)
68+
{
69+
case '*':
70+
// Check for ** pattern
71+
if (i + 1 < globPattern.Length && globPattern[i + 1] == '*')
72+
{
73+
regexPattern.Append(".*"); // ** matches any characters including path separators
74+
i++; // Skip the second *
75+
}
76+
else
77+
{
78+
regexPattern.Append(@"[^\\\/]*"); // * matches any characters except path separators
79+
}
80+
81+
break;
82+
83+
case '?':
84+
regexPattern.Append('.'); // ? matches any single character
85+
break;
86+
87+
case '\\':
88+
case '/':
89+
regexPattern.Append(@"[\\\/]"); // Allow both \ and / as path separators
90+
break;
91+
92+
default:
93+
// Escape special regex characters
94+
if ("()[]{}^$+.|".Contains(c))
95+
{
96+
regexPattern.Append('\\');
97+
}
98+
99+
regexPattern.Append(c);
100+
break;
101+
}
102+
}
103+
104+
// Anchor the pattern to match from the beginning
105+
return "^" + regexPattern.ToString() + ".*$";
106+
}
107+
}

src/Microsoft.Sbom.Common/Config/Configuration.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public class Configuration : IConfiguration
2929
private static readonly AsyncLocal<ConfigurationSetting<IList<ManifestInfo>>> manifestInfo = new();
3030
private static readonly AsyncLocal<ConfigurationSetting<AlgorithmName>> hashAlgorithm = new();
3131
private static readonly AsyncLocal<ConfigurationSetting<string>> rootFilterPath = new();
32+
private static readonly AsyncLocal<ConfigurationSetting<string>> rootPathPatterns = new();
3233
private static readonly AsyncLocal<ConfigurationSetting<string>> catalogFilePath = new();
3334
private static readonly AsyncLocal<ConfigurationSetting<bool>> validateSignature = new();
3435
private static readonly AsyncLocal<ConfigurationSetting<bool>> ignoreMissing = new();
@@ -161,6 +162,14 @@ public ConfigurationSetting<string> RootPathFilter
161162
set => rootFilterPath.Value = value;
162163
}
163164

165+
/// <inheritdoc cref="IConfiguration.RootPathPatterns" />
166+
[Path]
167+
public ConfigurationSetting<string> RootPathPatterns
168+
{
169+
get => rootPathPatterns.Value;
170+
set => rootPathPatterns.Value = value;
171+
}
172+
164173
/// <inheritdoc cref="IConfiguration.CatalogFilePath" />
165174
[Path]
166175
public ConfigurationSetting<string> CatalogFilePath

src/Microsoft.Sbom.Common/Config/IConfiguration.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,13 @@ public interface IConfiguration
8383
/// </summary>
8484
public ConfigurationSetting<string> RootPathFilter { get; set; }
8585

86+
/// <summary>
87+
/// Gets or sets file matching patterns (glob-style) for filtering root paths.
88+
/// Supports wildcards (* and **) for flexible file path matching.
89+
/// Semicolon-separated list of patterns. Takes precedence over RootPathFilter if specified.
90+
/// </summary>
91+
public ConfigurationSetting<string> RootPathPatterns { get; set; }
92+
8693
/// <summary>
8794
/// Gets or sets the path of the signed catalog file used to validate the manifest.json.
8895
/// </summary>

src/Microsoft.Sbom.Common/Config/InputConfiguration.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ public class InputConfiguration : IConfiguration
7070
[Path]
7171
public ConfigurationSetting<string> RootPathFilter { get; set; }
7272

73+
/// <inheritdoc cref="IConfiguration.RootPathPatterns" />
74+
[Path]
75+
public ConfigurationSetting<string> RootPathPatterns { get; set; }
76+
7377
/// <inheritdoc cref="IConfiguration.CatalogFilePath" />
7478
[Path]
7579
public ConfigurationSetting<string> CatalogFilePath { get; set; }

0 commit comments

Comments
 (0)