-
Notifications
You must be signed in to change notification settings - Fork 103
Correctly resolve absolute glob paths #308
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d2e7f7b
03765a0
fabbe0e
f91f040
1ddc2d7
31e3fd4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
|
@@ -64,8 +64,8 @@ static string GetVersion() | |||||||
internal sealed class SignCommand : Command | ||||||||
{ | ||||||||
private HashSet<string>? _allFiles; | ||||||||
private List<string> Files { get; set; } = []; | ||||||||
|
||||||||
internal List<string> Files { get; set; } = []; | ||||||||
internal string? KeyVaultUrl { get; set; } | ||||||||
internal string? KeyVaultClientId { get; set; } | ||||||||
internal string? KeyVaultClientSecret { get; set; } | ||||||||
|
@@ -100,52 +100,76 @@ internal HashSet<string> AllFiles | |||||||
if (_allFiles is null) | ||||||||
{ | ||||||||
_allFiles = []; | ||||||||
Matcher matcher = new(); | ||||||||
|
||||||||
foreach (string file in Files) | ||||||||
{ | ||||||||
Add(_allFiles, matcher, file); | ||||||||
} | ||||||||
|
||||||||
List<string> files = [..Files]; | ||||||||
if (!string.IsNullOrWhiteSpace(InputFileList)) | ||||||||
{ | ||||||||
foreach(string line in File.ReadLines(InputFileList)) | ||||||||
foreach (string line in File.ReadLines(InputFileList)) | ||||||||
{ | ||||||||
if (string.IsNullOrWhiteSpace(line)) | ||||||||
{ | ||||||||
continue; | ||||||||
} | ||||||||
|
||||||||
Add(_allFiles, matcher, line); | ||||||||
files.Add(line); | ||||||||
} | ||||||||
} | ||||||||
|
||||||||
PatternMatchingResult results = matcher.Execute(new DirectoryInfoWrapper(new DirectoryInfo("."))); | ||||||||
List<string> absGlobs = []; | ||||||||
Matcher relMatcher = new(); | ||||||||
|
||||||||
if (results.HasMatches) | ||||||||
foreach (var file in files) | ||||||||
{ | ||||||||
foreach (var result in results.Files) | ||||||||
// We require explicit glob pattern wildcards in order to treat it as a glob. e.g. | ||||||||
// dir/ will not be treated as a directory. It must be explicitly dir/*.exe or dir/**/*.exe, for example. | ||||||||
if (file.Contains('*')) | ||||||||
{ | ||||||||
_allFiles.Add(result.Path); | ||||||||
if (Path.IsPathRooted(file)) | ||||||||
{ | ||||||||
absGlobs.Add(file); | ||||||||
} | ||||||||
else | ||||||||
{ | ||||||||
relMatcher.AddInclude(file); | ||||||||
} | ||||||||
} | ||||||||
else | ||||||||
{ | ||||||||
_allFiles.Add(file); | ||||||||
} | ||||||||
} | ||||||||
} | ||||||||
|
||||||||
return _allFiles; | ||||||||
|
||||||||
static void Add(HashSet<string> collection, Matcher matcher, string item) | ||||||||
{ | ||||||||
// We require explicit glob pattern wildcards in order to treat it as a glob. e.g. | ||||||||
// dir/ will not be treated as a directory. It must be explicitly dir/*.exe or dir/**/*.exe, for example. | ||||||||
if (item.Contains('*')) | ||||||||
PatternMatchingResult relResults = relMatcher.Execute(new DirectoryInfoWrapper(new DirectoryInfo("."))); | ||||||||
if (relResults.HasMatches) | ||||||||
{ | ||||||||
matcher.AddInclude(item); | ||||||||
foreach (var result in relResults.Files) | ||||||||
{ | ||||||||
_allFiles.Add(result.Path); | ||||||||
} | ||||||||
} | ||||||||
else | ||||||||
|
||||||||
foreach (string absGlob in absGlobs) | ||||||||
{ | ||||||||
collection.Add(item); | ||||||||
string rootDir = GetPathRoot(absGlob); | ||||||||
if (!Directory.Exists(rootDir)) | ||||||||
{ | ||||||||
continue; | ||||||||
} | ||||||||
|
||||||||
var absMatcher = new Matcher(StringComparison.OrdinalIgnoreCase); | ||||||||
absMatcher.AddInclude(absGlob.Replace(rootDir, "")); | ||||||||
|
||||||||
var absoluteResults = absMatcher.Execute(new DirectoryInfoWrapper(new DirectoryInfo(rootDir))); | ||||||||
if (absoluteResults.HasMatches) | ||||||||
{ | ||||||||
foreach (var match in absoluteResults.Files) | ||||||||
{ | ||||||||
_allFiles.Add(Path.GetFullPath(Path.Combine(rootDir, match.Path))); | ||||||||
} | ||||||||
} | ||||||||
} | ||||||||
} | ||||||||
|
||||||||
return _allFiles; | ||||||||
} | ||||||||
} | ||||||||
|
||||||||
|
@@ -618,6 +642,23 @@ private static bool OneTrue(params bool[] values) | |||||||
return count == 1; | ||||||||
} | ||||||||
|
||||||||
private static string GetPathRoot(string fullPathPattern) | ||||||||
{ | ||||||||
int firstWildcardIndex = fullPathPattern.IndexOf('*'); | ||||||||
if (firstWildcardIndex == -1) | ||||||||
{ | ||||||||
return string.Empty; | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Returning an empty string when no wildcard is found is incorrect. This method should return the full path for non-glob patterns, or throw an exception since this method appears to be specifically for glob patterns based on its usage context.
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||
} | ||||||||
|
||||||||
int lastSeparatorIndex = fullPathPattern.LastIndexOfAny([Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar], firstWildcardIndex); | ||||||||
if (lastSeparatorIndex == -1) | ||||||||
{ | ||||||||
return Path.GetPathRoot(fullPathPattern) ?? string.Empty; | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When no directory separator is found before the wildcard, returning just the path root (e.g., 'C:\') is likely incorrect for most glob patterns. This would cause the matcher to search from the root drive rather than the intended directory. Consider whether this scenario represents an invalid glob pattern that should be handled differently.
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||
} | ||||||||
|
||||||||
return fullPathPattern[..lastSeparatorIndex]; | ||||||||
} | ||||||||
|
||||||||
private static readonly string[] s_hashAlgorithm = ["SHA1", "SHA256", "SHA384", "SHA512"]; | ||||||||
} | ||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
using System.Runtime.CompilerServices; | ||
[assembly: InternalsVisibleTo("AzureSignTool.Tests")] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
using System; | ||
using System.IO; | ||
using Xunit; | ||
|
||
namespace AzureSignTool.Tests; | ||
|
||
public class SignCommandTests | ||
{ | ||
[Fact] | ||
public void AllFiles_WithAbsoluteGlobPath_FindsFileCorrectly() | ||
{ | ||
var tempDirectory = Path.Combine(Path.GetTempPath(), $"absolute-glob-test-{Guid.NewGuid()}"); | ||
Directory.CreateDirectory(tempDirectory); | ||
var testFilePath = Path.Combine(tempDirectory, "file-to-sign.txt"); | ||
File.WriteAllText(testFilePath, "content"); | ||
|
||
var command = new SignCommand(); | ||
var absoluteGlobPattern = Path.Combine(tempDirectory, "**", "*.txt"); | ||
command.Files.Add(absoluteGlobPattern); | ||
|
||
try | ||
{ | ||
var foundFiles = command.AllFiles; | ||
var foundFile = Assert.Single(foundFiles); | ||
Assert.Equal(Path.GetFullPath(testFilePath), foundFile, ignoreCase: true); | ||
} | ||
finally | ||
{ | ||
if (Directory.Exists(tempDirectory)) | ||
Directory.Delete(tempDirectory, recursive: true); | ||
} | ||
} | ||
|
||
[Fact] | ||
public void AllFiles_WithSingleAbsoluteExistingFile_ReturnsOneFile() | ||
{ | ||
var tempFilePath = Path.Combine(Path.GetTempPath(), $"single-file-test-{Guid.NewGuid()}.tmp"); | ||
File.WriteAllText(tempFilePath, "content"); | ||
|
||
var command = new SignCommand(); | ||
command.Files.Add(tempFilePath); | ||
|
||
try | ||
{ | ||
var foundFiles = command.AllFiles; | ||
var foundFile = Assert.Single(foundFiles); | ||
Assert.Equal(Path.GetFullPath(tempFilePath), foundFile, ignoreCase: true); | ||
} | ||
finally | ||
{ | ||
if (File.Exists(tempFilePath)) | ||
File.Delete(tempFilePath); | ||
} | ||
} | ||
|
||
[Fact] | ||
public void AllFiles_ShouldIncludeExplicitPath_WhenFileDoesNotExist() | ||
{ | ||
var command = new SignCommand(); | ||
var nonExistentFilePath = Path.GetFullPath(Path.Combine("non", "existent", "path", $"file-{Guid.NewGuid()}.dll")); | ||
|
||
command.Files.Add(nonExistentFilePath); | ||
|
||
var foundFiles = command.AllFiles; | ||
|
||
var foundFile = Assert.Single(foundFiles); | ||
Assert.Equal(nonExistentFilePath, foundFile, ignoreCase: true); | ||
} | ||
|
||
[Fact] | ||
public void AllFiles_ShouldIncludeExplicitPath_WhenFileExists() | ||
{ | ||
var tempFile = Path.GetTempFileName(); | ||
var command = new SignCommand(); | ||
command.Files.Add(tempFile); | ||
|
||
try | ||
{ | ||
var foundFiles = command.AllFiles; | ||
var foundFile = Assert.Single(foundFiles); | ||
Assert.Equal(Path.GetFullPath(tempFile), foundFile, ignoreCase: true); | ||
} | ||
finally | ||
{ | ||
if (File.Exists(tempFile)) File.Delete(tempFile); | ||
} | ||
} | ||
|
||
[Fact] | ||
public void AllFiles_ShouldReturnEmpty_WhenGlobMatchesNoFiles() | ||
{ | ||
var tempDirectory = Path.Combine(Path.GetTempPath(), $"empty-glob-test-{Guid.NewGuid()}"); | ||
Directory.CreateDirectory(tempDirectory); | ||
|
||
var command = new SignCommand(); | ||
command.Files.Add(Path.Combine(tempDirectory, "*.nomatchtype")); | ||
|
||
try | ||
{ | ||
var foundFiles = command.AllFiles; | ||
Assert.Empty(foundFiles); | ||
} | ||
finally | ||
{ | ||
if (Directory.Exists(tempDirectory)) | ||
Directory.Delete(tempDirectory, true); | ||
} | ||
} | ||
|
||
[Fact] | ||
public void AllFiles_ShouldReturnCombinedSet_ForMixedInputs() | ||
{ | ||
var nonExistentFilePath = Path.GetFullPath(Path.Combine("c:", "path", "to", $"non-existent-file-{Guid.NewGuid()}.txt")); | ||
|
||
var tempDirectory = Path.Combine(Path.GetTempPath(), $"mixed-test-{Guid.NewGuid()}"); | ||
Directory.CreateDirectory(tempDirectory); | ||
var globbedFilePath = Path.Combine(tempDirectory, "app.exe"); | ||
File.WriteAllText(globbedFilePath, "content"); | ||
|
||
var command = new SignCommand(); | ||
command.Files.Add(nonExistentFilePath); | ||
command.Files.Add(Path.Combine(tempDirectory, "*.exe")); | ||
|
||
try | ||
{ | ||
var foundFiles = command.AllFiles; | ||
Assert.Equal(2, foundFiles.Count); | ||
Assert.Contains(nonExistentFilePath, foundFiles, StringComparer.OrdinalIgnoreCase); | ||
Assert.Contains(Path.GetFullPath(globbedFilePath), foundFiles, StringComparer.OrdinalIgnoreCase); | ||
} | ||
finally | ||
{ | ||
if (Directory.Exists(tempDirectory)) | ||
Directory.Delete(tempDirectory, true); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using string replacement to remove the root directory path is unsafe and can cause incorrect pattern matching. If the root directory path appears elsewhere in the glob pattern, it will be incorrectly replaced. Use
Path.GetRelativePath(rootDir, absGlob)
or substring operations based on the root directory length instead.Copilot uses AI. Check for mistakes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems "possible" on Linux paths but not paths with drives. @casuffitsharp what do you think?