// 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 { using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Xml.Linq; using WixToolset.Data; using WixToolset.Extensibility; /// /// The preprocessor core. /// internal class PreprocessorCore : IPreprocessorCore { private static readonly char[] variableSplitter = new char[] { '.' }; private static readonly char[] argumentSplitter = new char[] { ',' }; private Platform currentPlatform; private Dictionary extensionsByPrefix; private string sourceFile; private IDictionary variables; /// /// Instantiate a new PreprocessorCore. /// /// The extensions indexed by their prefixes. /// The message handler. /// The source file being preprocessed. /// The variables defined prior to preprocessing. internal PreprocessorCore(Dictionary extensionsByPrefix, string sourceFile, IDictionary variables) { this.extensionsByPrefix = extensionsByPrefix; this.sourceFile = String.IsNullOrEmpty(sourceFile) ? null : Path.GetFullPath(sourceFile); this.variables = new Dictionary(); foreach (var entry in variables) { this.AddVariable(null, entry.Key, entry.Value); } } /// /// Event for resolved variables. /// private event ResolvedVariableEventHandler ResolvedVariable; /// /// Sets event for ResolvedVariableEventHandler. /// public ResolvedVariableEventHandler ResolvedVariableHandler { set { this.ResolvedVariable = value; } } /// /// Gets or sets the platform which the compiler will use when defaulting 64-bit attributes and elements. /// /// The platform which the compiler will use when defaulting 64-bit attributes and elements. public Platform CurrentPlatform { get { return this.currentPlatform; } set { this.currentPlatform = value; } } /// /// Gets whether the core encountered an error while processing. /// /// Flag if core encountered an error during processing. public bool EncounteredError { get { return Messaging.Instance.EncounteredError; } } /// /// Replaces parameters in the source text. /// /// The source line information for the function. /// Text that may contain parameters to replace. /// Text after parameters have been replaced. public string PreprocessString(SourceLineNumber sourceLineNumbers, string value) { StringBuilder sb = new StringBuilder(); int currentPosition = 0; int end = 0; while (-1 != (currentPosition = value.IndexOf('$', end))) { if (end < currentPosition) { sb.Append(value, end, currentPosition - end); } end = currentPosition + 1; string remainder = value.Substring(end); if (remainder.StartsWith("$", StringComparison.Ordinal)) { sb.Append("$"); end++; } else if (remainder.StartsWith("(loc.", StringComparison.Ordinal)) { currentPosition = remainder.IndexOf(')'); if (-1 == currentPosition) { this.OnMessage(WixErrors.InvalidPreprocessorVariable(sourceLineNumbers, remainder)); break; } sb.Append("$"); // just put the resource reference back as was sb.Append(remainder, 0, currentPosition + 1); end += currentPosition + 1; } else if (remainder.StartsWith("(", StringComparison.Ordinal)) { int openParenCount = 1; int closingParenCount = 0; bool isFunction = false; bool foundClosingParen = false; // find the closing paren int closingParenPosition; for (closingParenPosition = 1; closingParenPosition < remainder.Length; closingParenPosition++) { switch (remainder[closingParenPosition]) { case '(': openParenCount++; isFunction = true; break; case ')': closingParenCount++; break; } if (openParenCount == closingParenCount) { foundClosingParen = true; break; } } // move the currentPosition to the closing paren currentPosition += closingParenPosition; if (!foundClosingParen) { if (isFunction) { this.OnMessage(WixErrors.InvalidPreprocessorFunction(sourceLineNumbers, remainder)); break; } else { this.OnMessage(WixErrors.InvalidPreprocessorVariable(sourceLineNumbers, remainder)); break; } } string subString = remainder.Substring(1, closingParenPosition - 1); string result = null; if (isFunction) { result = this.EvaluateFunction(sourceLineNumbers, subString); } else { result = this.GetVariableValue(sourceLineNumbers, subString, false); } if (null == result) { if (isFunction) { this.OnMessage(WixErrors.UndefinedPreprocessorFunction(sourceLineNumbers, subString)); break; } else { this.OnMessage(WixErrors.UndefinedPreprocessorVariable(sourceLineNumbers, subString)); break; } } else { if (!isFunction) { this.OnResolvedVariable(new ResolvedVariableEventArgs(sourceLineNumbers, subString, result)); } } sb.Append(result); end += closingParenPosition + 1; } else // just a floating "$" so put it in the final string (i.e. leave it alone) and keep processing { sb.Append('$'); } } if (end < value.Length) { sb.Append(value.Substring(end)); } return sb.ToString(); } /// /// Evaluate a Pragma. /// /// The source line information for the function. /// The pragma's full name (.). /// The arguments to the pragma. /// The parent element of the pragma. public void PreprocessPragma(SourceLineNumber sourceLineNumbers, string pragmaName, string args, XContainer parent) { string[] prefixParts = pragmaName.Split(variableSplitter, 2); // Check to make sure there are 2 parts and neither is an empty string. if (2 != prefixParts.Length) { throw new WixException(WixErrors.InvalidPreprocessorPragma(sourceLineNumbers, pragmaName)); } string prefix = prefixParts[0]; string pragma = prefixParts[1]; if (String.IsNullOrEmpty(prefix) || String.IsNullOrEmpty(pragma)) { throw new WixException(WixErrors.InvalidPreprocessorPragma(sourceLineNumbers, pragmaName)); } switch (prefix) { case "wix": switch (pragma) { // Add any core defined pragmas here default: this.OnMessage(WixWarnings.PreprocessorUnknownPragma(sourceLineNumbers, pragmaName)); break; } break; default: PreprocessorExtension extension = (PreprocessorExtension)this.extensionsByPrefix[prefix]; if (null == extension || !extension.ProcessPragma(sourceLineNumbers, prefix, pragma, args, parent)) { this.OnMessage(WixWarnings.PreprocessorUnknownPragma(sourceLineNumbers, pragmaName)); } break; } } /// /// Evaluate a function. /// /// The source line information for the function. /// The function expression including the prefix and name. /// The function value. public string EvaluateFunction(SourceLineNumber sourceLineNumbers, string function) { string[] prefixParts = function.Split(variableSplitter, 2); // Check to make sure there are 2 parts and neither is an empty string. if (2 != prefixParts.Length || 0 >= prefixParts[0].Length || 0 >= prefixParts[1].Length) { throw new WixException(WixErrors.InvalidPreprocessorFunction(sourceLineNumbers, function)); } string prefix = prefixParts[0]; string[] functionParts = prefixParts[1].Split(new char[] { '(' }, 2); // Check to make sure there are 2 parts, neither is an empty string, and the second part ends with a closing paren. if (2 != functionParts.Length || 0 >= functionParts[0].Length || 0 >= functionParts[1].Length || !functionParts[1].EndsWith(")", StringComparison.Ordinal)) { throw new WixException(WixErrors.InvalidPreprocessorFunction(sourceLineNumbers, function)); } string functionName = functionParts[0]; // Remove the trailing closing paren. string allArgs = functionParts[1].Substring(0, functionParts[1].Length - 1); // Parse the arguments and preprocess them. string[] args = allArgs.Split(argumentSplitter); for (int i = 0; i < args.Length; i++) { args[i] = this.PreprocessString(sourceLineNumbers, args[i].Trim()); } string result = this.EvaluateFunction(sourceLineNumbers, prefix, functionName, args); // If the function didn't evaluate, try to evaluate the original value as a variable to support // the use of open and closed parens inside variable names. Example: $(env.ProgramFiles(x86)) should resolve. if (null == result) { result = this.GetVariableValue(sourceLineNumbers, function, false); } return result; } /// /// Evaluate a function. /// /// The source line information for the function. /// The function prefix. /// The function name. /// The arguments for the function. /// The function value or null if the function is not defined. public string EvaluateFunction(SourceLineNumber sourceLineNumbers, string prefix, string function, string[] args) { if (String.IsNullOrEmpty(prefix)) { throw new ArgumentNullException("prefix"); } if (String.IsNullOrEmpty(function)) { throw new ArgumentNullException("function"); } switch (prefix) { case "fun": switch (function) { case "AutoVersion": // Make sure the base version is specified if (args.Length == 0 || String.IsNullOrEmpty(args[0])) { throw new WixException(WixErrors.InvalidPreprocessorFunctionAutoVersion(sourceLineNumbers)); } // Build = days since 1/1/2000; Revision = seconds since midnight / 2 DateTime now = DateTime.UtcNow; TimeSpan build = now - new DateTime(2000, 1, 1); TimeSpan revision = now - new DateTime(now.Year, now.Month, now.Day); return String.Join(".", args[0], (int)build.TotalDays, (int)(revision.TotalSeconds / 2)); default: return null; } default: PreprocessorExtension extension = (PreprocessorExtension)this.extensionsByPrefix[prefix]; if (null != extension) { try { return extension.EvaluateFunction(prefix, function, args); } catch (Exception e) { throw new WixException(WixErrors.PreprocessorExtensionEvaluateFunctionFailed(sourceLineNumbers, prefix, function, String.Join(",", args), e.Message)); } } else { return null; } } } /// /// Get the value of a variable expression like var.name. /// /// The source line information for the variable. /// The variable expression including the optional prefix and name. /// true to allow the variable prefix to be missing. /// The variable value. public string GetVariableValue(SourceLineNumber sourceLineNumbers, string variable, bool allowMissingPrefix) { // Strip the "$(" off the front. if (variable.StartsWith("$(", StringComparison.Ordinal)) { variable = variable.Substring(2); } string[] parts = variable.Split(variableSplitter, 2); if (1 == parts.Length) // missing prefix { if (allowMissingPrefix) { return this.GetVariableValue(sourceLineNumbers, "var", parts[0]); } else { throw new WixException(WixErrors.InvalidPreprocessorVariable(sourceLineNumbers, variable)); } } else { // check for empty variable name if (0 < parts[1].Length) { string result = this.GetVariableValue(sourceLineNumbers, parts[0], parts[1]); // If we didn't find it and we allow missing prefixes and the variable contains a dot, perhaps the dot isn't intended to indicate a prefix if (null == result && allowMissingPrefix && variable.Contains(".")) { result = this.GetVariableValue(sourceLineNumbers, "var", variable); } return result; } else { throw new WixException(WixErrors.InvalidPreprocessorVariable(sourceLineNumbers, variable)); } } } /// /// Get the value of a variable. /// /// The source line information for the function. /// The variable prefix. /// The variable name. /// The variable value or null if the variable is not set. public string GetVariableValue(SourceLineNumber sourceLineNumbers, string prefix, string name) { if (String.IsNullOrEmpty(prefix)) { throw new ArgumentNullException("prefix"); } if (String.IsNullOrEmpty(name)) { throw new ArgumentNullException("name"); } switch (prefix) { case "env": return Environment.GetEnvironmentVariable(name); case "sys": switch (name) { case "CURRENTDIR": return String.Concat(Directory.GetCurrentDirectory(), Path.DirectorySeparatorChar); case "SOURCEFILEDIR": return String.Concat(Path.GetDirectoryName(sourceLineNumbers.FileName), Path.DirectorySeparatorChar); case "SOURCEFILEPATH": return sourceLineNumbers.FileName; case "PLATFORM": this.OnMessage(WixWarnings.DeprecatedPreProcVariable(sourceLineNumbers, "$(sys.PLATFORM)", "$(sys.BUILDARCH)")); goto case "BUILDARCH"; case "BUILDARCH": switch (this.currentPlatform) { case Platform.X86: return "x86"; case Platform.X64: return "x64"; case Platform.IA64: return "ia64"; case Platform.ARM: return "arm"; default: throw new ArgumentException(WixStrings.EXP_UnknownPlatformEnum, this.currentPlatform.ToString()); } default: return null; } case "var": string result = null; return this.variables.TryGetValue(name, out result) ? result : null; default: PreprocessorExtension extension = (PreprocessorExtension)this.extensionsByPrefix[prefix]; if (null != extension) { try { return extension.GetVariableValue(prefix, name); } catch (Exception e) { throw new WixException(WixErrors.PreprocessorExtensionGetVariableValueFailed(sourceLineNumbers, prefix, name, e.Message)); } } else { return null; } } } /// /// Sends a message to the message delegate if there is one. /// /// Message event arguments. public void OnMessage(MessageEventArgs e) { Messaging.Instance.OnMessage(e); } /// /// Sends resolved variable to delegate if there is one. /// /// Message event arguments. public void OnResolvedVariable(ResolvedVariableEventArgs mea) { if (null != this.ResolvedVariable) { this.ResolvedVariable(this, mea); } } /// /// Add a variable. /// /// The source line information of the variable. /// The variable name. /// The variable value. internal void AddVariable(SourceLineNumber sourceLineNumbers, string name, string value) { this.AddVariable(sourceLineNumbers, name, value, true); } /// /// Add a variable. /// /// The source line information of the variable. /// The variable name. /// The variable value. /// Set to true to show variable overwrite warning. internal void AddVariable(SourceLineNumber sourceLineNumbers, string name, string value, bool showWarning) { string currentValue = this.GetVariableValue(sourceLineNumbers, "var", name); if (null == currentValue) { this.variables.Add(name, value); } else { if (showWarning) { this.OnMessage(WixWarnings.VariableDeclarationCollision(sourceLineNumbers, name, value, currentValue)); } this.variables[name] = value; } } /// /// Remove a variable. /// /// The source line information of the variable. /// The variable name. internal void RemoveVariable(SourceLineNumber sourceLineNumbers, string name) { if (!this.variables.Remove(name)) { this.OnMessage(WixErrors.CannotReundefineVariable(sourceLineNumbers, name)); } } } }