// 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.Text; using WixToolset.Data; using WixToolset.Data.Bind; using WixToolset.Extensibility.Services; /// /// WiX variable resolver. /// internal class WixVariableResolver : IVariableResolver { private readonly Dictionary locVariables; private readonly Dictionary wixVariables; private readonly Dictionary localizedControls; /// /// Instantiate a new WixVariableResolver. /// public WixVariableResolver(IMessaging messaging) { this.locVariables = new Dictionary(); this.wixVariables = new Dictionary(); this.localizedControls = new Dictionary(); this.Codepage = -1; this.Messaging = messaging; } private IMessaging Messaging { get; } public int Codepage { get; private set; } public int VariableCount => this.wixVariables.Count; public void AddLocalization(Localization localization) { if (-1 == this.Codepage) { this.Codepage = localization.Codepage; } foreach (var variable in localization.Variables) { if (!TryAddWixVariable(this.locVariables, variable)) { this.Messaging.Write(ErrorMessages.DuplicateLocalizationIdentifier(variable.SourceLineNumbers, variable.Id)); } } foreach (KeyValuePair localizedControl in localization.LocalizedControls) { if (!this.localizedControls.ContainsKey(localizedControl.Key)) { this.localizedControls.Add(localizedControl.Key, localizedControl.Value); } } } public void AddVariable(SourceLineNumber sourceLineNumber, string name, string value, bool overridable) { var bindVariable = new BindVariable { Id = name, Value = value, Overridable = overridable, SourceLineNumbers = sourceLineNumber }; if (!TryAddWixVariable(this.wixVariables, bindVariable)) { this.Messaging.Write(ErrorMessages.WixVariableCollision(sourceLineNumber, name)); } } public VariableResolution ResolveVariables(SourceLineNumber sourceLineNumbers, string value, bool localizationOnly) { return this.ResolveVariables(sourceLineNumbers, value, localizationOnly, true); } public bool TryGetLocalizedControl(string dialog, string control, out LocalizedControl localizedControl) { var key = LocalizedControl.GetKey(dialog, control); return this.localizedControls.TryGetValue(key, out localizedControl); } /// /// 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. internal VariableResolution ResolveVariables(SourceLineNumber sourceLineNumbers, string value, bool localizationOnly, bool errorOnUnknown) { var matches = Common.WixVariableRegex.Matches(value); // the value is the default unless its substituted further down var result = new VariableResolution { IsDefault = true, Value = value }; if (0 < matches.Count) { var 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--) { var variableNamespace = matches[i].Groups["namespace"].Value; var 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) { this.Messaging.Write(ErrorMessages.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); result.UpdatedValue = true; } } else { string resolvedValue = null; if ("loc" == variableNamespace) { // warn about deprecated syntax of $(loc.var) if ('$' == sb[matches[i].Index]) { this.Messaging.Write(WarningMessages.DeprecatedLocalizationVariablePrefix(sourceLineNumbers, variableId)); } if (this.locVariables.TryGetValue(variableId, out var bindVariable)) { resolvedValue = bindVariable.Value; } } else if (!localizationOnly && "wix" == variableNamespace) { // illegal syntax of $(wix.var) if ('$' == sb[matches[i].Index]) { this.Messaging.Write(ErrorMessages.IllegalWixVariablePrefix(sourceLineNumbers, variableId)); } else { if (this.wixVariables.TryGetValue(variableId, out var bindVariable)) { resolvedValue = bindVariable.Value ?? String.Empty; result.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 result.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); result.UpdatedValue = true; } else if ("loc" == variableNamespace && errorOnUnknown) // unresolved loc variable { this.Messaging.Write(ErrorMessages.LocalizationVariableUnknown(sourceLineNumbers, variableId)); } else if (!localizationOnly && "wix" == variableNamespace && errorOnUnknown) // unresolved wix variable { this.Messaging.Write(ErrorMessages.WixVariableUnknown(sourceLineNumbers, variableId)); } } } } result.Value = sb.ToString(); } return result; } private static bool TryAddWixVariable(IDictionary variables, BindVariable variable) { if (!variables.TryGetValue(variable.Id, out var existingWixVariableRow) || (existingWixVariableRow.Overridable && !variable.Overridable)) { variables[variable.Id] = variable; return true; } return variable.Overridable; } } }