// 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.Globalization; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Xml; using System.Xml.Linq; using WixToolset.Core.Preprocess; using WixToolset.Data; using WixToolset.Extensibility; using WixToolset.Extensibility.Data; using WixToolset.Extensibility.Services; /// /// Preprocessor object /// internal class Preprocessor { private static readonly Regex DefineRegex = new Regex(@"^\s*(?.+?)\s*(=\s*(?.+?)\s*)?$", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.ExplicitCapture); private static readonly Regex PragmaRegex = new Regex(@"^\s*(?.+?)(?[\s\(].+?)?$", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.ExplicitCapture); private static readonly XmlReaderSettings DocumentXmlReaderSettings = new XmlReaderSettings() { ValidationFlags = System.Xml.Schema.XmlSchemaValidationFlags.None, XmlResolver = null, }; private static readonly XmlReaderSettings FragmentXmlReaderSettings = new XmlReaderSettings() { ConformanceLevel = ConformanceLevel.Fragment, ValidationFlags = System.Xml.Schema.XmlSchemaValidationFlags.None, XmlResolver = null, }; internal Preprocessor(IServiceProvider serviceProvider) { this.ServiceProvider = serviceProvider; this.Messaging = this.ServiceProvider.GetService(); } public IEnumerable IncludeSearchPaths { get; set; } public Platform Platform { get; set; } public string SourcePath { get; set; } public IDictionary Variables { get; set; } private IServiceProvider ServiceProvider { get; } private IMessaging Messaging { get; } private IPreprocessContext Context { get; set; } private Stack CurrentFileStack { get; } = new Stack(); private Dictionary ExtensionsByPrefix { get; } = new Dictionary(); private Stack IncludeNextStack { get; } = new Stack(); private Stack SourceStack { get; } = new Stack(); private IPreprocessHelper Helper { get; set; } /// /// Event for ifdef/ifndef directives. /// public event IfDefEventHandler IfDef; /// /// Event for included files. /// public event IncludedFileEventHandler IncludedFile; /// /// Event for preprocessed stream. /// public event ProcessedStreamEventHandler ProcessedStream; /// /// Event for resolved variables. /// public event ResolvedVariableEventHandler ResolvedVariable; /// /// Get the source line information for the current element. The precompiler will insert /// special source line number information for each element that it encounters. /// /// Element to get source line information for. /// /// The source line number used to author the element being processed or /// null if the preprocessor did not process the element or the node is /// not an element. /// public static SourceLineNumber GetSourceLineNumbers(XObject node) { return SourceLineNumber.GetFromXAnnotation(node); } /// /// Preprocesses a file. /// /// The preprocessing context. /// XDocument with the postprocessed data. public XDocument Execute() { this.Context = this.CreateContext(); this.PreProcess(); XDocument document; using (XmlReader reader = XmlReader.Create(this.Context.SourceFile, DocumentXmlReaderSettings)) { document = this.Process(reader); } return PostProcess(document); } /// /// Preprocesses a file. /// /// The preprocessing context. /// XmlReader to processing the context. /// XDocument with the postprocessed data. public XDocument Execute(XmlReader reader) { if (String.IsNullOrEmpty(this.SourcePath) && !String.IsNullOrEmpty(reader.BaseURI)) { var uri = new Uri(reader.BaseURI); this.SourcePath = uri.AbsolutePath; } this.Context = this.CreateContext(); this.PreProcess(); var document = this.Process(reader); return PostProcess(document); } /// /// Preprocesses a file. /// /// The preprocessing context. /// XmlReader to processing the context. /// XDocument with the postprocessed data. private XDocument Process(XmlReader reader) { this.Helper = this.ServiceProvider.GetService(); this.CurrentFileStack.Clear(); this.CurrentFileStack.Push(this.Helper.GetVariableValue(this.Context, "sys", "SOURCEFILEDIR")); // Process the reader into the output. XDocument output = new XDocument(); try { this.PreprocessReader(false, reader, output, 0); // Fire event with post-processed document. this.ProcessedStream?.Invoke(this, new ProcessedStreamEventArgs(this.Context.SourceFile, output)); } catch (XmlException e) { this.UpdateCurrentLineNumber(reader, 0); throw new WixException(ErrorMessages.InvalidXml(this.Context.CurrentSourceLineNumber, "source", e.Message)); } return this.Messaging.EncounteredError ? null : output; } /// /// Determins if string is an operator. /// /// String to check. /// true if string is an operator. private static bool IsOperator(string operation) { if (operation == null) { return false; } operation = operation.Trim(); if (0 == operation.Length) { return false; } if ("=" == operation || "!=" == operation || "<" == operation || "<=" == operation || ">" == operation || ">=" == operation || "~=" == operation) { return true; } return false; } /// /// Determines if expression is currently inside quotes. /// /// Expression to evaluate. /// Index to start searching in expression. /// true if expression is inside in quotes. private static bool InsideQuotes(string expression, int index) { if (index == -1) { return false; } int numQuotes = 0; int tmpIndex = 0; while (-1 != (tmpIndex = expression.IndexOf('\"', tmpIndex, index - tmpIndex))) { numQuotes++; tmpIndex++; } // found an even number of quotes before the index, so we're not inside if (numQuotes % 2 == 0) { return false; } // found an odd number of quotes, so we are inside return true; } /// /// Tests expression to see if it starts with a keyword. /// /// Expression to test. /// Operation to test for. /// true if expression starts with a keyword. private static bool StartsWithKeyword(string expression, PreprocessorOperation operation) { expression = expression.ToUpperInvariant(); switch (operation) { case PreprocessorOperation.Not: if (expression.StartsWith("NOT ", StringComparison.Ordinal) || expression.StartsWith("NOT(", StringComparison.Ordinal)) { return true; } break; case PreprocessorOperation.And: if (expression.StartsWith("AND ", StringComparison.Ordinal) || expression.StartsWith("AND(", StringComparison.Ordinal)) { return true; } break; case PreprocessorOperation.Or: if (expression.StartsWith("OR ", StringComparison.Ordinal) || expression.StartsWith("OR(", StringComparison.Ordinal)) { return true; } break; default: break; } return false; } /// /// Processes an xml reader into an xml writer. /// /// Specifies if reader is from an included file. /// Reader for the source document. /// Node where content should be added. /// Original offset for the line numbers being processed. private void PreprocessReader(bool include, XmlReader reader, XContainer container, int offset) { XContainer currentContainer = container; Stack containerStack = new Stack(); IfContext ifContext = new IfContext(true, true, IfState.Unknown); // start by assuming we want to keep the nodes in the source code Stack ifStack = new Stack(); // process the reader into the writer while (reader.Read()) { // update information here in case an error occurs before the next read this.UpdateCurrentLineNumber(reader, offset); var sourceLineNumbers = this.Context.CurrentSourceLineNumber; // check for changes in conditional processing if (XmlNodeType.ProcessingInstruction == reader.NodeType) { bool ignore = false; string name = null; switch (reader.LocalName) { case "if": ifStack.Push(ifContext); if (ifContext.IsTrue) { ifContext = new IfContext(ifContext.IsTrue & ifContext.Active, this.EvaluateExpression(reader.Value), IfState.If); } else // Use a default IfContext object so we don't try to evaluate the expression if the context isn't true { ifContext = new IfContext(); } ignore = true; break; case "ifdef": ifStack.Push(ifContext); name = reader.Value.Trim(); if (ifContext.IsTrue) { ifContext = new IfContext(ifContext.IsTrue & ifContext.Active, (null != this.Helper.GetVariableValue(this.Context, name, true)), IfState.If); } else // Use a default IfContext object so we don't try to evaluate the expression if the context isn't true { ifContext = new IfContext(); } ignore = true; this.IfDef?.Invoke(this, new IfDefEventArgs(sourceLineNumbers, true, ifContext.IsTrue, name)); break; case "ifndef": ifStack.Push(ifContext); name = reader.Value.Trim(); if (ifContext.IsTrue) { ifContext = new IfContext(ifContext.IsTrue & ifContext.Active, (null == this.Helper.GetVariableValue(this.Context, name, true)), IfState.If); } else // Use a default IfContext object so we don't try to evaluate the expression if the context isn't true { ifContext = new IfContext(); } ignore = true; this.IfDef?.Invoke(this, new IfDefEventArgs(sourceLineNumbers, false, !ifContext.IsTrue, name)); break; case "elseif": if (0 == ifStack.Count) { throw new WixException(ErrorMessages.UnmatchedPreprocessorInstruction(sourceLineNumbers, "if", "elseif")); } if (IfState.If != ifContext.IfState && IfState.ElseIf != ifContext.IfState) { throw new WixException(ErrorMessages.UnmatchedPreprocessorInstruction(sourceLineNumbers, "if", "elseif")); } ifContext.IfState = IfState.ElseIf; // we're now in an elseif if (!ifContext.WasEverTrue) // if we've never evaluated the if context to true, then we can try this test { ifContext.IsTrue = this.EvaluateExpression(reader.Value); } else if (ifContext.IsTrue) { ifContext.IsTrue = false; } ignore = true; break; case "else": if (0 == ifStack.Count) { throw new WixException(ErrorMessages.UnmatchedPreprocessorInstruction(sourceLineNumbers, "if", "else")); } if (IfState.If != ifContext.IfState && IfState.ElseIf != ifContext.IfState) { throw new WixException(ErrorMessages.UnmatchedPreprocessorInstruction(sourceLineNumbers, "if", "else")); } ifContext.IfState = IfState.Else; // we're now in an else ifContext.IsTrue = !ifContext.WasEverTrue; // if we were never true, we can be true now ignore = true; break; case "endif": if (0 == ifStack.Count) { throw new WixException(ErrorMessages.UnmatchedPreprocessorInstruction(sourceLineNumbers, "if", "endif")); } ifContext = (IfContext)ifStack.Pop(); ignore = true; break; } if (ignore) // ignore this node since we just handled it above { continue; } } if (!ifContext.Active || !ifContext.IsTrue) // if our context is not true then skip the rest of the processing and just read the next thing { continue; } switch (reader.NodeType) { case XmlNodeType.XmlDeclaration: XDocument document = currentContainer as XDocument; if (null != document) { document.Declaration = new XDeclaration(null, null, null); while (reader.MoveToNextAttribute()) { switch (reader.LocalName) { case "version": document.Declaration.Version = reader.Value; break; case "encoding": document.Declaration.Encoding = reader.Value; break; case "standalone": document.Declaration.Standalone = reader.Value; break; } } } //else //{ // display an error? Can this happen? //} break; case XmlNodeType.ProcessingInstruction: switch (reader.LocalName) { case "define": this.PreprocessDefine(reader.Value); break; case "error": this.PreprocessError(reader.Value); break; case "warning": this.PreprocessWarning(reader.Value); break; case "undef": this.PreprocessUndef(reader.Value); break; case "include": this.UpdateCurrentLineNumber(reader, offset); this.PreprocessInclude(reader.Value, currentContainer); break; case "foreach": this.PreprocessForeach(reader, currentContainer, offset); break; case "endforeach": // endforeach is handled in PreprocessForeach, so seeing it here is an error throw new WixException(ErrorMessages.UnmatchedPreprocessorInstruction(sourceLineNumbers, "foreach", "endforeach")); case "pragma": this.PreprocessPragma(reader.Value, currentContainer); break; default: // unknown processing instructions are currently ignored break; } break; case XmlNodeType.Element: if (0 < this.IncludeNextStack.Count && this.IncludeNextStack.Peek()) { if ("Include" != reader.LocalName) { this.Messaging.Write(ErrorMessages.InvalidDocumentElement(sourceLineNumbers, reader.Name, "include", "Include")); } this.IncludeNextStack.Pop(); this.IncludeNextStack.Push(false); break; } var empty = reader.IsEmptyElement; var ns = XNamespace.Get(reader.NamespaceURI); var element = new XElement(ns + reader.LocalName); currentContainer.Add(element); this.UpdateCurrentLineNumber(reader, offset); element.AddAnnotation(sourceLineNumbers); while (reader.MoveToNextAttribute()) { var value = this.Helper.PreprocessString(this.Context, reader.Value); var attribNamespace = XNamespace.Get(reader.NamespaceURI); attribNamespace = XNamespace.Xmlns == attribNamespace && reader.LocalName.Equals("xmlns") ? XNamespace.None : attribNamespace; element.Add(new XAttribute(attribNamespace + reader.LocalName, value)); } if (!empty) { containerStack.Push(currentContainer); currentContainer = element; } break; case XmlNodeType.EndElement: if (0 < reader.Depth || !include) { currentContainer = containerStack.Pop(); } break; case XmlNodeType.Text: string postprocessedText = this.Helper.PreprocessString(this.Context, reader.Value); currentContainer.Add(postprocessedText); break; case XmlNodeType.CDATA: string postprocessedValue = this.Helper.PreprocessString(this.Context, reader.Value); currentContainer.Add(new XCData(postprocessedValue)); break; default: break; } } if (0 != ifStack.Count) { throw new WixException(ErrorMessages.NonterminatedPreprocessorInstruction(this.Context.CurrentSourceLineNumber, "if", "endif")); } // TODO: can this actually happen? if (0 != containerStack.Count) { throw new WixException(ErrorMessages.NonterminatedPreprocessorInstruction(this.Context.CurrentSourceLineNumber, "nodes", "nodes")); } } /// /// Processes an error processing instruction. /// /// Text from source. private void PreprocessError(string errorMessage) { // Resolve other variables in the error message. errorMessage = this.Helper.PreprocessString(this.Context, errorMessage); throw new WixException(ErrorMessages.PreprocessorError(this.Context.CurrentSourceLineNumber, errorMessage)); } /// /// Processes a warning processing instruction. /// /// Text from source. private void PreprocessWarning(string warningMessage) { // Resolve other variables in the warning message. warningMessage = this.Helper.PreprocessString(this.Context, warningMessage); this.Messaging.Write(WarningMessages.PreprocessorWarning(this.Context.CurrentSourceLineNumber, warningMessage)); } /// /// Processes a define processing instruction and creates the appropriate parameter. /// /// Text from source. private void PreprocessDefine(string originalDefine) { var match = DefineRegex.Match(originalDefine); if (!match.Success) { throw new WixException(ErrorMessages.IllegalDefineStatement(this.Context.CurrentSourceLineNumber, originalDefine)); } var defineName = match.Groups["varName"].Value; var defineValue = match.Groups["varValue"].Value; // strip off the optional quotes if (1 < defineValue.Length && ((defineValue.StartsWith("\"", StringComparison.Ordinal) && defineValue.EndsWith("\"", StringComparison.Ordinal)) || (defineValue.StartsWith("'", StringComparison.Ordinal) && defineValue.EndsWith("'", StringComparison.Ordinal)))) { defineValue = defineValue.Substring(1, defineValue.Length - 2); } // resolve other variables in the variable value defineValue = this.Helper.PreprocessString(this.Context, defineValue); if (defineName.StartsWith("var.", StringComparison.Ordinal)) { this.Helper.AddVariable(this.Context, defineName.Substring(4), defineValue); } else { this.Helper.AddVariable(this.Context, defineName, defineValue); } } /// /// Processes an undef processing instruction and creates the appropriate parameter. /// /// Text from source. private void PreprocessUndef(string originalDefine) { var name = this.Helper.PreprocessString(this.Context, originalDefine.Trim()); if (name.StartsWith("var.", StringComparison.Ordinal)) { this.Helper.RemoveVariable(this.Context, name.Substring(4)); } else { this.Helper.RemoveVariable(this.Context, name); } } /// /// Processes an included file. /// /// Path to included file. /// Parent container for included content. private void PreprocessInclude(string includePath, XContainer parent) { var sourceLineNumbers = this.Context.CurrentSourceLineNumber; // Preprocess variables in the path. includePath = this.Helper.PreprocessString(this.Context, includePath); var includeFile = this.GetIncludeFile(includePath); if (null == includeFile) { throw new WixException(ErrorMessages.FileNotFound(sourceLineNumbers, includePath, "include")); } using (XmlReader reader = XmlReader.Create(includeFile, DocumentXmlReaderSettings)) { this.PushInclude(includeFile); // process the included reader into the writer try { this.PreprocessReader(true, reader, parent, 0); } catch (XmlException e) { this.UpdateCurrentLineNumber(reader, 0); throw new WixException(ErrorMessages.InvalidXml(sourceLineNumbers, "source", e.Message)); } this.IncludedFile?.Invoke(this, new IncludedFileEventArgs(sourceLineNumbers, includeFile)); this.PopInclude(); } } /// /// Preprocess a foreach processing instruction. /// /// The xml reader. /// The container where to output processed data. /// Offset for the line numbers. private void PreprocessForeach(XmlReader reader, XContainer container, int offset) { // Find the "in" token. var indexOfInToken = reader.Value.IndexOf(" in ", StringComparison.Ordinal); if (0 > indexOfInToken) { throw new WixException(ErrorMessages.IllegalForeach(this.Context.CurrentSourceLineNumber, reader.Value)); } // parse out the variable name string varName = reader.Value.Substring(0, indexOfInToken).Trim(); string varValuesString = reader.Value.Substring(indexOfInToken + 4).Trim(); // preprocess the variable values string because it might be a variable itself varValuesString = this.Helper.PreprocessString(this.Context, varValuesString); string[] varValues = varValuesString.Split(';'); // go through all the empty strings while (reader.Read() && XmlNodeType.Whitespace == reader.NodeType) { } // get the offset of this xml fragment (for some reason its always off by 1) IXmlLineInfo lineInfoReader = reader as IXmlLineInfo; if (null != lineInfoReader) { offset += lineInfoReader.LineNumber - 1; } XmlTextReader textReader = reader as XmlTextReader; // dump the xml to a string (maintaining whitespace if possible) if (null != textReader) { textReader.WhitespaceHandling = WhitespaceHandling.All; } StringBuilder fragmentBuilder = new StringBuilder(); int nestedForeachCount = 1; while (nestedForeachCount != 0) { if (reader.NodeType == XmlNodeType.ProcessingInstruction) { switch (reader.LocalName) { case "foreach": ++nestedForeachCount; // Output the foreach statement fragmentBuilder.AppendFormat("", reader.Value); break; case "endforeach": --nestedForeachCount; if (0 != nestedForeachCount) { fragmentBuilder.Append(""); } break; default: fragmentBuilder.AppendFormat("", reader.LocalName, reader.Value); break; } } else if (reader.NodeType == XmlNodeType.Element) { fragmentBuilder.Append(reader.ReadOuterXml()); continue; } else if (reader.NodeType == XmlNodeType.Whitespace) { // Or output the whitespace fragmentBuilder.Append(reader.Value); } else if (reader.NodeType == XmlNodeType.None) { throw new WixException(ErrorMessages.ExpectedEndforeach(this.Context.CurrentSourceLineNumber)); } reader.Read(); } using (var fragmentStream = new MemoryStream(Encoding.UTF8.GetBytes(fragmentBuilder.ToString()))) { // process each iteration, updating the variable's value each time foreach (string varValue in varValues) { using (var loopReader = XmlReader.Create(fragmentStream, FragmentXmlReaderSettings)) { // Always overwrite foreach variables. this.Helper.AddVariable(this.Context, varName, varValue, false); try { this.PreprocessReader(false, loopReader, container, offset); } catch (XmlException e) { this.UpdateCurrentLineNumber(loopReader, offset); throw new WixException(ErrorMessages.InvalidXml(this.Context.CurrentSourceLineNumber, "source", e.Message)); } fragmentStream.Position = 0; // seek back to the beginning for the next loop. } } } } /// /// Processes a pragma processing instruction /// /// Text from source. private void PreprocessPragma(string pragmaText, XContainer parent) { var match = PragmaRegex.Match(pragmaText); if (!match.Success) { throw new WixException(ErrorMessages.InvalidPreprocessorPragma(this.Context.CurrentSourceLineNumber, pragmaText)); } // resolve other variables in the pragma argument(s) string pragmaArgs = this.Helper.PreprocessString(this.Context, match.Groups["pragmaValue"].Value).Trim(); try { this.Helper.PreprocessPragma(this.Context, match.Groups["pragmaName"].Value.Trim(), pragmaArgs, parent); } catch (Exception e) { throw new WixException(ErrorMessages.PreprocessorExtensionPragmaFailed(this.Context.CurrentSourceLineNumber, pragmaText, e.Message)); } } /// /// Gets the next token in an expression. /// /// Expression to parse. /// Expression with token removed. /// Flag if token is a string literal instead of a variable. /// Next token. private string GetNextToken(string originalExpression, ref string expression, out bool stringLiteral) { stringLiteral = false; string token = String.Empty; expression = expression.Trim(); if (0 == expression.Length) { return String.Empty; } if (expression.StartsWith("\"", StringComparison.Ordinal)) { stringLiteral = true; int endingQuotes = expression.IndexOf('\"', 1); if (-1 == endingQuotes) { throw new WixException(ErrorMessages.UnmatchedQuotesInExpression(this.Context.CurrentSourceLineNumber, originalExpression)); } // cut the quotes off the string token = this.Helper.PreprocessString(this.Context, expression.Substring(1, endingQuotes - 1)); // advance past this string expression = expression.Substring(endingQuotes + 1).Trim(); } else if (expression.StartsWith("$(", StringComparison.Ordinal)) { // Find the ending paren of the expression int endingParen = -1; int openedCount = 1; for (int i = 2; i < expression.Length; i++) { if ('(' == expression[i]) { openedCount++; } else if (')' == expression[i]) { openedCount--; } if (openedCount == 0) { endingParen = i; break; } } if (-1 == endingParen) { throw new WixException(ErrorMessages.UnmatchedParenthesisInExpression(this.Context.CurrentSourceLineNumber, originalExpression)); } token = expression.Substring(0, endingParen + 1); // Advance past this variable expression = expression.Substring(endingParen + 1).Trim(); } else { // Cut the token off at the next equal, space, inequality operator, // or end of string, whichever comes first int space = expression.IndexOf(" ", StringComparison.Ordinal); int equals = expression.IndexOf("=", StringComparison.Ordinal); int lessThan = expression.IndexOf("<", StringComparison.Ordinal); int lessThanEquals = expression.IndexOf("<=", StringComparison.Ordinal); int greaterThan = expression.IndexOf(">", StringComparison.Ordinal); int greaterThanEquals = expression.IndexOf(">=", StringComparison.Ordinal); int notEquals = expression.IndexOf("!=", StringComparison.Ordinal); int equalsNoCase = expression.IndexOf("~=", StringComparison.Ordinal); int closingIndex; if (space == -1) { space = Int32.MaxValue; } if (equals == -1) { equals = Int32.MaxValue; } if (lessThan == -1) { lessThan = Int32.MaxValue; } if (lessThanEquals == -1) { lessThanEquals = Int32.MaxValue; } if (greaterThan == -1) { greaterThan = Int32.MaxValue; } if (greaterThanEquals == -1) { greaterThanEquals = Int32.MaxValue; } if (notEquals == -1) { notEquals = Int32.MaxValue; } if (equalsNoCase == -1) { equalsNoCase = Int32.MaxValue; } closingIndex = Math.Min(space, Math.Min(equals, Math.Min(lessThan, Math.Min(lessThanEquals, Math.Min(greaterThan, Math.Min(greaterThanEquals, Math.Min(equalsNoCase, notEquals))))))); if (Int32.MaxValue == closingIndex) { closingIndex = expression.Length; } // If the index is 0, we hit an operator, so return it if (0 == closingIndex) { // Length 2 operators if (closingIndex == lessThanEquals || closingIndex == greaterThanEquals || closingIndex == notEquals || closingIndex == equalsNoCase) { closingIndex = 2; } else // Length 1 operators { closingIndex = 1; } } // Cut out the new token token = expression.Substring(0, closingIndex).Trim(); expression = expression.Substring(closingIndex).Trim(); } return token; } /// /// Gets the value for a variable. /// /// Original expression for error message. /// Variable to evaluate. /// Value of variable. private string EvaluateVariable(string originalExpression, string variable) { // By default it's a literal and will only be evaluated if it // matches the variable format string varValue = variable; if (variable.StartsWith("$(", StringComparison.Ordinal)) { try { varValue = this.Helper.PreprocessString(this.Context, variable); } catch (ArgumentNullException) { // non-existent variables are expected varValue = null; } } else if (variable.IndexOf("(", StringComparison.Ordinal) != -1 || variable.IndexOf(")", StringComparison.Ordinal) != -1) { // make sure it doesn't contain parenthesis throw new WixException(ErrorMessages.UnmatchedParenthesisInExpression(this.Context.CurrentSourceLineNumber, originalExpression)); } else if (variable.IndexOf("\"", StringComparison.Ordinal) != -1) { // shouldn't contain quotes throw new WixException(ErrorMessages.UnmatchedQuotesInExpression(this.Context.CurrentSourceLineNumber, originalExpression)); } return varValue; } /// /// Gets the left side value, operator, and right side value of an expression. /// /// Original expression to evaluate. /// Expression modified while processing. /// Left side value from expression. /// Operation in expression. /// Right side value from expression. private void GetNameValuePair(string originalExpression, ref string expression, out string leftValue, out string operation, out string rightValue) { bool stringLiteral; leftValue = this.GetNextToken(originalExpression, ref expression, out stringLiteral); // If it wasn't a string literal, evaluate it if (!stringLiteral) { leftValue = this.EvaluateVariable(originalExpression, leftValue); } // Get the operation operation = this.GetNextToken(originalExpression, ref expression, out stringLiteral); if (IsOperator(operation)) { if (stringLiteral) { throw new WixException(ErrorMessages.UnmatchedQuotesInExpression(this.Context.CurrentSourceLineNumber, originalExpression)); } rightValue = this.GetNextToken(originalExpression, ref expression, out stringLiteral); // If it wasn't a string literal, evaluate it if (!stringLiteral) { rightValue = this.EvaluateVariable(originalExpression, rightValue); } } else { // Prepend the token back on the expression since it wasn't an operator // and put the quotes back on the literal if necessary if (stringLiteral) { operation = "\"" + operation + "\""; } expression = (operation + " " + expression).Trim(); // If no operator, just check for existence operation = ""; rightValue = ""; } } /// /// Evaluates an expression. /// /// Original expression to evaluate. /// Expression modified while processing. /// true if expression evaluates to true. private bool EvaluateAtomicExpression(string originalExpression, ref string expression) { // Quick test to see if the first token is a variable bool startsWithVariable = expression.StartsWith("$(", StringComparison.Ordinal); string leftValue; string rightValue; string operation; this.GetNameValuePair(originalExpression, ref expression, out leftValue, out operation, out rightValue); bool expressionValue = false; // If the variables don't exist, they were evaluated to null if (null == leftValue || null == rightValue) { if (operation.Length > 0) { throw new WixException(ErrorMessages.ExpectedVariable(this.Context.CurrentSourceLineNumber, originalExpression)); } // false expression } else if (operation.Length == 0) { // There is no right side of the equation. // If the variable was evaluated, it exists, so the expression is true if (startsWithVariable) { expressionValue = true; } else { throw new WixException(ErrorMessages.UnexpectedLiteral(this.Context.CurrentSourceLineNumber, originalExpression)); } } else { leftValue = leftValue.Trim(); rightValue = rightValue.Trim(); if ("=" == operation) { if (leftValue == rightValue) { expressionValue = true; } } else if ("!=" == operation) { if (leftValue != rightValue) { expressionValue = true; } } else if ("~=" == operation) { if (String.Equals(leftValue, rightValue, StringComparison.OrdinalIgnoreCase)) { expressionValue = true; } } else { // Convert the numbers from strings int rightInt; int leftInt; try { rightInt = Int32.Parse(rightValue, CultureInfo.InvariantCulture); leftInt = Int32.Parse(leftValue, CultureInfo.InvariantCulture); } catch (FormatException) { throw new WixException(ErrorMessages.IllegalIntegerInExpression(this.Context.CurrentSourceLineNumber, originalExpression)); } catch (OverflowException) { throw new WixException(ErrorMessages.IllegalIntegerInExpression(this.Context.CurrentSourceLineNumber, originalExpression)); } // Compare the numbers if ("<" == operation && leftInt < rightInt || "<=" == operation && leftInt <= rightInt || ">" == operation && leftInt > rightInt || ">=" == operation && leftInt >= rightInt) { expressionValue = true; } } } return expressionValue; } /// /// Gets a sub-expression in parenthesis. /// /// Original expression to evaluate. /// Expression modified while processing. /// Index of end of sub-expression. /// Sub-expression in parenthesis. private string GetParenthesisExpression(string originalExpression, string expression, out int endSubExpression) { endSubExpression = 0; // if the expression doesn't start with parenthesis, leave it alone if (!expression.StartsWith("(", StringComparison.Ordinal)) { return expression; } // search for the end of the expression with the matching paren int openParenIndex = 0; int closeParenIndex = 1; while (openParenIndex != -1 && openParenIndex < closeParenIndex) { closeParenIndex = expression.IndexOf(')', closeParenIndex); if (closeParenIndex == -1) { throw new WixException(ErrorMessages.UnmatchedParenthesisInExpression(this.Context.CurrentSourceLineNumber, originalExpression)); } if (InsideQuotes(expression, closeParenIndex)) { // ignore stuff inside quotes (it's a string literal) } else { // Look to see if there is another open paren before the close paren // and skip over the open parens while they are in a string literal do { openParenIndex++; openParenIndex = expression.IndexOf('(', openParenIndex, closeParenIndex - openParenIndex); } while (InsideQuotes(expression, openParenIndex)); } // Advance past the closing paren closeParenIndex++; } endSubExpression = closeParenIndex; // Return the expression minus the parenthesis return expression.Substring(1, closeParenIndex - 2); } /// /// Updates expression based on operation. /// /// State to update. /// Operation to apply to current value. /// Previous result. private void UpdateExpressionValue(ref bool currentValue, PreprocessorOperation operation, bool prevResult) { switch (operation) { case PreprocessorOperation.And: currentValue = currentValue && prevResult; break; case PreprocessorOperation.Or: currentValue = currentValue || prevResult; break; case PreprocessorOperation.Not: currentValue = !currentValue; break; default: throw new WixException(ErrorMessages.UnexpectedPreprocessorOperator(this.Context.CurrentSourceLineNumber, operation.ToString())); } } /// /// Evaluate an expression. /// /// Expression to evaluate. /// Boolean result of expression. private bool EvaluateExpression(string expression) { string tmpExpression = expression; return this.EvaluateExpressionRecurse(expression, ref tmpExpression, PreprocessorOperation.And, true); } /// /// Recurse through the expression to evaluate if it is true or false. /// The expression is evaluated left to right. /// The expression is case-sensitive (converted to upper case) with the /// following exceptions: variable names and keywords (and, not, or). /// Comparisons with = and != are string comparisons. /// Comparisons with inequality operators must be done on valid integers. /// /// The operator precedence is: /// "" /// () /// <, >, <=, >=, =, != /// Not /// And, Or /// /// Valid expressions include: /// not $(var.B) or not $(var.C) /// (($(var.A))and $(var.B) ="2")or Not((($(var.C))) and $(var.A)) /// (($(var.A)) and $(var.B) = " 3 ") or $(var.C) /// $(var.A) and $(var.C) = "3" or $(var.C) and $(var.D) = $(env.windir) /// $(var.A) and $(var.B)>2 or $(var.B) <= 2 /// $(var.A) != "2" /// /// The original expression /// The expression currently being evaluated /// The operation to apply to this result /// The previous result to apply to this result /// Boolean to indicate if the expression is true or false private bool EvaluateExpressionRecurse(string originalExpression, ref string expression, PreprocessorOperation prevResultOperation, bool prevResult) { bool expressionValue = false; expression = expression.Trim(); if (expression.Length == 0) { throw new WixException(ErrorMessages.UnexpectedEmptySubexpression(this.Context.CurrentSourceLineNumber, originalExpression)); } // If the expression starts with parenthesis, evaluate it if (expression.IndexOf('(') == 0) { int endSubExpressionIndex; string subExpression = this.GetParenthesisExpression(originalExpression, expression, out endSubExpressionIndex); expressionValue = this.EvaluateExpressionRecurse(originalExpression, ref subExpression, PreprocessorOperation.And, true); // Now get the rest of the expression that hasn't been evaluated expression = expression.Substring(endSubExpressionIndex).Trim(); } else { // Check for NOT if (StartsWithKeyword(expression, PreprocessorOperation.Not)) { expression = expression.Substring(3).Trim(); if (expression.Length == 0) { throw new WixException(ErrorMessages.ExpectedExpressionAfterNot(this.Context.CurrentSourceLineNumber, originalExpression)); } expressionValue = this.EvaluateExpressionRecurse(originalExpression, ref expression, PreprocessorOperation.Not, true); } else // Expect a literal { expressionValue = this.EvaluateAtomicExpression(originalExpression, ref expression); // Expect the literal that was just evaluated to already be cut off } } this.UpdateExpressionValue(ref expressionValue, prevResultOperation, prevResult); // If there's still an expression left, it must start with AND or OR. if (expression.Trim().Length > 0) { if (StartsWithKeyword(expression, PreprocessorOperation.And)) { expression = expression.Substring(3); return this.EvaluateExpressionRecurse(originalExpression, ref expression, PreprocessorOperation.And, expressionValue); } else if (StartsWithKeyword(expression, PreprocessorOperation.Or)) { expression = expression.Substring(2); return this.EvaluateExpressionRecurse(originalExpression, ref expression, PreprocessorOperation.Or, expressionValue); } else { throw new WixException(ErrorMessages.InvalidSubExpression(this.Context.CurrentSourceLineNumber, expression, originalExpression)); } } return expressionValue; } /// /// Update the current line number with the reader's current state. /// /// The xml reader for the preprocessor. /// This is the artificial offset of the line numbers from the reader. Used for the foreach processing. private void UpdateCurrentLineNumber(XmlReader reader, int offset) { IXmlLineInfo lineInfoReader = reader as IXmlLineInfo; if (null != lineInfoReader) { int newLine = lineInfoReader.LineNumber + offset; if (this.Context.CurrentSourceLineNumber.LineNumber != newLine) { this.Context.CurrentSourceLineNumber = new SourceLineNumber(this.Context.CurrentSourceLineNumber.FileName, newLine); } } } /// /// Pushes a file name on the stack of included files. /// /// Name to push on to the stack of included files. private void PushInclude(string fileName) { if (1023 < this.CurrentFileStack.Count) { throw new WixException(ErrorMessages.TooDeeplyIncluded(this.Context.CurrentSourceLineNumber, this.CurrentFileStack.Count)); } this.CurrentFileStack.Push(fileName); this.SourceStack.Push(this.Context.CurrentSourceLineNumber); this.Context.CurrentSourceLineNumber = new SourceLineNumber(fileName); this.IncludeNextStack.Push(true); } /// /// Pops a file name from the stack of included files. /// private void PopInclude() { this.Context.CurrentSourceLineNumber = this.SourceStack.Pop(); this.CurrentFileStack.Pop(); this.IncludeNextStack.Pop(); } /// /// Go through search paths, looking for a matching include file. /// Start the search in the directory of the source file, then go /// through the search paths in the order given on the command line /// (leftmost first, ...). /// /// User-specified path to the included file (usually just the file name). /// Returns a FileInfo for the found include file, or null if the file cannot be found. private string GetIncludeFile(string includePath) { string finalIncludePath = null; includePath = includePath.Trim(); // remove quotes (only if they match) if ((includePath.StartsWith("\"", StringComparison.Ordinal) && includePath.EndsWith("\"", StringComparison.Ordinal)) || (includePath.StartsWith("'", StringComparison.Ordinal) && includePath.EndsWith("'", StringComparison.Ordinal))) { includePath = includePath.Substring(1, includePath.Length - 2); } // check if the include file is a full path if (Path.IsPathRooted(includePath)) { if (File.Exists(includePath)) { finalIncludePath = includePath; } } else // relative path { // build a string to test the directory containing the source file first var currentFolder = this.CurrentFileStack.Peek(); var includeTestPath = Path.Combine(Path.GetDirectoryName(currentFolder), includePath); // test the source file directory if (File.Exists(includeTestPath)) { finalIncludePath = includeTestPath; } else // test all search paths in the order specified on the command line { foreach (var includeSearchPath in this.Context.IncludeSearchPaths) { // if the path exists, we have found the final string includeTestPath = Path.Combine(includeSearchPath, includePath); if (File.Exists(includeTestPath)) { finalIncludePath = includeTestPath; break; } } } } return finalIncludePath; } private IPreprocessContext CreateContext() { var context = this.ServiceProvider.GetService(); context.Extensions = this.ServiceProvider.GetService().Create(); context.CurrentSourceLineNumber = new SourceLineNumber(this.SourcePath); context.Platform = this.Platform; context.IncludeSearchPaths = this.IncludeSearchPaths?.ToList() ?? new List(); context.SourceFile = this.SourcePath; context.Variables = new Dictionary(this.Variables); return context; } private void PreProcess() { foreach (var extension in this.Context.Extensions) { if (extension.Prefixes != null) { foreach (var prefix in extension.Prefixes) { if (!this.ExtensionsByPrefix.TryGetValue(prefix, out var collidingExtension)) { this.ExtensionsByPrefix.Add(prefix, extension); } else { this.Messaging.Write(ErrorMessages.DuplicateExtensionPreprocessorType(extension.GetType().ToString(), prefix, collidingExtension.GetType().ToString())); } } } extension.PrePreprocess(this.Context); } } private XDocument PostProcess(XDocument document) { foreach (var extension in this.Context.Extensions) { extension.PostPreprocess(document); } return document; } } }