|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.NET.StringTools;
#nullable disable
#if BUILD_ENGINE
namespace Microsoft.Build.BackEnd
#else
using Microsoft.Build.Utilities;
namespace Microsoft.Build.Tasks
#endif
{
internal static class PropertyParser
{
/// <summary>
/// Given a string of semi-colon delimited name=value pairs, this method parses it and creates
/// a hash table containing the property names as keys and the property values as values.
/// </summary>
/// <returns>true on success, false on failure.</returns>
internal static bool GetTable(TaskLoggingHelper log, string parameterName, string[] propertyList, out Dictionary<string, string> propertiesTable)
{
propertiesTable = null;
if (propertyList != null)
{
propertiesTable = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
// Loop through the array. Each string in the array should be of the form:
// MyPropName=MyPropValue
foreach (string propertyNameValuePair in propertyList)
{
string propertyName = String.Empty;
string propertyValue = String.Empty;
// Find the first '=' sign in the string.
int indexOfEqualsSign = propertyNameValuePair.IndexOf('=');
// If we found one, then grab the stuff before it and put it into "propertyName",
// and grab the stuff after it and put it into "propertyValue". But trim the
// whitespace from beginning and end of both name and value. (When authoring a
// project/targets file, people like to use whitespace and newlines to pretty up
// the file format.)
if (indexOfEqualsSign != -1)
{
propertyName = propertyNameValuePair.Substring(0, indexOfEqualsSign).Trim();
propertyValue = propertyNameValuePair.Substring(indexOfEqualsSign + 1).Trim();
}
// Make sure we have a property name and property value (though the value is allowed to be blank).
if (propertyName.Length == 0)
{
// No equals sign? No property name? That's no good to us.
log?.LogErrorWithCodeFromResources("General.InvalidPropertyError", parameterName, propertyNameValuePair);
return false;
}
// Bag the property and its value. Trim whitespace from beginning and end of
// both name and value. (When authoring a project/targets file, people like to
// use whitespace and newlines to pretty up the file format.)
propertiesTable[propertyName] = propertyValue;
}
}
return true;
}
/// <summary>
/// Given a string of semi-colon delimited name=value pairs, this method parses it and creates
/// a hash table containing the property names as keys and the property values as values.
/// This method escapes any special characters found in the property values, in case they
/// are going to be passed to a method (such as that expects the appropriate escaping to have happened
/// already.
/// </summary>
/// <returns>true on success, false on failure.</returns>
internal static bool GetTableWithEscaping(TaskLoggingHelper log, string parameterName, string syntaxName, string[] propertyNameValueStrings, out Dictionary<string, string> finalPropertiesTable)
{
finalPropertiesTable = null;
if (propertyNameValueStrings != null)
{
finalPropertiesTable = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var finalPropertiesList = new List<PropertyNameValuePair>();
// Loop through the array. Each string in the array should be of the form:
// MyPropName=MyPropValue
foreach (string propertyNameValueString in propertyNameValueStrings)
{
// Find the first '=' sign in the string.
int indexOfEqualsSign = propertyNameValueString.IndexOf('=');
if (indexOfEqualsSign != -1)
{
// If we found one, then grab the stuff before it and put it into "propertyName",
// and grab the stuff after it and put it into "propertyValue". But trim the
// whitespace from beginning and end of both name and value. (When authoring a
// project/targets file, people like to use whitespace and newlines to pretty up
// the file format.)
string propertyName = propertyNameValueString.Substring(0, indexOfEqualsSign).Trim();
string propertyValue = EscapingUtilities.Escape(propertyNameValueString.Substring(indexOfEqualsSign + 1).Trim());
// Make sure we have a property name and property value (though the value is allowed to be blank).
if (propertyName.Length == 0)
{
// No property name? That's no good to us.
log?.LogErrorWithCodeFromResources("General.InvalidPropertyError", syntaxName, propertyNameValueString);
return false;
}
// Store the property in our list.
finalPropertiesList.Add(new PropertyNameValuePair(propertyName, propertyValue));
}
else
{
// There's no '=' sign in the string. When this happens, we treat this string as basically
// an appendage on the value of the previous property. For example, if the project file contains
//
// <PropertyGroup>
// <WarningsAsErrors>1234;5678;9999</WarningsAsErrors>
// </PropertyGroup>
// <Target Name="Build">
// <MSBuild Projects="ConsoleApplication1.csproj"
// Properties="WarningsAsErrors=$(WarningsAsErrors)"/>
// </Target>
//
// , then this method (GetTableWithEscaping) will see this:
//
// propertyNameValueStrings[0] = "WarningsAsErrors=1234"
// propertyNameValueStrings[1] = "5678"
// propertyNameValueStrings[2] = "9999"
//
// And what we actually want to end up with in our final hashtable is this:
//
// NAME VALUE
// =================== ================================
// WarningsAsErrors 1234;5678;9999
//
if (finalPropertiesList.Count > 0)
{
// There was a property definition previous to this one. Append the current string
// to that previous value, using semicolon as a separator.
string propertyValue = EscapingUtilities.Escape(propertyNameValueString.Trim());
finalPropertiesList[finalPropertiesList.Count - 1].Value.Add(propertyValue);
}
else
{
// No equals sign in the very first property? That's a problem.
log?.LogErrorWithCodeFromResources("General.InvalidPropertyError", syntaxName, propertyNameValueString);
return false;
}
}
}
// Convert the data in the List to a Hashtable, because that's what the MSBuild task eventually
// needs to pass onto the engine.
log?.LogMessageFromText(parameterName, MessageImportance.Low);
using SpanBasedStringBuilder stringBuilder = Strings.GetSpanBasedStringBuilder();
foreach (PropertyNameValuePair propertyNameValuePair in finalPropertiesList)
{
stringBuilder.Clear();
bool needsSemicolon = false;
foreach (string valueFragment in propertyNameValuePair.Value)
{
if (needsSemicolon)
{
stringBuilder.Append(";");
}
needsSemicolon = true;
stringBuilder.Append(valueFragment);
}
string propertyValue = stringBuilder.ToString();
finalPropertiesTable[propertyNameValuePair.Name] = propertyValue;
log?.LogMessageFromText(
$" {propertyNameValuePair.Name}={propertyValue}",
MessageImportance.Low);
}
}
return true;
}
/// <summary>
/// A very simple class that holds two strings, a property name and property value.
/// </summary>
private class PropertyNameValuePair
{
/// <summary>
/// Property name
/// </summary>
internal string Name { get; }
/// <summary>
/// Property value fragments. Join with semicolon to get the final value.
/// </summary>
internal List<string> Value { get; }
internal PropertyNameValuePair(string propertyName, string propertyValue)
{
Name = propertyName;
Value = new List<string>
{
propertyValue
};
}
}
}
}
|