// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
namespace WixToolset.Core
{
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using WixToolset.Data;
using WixToolset.Data.Tuples;
using WixToolset.Extensibility;
///
/// WiX variable resolver.
///
internal sealed class WixVariableResolver : IBindVariableResolver
{
private Dictionary wixVariables;
///
/// Instantiate a new WixVariableResolver.
///
public WixVariableResolver(Localizer localizer = null)
{
this.wixVariables = new Dictionary();
this.Localizer = localizer;
}
///
/// Gets or sets the localizer.
///
/// The localizer.
private Localizer Localizer { get; }
///
/// Gets the count of variables added to the resolver.
///
public int VariableCount => this.wixVariables.Count;
///
/// Add a variable.
///
/// The name of the variable.
/// The value of the variable.
public void AddVariable(string name, string value)
{
try
{
this.wixVariables.Add(name, value);
}
catch (ArgumentException)
{
Messaging.Instance.OnMessage(WixErrors.WixVariableCollision(null, name));
}
}
///
/// Add a variable.
///
/// The WixVariableRow to add.
public void AddVariable(WixVariableTuple wixVariableRow)
{
try
{
this.wixVariables.Add(wixVariableRow.WixVariable, wixVariableRow.Value);
}
catch (ArgumentException)
{
if (!wixVariableRow.Overridable) // collision
{
Messaging.Instance.OnMessage(WixErrors.WixVariableCollision(wixVariableRow.SourceLineNumbers, wixVariableRow.WixVariable));
}
}
}
///
/// Resolve the wix variables in a value.
///
/// The source line information for the value.
/// The value to resolve.
/// true to only resolve localization variables; false otherwise.
/// The resolved value.
public string ResolveVariables(SourceLineNumber sourceLineNumbers, string value, bool localizationOnly)
{
return this.ResolveVariables(sourceLineNumbers, value, localizationOnly, out var defaultIgnored, out var delayedIgnored);
}
///
/// Resolve the wix variables in a value.
///
/// The source line information for the value.
/// The value to resolve.
/// true to only resolve localization variables; false otherwise.
/// true if the resolved value was the default.
/// The resolved value.
public string ResolveVariables(SourceLineNumber sourceLineNumbers, string value, bool localizationOnly, out bool isDefault)
{
return this.ResolveVariables(sourceLineNumbers, value, localizationOnly, out isDefault, out var ignored);
}
///
/// Resolve the wix variables in a value.
///
/// The source line information for the value.
/// The value to resolve.
/// true to only resolve localization variables; false otherwise.
/// true if unknown variables should throw errors.
/// true if the resolved value was the default.
/// true if the value has variables that cannot yet be resolved.
/// The resolved value.
public string ResolveVariables(SourceLineNumber sourceLineNumbers, string value, bool localizationOnly, out bool isDefault, out bool delayedResolve)
{
return this.ResolveVariables(sourceLineNumbers, value, localizationOnly, true, out isDefault, out delayedResolve);
}
///
/// Resolve the wix variables in a value.
///
/// The source line information for the value.
/// The value to resolve.
/// true to only resolve localization variables; false otherwise.
/// true if unknown variables should throw errors.
/// true if the resolved value was the default.
/// true if the value has variables that cannot yet be resolved.
/// The resolved value.
public string ResolveVariables(SourceLineNumber sourceLineNumbers, string value, bool localizationOnly, bool errorOnUnknown, out bool isDefault, out bool delayedResolve)
{
MatchCollection matches = Common.WixVariableRegex.Matches(value);
// the value is the default unless its substituted further down
isDefault = true;
delayedResolve = false;
if (0 < matches.Count)
{
StringBuilder sb = new StringBuilder(value);
// notice how this code walks backward through the list
// because it modifies the string as we through it
for (int i = matches.Count - 1; 0 <= i; i--)
{
string variableNamespace = matches[i].Groups["namespace"].Value;
string variableId = matches[i].Groups["fullname"].Value;
string variableDefaultValue = null;
// get the default value if one was specified
if (matches[i].Groups["value"].Success)
{
variableDefaultValue = matches[i].Groups["value"].Value;
// localization variables to not support inline default values
if ("loc" == variableNamespace)
{
Messaging.Instance.OnMessage(WixErrors.IllegalInlineLocVariable(sourceLineNumbers, variableId, variableDefaultValue));
}
}
// get the scope if one was specified
if (matches[i].Groups["scope"].Success)
{
if ("bind" == variableNamespace)
{
variableId = matches[i].Groups["name"].Value;
}
}
// check for an escape sequence of !! indicating the match is not a variable expression
if (0 < matches[i].Index && '!' == sb[matches[i].Index - 1])
{
if (!localizationOnly)
{
sb.Remove(matches[i].Index - 1, 1);
}
}
else
{
string resolvedValue = null;
if ("loc" == variableNamespace)
{
// warn about deprecated syntax of $(loc.var)
if ('$' == sb[matches[i].Index])
{
Messaging.Instance.OnMessage(WixWarnings.DeprecatedLocalizationVariablePrefix(sourceLineNumbers, variableId));
}
resolvedValue = this.Localizer?.GetLocalizedValue(variableId);
}
else if (!localizationOnly && "wix" == variableNamespace)
{
// illegal syntax of $(wix.var)
if ('$' == sb[matches[i].Index])
{
Messaging.Instance.OnMessage(WixErrors.IllegalWixVariablePrefix(sourceLineNumbers, variableId));
}
else
{
if (this.wixVariables.TryGetValue(variableId, out resolvedValue))
{
resolvedValue = resolvedValue ?? String.Empty;
isDefault = false;
}
else if (null != variableDefaultValue) // default the resolved value to the inline value if one was specified
{
resolvedValue = variableDefaultValue;
}
}
}
if ("bind" == variableNamespace)
{
// can't resolve these yet, but keep track of where we find them so they can be resolved later with less effort
delayedResolve = true;
}
else
{
// insert the resolved value if it was found or display an error
if (null != resolvedValue)
{
sb.Remove(matches[i].Index, matches[i].Length);
sb.Insert(matches[i].Index, resolvedValue);
}
else if ("loc" == variableNamespace && errorOnUnknown) // unresolved loc variable
{
Messaging.Instance.OnMessage(WixErrors.LocalizationVariableUnknown(sourceLineNumbers, variableId));
}
else if (!localizationOnly && "wix" == variableNamespace && errorOnUnknown) // unresolved wix variable
{
Messaging.Instance.OnMessage(WixErrors.WixVariableUnknown(sourceLineNumbers, variableId));
}
}
}
}
value = sb.ToString();
}
return value;
}
///
/// Try to find localization information for dialog and (optional) control.
///
/// Dialog identifier.
/// Optional control identifier.
/// Found localization information.
/// True if localized control was found, otherwise false.
public bool TryGetLocalizedControl(string dialog, string control, out LocalizedControl localizedControl)
{
localizedControl = this.Localizer?.GetLocalizedControl(dialog, control);
return localizedControl != null;
}
///
/// Resolve the delay variables in a value.
///
/// The source line information for the value.
/// The value to resolve.
///
/// The resolved value.
[SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "sourceLineNumbers")]
[SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "This string is not round tripped, and not used for any security decisions")]
public static string ResolveDelayedVariables(SourceLineNumber sourceLineNumbers, string value, IDictionary resolutionData)
{
MatchCollection matches = Common.WixVariableRegex.Matches(value);
if (0 < matches.Count)
{
StringBuilder sb = new StringBuilder(value);
// notice how this code walks backward through the list
// because it modifies the string as we go through it
for (int i = matches.Count - 1; 0 <= i; i--)
{
string variableNamespace = matches[i].Groups["namespace"].Value;
string variableId = matches[i].Groups["fullname"].Value;
string variableDefaultValue = null;
string variableScope = null;
// get the default value if one was specified
if (matches[i].Groups["value"].Success)
{
variableDefaultValue = matches[i].Groups["value"].Value;
}
// get the scope if one was specified
if (matches[i].Groups["scope"].Success)
{
variableScope = matches[i].Groups["scope"].Value;
if ("bind" == variableNamespace)
{
variableId = matches[i].Groups["name"].Value;
}
}
// check for an escape sequence of !! indicating the match is not a variable expression
if (0 < matches[i].Index && '!' == sb[matches[i].Index - 1])
{
sb.Remove(matches[i].Index - 1, 1);
}
else
{
string key = String.Format(CultureInfo.InvariantCulture, "{0}.{1}", variableId, variableScope).ToLower(CultureInfo.InvariantCulture);
string resolvedValue = variableDefaultValue;
if (resolutionData.ContainsKey(key))
{
resolvedValue = resolutionData[key];
}
if ("bind" == variableNamespace)
{
// insert the resolved value if it was found or display an error
if (null != resolvedValue)
{
sb.Remove(matches[i].Index, matches[i].Length);
sb.Insert(matches[i].Index, resolvedValue);
}
else
{
throw new WixException(WixErrors.UnresolvedBindReference(sourceLineNumbers, value));
}
}
}
}
value = sb.ToString();
}
return value;
}
}
}