// 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;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using System.Xml.Linq;
using WixToolset.Data;
using WixToolset.Extensibility;
using WixToolset.Preprocess;
///
/// Preprocessor object
///
public sealed class Preprocessor
{
private readonly Regex defineRegex = new Regex(@"^\s*(?.+?)\s*(=\s*(?.+?)\s*)?$", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.ExplicitCapture);
private readonly Regex pragmaRegex = new Regex(@"^\s*(?.+?)(?[\s\(].+?)?$", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.ExplicitCapture);
private readonly XmlReaderSettings DocumentXmlReaderSettings = new XmlReaderSettings()
{
ValidationFlags = System.Xml.Schema.XmlSchemaValidationFlags.None,
XmlResolver = null,
};
private readonly XmlReaderSettings FragmentXmlReaderSettings = new XmlReaderSettings()
{
ConformanceLevel = System.Xml.ConformanceLevel.Fragment,
ValidationFlags = System.Xml.Schema.XmlSchemaValidationFlags.None,
XmlResolver = null,
};
private List extensions;
private Dictionary extensionsByPrefix;
private List inspectorExtensions;
private SourceLineNumber currentLineNumber;
private Stack sourceStack;
private PreprocessorCore core;
private TextWriter preprocessOut;
private Stack includeNextStack;
private Stack currentFileStack;
private Platform currentPlatform;
///
/// Creates a new preprocesor.
///
public Preprocessor()
{
this.IncludeSearchPaths = new List();
this.extensions = new List();
this.extensionsByPrefix = new Dictionary();
this.inspectorExtensions = new List();
this.sourceStack = new Stack();
this.includeNextStack = new Stack();
this.currentFileStack = new Stack();
this.currentPlatform = Platform.X86;
}
///
/// 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;
///
/// Enumeration for preprocessor operations in if statements.
///
private enum PreprocessorOperation
{
/// The and operator.
And,
/// The or operator.
Or,
/// The not operator.
Not
}
///
/// 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; }
}
///
/// Ordered list of search paths that the precompiler uses to find included files.
///
/// List of ordered search paths to use during precompiling.
public IList IncludeSearchPaths { get; private set; }
///
/// Specifies the text stream to display the postprocessed data to.
///
/// TextWriter to write preprocessed xml to.
public TextWriter PreprocessOut
{
get { return this.preprocessOut; }
set { this.preprocessOut = value; }
}
///
/// Get the source line information for the current element. The precompiler will insert
/// special source line number processing instructions before each element that it
/// encounters. This is where those line numbers are read and processed. This function
/// may return an array of source line numbers because the element may have come from
/// an included file, in which case the chain of imports is expressed in the array.
///
/// Element to get source line information for.
/// Returns the stack of imports used to author the element being processed.
[SuppressMessage("Microsoft.Design", "CA1059:MembersShouldNotExposeCertainConcreteTypes")]
public static SourceLineNumber GetSourceLineNumbers(XmlNode node)
{
return null;
}
///
/// 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);
}
///
/// Adds an extension.
///
/// The extension to add.
public void AddExtension(IPreprocessorExtension extension)
{
this.extensions.Add(extension);
if (null != extension.Prefixes)
{
foreach (string prefix in extension.Prefixes)
{
IPreprocessorExtension collidingExtension;
if (!this.extensionsByPrefix.TryGetValue(prefix, out collidingExtension))
{
this.extensionsByPrefix.Add(prefix, extension);
}
else
{
Messaging.Instance.OnMessage(WixErrors.DuplicateExtensionPreprocessorType(extension.GetType().ToString(), prefix, collidingExtension.GetType().ToString()));
}
}
}
//if (null != extension.InspectorExtension)
//{
// this.inspectorExtensions.Add(extension.InspectorExtension);
//}
}
///
/// Preprocesses a file.
///
/// The file to preprocess.
/// The variables defined prior to preprocessing.
/// XDocument with the postprocessed data.
[SuppressMessage("Microsoft.Design", "CA1059:MembersShouldNotExposeCertainConcreteTypes")]
public XDocument Process(string sourceFile, IDictionary variables)
{
using (Stream sourceStream = new FileStream(sourceFile, FileMode.Open, FileAccess.Read, FileShare.Read))
{
InspectorCore inspectorCore = new InspectorCore();
foreach (InspectorExtension inspectorExtension in this.inspectorExtensions)
{
inspectorExtension.Core = inspectorCore;
inspectorExtension.InspectSource(sourceStream);
// reset
inspectorExtension.Core = null;
sourceStream.Position = 0;
}
if (inspectorCore.EncounteredError)
{
return null;
}
using (XmlReader reader = XmlReader.Create(sourceFile, DocumentXmlReaderSettings))
{
return Process(reader, variables, sourceFile);
}
}
}
///
/// Preprocesses a file.
///
/// The file to preprocess.
/// The variables defined prior to preprocessing.
/// XDocument with the postprocessed data.
[SuppressMessage("Microsoft.Design", "CA1059:MembersShouldNotExposeCertainConcreteTypes")]
public XDocument Process(XmlReader reader, IDictionary variables, string sourceFile = null)
{
if (String.IsNullOrEmpty(sourceFile) && !String.IsNullOrEmpty(reader.BaseURI))
{
Uri uri = new Uri(reader.BaseURI);
sourceFile = uri.AbsolutePath;
}
this.core = new PreprocessorCore(this.extensionsByPrefix, sourceFile, variables);
this.core.ResolvedVariableHandler = this.ResolvedVariable;
this.core.CurrentPlatform = this.currentPlatform;
this.currentLineNumber = new SourceLineNumber(sourceFile);
this.currentFileStack.Clear();
this.currentFileStack.Push(this.core.GetVariableValue(this.currentLineNumber, "sys", "SOURCEFILEDIR"));
// Process the reader into the output.
XDocument output = new XDocument();
try
{
foreach (PreprocessorExtension extension in this.extensions)
{
extension.Core = this.core;
extension.Initialize();
}
this.PreprocessReader(false, reader, output, 0);
}
catch (XmlException e)
{
this.UpdateCurrentLineNumber(reader, 0);
throw new WixException(WixErrors.InvalidXml(this.currentLineNumber, "source", e.Message));
}
// Fire event with post-processed document.
ProcessedStreamEventArgs args = new ProcessedStreamEventArgs(sourceFile, output);
this.OnProcessedStream(args);
// preprocess the generated XML Document
foreach (PreprocessorExtension extension in this.extensions)
{
extension.PreprocessDocument(output);
}
// finalize the preprocessing
foreach (PreprocessorExtension extension in this.extensions)
{
extension.Finish();
extension.Core = null;
}
if (this.core.EncounteredError)
{
return null;
}
else
{
if (null != this.preprocessOut)
{
output.Save(this.preprocessOut);
this.preprocessOut.Flush();
}
return 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;
}
///
/// Fires an event when an ifdef/ifndef directive is processed.
///
/// ifdef/ifndef event arguments.
private void OnIfDef(IfDefEventArgs ea)
{
if (null != this.IfDef)
{
this.IfDef(this, ea);
}
}
///
/// Fires an event when an included file is processed.
///
/// Included file event arguments.
private void OnIncludedFile(IncludedFileEventArgs ea)
{
if (null != this.IncludedFile)
{
this.IncludedFile(this, ea);
}
}
///
/// Fires an event after the file is preprocessed.
///
/// Included file event arguments.
private void OnProcessedStream(ProcessedStreamEventArgs ea)
{
if (null != this.ProcessedStream)
{
this.ProcessedStream(this, ea);
}
}
///
/// 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.ToUpper(CultureInfo.InvariantCulture);
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);
SourceLineNumber sourceLineNumbers = this.currentLineNumber;
// 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.core.GetVariableValue(sourceLineNumbers, 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;
OnIfDef(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.core.GetVariableValue(sourceLineNumbers, 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;
OnIfDef(new IfDefEventArgs(sourceLineNumbers, false, !ifContext.IsTrue, name));
break;
case "elseif":
if (0 == ifStack.Count)
{
throw new WixException(WixErrors.UnmatchedPreprocessorInstruction(this.currentLineNumber, "if", "elseif"));
}
if (IfState.If != ifContext.IfState && IfState.ElseIf != ifContext.IfState)
{
throw new WixException(WixErrors.UnmatchedPreprocessorInstruction(this.currentLineNumber, "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(WixErrors.UnmatchedPreprocessorInstruction(this.currentLineNumber, "if", "else"));
}
if (IfState.If != ifContext.IfState && IfState.ElseIf != ifContext.IfState)
{
throw new WixException(WixErrors.UnmatchedPreprocessorInstruction(this.currentLineNumber, "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(WixErrors.UnmatchedPreprocessorInstruction(this.currentLineNumber, "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(WixErrors.UnmatchedPreprocessorInstruction(this.currentLineNumber, "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.core.OnMessage(WixErrors.InvalidDocumentElement(this.currentLineNumber, reader.Name, "include", "Include"));
}
this.includeNextStack.Pop();
this.includeNextStack.Push(false);
break;
}
bool empty = reader.IsEmptyElement;
XNamespace ns = XNamespace.Get(reader.NamespaceURI);
XElement element = new XElement(ns + reader.LocalName);
currentContainer.Add(element);
this.UpdateCurrentLineNumber(reader, offset);
element.AddAnnotation(this.currentLineNumber);
while (reader.MoveToNextAttribute())
{
string value = this.core.PreprocessString(this.currentLineNumber, reader.Value);
XNamespace 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.core.PreprocessString(this.currentLineNumber, reader.Value);
currentContainer.Add(postprocessedText);
break;
case XmlNodeType.CDATA:
string postprocessedValue = this.core.PreprocessString(this.currentLineNumber, reader.Value);
currentContainer.Add(new XCData(postprocessedValue));
break;
default:
break;
}
}
if (0 != ifStack.Count)
{
throw new WixException(WixErrors.NonterminatedPreprocessorInstruction(this.currentLineNumber, "if", "endif"));
}
// TODO: can this actually happen?
if (0 != containerStack.Count)
{
throw new WixException(WixErrors.NonterminatedPreprocessorInstruction(this.currentLineNumber, "nodes", "nodes"));
}
}
///
/// Processes an error processing instruction.
///
/// Text from source.
private void PreprocessError(string errorMessage)
{
SourceLineNumber sourceLineNumbers = this.currentLineNumber;
// resolve other variables in the error message
errorMessage = this.core.PreprocessString(sourceLineNumbers, errorMessage);
throw new WixException(WixErrors.PreprocessorError(sourceLineNumbers, errorMessage));
}
///
/// Processes a warning processing instruction.
///
/// Text from source.
private void PreprocessWarning(string warningMessage)
{
SourceLineNumber sourceLineNumbers = this.currentLineNumber;
// resolve other variables in the warning message
warningMessage = this.core.PreprocessString(sourceLineNumbers, warningMessage);
this.core.OnMessage(WixWarnings.PreprocessorWarning(sourceLineNumbers, warningMessage));
}
///
/// Processes a define processing instruction and creates the appropriate parameter.
///
/// Text from source.
private void PreprocessDefine(string originalDefine)
{
Match match = defineRegex.Match(originalDefine);
SourceLineNumber sourceLineNumbers = this.currentLineNumber;
if (!match.Success)
{
throw new WixException(WixErrors.IllegalDefineStatement(sourceLineNumbers, originalDefine));
}
string defineName = match.Groups["varName"].Value;
string 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.core.PreprocessString(sourceLineNumbers, defineValue);
if (defineName.StartsWith("var.", StringComparison.Ordinal))
{
this.core.AddVariable(sourceLineNumbers, defineName.Substring(4), defineValue);
}
else
{
this.core.AddVariable(sourceLineNumbers, defineName, defineValue);
}
}
///
/// Processes an undef processing instruction and creates the appropriate parameter.
///
/// Text from source.
private void PreprocessUndef(string originalDefine)
{
SourceLineNumber sourceLineNumbers = this.currentLineNumber;
string name = this.core.PreprocessString(sourceLineNumbers, originalDefine.Trim());
if (name.StartsWith("var.", StringComparison.Ordinal))
{
this.core.RemoveVariable(sourceLineNumbers, name.Substring(4));
}
else
{
this.core.RemoveVariable(sourceLineNumbers, name);
}
}
///
/// Processes an included file.
///
/// Path to included file.
/// Parent container for included content.
private void PreprocessInclude(string includePath, XContainer parent)
{
SourceLineNumber sourceLineNumbers = this.currentLineNumber;
// preprocess variables in the path
includePath = this.core.PreprocessString(sourceLineNumbers, includePath);
string includeFile = this.GetIncludeFile(includePath);
if (null == includeFile)
{
throw new WixException(WixErrors.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(WixErrors.InvalidXml(sourceLineNumbers, "source", e.Message));
}
this.OnIncludedFile(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
int indexOfInToken = reader.Value.IndexOf(" in ", StringComparison.Ordinal);
if (0 > indexOfInToken)
{
throw new WixException(WixErrors.IllegalForeach(this.currentLineNumber, 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.core.PreprocessString(this.currentLineNumber, 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("{0} {1}?>", 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(WixErrors.ExpectedEndforeach(this.currentLineNumber));
}
reader.Read();
}
using (MemoryStream fragmentStream = new MemoryStream(Encoding.UTF8.GetBytes(fragmentBuilder.ToString())))
using (XmlReader loopReader = XmlReader.Create(fragmentStream, FragmentXmlReaderSettings))
{
// process each iteration, updating the variable's value each time
foreach (string varValue in varValues)
{
// Always overwrite foreach variables.
this.core.AddVariable(this.currentLineNumber, varName, varValue, false);
try
{
this.PreprocessReader(false, loopReader, container, offset);
}
catch (XmlException e)
{
this.UpdateCurrentLineNumber(loopReader, offset);
throw new WixException(WixErrors.InvalidXml(this.currentLineNumber, "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)
{
Match match = pragmaRegex.Match(pragmaText);
SourceLineNumber sourceLineNumbers = this.currentLineNumber;
if (!match.Success)
{
throw new WixException(WixErrors.InvalidPreprocessorPragma(sourceLineNumbers, pragmaText));
}
// resolve other variables in the pragma argument(s)
string pragmaArgs = this.core.PreprocessString(sourceLineNumbers, match.Groups["pragmaValue"].Value).Trim();
try
{
this.core.PreprocessPragma(sourceLineNumbers, match.Groups["pragmaName"].Value.Trim(), pragmaArgs, parent);
}
catch (Exception e)
{
throw new WixException(WixErrors.PreprocessorExtensionPragmaFailed(sourceLineNumbers, 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(WixErrors.UnmatchedQuotesInExpression(this.currentLineNumber, originalExpression));
}
// cut the quotes off the string
token = this.core.PreprocessString(this.currentLineNumber, 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(WixErrors.UnmatchedParenthesisInExpression(this.currentLineNumber, 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.core.PreprocessString(this.currentLineNumber, 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(WixErrors.UnmatchedParenthesisInExpression(this.currentLineNumber, originalExpression));
}
else if (variable.IndexOf("\"", StringComparison.Ordinal) != -1)
{
// shouldn't contain quotes
throw new WixException(WixErrors.UnmatchedQuotesInExpression(this.currentLineNumber, 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(WixErrors.UnmatchedQuotesInExpression(this.currentLineNumber, 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(WixErrors.ExpectedVariable(this.currentLineNumber, 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(WixErrors.UnexpectedLiteral(this.currentLineNumber, 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(WixErrors.IllegalIntegerInExpression(this.currentLineNumber, originalExpression));
}
catch (OverflowException)
{
throw new WixException(WixErrors.IllegalIntegerInExpression(this.currentLineNumber, 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(WixErrors.UnmatchedParenthesisInExpression(this.currentLineNumber, 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(WixErrors.UnexpectedPreprocessorOperator(this.currentLineNumber, 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(WixErrors.UnexpectedEmptySubexpression(this.currentLineNumber, 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(WixErrors.ExpectedExpressionAfterNot(this.currentLineNumber, 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(WixErrors.InvalidSubExpression(this.currentLineNumber, 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.currentLineNumber.LineNumber != newLine)
{
this.currentLineNumber = new SourceLineNumber(this.currentLineNumber.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(WixErrors.TooDeeplyIncluded(this.currentLineNumber, this.currentFileStack.Count));
}
this.currentFileStack.Push(fileName);
this.sourceStack.Push(this.currentLineNumber);
this.currentLineNumber = new SourceLineNumber(fileName);
this.includeNextStack.Push(true);
}
///
/// Pops a file name from the stack of included files.
///
private void PopInclude()
{
this.currentLineNumber = 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
string currentFolder = this.currentFileStack.Peek();
string includeTestPath = Path.Combine(Path.GetDirectoryName(currentFolder) ?? String.Empty, 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 (string includeSearchPath in this.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;
}
}
}