Skip to content

Commit 4910040

Browse files
committed
feat: escape msbuild argument when contains special chars
1 parent 8b14cbb commit 4910040

File tree

3 files changed

+122
-2
lines changed

3 files changed

+122
-2
lines changed

src/BenchmarkDotNet/Jobs/Argument.cs

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,71 @@ public MonoArgument(string value) : base(value)
4747
[PublicAPI]
4848
public class MsBuildArgument : Argument
4949
{
50+
// Characters that need to be escaped.
51+
// 1. Space
52+
// 2. Comma (Special char that is used for separater for value of `-property:{name}={value}` and `-restoreProperty:{name}={value}`)
53+
// 3. Other MSBuild special chars (https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-special-characters?view=vs-2022)
54+
private static readonly char[] MSBuildCharsToEscape = [' ', ',', '%', '$', '@', '\'', '(', ')', ';', '?', '*'];
55+
5056
public MsBuildArgument(string value) : base(value) { }
57+
58+
/// <summary>
59+
/// Gets the MSBuild argument that is used for build script.
60+
/// </summary>
61+
internal string GetEscapedTextRepresentation()
62+
{
63+
var originalArgument = TextRepresentation;
64+
65+
// If entire argument surrounded with double quote, returns original argument.
66+
// In this case. MSBuild special chars must be escaped by user. https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-special-characters
67+
if (originalArgument.StartsWith("\""))
68+
return originalArgument;
69+
70+
// Process MSBuildArgument that contains '=' char. (e.g. `--property:{key}={value}` and `-restoreProperty:{key}={value}`)
71+
// See: https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-command-line-reference?view=vs-2022
72+
var values = originalArgument.Split(['='], 2);
73+
if (values.Length != 2)
74+
return originalArgument;
75+
76+
var key = values[0];
77+
var value = values[1];
78+
79+
// If value starts with `\` char. It is expected that the escaped value is specified by the user.
80+
if (value.StartsWith("\\"))
81+
return originalArgument;
82+
83+
// If value don't contains special chars. return original value.
84+
if (value.IndexOfAny(MSBuildCharsToEscape) < 0)
85+
return originalArgument;
86+
87+
return $"{key}={GetEscapedValue(value)}";
88+
}
89+
90+
private static string GetEscapedValue(string value)
91+
{
92+
// If value starts with double quote. Trim leading/trailing double quote
93+
if (value.StartsWith("\""))
94+
value = value.Trim(['"']);
95+
96+
bool isWindows = true;
97+
#if NET
98+
isWindows = OperatingSystem.IsWindows();
99+
#endif
100+
if (isWindows)
101+
{
102+
// On Windows environment.
103+
// Returns double-quoted value. (Command line execution and `.bat` file requires escape double quote with `\`)
104+
return $"""
105+
\"{value}\"
106+
""";
107+
}
108+
109+
// On non-Windows environment.
110+
// Returns value that surround with `'"` and `"'`. See: https://github.com/dotnet/sdk/issues/8792#issuecomment-393756980
111+
// It requires escape with `\` when running command with `.sh` file. )
112+
return $"""
113+
\'\"{value}\"\'
114+
""";
115+
}
51116
}
52-
}
117+
}

src/BenchmarkDotNet/Toolchains/DotNetCli/DotNetCliCommand.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ private static string GetCustomMsBuildArguments(BenchmarkCase benchmarkCase, IRe
201201

202202
var msBuildArguments = benchmarkCase.Job.ResolveValue(InfrastructureMode.ArgumentsCharacteristic, resolver).OfType<MsBuildArgument>();
203203

204-
return string.Join(" ", msBuildArguments.Select(arg => arg.TextRepresentation));
204+
return string.Join(" ", msBuildArguments.Select(arg => arg.GetEscapedTextRepresentation()));
205205
}
206206

207207
private static IEnumerable<string> GetNuGetAddPackageCommands(BenchmarkCase benchmarkCase, IResolver resolver)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using BenchmarkDotNet.Jobs;
2+
using System;
3+
using Xunit;
4+
5+
namespace BenchmarkDotNet.Tests
6+
{
7+
public class MsBuildArgumentTests
8+
{
9+
[Theory]
10+
[InlineData("/p:CustomProperty=100%", "100%")] // Contains percentage
11+
[InlineData("/p:CustomProperty=AAA;BBB", "AAA;BBB")] // Contains semicolon
12+
[InlineData("/p:CustomProperty=AAA,BBB", "AAA,BBB")] // Contains comma
13+
[InlineData("/p:CustomProperty=AAA BBB", "AAA BBB")] // Contains space
14+
[InlineData("/p:CustomProperty=AAA%3BBBB", "AAA%3BBBB")] // Contains escaped semicolon
15+
[InlineData("/p:CustomProperty=\"AAA%3BBBB\"", "AAA%3BBBB")] // Contains escaped semicolon. and surrounded with non-escaped double quote
16+
[InlineData("/p:NoWarn=1591;1573;3001", "1591;1573;3001")] // NoWarn property that contains semicolon
17+
public void MSBuildArgument_ContainsSpecialChars(string argument, string expected)
18+
{
19+
var result = new MsBuildArgument(argument).GetEscapedTextRepresentation();
20+
AssertEscapedValue(result, expected);
21+
}
22+
23+
[Theory]
24+
[InlineData("/p:CustomProperty=AAA_BBB")] // Argument that don't contains special chars
25+
[InlineData("\"/p:CustomProperty=AAA%3BBBB\"")] // Argument is surrounded with double quote and value is escaped
26+
[InlineData("/p:CustomProperty=\\\"100%3B200\\\"")] // Value is surrounded with double quote and value is escaped (For Windows environment)
27+
[InlineData("/p:CustomProperty=\\\'\\\"100%3B200\\\"\\\'")] // Value is surrounded with double quote and value is escaped (For non-Windows environment)
28+
[InlineData("/noWarn:1591;1573;3001")] // Other argument should not be escaped
29+
public void MSBuildArgument_ShouldNotBeEscaped(string originalArg)
30+
{
31+
var result = new MsBuildArgument(originalArg).GetEscapedTextRepresentation();
32+
Assert.Equal(expected: originalArg, result);
33+
}
34+
35+
private static void AssertEscapedValue(string escapedArgument, string expectedValue)
36+
{
37+
var values = escapedArgument.Split(['='], 2);
38+
Assert.Equal(2, values.Length);
39+
40+
var key = values[0];
41+
var value = values[1];
42+
43+
#if NET
44+
// On non-Windows environment value should be surrounded with escaped `'"` and `"'`
45+
if (!OperatingSystem.IsWindows())
46+
{
47+
Assert.Equal("\\\'\\\"" + expectedValue + "\\\"\\\'", value);
48+
return;
49+
}
50+
#endif
51+
// On Windows environment. Value should be quote with escaped `\"`
52+
Assert.Equal("\\\"" + expectedValue + "\\\"", value);
53+
}
54+
}
55+
}

0 commit comments

Comments
 (0)