Skip to content

Commit

Permalink
Parser for actual MSBuild Property option syntax (dotnet#27086)
Browse files Browse the repository at this point in the history
Adds a parser that can handle all the msbuild property variations, so that arguments that contain a semicolon-
delimited key/value list are correctly escaped.
  • Loading branch information
baronfel authored and nagilson committed Aug 16, 2022
1 parent 0d5fac1 commit 6e0e9b0
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 2 deletions.
93 changes: 93 additions & 0 deletions src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildPropertyParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;

#nullable enable

namespace Microsoft.DotNet.Cli.Utils;

public static class MSBuildPropertyParser {
public static IEnumerable<(string key, string value)> ParseProperties(string input) {
var currentPos = 0;
StringBuilder currentKey = new StringBuilder();
StringBuilder currentValue = new StringBuilder();

(string key, string value) EmitAndReset() {
var key = currentKey.ToString();
var value= currentValue.ToString();
currentKey.Clear();
currentValue.Clear();
return (key, value);
}

char? Peek() => currentPos < input.Length ? input[currentPos] : null;

bool TryConsume(out char? consumed) {
if(input.Length > currentPos) {
consumed = input[currentPos];
currentPos++;
return true;
} else {
consumed = null;
return false;
}
}

void ParseKey() {
while (TryConsume(out var c) && c != '=') {
currentKey.Append(c);
}
}

void ParseQuotedValue() {
TryConsume(out var leadingQuote); // consume the leading quote, which we know is there
currentValue.Append(leadingQuote);
while(TryConsume(out char? c)) {
currentValue.Append(c);
if (c == '"') {
// we're done
return;
}
if (c == '\\' && Peek() == '"')
{
// consume the escaped quote
TryConsume(out var c2);
currentValue.Append(c2);
}
}
}

void ParseUnquotedValue() {
while(TryConsume(out char? c) && c != ';') {
currentValue.Append(c);
}
}

void ParseValue() {
if (Peek() == '"') {
ParseQuotedValue();
} else {
ParseUnquotedValue();
}
}

(string key, string value) ParseKeyValue() {
ParseKey();
ParseValue();
return EmitAndReset();
}

bool AtEnd() => currentPos == input.Length;

while (!(AtEnd())) {
yield return ParseKeyValue();
if (Peek() is char c && (c == ';' || c== ',')) {
TryConsume(out _); // swallow the next semicolon or comma delimiter
}
}
}
}
5 changes: 3 additions & 2 deletions src/Cli/dotnet/OptionForwardingExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ public static class OptionForwardingExtensions
public static ForwardedOption<string[]> ForwardAsProperty(this ForwardedOption<string[]> option) => option
.SetForwardingFunction((optionVals) =>
optionVals
.Select(optionVal => optionVal.Replace(";", "%3B")) // must escape semicolon-delimited property values when forwarding them to MSBuild
.Select(optionVal => $"{option.Aliases.FirstOrDefault()}:{optionVal}")
.SelectMany(Microsoft.DotNet.Cli.Utils.MSBuildPropertyParser.ParseProperties)
// must escape semicolon-delimited property values when forwarding them to MSBuild
.Select(keyValue => $"{option.Aliases.FirstOrDefault()}:{keyValue.key}={keyValue.value.Replace(";", "%3B")}")
);

public static Option<T> ForwardAsMany<T>(this ForwardedOption<T> option, Func<T, IEnumerable<string>> format) => option.SetForwardingFunction(format);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ public void MSBuildArgumentsAreForwardedCorrectly(string[] arguments, bool build
[InlineData(new string[] { "-p:teamcity_buildConfName=\"Build, Test and Publish\"" }, new string[] { "--property:teamcity_buildConfName=\"Build, Test and Publish\"" })]
[InlineData(new string[] { "-p:prop1=true", "-p:prop2=false" }, new string[] { "--property:prop1=true", "--property:prop2=false" })]
[InlineData(new string[] { "-p:prop1=\".;/opt/usr\"" }, new string[] { "--property:prop1=\".%3B/opt/usr\"" })]
[InlineData(new string[] { "-p:prop1=true;prop2=false;prop3=\"wut\";prop4=\"1;2;3\"" }, new string[]{ "--property:prop1=true", "--property:prop2=false", "--property:prop3=\"wut\"", "--property:prop4=\"1%3B2%3B3\""})]
[InlineData(new string[] { "-p:prop4=\"1;2;3\"" }, new string[]{ "--property:prop4=\"1%3B2%3B3\""})]
[InlineData(new string[] { "-p:prop4=\"1 ;2 ;3 \"" }, new string[]{ "--property:prop4=\"1 %3B2 %3B3 \""})]
public void Can_pass_msbuild_properties_safely(string[] tokens, string[] forwardedTokens) {
var forwardingFunction = (CommonOptions.PropertiesOption as ForwardedOption<string[]>).GetForwardingFunction();
var result = CommonOptions.PropertiesOption.Parse(tokens);
Expand Down

0 comments on commit 6e0e9b0

Please sign in to comment.