diff --git a/src/BenchmarkDotNet/Jobs/Argument.cs b/src/BenchmarkDotNet/Jobs/Argument.cs index c86c83906a..c451f4d6d3 100644 --- a/src/BenchmarkDotNet/Jobs/Argument.cs +++ b/src/BenchmarkDotNet/Jobs/Argument.cs @@ -1,9 +1,11 @@ using System; +using System.Collections.Generic; +using System.Text; using JetBrains.Annotations; namespace BenchmarkDotNet.Jobs { - public abstract class Argument: IEquatable + public abstract class Argument : IEquatable { [PublicAPI] public string TextRepresentation { get; } @@ -47,6 +49,43 @@ public MonoArgument(string value) : base(value) [PublicAPI] public class MsBuildArgument : Argument { - public MsBuildArgument(string value) : base(value) { } + private static readonly Dictionary MsBuildEscapes = new () + { + { '%', "%25" }, + { '$', "%24" }, + { '@', "%40" }, + { '\'', "%27" }, + { '(', "%28" }, + { ')', "%29" }, + { ';', "%3B" }, + { '?', "%3F" }, + { '*', "%2A" } + }; + + private static string EscapeMsBuildSpecialChars(string value) + { + if (string.IsNullOrEmpty(value)) + return value; + + var sb = new StringBuilder(value.Length); + foreach (char c in value) + { + if (MsBuildEscapes.TryGetValue(c, out var escaped)) + sb.Append(escaped); + else + sb.Append(c); + } + + return sb.ToString(); + } + /// + /// Represents an MSBuild command-line argument. + /// The raw or escaped argument value (e.g., "/p:DefineConstants=TEST1;TEST2"). + /// + /// If true (default), special MSBuild characters like %, ;, *, etc. will be escaped. + /// If false, the value is used as-is — use this only if you're passing a fully pre-escaped string. + /// + /// + public MsBuildArgument(string value, bool escapeSpecialChars = true) : base(escapeSpecialChars ? EscapeMsBuildSpecialChars(value) : value) { } } } \ No newline at end of file diff --git a/tests/BenchmarkDotNet.IntegrationTests.ManualRunning/MsBuildArgumentTests.cs b/tests/BenchmarkDotNet.IntegrationTests.ManualRunning/MsBuildArgumentTests.cs index f4619040cc..a9f333d84b 100644 --- a/tests/BenchmarkDotNet.IntegrationTests.ManualRunning/MsBuildArgumentTests.cs +++ b/tests/BenchmarkDotNet.IntegrationTests.ManualRunning/MsBuildArgumentTests.cs @@ -43,6 +43,27 @@ public void MultipleProcessesAreBuiltWithCorrectProperties() CanExecute(config); } + [Fact] + public void EscapesSemicolonInDefineConstants() + { + var arg = new MsBuildArgument("/p:DefineConstants=TEST1;TEST2"); + Assert.Equal("/p:DefineConstants=TEST1%3BTEST2", arg.ToString()); + } + + [Fact] + public void EscapesPercentSign() + { + var arg = new MsBuildArgument("/p:SomeValue=100%"); + Assert.Equal("/p:SomeValue=100%25", arg.ToString()); + } + + [Fact] + public void DoesNotDoubleEscapeAlreadyEscapedPercent() + { + var arg = new MsBuildArgument("/p:SomeValue=100%25", false); + Assert.Equal("/p:SomeValue=100%25", arg.ToString()); + } + public class PropertyDefine { private const bool customPropWasSet =