Skip to content

Commit d2e7f7b

Browse files
committed
fix(SignCommand): Correctly resolve absolute glob paths
Refactors path resolution to support absolute glob patterns.
1 parent e163569 commit d2e7f7b

File tree

3 files changed

+241
-57
lines changed

3 files changed

+241
-57
lines changed
Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,42 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

3-
<PropertyGroup>
4-
<OutputType>Exe</OutputType>
5-
<TargetFramework>net8.0</TargetFramework>
6-
<PackAsTool>true</PackAsTool>
7-
<ToolCommandName>azuresigntool</ToolCommandName>
8-
<Description>Azure Sign Tool is similar to signtool in the Windows SDK, with the major difference being that it uses Azure Key Vault for performing the signing process. The usage is like signtool, except with a limited set of options for signing and options for authenticating to Azure Key Vault.</Description>
9-
<RuntimeIdentifiers>win-x64;win-arm64;win-x86</RuntimeIdentifiers>
10-
<MinVerTagPrefix>v</MinVerTagPrefix>
11-
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
12-
<PublishAot Condition="$(RuntimeIdentifier.EndsWith('64'))">true</PublishAot>
13-
<EventSourceSupport>false</EventSourceSupport>
14-
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
15-
<OptimizationPreference>Size</OptimizationPreference>
16-
<PackageLicenseExpression>MIT</PackageLicenseExpression>
17-
</PropertyGroup>
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<PackAsTool>true</PackAsTool>
7+
<ToolCommandName>azuresigntool</ToolCommandName>
8+
<Description>Azure Sign Tool is similar to signtool in the Windows SDK, with the major difference being that it uses Azure Key Vault for performing the signing process. The usage is like signtool, except with a limited set of options for signing and options for authenticating to Azure Key Vault.</Description>
9+
<RuntimeIdentifiers>win-x64;win-arm64;win-x86</RuntimeIdentifiers>
10+
<MinVerTagPrefix>v</MinVerTagPrefix>
11+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
12+
<PublishAot Condition="$(RuntimeIdentifier.EndsWith('64'))">true</PublishAot>
13+
<EventSourceSupport>false</EventSourceSupport>
14+
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
15+
<OptimizationPreference>Size</OptimizationPreference>
16+
<PackageLicenseExpression>MIT</PackageLicenseExpression>
17+
</PropertyGroup>
1818

19-
<ItemGroup>
20-
<PackageReference Include="Azure.Identity" />
21-
<PackageReference Include="Azure.Security.KeyVault.Certificates" />
22-
<PackageReference Include="Azure.Security.KeyVault.Keys" />
23-
<PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" />
24-
<PackageReference Include="XenoAtom.CommandLine" />
25-
<PackageReference Include="Microsoft.Extensions.Logging" />
26-
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
27-
<PackageReference Include="MinVer">
28-
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
29-
<PrivateAssets>all</PrivateAssets>
30-
</PackageReference>
31-
</ItemGroup>
19+
<ItemGroup>
20+
<PackageReference Include="Azure.Identity" />
21+
<PackageReference Include="Azure.Security.KeyVault.Certificates" />
22+
<PackageReference Include="Azure.Security.KeyVault.Keys" />
23+
<PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" />
24+
<PackageReference Include="XenoAtom.CommandLine" />
25+
<PackageReference Include="Microsoft.Extensions.Logging" />
26+
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
27+
<PackageReference Include="MinVer">
28+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
29+
<PrivateAssets>all</PrivateAssets>
30+
</PackageReference>
31+
</ItemGroup>
3232

33-
<ItemGroup>
34-
<ProjectReference Include="..\AzureSign.Core\AzureSign.Core.csproj" />
35-
</ItemGroup>
33+
<ItemGroup>
34+
<ProjectReference Include="..\AzureSign.Core\AzureSign.Core.csproj" />
35+
</ItemGroup>
36+
37+
<ItemGroup>
38+
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
39+
<_Parameter1>AzureSignTool.Tests</_Parameter1>
40+
</AssemblyAttribute>
41+
</ItemGroup>
3642
</Project>

src/AzureSignTool/Program.cs

Lines changed: 66 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ static string GetVersion()
6464
internal sealed class SignCommand : Command
6565
{
6666
private HashSet<string>? _allFiles;
67-
private List<string> Files { get; set; } = [];
6867

68+
internal List<string> Files { get; set; } = [];
6969
internal string? KeyVaultUrl { get; set; }
7070
internal string? KeyVaultClientId { get; set; }
7171
internal string? KeyVaultClientSecret { get; set; }
@@ -100,52 +100,76 @@ internal HashSet<string> AllFiles
100100
if (_allFiles is null)
101101
{
102102
_allFiles = [];
103-
Matcher matcher = new();
104-
105-
foreach (string file in Files)
106-
{
107-
Add(_allFiles, matcher, file);
108-
}
109-
103+
List<string> files = [..Files];
110104
if (!string.IsNullOrWhiteSpace(InputFileList))
111105
{
112-
foreach(string line in File.ReadLines(InputFileList))
106+
foreach (string line in File.ReadLines(InputFileList))
113107
{
114108
if (string.IsNullOrWhiteSpace(line))
115109
{
116110
continue;
117111
}
118112

119-
Add(_allFiles, matcher, line);
113+
files.Add(line);
120114
}
121115
}
122116

123-
PatternMatchingResult results = matcher.Execute(new DirectoryInfoWrapper(new DirectoryInfo(".")));
117+
List<string> absGlobs = [];
118+
Matcher relMatcher = new();
124119

125-
if (results.HasMatches)
120+
foreach (var file in files)
126121
{
127-
foreach (var result in results.Files)
122+
// We require explicit glob pattern wildcards in order to treat it as a glob. e.g.
123+
// dir/ will not be treated as a directory. It must be explicitly dir/*.exe or dir/**/*.exe, for example.
124+
if (file.Contains('*'))
128125
{
129-
_allFiles.Add(result.Path);
126+
if (Path.IsPathRooted(file))
127+
{
128+
absGlobs.Add(file);
129+
}
130+
else
131+
{
132+
relMatcher.AddInclude(file);
133+
}
134+
}
135+
else
136+
{
137+
_allFiles.Add(file);
130138
}
131139
}
132-
}
133-
134-
return _allFiles;
135140

136-
static void Add(HashSet<string> collection, Matcher matcher, string item)
137-
{
138-
// We require explicit glob pattern wildcards in order to treat it as a glob. e.g.
139-
// dir/ will not be treated as a directory. It must be explicitly dir/*.exe or dir/**/*.exe, for example.
140-
if (item.Contains('*'))
141+
PatternMatchingResult relResults = relMatcher.Execute(new DirectoryInfoWrapper(new DirectoryInfo(".")));
142+
if (relResults.HasMatches)
141143
{
142-
matcher.AddInclude(item);
144+
foreach (var result in relResults.Files)
145+
{
146+
_allFiles.Add(result.Path);
147+
}
143148
}
144-
else
149+
150+
foreach (string absGlob in absGlobs)
145151
{
146-
collection.Add(item);
152+
string rootDir = GetPathRoot(absGlob);
153+
if (!Directory.Exists(rootDir))
154+
{
155+
continue;
156+
}
157+
158+
var absMatcher = new Matcher(StringComparison.OrdinalIgnoreCase);
159+
absMatcher.AddInclude(absGlob.Replace(rootDir, ""));
160+
161+
var absoluteResults = absMatcher.Execute(new DirectoryInfoWrapper(new DirectoryInfo(rootDir)));
162+
if (absoluteResults.HasMatches)
163+
{
164+
foreach (var match in absoluteResults.Files)
165+
{
166+
_allFiles.Add(Path.GetFullPath(Path.Combine(rootDir, match.Path)));
167+
}
168+
}
147169
}
148170
}
171+
172+
return _allFiles;
149173
}
150174
}
151175

@@ -618,6 +642,23 @@ private static bool OneTrue(params bool[] values)
618642
return count == 1;
619643
}
620644

645+
private static string GetPathRoot(string fullPathPattern)
646+
{
647+
int firstWildcardIndex = fullPathPattern.IndexOf('*');
648+
if (firstWildcardIndex == -1)
649+
{
650+
return string.Empty;
651+
}
652+
653+
int lastSeparatorIndex = fullPathPattern.LastIndexOfAny([Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar], firstWildcardIndex);
654+
if (lastSeparatorIndex == -1)
655+
{
656+
return Path.GetPathRoot(fullPathPattern) ?? string.Empty;
657+
}
658+
659+
return fullPathPattern[..lastSeparatorIndex];
660+
}
661+
621662
private static readonly string[] s_hashAlgorithm = ["SHA1", "SHA256", "SHA384", "SHA512"];
622663
}
623664
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
using System;
2+
using System.IO;
3+
using Xunit;
4+
5+
namespace AzureSignTool.Tests;
6+
7+
public class SignCommandTests
8+
{
9+
[Fact]
10+
public void AllFiles_WithAbsoluteGlobPath_FindsFileCorrectly()
11+
{
12+
var tempDirectory = Path.Combine(Path.GetTempPath(), $"absolute-glob-test-{Guid.NewGuid()}");
13+
Directory.CreateDirectory(tempDirectory);
14+
var testFilePath = Path.Combine(tempDirectory, "file-to-sign.txt");
15+
File.WriteAllText(testFilePath, "content");
16+
17+
var command = new SignCommand();
18+
var absoluteGlobPattern = Path.Combine(tempDirectory, "**", "*.txt");
19+
command.Files.Add(absoluteGlobPattern);
20+
21+
try
22+
{
23+
var foundFiles = command.AllFiles;
24+
var foundFile = Assert.Single(foundFiles);
25+
Assert.Equal(Path.GetFullPath(testFilePath), foundFile, ignoreCase: true);
26+
}
27+
finally
28+
{
29+
if (Directory.Exists(tempDirectory))
30+
Directory.Delete(tempDirectory, recursive: true);
31+
}
32+
}
33+
34+
[Fact]
35+
public void AllFiles_WithSingleAbsoluteExistingFile_ReturnsOneFile()
36+
{
37+
var tempFilePath = Path.Combine(Path.GetTempPath(), $"single-file-test-{Guid.NewGuid()}.tmp");
38+
File.WriteAllText(tempFilePath, "content");
39+
40+
var command = new SignCommand();
41+
command.Files.Add(tempFilePath);
42+
43+
try
44+
{
45+
var foundFiles = command.AllFiles;
46+
var foundFile = Assert.Single(foundFiles);
47+
Assert.Equal(Path.GetFullPath(tempFilePath), foundFile, ignoreCase: true);
48+
}
49+
finally
50+
{
51+
if (File.Exists(tempFilePath))
52+
File.Delete(tempFilePath);
53+
}
54+
}
55+
56+
[Fact]
57+
public void AllFiles_ShouldIncludeExplicitPath_WhenFileDoesNotExist()
58+
{
59+
var command = new SignCommand();
60+
var nonExistentFilePath = Path.GetFullPath(Path.Combine("non", "existent", "path", $"file-{Guid.NewGuid()}.dll"));
61+
62+
command.Files.Add(nonExistentFilePath);
63+
64+
var foundFiles = command.AllFiles;
65+
66+
var foundFile = Assert.Single(foundFiles);
67+
Assert.Equal(nonExistentFilePath, foundFile, ignoreCase: true);
68+
}
69+
70+
[Fact]
71+
public void AllFiles_ShouldIncludeExplicitPath_WhenFileExists()
72+
{
73+
var tempFile = Path.GetTempFileName();
74+
var command = new SignCommand();
75+
command.Files.Add(tempFile);
76+
77+
try
78+
{
79+
var foundFiles = command.AllFiles;
80+
var foundFile = Assert.Single(foundFiles);
81+
Assert.Equal(Path.GetFullPath(tempFile), foundFile, ignoreCase: true);
82+
}
83+
finally
84+
{
85+
if (File.Exists(tempFile)) File.Delete(tempFile);
86+
}
87+
}
88+
89+
[Fact]
90+
public void AllFiles_ShouldReturnEmpty_WhenGlobMatchesNoFiles()
91+
{
92+
var tempDirectory = Path.Combine(Path.GetTempPath(), $"empty-glob-test-{Guid.NewGuid()}");
93+
Directory.CreateDirectory(tempDirectory);
94+
95+
var command = new SignCommand();
96+
command.Files.Add(Path.Combine(tempDirectory, "*.nomatchtype"));
97+
98+
try
99+
{
100+
var foundFiles = command.AllFiles;
101+
Assert.Empty(foundFiles);
102+
}
103+
finally
104+
{
105+
if (Directory.Exists(tempDirectory))
106+
Directory.Delete(tempDirectory, true);
107+
}
108+
}
109+
110+
[Fact]
111+
public void AllFiles_ShouldReturnCombinedSet_ForMixedInputs()
112+
{
113+
var nonExistentFilePath = Path.GetFullPath(Path.Combine("c:", "path", "to", $"non-existent-file-{Guid.NewGuid()}.txt"));
114+
115+
var tempDirectory = Path.Combine(Path.GetTempPath(), $"mixed-test-{Guid.NewGuid()}");
116+
Directory.CreateDirectory(tempDirectory);
117+
var globbedFilePath = Path.Combine(tempDirectory, "app.exe");
118+
File.WriteAllText(globbedFilePath, "content");
119+
120+
var command = new SignCommand();
121+
command.Files.Add(nonExistentFilePath);
122+
command.Files.Add(Path.Combine(tempDirectory, "*.exe"));
123+
124+
try
125+
{
126+
var foundFiles = command.AllFiles;
127+
Assert.Equal(2, foundFiles.Count);
128+
Assert.Contains(nonExistentFilePath, foundFiles, StringComparer.OrdinalIgnoreCase);
129+
Assert.Contains(Path.GetFullPath(globbedFilePath), foundFiles, StringComparer.OrdinalIgnoreCase);
130+
}
131+
finally
132+
{
133+
if (Directory.Exists(tempDirectory))
134+
Directory.Delete(tempDirectory, true);
135+
}
136+
}
137+
}

0 commit comments

Comments
 (0)