main
cs 1,535 lines 64.1 KB
Raw
1 // 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.
2
3 namespace WixToolset.Core
4 {
5 using System;
6 using System.Collections.Generic;
7 using System.Globalization;
8 using System.IO;
9 using System.Text;
10 using System.Text.RegularExpressions;
11 using System.Xml;
12 using System.Xml.Linq;
13 using WixToolset.Core.Preprocess;
14 using WixToolset.Data;
15 using WixToolset.Extensibility;
16 using WixToolset.Extensibility.Data;
17 using WixToolset.Extensibility.Services;
18
19 /// <summary>
20 /// Preprocessor object
21 /// </summary>
22 internal class Preprocessor : IPreprocessor
23 {
24 private static readonly Regex DefineRegex = new Regex(@"^\s*(?<varName>.+?)\s*(=\s*(?<varValue>.+?)\s*)?$", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.ExplicitCapture);
25 private static readonly Regex PragmaRegex = new Regex(@"^\s*(?<pragmaName>.+?)(?<pragmaValue>[\s\(].+?)?$", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.ExplicitCapture);
26
27 private static readonly XmlReaderSettings DocumentXmlReaderSettings = new XmlReaderSettings()
28 {
29 ValidationFlags = System.Xml.Schema.XmlSchemaValidationFlags.None,
30 XmlResolver = null,
31 };
32
33 private static readonly XmlReaderSettings FragmentXmlReaderSettings = new XmlReaderSettings()
34 {
35 ConformanceLevel = ConformanceLevel.Fragment,
36 ValidationFlags = System.Xml.Schema.XmlSchemaValidationFlags.None,
37 XmlResolver = null,
38 };
39
40 internal Preprocessor(IServiceProvider serviceProvider)
41 {
42 this.ServiceProvider = serviceProvider;
43
44 this.Messaging = this.ServiceProvider.GetService<IMessaging>();
45 }
46
47 private IServiceProvider ServiceProvider { get; }
48
49 private IMessaging Messaging { get; }
50
51 /// <summary>
52 /// Event for ifdef/ifndef directives.
53 /// </summary>
54 public event IfDefEventHandler IfDef;
55
56 /// <summary>
57 /// Event for included files.
58 /// </summary>
59 public event IncludedFileEventHandler IncludedFile;
60
61 /// <summary>
62 /// Event for preprocessed stream.
63 /// </summary>
64 public event ProcessedStreamEventHandler ProcessedStream;
65
66 // <summary>
67 // Event for resolved variables.
68 // </summary>
69 // TOOD: Remove?
70 //public event ResolvedVariableEventHandler ResolvedVariable;
71
72 /// <summary>
73 /// Get the source line information for the current element. The precompiler will insert
74 /// special source line number information for each element that it encounters.
75 /// </summary>
76 /// <param name="node">Element to get source line information for.</param>
77 /// <returns>
78 /// The source line number used to author the element being processed or
79 /// null if the preprocessor did not process the element or the node is
80 /// not an element.
81 /// </returns>
82 public static SourceLineNumber GetSourceLineNumbers(XObject node)
83 {
84 return SourceLineNumber.GetFromXAnnotation(node);
85 }
86
87 /// <summary>
88 /// Preprocesses a file.
89 /// </summary>
90 /// <param name="context">The preprocessing context.</param>
91 /// <returns>XDocument with the postprocessed data.</returns>
92 public IPreprocessResult Preprocess(IPreprocessContext context)
93 {
94 var state = new ProcessingState(this.ServiceProvider, context);
95
96 this.PreProcess(state);
97
98 IPreprocessResult result;
99 using (var reader = XmlReader.Create(state.Context.SourcePath, DocumentXmlReaderSettings))
100 {
101 result = this.Process(state, reader);
102 }
103
104 this.PostProcess(state, result);
105
106 return result;
107 }
108
109 /// <summary>
110 /// Preprocesses a file.
111 /// </summary>
112 /// <param name="context">The preprocessing context.</param>
113 /// <param name="reader">XmlReader to processing the context.</param>
114 /// <returns>XDocument with the postprocessed data.</returns>
115 public IPreprocessResult Preprocess(IPreprocessContext context, XmlReader reader)
116 {
117 if (String.IsNullOrEmpty(context.SourcePath) && !String.IsNullOrEmpty(reader.BaseURI))
118 {
119 var uri = new Uri(reader.BaseURI);
120 context.SourcePath = uri.AbsolutePath;
121 }
122
123 var state = new ProcessingState(this.ServiceProvider, context);
124
125 this.PreProcess(state);
126
127 var result = this.Process(state, reader);
128
129 this.PostProcess(state, result);
130
131 return result;
132 }
133
134 /// <summary>
135 /// Preprocesses a file.
136 /// </summary>
137 /// <param name="state">The preprocessing context.</param>
138 /// <param name="reader">XmlReader to processing the context.</param>
139 /// <returns>XDocument with the postprocessed data.</returns>
140 private IPreprocessResult Process(ProcessingState state, XmlReader reader)
141 {
142 state.CurrentFileStack.Push(state.Helper.GetVariableValue(state.Context, "sys", "SOURCEFILEDIR"));
143
144 var beforeErrorCount = this.Messaging.ErrorCount;
145
146 // Process the reader into the output.
147 IPreprocessResult result = null;
148 try
149 {
150 this.PreprocessReader(state, false, reader, state.Output, 0);
151
152 // Fire event with post-processed document.
153 this.ProcessedStream?.Invoke(this, new ProcessedStreamEventArgs(state.Context.SourcePath, state.Output));
154
155 if (beforeErrorCount == this.Messaging.ErrorCount)
156 {
157 result = this.ServiceProvider.GetService<IPreprocessResult>();
158 result.Document = state.Output;
159 result.IncludedFiles = state.IncludedFiles;
160 }
161 }
162 catch (XmlException e)
163 {
164 this.UpdateCurrentLineNumber(state, reader, 0);
165 throw new WixException(ErrorMessages.InvalidXml(state.Context.CurrentSourceLineNumber, "source", e.Message));
166 }
167
168 return result;
169 }
170
171 /// <summary>
172 /// Determins if string is an operator.
173 /// </summary>
174 /// <param name="operation">String to check.</param>
175 /// <returns>true if string is an operator.</returns>
176 private static bool IsOperator(string operation)
177 {
178 if (operation == null)
179 {
180 return false;
181 }
182
183 operation = operation.Trim();
184 if (0 == operation.Length)
185 {
186 return false;
187 }
188
189 if ("=" == operation ||
190 "==" == operation ||
191 "!=" == operation ||
192 "<" == operation ||
193 "<=" == operation ||
194 ">" == operation ||
195 ">=" == operation ||
196 "~=" == operation)
197 {
198 return true;
199 }
200 return false;
201 }
202
203 /// <summary>
204 /// Determines if expression is currently inside quotes.
205 /// </summary>
206 /// <param name="expression">Expression to evaluate.</param>
207 /// <param name="index">Index to start searching in expression.</param>
208 /// <returns>true if expression is inside in quotes.</returns>
209 private static bool InsideQuotes(string expression, int index)
210 {
211 if (index == -1)
212 {
213 return false;
214 }
215
216 var numQuotes = 0;
217 var tmpIndex = 0;
218 while (-1 != (tmpIndex = expression.IndexOf('\"', tmpIndex, index - tmpIndex)))
219 {
220 numQuotes++;
221 tmpIndex++;
222 }
223
224 // found an even number of quotes before the index, so we're not inside
225 if (numQuotes % 2 == 0)
226 {
227 return false;
228 }
229
230 // found an odd number of quotes, so we are inside
231 return true;
232 }
233
234 /// <summary>
235 /// Tests expression to see if it starts with a keyword.
236 /// </summary>
237 /// <param name="expression">Expression to test.</param>
238 /// <param name="operation">Operation to test for.</param>
239 /// <returns>true if expression starts with a keyword.</returns>
240 private static bool StartsWithKeyword(string expression, PreprocessorOperation operation)
241 {
242 expression = expression.ToUpperInvariant();
243 switch (operation)
244 {
245 case PreprocessorOperation.Not:
246 if (expression.StartsWith("NOT ", StringComparison.Ordinal) || expression.StartsWith("NOT(", StringComparison.Ordinal))
247 {
248 return true;
249 }
250 break;
251 case PreprocessorOperation.And:
252 if (expression.StartsWith("AND ", StringComparison.Ordinal) || expression.StartsWith("AND(", StringComparison.Ordinal))
253 {
254 return true;
255 }
256 break;
257 case PreprocessorOperation.Or:
258 if (expression.StartsWith("OR ", StringComparison.Ordinal) || expression.StartsWith("OR(", StringComparison.Ordinal))
259 {
260 return true;
261 }
262 break;
263 default:
264 break;
265 }
266 return false;
267 }
268
269 /// <summary>
270 /// Processes an xml reader into an xml writer.
271 /// </summary>
272 /// <param name="state"></param>
273 /// <param name="include">Specifies if reader is from an included file.</param>
274 /// <param name="reader">Reader for the source document.</param>
275 /// <param name="container">Node where content should be added.</param>
276 /// <param name="offset">Original offset for the line numbers being processed.</param>
277 private void PreprocessReader(ProcessingState state, bool include, XmlReader reader, XContainer container, int offset)
278 {
279 var currentContainer = container;
280 var containerStack = new Stack<XContainer>();
281
282 var ifContext = new IfContext(true, true, IfState.Unknown); // start by assuming we want to keep the nodes in the source code
283 var ifStack = new Stack<IfContext>();
284
285 // process the reader into the writer
286 while (reader.Read())
287 {
288 // update information here in case an error occurs before the next read
289 this.UpdateCurrentLineNumber(state, reader, offset);
290
291 var sourceLineNumbers = state.Context.CurrentSourceLineNumber;
292
293 // check for changes in conditional processing
294 if (XmlNodeType.ProcessingInstruction == reader.NodeType)
295 {
296 var ignore = false;
297 string name;
298
299 switch (reader.LocalName)
300 {
301 case "if":
302 ifStack.Push(ifContext);
303 if (ifContext.IsTrue)
304 {
305 ifContext = new IfContext(ifContext.IsTrue & ifContext.Active, this.EvaluateExpression(state, reader.Value), IfState.If);
306 }
307 else // Use a default IfContext object so we don't try to evaluate the expression if the context isn't true
308 {
309 ifContext = new IfContext();
310 }
311 ignore = true;
312 break;
313
314 case "ifdef":
315 ifStack.Push(ifContext);
316 name = reader.Value.Trim();
317 if (ifContext.IsTrue)
318 {
319 ifContext = new IfContext(ifContext.IsTrue & ifContext.Active, (null != state.Helper.GetVariableValue(state.Context, name, true)), IfState.If);
320 }
321 else // Use a default IfContext object so we don't try to evaluate the expression if the context isn't true
322 {
323 ifContext = new IfContext();
324 }
325 ignore = true;
326 this.IfDef?.Invoke(this, new IfDefEventArgs(sourceLineNumbers, true, ifContext.IsTrue, name));
327 break;
328
329 case "ifndef":
330 ifStack.Push(ifContext);
331 name = reader.Value.Trim();
332 if (ifContext.IsTrue)
333 {
334 ifContext = new IfContext(ifContext.IsTrue & ifContext.Active, (null == state.Helper.GetVariableValue(state.Context, name, true)), IfState.If);
335 }
336 else // Use a default IfContext object so we don't try to evaluate the expression if the context isn't true
337 {
338 ifContext = new IfContext();
339 }
340 ignore = true;
341 this.IfDef?.Invoke(this, new IfDefEventArgs(sourceLineNumbers, false, !ifContext.IsTrue, name));
342 break;
343
344 case "elseif":
345 if (0 == ifStack.Count)
346 {
347 throw new WixException(ErrorMessages.UnmatchedPreprocessorInstruction(sourceLineNumbers, "if", "elseif"));
348 }
349
350 if (IfState.If != ifContext.IfState && IfState.ElseIf != ifContext.IfState)
351 {
352 throw new WixException(ErrorMessages.UnmatchedPreprocessorInstruction(sourceLineNumbers, "if", "elseif"));
353 }
354
355 ifContext.IfState = IfState.ElseIf; // we're now in an elseif
356 if (!ifContext.WasEverTrue) // if we've never evaluated the if context to true, then we can try this test
357 {
358 ifContext.IsTrue = this.EvaluateExpression(state, reader.Value);
359 }
360 else if (ifContext.IsTrue)
361 {
362 ifContext.IsTrue = false;
363 }
364 ignore = true;
365 break;
366
367 case "else":
368 if (0 == ifStack.Count)
369 {
370 throw new WixException(ErrorMessages.UnmatchedPreprocessorInstruction(sourceLineNumbers, "if", "else"));
371 }
372
373 if (IfState.If != ifContext.IfState && IfState.ElseIf != ifContext.IfState)
374 {
375 throw new WixException(ErrorMessages.UnmatchedPreprocessorInstruction(sourceLineNumbers, "if", "else"));
376 }
377
378 ifContext.IfState = IfState.Else; // we're now in an else
379 ifContext.IsTrue = !ifContext.WasEverTrue; // if we were never true, we can be true now
380 ignore = true;
381 break;
382
383 case "endif":
384 if (0 == ifStack.Count)
385 {
386 throw new WixException(ErrorMessages.UnmatchedPreprocessorInstruction(sourceLineNumbers, "if", "endif"));
387 }
388
389 ifContext = ifStack.Pop();
390 ignore = true;
391 break;
392 }
393
394 if (ignore) // ignore this node since we just handled it above
395 {
396 continue;
397 }
398 }
399
400 if (!ifContext.Active || !ifContext.IsTrue) // if our context is not true then skip the rest of the processing and just read the next thing
401 {
402 continue;
403 }
404
405 switch (reader.NodeType)
406 {
407 case XmlNodeType.XmlDeclaration:
408 if (currentContainer is XDocument document)
409 {
410 document.Declaration = new XDeclaration(null, null, null);
411 while (reader.MoveToNextAttribute())
412 {
413 switch (reader.LocalName)
414 {
415 case "version":
416 document.Declaration.Version = reader.Value;
417 break;
418
419 case "encoding":
420 document.Declaration.Encoding = reader.Value;
421 break;
422
423 case "standalone":
424 document.Declaration.Standalone = reader.Value;
425 break;
426 }
427 }
428 }
429 //else
430 //{
431 // display an error? Can this happen?
432 //}
433 break;
434
435 case XmlNodeType.ProcessingInstruction:
436 switch (reader.LocalName)
437 {
438 case "define":
439 this.PreprocessDefine(state, reader.Value);
440 break;
441
442 case "error":
443 this.PreprocessError(state, reader.Value);
444 break;
445
446 case "warning":
447 this.PreprocessWarning(state, reader.Value);
448 break;
449
450 case "undef":
451 this.PreprocessUndef(state, reader.Value);
452 break;
453
454 case "include":
455 this.UpdateCurrentLineNumber(state, reader, offset);
456 this.PreprocessInclude(state, reader.Value, currentContainer);
457 break;
458
459 case "foreach":
460 this.PreprocessForeach(state, reader, currentContainer, offset);
461 break;
462
463 case "endforeach": // endforeach is handled in PreprocessForeach, so seeing it here is an error
464 throw new WixException(ErrorMessages.UnmatchedPreprocessorInstruction(sourceLineNumbers, "foreach", "endforeach"));
465
466 case "pragma":
467 this.PreprocessPragma(state, reader.Value, currentContainer);
468 break;
469
470 default:
471 // unknown processing instructions are currently ignored
472 break;
473 }
474 break;
475
476 case XmlNodeType.Element:
477 if (0 < state.IncludeNextStack.Count && state.IncludeNextStack.Peek())
478 {
479 if ("Include" != reader.LocalName)
480 {
481 this.Messaging.Write(ErrorMessages.InvalidDocumentElement(sourceLineNumbers, reader.Name, "include", "Include"));
482 }
483
484 state.IncludeNextStack.Pop();
485 state.IncludeNextStack.Push(false);
486 break;
487 }
488
489 var empty = reader.IsEmptyElement;
490 var ns = XNamespace.Get(reader.NamespaceURI);
491 var element = new XElement(ns + reader.LocalName);
492 currentContainer.Add(element);
493
494 this.UpdateCurrentLineNumber(state, reader, offset);
495 element.AddAnnotation(sourceLineNumbers);
496
497 while (reader.MoveToNextAttribute())
498 {
499 var value = state.Helper.PreprocessString(state.Context, reader.Value);
500
501 var attribNamespace = XNamespace.Get(reader.NamespaceURI);
502 attribNamespace = XNamespace.Xmlns == attribNamespace && reader.LocalName.Equals("xmlns") ? XNamespace.None : attribNamespace;
503
504 element.Add(new XAttribute(attribNamespace + reader.LocalName, value));
505 }
506
507 if (!empty)
508 {
509 containerStack.Push(currentContainer);
510 currentContainer = element;
511 }
512 break;
513
514 case XmlNodeType.EndElement:
515 if (0 < reader.Depth || !include)
516 {
517 currentContainer = containerStack.Pop();
518 }
519 break;
520
521 case XmlNodeType.Text:
522 var postprocessedText = state.Helper.PreprocessString(state.Context, reader.Value);
523 currentContainer.Add(postprocessedText);
524 break;
525
526 case XmlNodeType.CDATA:
527 var postprocessedValue = state.Helper.PreprocessString(state.Context, reader.Value);
528 currentContainer.Add(new XCData(postprocessedValue));
529 break;
530
531 default:
532 break;
533 }
534 }
535
536 if (0 != ifStack.Count)
537 {
538 throw new WixException(ErrorMessages.NonterminatedPreprocessorInstruction(state.Context.CurrentSourceLineNumber, "if", "endif"));
539 }
540
541 // TODO: can this actually happen?
542 if (0 != containerStack.Count)
543 {
544 throw new WixException(ErrorMessages.NonterminatedPreprocessorInstruction(state.Context.CurrentSourceLineNumber, "nodes", "nodes"));
545 }
546 }
547
548 /// <summary>
549 /// Processes an error processing instruction.
550 /// </summary>
551 /// <param name="state"></param>
552 /// <param name="errorMessage">Text from source.</param>
553 private void PreprocessError(ProcessingState state, string errorMessage)
554 {
555 // Resolve other variables in the error message.
556 errorMessage = state.Helper.PreprocessString(state.Context, errorMessage);
557
558 throw new WixException(ErrorMessages.PreprocessorError(state.Context.CurrentSourceLineNumber, errorMessage));
559 }
560
561 /// <summary>
562 /// Processes a warning processing instruction.
563 /// </summary>
564 /// <param name="state"></param>
565 /// <param name="warningMessage">Text from source.</param>
566 private void PreprocessWarning(ProcessingState state, string warningMessage)
567 {
568 // Resolve other variables in the warning message.
569 warningMessage = state.Helper.PreprocessString(state.Context, warningMessage);
570
571 this.Messaging.Write(WarningMessages.PreprocessorWarning(state.Context.CurrentSourceLineNumber, warningMessage));
572 }
573
574 /// <summary>
575 /// Processes a define processing instruction and creates the appropriate parameter.
576 /// </summary>
577 /// <param name="state"></param>
578 /// <param name="originalDefine">Text from source.</param>
579 private void PreprocessDefine(ProcessingState state, string originalDefine)
580 {
581 var match = DefineRegex.Match(originalDefine);
582
583 if (!match.Success)
584 {
585 throw new WixException(ErrorMessages.IllegalDefineStatement(state.Context.CurrentSourceLineNumber, originalDefine));
586 }
587
588 var defineName = match.Groups["varName"].Value;
589 var defineValue = match.Groups["varValue"].Value;
590
591 // strip off the optional quotes
592 if (1 < defineValue.Length &&
593 ((defineValue.StartsWith("\"", StringComparison.Ordinal) && defineValue.EndsWith("\"", StringComparison.Ordinal))
594 || (defineValue.StartsWith("'", StringComparison.Ordinal) && defineValue.EndsWith("'", StringComparison.Ordinal))))
595 {
596 defineValue = defineValue.Substring(1, defineValue.Length - 2);
597 }
598
599 // resolve other variables in the variable value
600 defineValue = state.Helper.PreprocessString(state.Context, defineValue);
601
602 if (defineName.StartsWith("var.", StringComparison.Ordinal))
603 {
604 state.Helper.AddVariable(state.Context, defineName.Substring(4), defineValue);
605 }
606 else
607 {
608 state.Helper.AddVariable(state.Context, defineName, defineValue);
609 }
610 }
611
612 /// <summary>
613 /// Processes an undef processing instruction and creates the appropriate parameter.
614 /// </summary>
615 /// <param name="state"></param>
616 /// <param name="originalDefine">Text from source.</param>
617 private void PreprocessUndef(ProcessingState state, string originalDefine)
618 {
619 var name = state.Helper.PreprocessString(state.Context, originalDefine.Trim());
620
621 if (name.StartsWith("var.", StringComparison.Ordinal))
622 {
623 state.Helper.RemoveVariable(state.Context, name.Substring(4));
624 }
625 else
626 {
627 state.Helper.RemoveVariable(state.Context, name);
628 }
629 }
630
631 /// <summary>
632 /// Processes an included file.
633 /// </summary>
634 /// <param name="state"></param>
635 /// <param name="includePath">Path to included file.</param>
636 /// <param name="parent">Parent container for included content.</param>
637 private void PreprocessInclude(ProcessingState state, string includePath, XContainer parent)
638 {
639 var sourceLineNumbers = state.Context.CurrentSourceLineNumber;
640
641 // Preprocess variables in the path.
642 includePath = state.Helper.PreprocessString(state.Context, includePath);
643
644 var includeFile = this.GetIncludeFile(state, includePath);
645
646 if (null == includeFile)
647 {
648 throw new WixException(ErrorMessages.FileNotFound(sourceLineNumbers, includePath, "include"));
649 }
650
651 using (var reader = XmlReader.Create(includeFile, DocumentXmlReaderSettings))
652 {
653 this.PushInclude(state, includeFile);
654
655 // process the included reader into the writer
656 try
657 {
658 this.PreprocessReader(state, true, reader, parent, 0);
659 }
660 catch (XmlException e)
661 {
662 this.UpdateCurrentLineNumber(state, reader, 0);
663 throw new WixException(ErrorMessages.InvalidXml(sourceLineNumbers, "source", e.Message));
664 }
665
666 this.IncludedFile?.Invoke(this, new IncludedFileEventArgs(sourceLineNumbers, includeFile));
667
668 var includedFile = this.ServiceProvider.GetService<IIncludedFile>();
669 includedFile.Path = includeFile;
670 includedFile.SourceLineNumbers = sourceLineNumbers;
671
672 state.IncludedFiles.Add(includedFile);
673
674 this.PopInclude(state);
675 }
676 }
677
678 /// <summary>
679 /// Preprocess a foreach processing instruction.
680 /// </summary>
681 /// <param name="state"></param>
682 /// <param name="reader">The xml reader.</param>
683 /// <param name="container">The container where to output processed data.</param>
684 /// <param name="offset">Offset for the line numbers.</param>
685 private void PreprocessForeach(ProcessingState state, XmlReader reader, XContainer container, int offset)
686 {
687 // Find the "in" token.
688 var indexOfInToken = reader.Value.IndexOf(" in ", StringComparison.Ordinal);
689 if (0 > indexOfInToken)
690 {
691 throw new WixException(ErrorMessages.IllegalForeach(state.Context.CurrentSourceLineNumber, reader.Value));
692 }
693
694 // parse out the variable name
695 var varName = reader.Value.Substring(0, indexOfInToken).Trim();
696 var varValuesString = reader.Value.Substring(indexOfInToken + 4).Trim();
697
698 if (varValuesString.StartsWith("\"", StringComparison.Ordinal))
699 {
700 if (!varValuesString.EndsWith("\"", StringComparison.Ordinal))
701 {
702 throw new WixException(ErrorMessages.UnmatchedQuotesInExpression(state.Context.CurrentSourceLineNumber, varValuesString));
703 }
704
705 // cut the quotes off the string
706 varValuesString = varValuesString.Substring(1, varValuesString.Length - 2);
707 }
708
709 // preprocess the variable values string because it might be a variable itself
710 varValuesString = state.Helper.PreprocessString(state.Context, varValuesString);
711
712 var varValues = varValuesString.Split(';');
713
714 // go through all the empty strings
715 while (reader.Read() && XmlNodeType.Whitespace == reader.NodeType)
716 {
717 }
718
719 // get the offset of this xml fragment (for some reason its always off by 1)
720 if (reader is IXmlLineInfo lineInfoReader)
721 {
722 offset += lineInfoReader.LineNumber - 1;
723 }
724
725 // dump the xml to a string (maintaining whitespace if possible)
726 if (reader is XmlTextReader textReader)
727 {
728 textReader.WhitespaceHandling = WhitespaceHandling.All;
729 }
730
731 var fragmentBuilder = new StringBuilder();
732 var nestedForeachCount = 1;
733 while (nestedForeachCount != 0)
734 {
735 if (reader.NodeType == XmlNodeType.ProcessingInstruction)
736 {
737 switch (reader.LocalName)
738 {
739 case "foreach":
740 ++nestedForeachCount;
741 // Output the foreach statement
742 fragmentBuilder.AppendFormat("<?foreach {0}?>", reader.Value);
743 break;
744
745 case "endforeach":
746 --nestedForeachCount;
747 if (0 != nestedForeachCount)
748 {
749 fragmentBuilder.Append("<?endforeach ?>");
750 }
751 break;
752
753 default:
754 fragmentBuilder.AppendFormat("<?{0} {1}?>", reader.LocalName, reader.Value);
755 break;
756 }
757 }
758 else if (reader.NodeType == XmlNodeType.Element)
759 {
760 fragmentBuilder.Append(reader.ReadOuterXml());
761 continue;
762 }
763 else if (reader.NodeType == XmlNodeType.Whitespace)
764 {
765 // Or output the whitespace
766 fragmentBuilder.Append(reader.Value);
767 }
768 else if (reader.NodeType == XmlNodeType.None)
769 {
770 throw new WixException(ErrorMessages.ExpectedEndforeach(state.Context.CurrentSourceLineNumber));
771 }
772
773 reader.Read();
774 }
775
776 using (var fragmentStream = new MemoryStream(Encoding.UTF8.GetBytes(fragmentBuilder.ToString())))
777 {
778 // process each iteration, updating the variable's value each time
779 foreach (var varValue in varValues)
780 {
781 using (var loopReader = XmlReader.Create(fragmentStream, FragmentXmlReaderSettings))
782 {
783 // Always overwrite foreach variables.
784 state.Helper.AddVariable(state.Context, varName, varValue, false);
785
786 try
787 {
788 this.PreprocessReader(state, false, loopReader, container, offset);
789 }
790 catch (XmlException e)
791 {
792 this.UpdateCurrentLineNumber(state, loopReader, offset);
793 throw new WixException(ErrorMessages.InvalidXml(state.Context.CurrentSourceLineNumber, "source", e.Message));
794 }
795
796 fragmentStream.Position = 0; // seek back to the beginning for the next loop.
797 }
798 }
799 }
800 }
801
802 /// <summary>
803 /// Processes a pragma processing instruction
804 /// </summary>
805 /// <param name="state"></param>
806 /// <param name="pragmaText">Text from source.</param>
807 /// <param name="parent"></param>
808 private void PreprocessPragma(ProcessingState state, string pragmaText, XContainer parent)
809 {
810 var match = PragmaRegex.Match(pragmaText);
811
812 if (!match.Success)
813 {
814 throw new WixException(ErrorMessages.InvalidPreprocessorPragma(state.Context.CurrentSourceLineNumber, pragmaText));
815 }
816
817 // resolve other variables in the pragma argument(s)
818 var pragmaArgs = state.Helper.PreprocessString(state.Context, match.Groups["pragmaValue"].Value).Trim();
819
820 try
821 {
822 state.Helper.PreprocessPragma(state.Context, match.Groups["pragmaName"].Value.Trim(), pragmaArgs, parent);
823 }
824 catch (Exception e)
825 {
826 throw new WixException(ErrorMessages.PreprocessorExtensionPragmaFailed(state.Context.CurrentSourceLineNumber, pragmaText, e.Message));
827 }
828 }
829
830 /// <summary>
831 /// Gets the next token in an expression.
832 /// </summary>
833 /// <param name="state"></param>
834 /// <param name="originalExpression">Expression to parse.</param>
835 /// <param name="expression">Expression with token removed.</param>
836 /// <param name="stringLiteral">Flag if token is a string literal instead of a variable.</param>
837 /// <returns>Next token.</returns>
838 private string GetNextToken(ProcessingState state, string originalExpression, ref string expression, out bool stringLiteral)
839 {
840 stringLiteral = false;
841 string token;
842 expression = expression.Trim();
843 if (0 == expression.Length)
844 {
845 return String.Empty;
846 }
847
848 if (expression.StartsWith("\"", StringComparison.Ordinal))
849 {
850 stringLiteral = true;
851 var endingQuotes = expression.IndexOf('\"', 1);
852 if (-1 == endingQuotes)
853 {
854 throw new WixException(ErrorMessages.UnmatchedQuotesInExpression(state.Context.CurrentSourceLineNumber, originalExpression));
855 }
856
857 // cut the quotes off the string
858 token = state.Helper.PreprocessString(state.Context, expression.Substring(1, endingQuotes - 1));
859
860 // advance past this string
861 expression = expression.Substring(endingQuotes + 1).Trim();
862 }
863 else if (expression.StartsWith("$(", StringComparison.Ordinal))
864 {
865 // Find the ending paren of the expression
866 var endingParen = -1;
867 var openedCount = 1;
868 for (var i = 2; i < expression.Length; i++)
869 {
870 if ('(' == expression[i])
871 {
872 openedCount++;
873 }
874 else if (')' == expression[i])
875 {
876 openedCount--;
877 }
878
879 if (openedCount == 0)
880 {
881 endingParen = i;
882 break;
883 }
884 }
885
886 if (-1 == endingParen)
887 {
888 throw new WixException(ErrorMessages.UnmatchedParenthesisInExpression(state.Context.CurrentSourceLineNumber, originalExpression));
889 }
890 token = expression.Substring(0, endingParen + 1);
891
892 // Advance past this variable
893 expression = expression.Substring(endingParen + 1).Trim();
894 }
895 else
896 {
897 // Cut the token off at the next equal, space, inequality operator,
898 // or end of string, whichever comes first
899 var space = expression.IndexOf(" ", StringComparison.Ordinal);
900 var equals = expression.IndexOf("=", StringComparison.Ordinal);
901 var doubleEquals = expression.IndexOf("==", StringComparison.Ordinal);
902 var lessThan = expression.IndexOf("<", StringComparison.Ordinal);
903 var lessThanEquals = expression.IndexOf("<=", StringComparison.Ordinal);
904 var greaterThan = expression.IndexOf(">", StringComparison.Ordinal);
905 var greaterThanEquals = expression.IndexOf(">=", StringComparison.Ordinal);
906 var notEquals = expression.IndexOf("!=", StringComparison.Ordinal);
907 var equalsNoCase = expression.IndexOf("~=", StringComparison.Ordinal);
908 int closingIndex;
909
910 if (space == -1)
911 {
912 space = Int32.MaxValue;
913 }
914
915 if (equals == -1)
916 {
917 equals = Int32.MaxValue;
918 }
919
920 if (doubleEquals == -1)
921 {
922 doubleEquals = Int32.MaxValue;
923 }
924
925 if (lessThan == -1)
926 {
927 lessThan = Int32.MaxValue;
928 }
929
930 if (lessThanEquals == -1)
931 {
932 lessThanEquals = Int32.MaxValue;
933 }
934
935 if (greaterThan == -1)
936 {
937 greaterThan = Int32.MaxValue;
938 }
939
940 if (greaterThanEquals == -1)
941 {
942 greaterThanEquals = Int32.MaxValue;
943 }
944
945 if (notEquals == -1)
946 {
947 notEquals = Int32.MaxValue;
948 }
949
950 if (equalsNoCase == -1)
951 {
952 equalsNoCase = Int32.MaxValue;
953 }
954
955 closingIndex = Math.Min(space, Math.Min(equals, Math.Min(doubleEquals, Math.Min(lessThan, Math.Min(lessThanEquals, Math.Min(greaterThan, Math.Min(greaterThanEquals, Math.Min(equalsNoCase, notEquals))))))));
956
957 if (Int32.MaxValue == closingIndex)
958 {
959 closingIndex = expression.Length;
960 }
961
962 // If the index is 0, we hit an operator, so return it
963 if (0 == closingIndex)
964 {
965 // Length 2 operators
966 if (closingIndex == doubleEquals || closingIndex == lessThanEquals || closingIndex == greaterThanEquals || closingIndex == notEquals || closingIndex == equalsNoCase)
967 {
968 closingIndex = 2;
969 }
970 else // Length 1 operators
971 {
972 closingIndex = 1;
973 }
974 }
975
976 // Cut out the new token
977 token = expression.Substring(0, closingIndex).Trim();
978 expression = expression.Substring(closingIndex).Trim();
979 }
980
981 return token;
982 }
983
984 /// <summary>
985 /// Gets the value for a variable.
986 /// </summary>
987 /// <param name="state"></param>
988 /// <param name="originalExpression">Original expression for error message.</param>
989 /// <param name="variable">Variable to evaluate.</param>
990 /// <returns>Value of variable.</returns>
991 private string EvaluateVariable(ProcessingState state, string originalExpression, string variable)
992 {
993 // By default it's a literal and will only be evaluated if it
994 // matches the variable format
995 var varValue = variable;
996
997 if (variable.StartsWith("$(", StringComparison.Ordinal))
998 {
999 try
1000 {
1001 varValue = state.Helper.PreprocessString(state.Context, variable);
1002 }
1003 catch (ArgumentNullException)
1004 {
1005 // non-existent variables are expected
1006 varValue = null;
1007 }
1008 }
1009 else if (variable.IndexOf("(", StringComparison.Ordinal) != -1 || variable.IndexOf(")", StringComparison.Ordinal) != -1)
1010 {
1011 // make sure it doesn't contain parenthesis
1012 throw new WixException(ErrorMessages.UnmatchedParenthesisInExpression(state.Context.CurrentSourceLineNumber, originalExpression));
1013 }
1014 else if (variable.IndexOf("\"", StringComparison.Ordinal) != -1)
1015 {
1016 // shouldn't contain quotes
1017 throw new WixException(ErrorMessages.UnmatchedQuotesInExpression(state.Context.CurrentSourceLineNumber, originalExpression));
1018 }
1019
1020 return varValue;
1021 }
1022
1023 /// <summary>
1024 /// Gets the left side value, operator, and right side value of an expression.
1025 /// </summary>
1026 /// <param name="state"></param>
1027 /// <param name="originalExpression">Original expression to evaluate.</param>
1028 /// <param name="expression">Expression modified while processing.</param>
1029 /// <param name="leftValue">Left side value from expression.</param>
1030 /// <param name="operation">Operation in expression.</param>
1031 /// <param name="rightValue">Right side value from expression.</param>
1032 private void GetNameValuePair(ProcessingState state, string originalExpression, ref string expression, out string leftValue, out string operation, out string rightValue)
1033 {
1034 leftValue = this.GetNextToken(state, originalExpression, ref expression, out var stringLiteral);
1035
1036 // If it wasn't a string literal, evaluate it
1037 if (!stringLiteral)
1038 {
1039 leftValue = this.EvaluateVariable(state, originalExpression, leftValue);
1040 }
1041
1042 // Get the operation
1043 operation = this.GetNextToken(state, originalExpression, ref expression, out stringLiteral);
1044 if (IsOperator(operation))
1045 {
1046 if (stringLiteral)
1047 {
1048 throw new WixException(ErrorMessages.UnmatchedQuotesInExpression(state.Context.CurrentSourceLineNumber, originalExpression));
1049 }
1050
1051 rightValue = this.GetNextToken(state, originalExpression, ref expression, out stringLiteral);
1052
1053 // If it wasn't a string literal, evaluate it
1054 if (!stringLiteral)
1055 {
1056 rightValue = this.EvaluateVariable(state, originalExpression, rightValue);
1057 }
1058 }
1059 else
1060 {
1061 // Prepend the token back on the expression since it wasn't an operator
1062 // and put the quotes back on the literal if necessary
1063
1064 if (stringLiteral)
1065 {
1066 operation = "\"" + operation + "\"";
1067 }
1068 expression = (operation + " " + expression).Trim();
1069
1070 // If no operator, just check for existence
1071 operation = "";
1072 rightValue = "";
1073 }
1074 }
1075
1076 /// <summary>
1077 /// Evaluates an expression.
1078 /// </summary>
1079 /// <param name="state"></param>
1080 /// <param name="originalExpression">Original expression to evaluate.</param>
1081 /// <param name="expression">Expression modified while processing.</param>
1082 /// <returns>true if expression evaluates to true.</returns>
1083 private bool EvaluateAtomicExpression(ProcessingState state, string originalExpression, ref string expression)
1084 {
1085 // Quick test to see if the first token is a variable
1086 var startsWithVariable = expression.StartsWith("$(", StringComparison.Ordinal);
1087 this.GetNameValuePair(state, originalExpression, ref expression, out var leftValue, out var operation, out var rightValue);
1088
1089 var expressionValue = false;
1090
1091 // If the variables don't exist, they were evaluated to null
1092 if (null == leftValue || null == rightValue)
1093 {
1094 if (operation.Length > 0)
1095 {
1096 throw new WixException(ErrorMessages.ExpectedVariable(state.Context.CurrentSourceLineNumber, originalExpression));
1097 }
1098
1099 // false expression
1100 }
1101 else if (operation.Length == 0)
1102 {
1103 // There is no right side of the equation.
1104 // If the variable was evaluated, test to see if it is a "false" value.
1105 if (startsWithVariable)
1106 {
1107 expressionValue = !(leftValue == String.Empty || leftValue == "0" || leftValue == "false" || leftValue == "no");
1108 }
1109 else
1110 {
1111 throw new WixException(ErrorMessages.UnexpectedLiteral(state.Context.CurrentSourceLineNumber, originalExpression));
1112 }
1113 }
1114 else
1115 {
1116 leftValue = leftValue.Trim();
1117 rightValue = rightValue.Trim();
1118 if ("=" == operation || "==" == operation)
1119 {
1120 if (leftValue == rightValue)
1121 {
1122 expressionValue = true;
1123 }
1124 }
1125 else if ("!=" == operation)
1126 {
1127 if (leftValue != rightValue)
1128 {
1129 expressionValue = true;
1130 }
1131 }
1132 else if ("~=" == operation)
1133 {
1134 if (String.Equals(leftValue, rightValue, StringComparison.OrdinalIgnoreCase))
1135 {
1136 expressionValue = true;
1137 }
1138 }
1139 else
1140 {
1141 // Convert the numbers from strings
1142 int rightInt;
1143 int leftInt;
1144 try
1145 {
1146 rightInt = Int32.Parse(rightValue, CultureInfo.InvariantCulture);
1147 leftInt = Int32.Parse(leftValue, CultureInfo.InvariantCulture);
1148 }
1149 catch (FormatException)
1150 {
1151 throw new WixException(ErrorMessages.IllegalIntegerInExpression(state.Context.CurrentSourceLineNumber, originalExpression));
1152 }
1153 catch (OverflowException)
1154 {
1155 throw new WixException(ErrorMessages.IllegalIntegerInExpression(state.Context.CurrentSourceLineNumber, originalExpression));
1156 }
1157
1158 // Compare the numbers
1159 if ("<" == operation && leftInt < rightInt ||
1160 "<=" == operation && leftInt <= rightInt ||
1161 ">" == operation && leftInt > rightInt ||
1162 ">=" == operation && leftInt >= rightInt)
1163 {
1164 expressionValue = true;
1165 }
1166 }
1167 }
1168
1169 return expressionValue;
1170 }
1171
1172 /// <summary>
1173 /// Gets a sub-expression in parenthesis.
1174 /// </summary>
1175 /// <param name="state"></param>
1176 /// <param name="originalExpression">Original expression to evaluate.</param>
1177 /// <param name="expression">Expression modified while processing.</param>
1178 /// <param name="endSubExpression">Index of end of sub-expression.</param>
1179 /// <returns>Sub-expression in parenthesis.</returns>
1180 private string GetParenthesisExpression(ProcessingState state, string originalExpression, string expression, out int endSubExpression)
1181 {
1182 endSubExpression = 0;
1183
1184 // if the expression doesn't start with parenthesis, leave it alone
1185 if (!expression.StartsWith("(", StringComparison.Ordinal))
1186 {
1187 return expression;
1188 }
1189
1190 // search for the end of the expression with the matching paren
1191 var openParenIndex = 0;
1192 var closeParenIndex = 1;
1193 while (openParenIndex != -1 && openParenIndex < closeParenIndex)
1194 {
1195 closeParenIndex = expression.IndexOf(')', closeParenIndex);
1196 if (closeParenIndex == -1)
1197 {
1198 throw new WixException(ErrorMessages.UnmatchedParenthesisInExpression(state.Context.CurrentSourceLineNumber, originalExpression));
1199 }
1200
1201 if (InsideQuotes(expression, closeParenIndex))
1202 {
1203 // ignore stuff inside quotes (it's a string literal)
1204 }
1205 else
1206 {
1207 // Look to see if there is another open paren before the close paren
1208 // and skip over the open parens while they are in a string literal
1209 do
1210 {
1211 openParenIndex++;
1212 openParenIndex = expression.IndexOf('(', openParenIndex, closeParenIndex - openParenIndex);
1213 }
1214 while (InsideQuotes(expression, openParenIndex));
1215 }
1216
1217 // Advance past the closing paren
1218 closeParenIndex++;
1219 }
1220
1221 endSubExpression = closeParenIndex;
1222
1223 // Return the expression minus the parenthesis
1224 return expression.Substring(1, closeParenIndex - 2);
1225 }
1226
1227 /// <summary>
1228 /// Updates expression based on operation.
1229 /// </summary>
1230 /// <param name="state"></param>
1231 /// <param name="currentValue">State to update.</param>
1232 /// <param name="operation">Operation to apply to current value.</param>
1233 /// <param name="prevResult">Previous result.</param>
1234 private void UpdateExpressionValue(ProcessingState state, ref bool currentValue, PreprocessorOperation operation, bool prevResult)
1235 {
1236 switch (operation)
1237 {
1238 case PreprocessorOperation.And:
1239 currentValue = currentValue && prevResult;
1240 break;
1241 case PreprocessorOperation.Or:
1242 currentValue = currentValue || prevResult;
1243 break;
1244 case PreprocessorOperation.Not:
1245 currentValue = !currentValue;
1246 break;
1247 default:
1248 throw new WixException(ErrorMessages.UnexpectedPreprocessorOperator(state.Context.CurrentSourceLineNumber, operation.ToString()));
1249 }
1250 }
1251
1252 /// <summary>
1253 /// Evaluate an expression.
1254 /// </summary>
1255 /// <param name="state"></param>
1256 /// <param name="expression">Expression to evaluate.</param>
1257 /// <returns>Boolean result of expression.</returns>
1258 private bool EvaluateExpression(ProcessingState state, string expression)
1259 {
1260 var tmpExpression = expression;
1261 return this.EvaluateExpressionRecurse(state, expression, ref tmpExpression, PreprocessorOperation.And, true);
1262 }
1263
1264 /// <summary>
1265 /// Recurse through the expression to evaluate if it is true or false.
1266 /// The expression is evaluated left to right.
1267 /// The expression is case-sensitive (converted to upper case) with the
1268 /// following exceptions: variable names and keywords (and, not, or).
1269 /// Comparisons with = and != are string comparisons.
1270 /// Comparisons with inequality operators must be done on valid integers.
1271 ///
1272 /// The operator precedence is:
1273 /// ""
1274 /// ()
1275 /// &lt;, &gt;, &lt;=, &gt;=, =, !=
1276 /// Not
1277 /// And, Or
1278 ///
1279 /// Valid expressions include:
1280 /// not $(var.B) or not $(var.C)
1281 /// (($(var.A))and $(var.B) ="2")or Not((($(var.C))) and $(var.A))
1282 /// (($(var.A)) and $(var.B) = " 3 ") or $(var.C)
1283 /// $(var.A) and $(var.C) = "3" or $(var.C) and $(var.D) = $(env.windir)
1284 /// $(var.A) and $(var.B)>2 or $(var.B) &lt;= 2
1285 /// $(var.A) != "2"
1286 /// </summary>
1287 /// <param name="state"></param>
1288 /// <param name="originalExpression">The original expression</param>
1289 /// <param name="expression">The expression currently being evaluated</param>
1290 /// <param name="prevResultOperation">The operation to apply to this result</param>
1291 /// <param name="prevResult">The previous result to apply to this result</param>
1292 /// <returns>Boolean to indicate if the expression is true or false</returns>
1293 private bool EvaluateExpressionRecurse(ProcessingState state, string originalExpression, ref string expression, PreprocessorOperation prevResultOperation, bool prevResult)
1294 {
1295 bool expressionValue;
1296 expression = expression.Trim();
1297 if (expression.Length == 0)
1298 {
1299 throw new WixException(ErrorMessages.UnexpectedEmptySubexpression(state.Context.CurrentSourceLineNumber, originalExpression));
1300 }
1301
1302 // If the expression starts with parenthesis, evaluate it
1303 if (expression.IndexOf('(') == 0)
1304 {
1305 var subExpression = this.GetParenthesisExpression(state, originalExpression, expression, out var endSubExpressionIndex);
1306 expressionValue = this.EvaluateExpressionRecurse(state, originalExpression, ref subExpression, PreprocessorOperation.And, true);
1307
1308 // Now get the rest of the expression that hasn't been evaluated
1309 expression = expression.Substring(endSubExpressionIndex).Trim();
1310 }
1311 else
1312 {
1313 // Check for NOT
1314 if (StartsWithKeyword(expression, PreprocessorOperation.Not))
1315 {
1316 expression = expression.Substring(3).Trim();
1317 if (expression.Length == 0)
1318 {
1319 throw new WixException(ErrorMessages.ExpectedExpressionAfterNot(state.Context.CurrentSourceLineNumber, originalExpression));
1320 }
1321
1322 expressionValue = this.EvaluateExpressionRecurse(state, originalExpression, ref expression, PreprocessorOperation.Not, true);
1323 }
1324 else // Expect a literal
1325 {
1326 expressionValue = this.EvaluateAtomicExpression(state, originalExpression, ref expression);
1327
1328 // Expect the literal that was just evaluated to already be cut off
1329 }
1330 }
1331 this.UpdateExpressionValue(state, ref expressionValue, prevResultOperation, prevResult);
1332
1333 // If there's still an expression left, it must start with AND or OR.
1334 if (expression.Trim().Length > 0)
1335 {
1336 if (StartsWithKeyword(expression, PreprocessorOperation.And))
1337 {
1338 expression = expression.Substring(3);
1339 return this.EvaluateExpressionRecurse(state, originalExpression, ref expression, PreprocessorOperation.And, expressionValue);
1340 }
1341 else if (StartsWithKeyword(expression, PreprocessorOperation.Or))
1342 {
1343 expression = expression.Substring(2);
1344 return this.EvaluateExpressionRecurse(state, originalExpression, ref expression, PreprocessorOperation.Or, expressionValue);
1345 }
1346 else
1347 {
1348 throw new WixException(ErrorMessages.InvalidSubExpression(state.Context.CurrentSourceLineNumber, expression, originalExpression));
1349 }
1350 }
1351
1352 return expressionValue;
1353 }
1354
1355 /// <summary>
1356 /// Update the current line number with the reader's current state.
1357 /// </summary>
1358 /// <param name="state"></param>
1359 /// <param name="reader">The xml reader for the preprocessor.</param>
1360 /// <param name="offset">This is the artificial offset of the line numbers from the reader. Used for the foreach processing.</param>
1361 private void UpdateCurrentLineNumber(ProcessingState state, XmlReader reader, int offset)
1362 {
1363 if (reader is IXmlLineInfo lineInfoReader)
1364 {
1365 var newLine = lineInfoReader.LineNumber + offset;
1366
1367 if (state.Context.CurrentSourceLineNumber.LineNumber != newLine)
1368 {
1369 state.Context.CurrentSourceLineNumber = new SourceLineNumber(state.Context.CurrentSourceLineNumber.FileName, state.Context.CurrentSourceLineNumber.Parent, newLine);
1370 }
1371 }
1372 }
1373
1374 /// <summary>
1375 /// Pushes a file name on the stack of included files.
1376 /// </summary>
1377 /// <param name="state"></param>
1378 /// <param name="fileName">Name to push on to the stack of included files.</param>
1379 private void PushInclude(ProcessingState state, string fileName)
1380 {
1381 if (1023 < state.CurrentFileStack.Count)
1382 {
1383 throw new WixException(ErrorMessages.TooDeeplyIncluded(state.Context.CurrentSourceLineNumber, state.CurrentFileStack.Count));
1384 }
1385
1386 var path = Path.GetFullPath(fileName);
1387
1388 state.CurrentFileStack.Push(path);
1389 state.SourceStack.Push(state.Context.CurrentSourceLineNumber);
1390 state.Context.CurrentSourceLineNumber = new SourceLineNumber(path, state.Context.CurrentSourceLineNumber);
1391 state.IncludeNextStack.Push(true);
1392 }
1393
1394 /// <summary>
1395 /// Pops a file name from the stack of included files.
1396 /// </summary>
1397 private void PopInclude(ProcessingState state)
1398 {
1399 state.Context.CurrentSourceLineNumber = state.SourceStack.Pop();
1400
1401 state.CurrentFileStack.Pop();
1402 state.IncludeNextStack.Pop();
1403 }
1404
1405 /// <summary>
1406 /// Go through search paths, looking for a matching include file.
1407 /// Start the search in the directory of the source file, then go
1408 /// through the search paths in the order given on the command line
1409 /// (leftmost first, ...).
1410 /// </summary>
1411 /// <param name="state"></param>
1412 /// <param name="includePath">User-specified path to the included file (usually just the file name).</param>
1413 /// <returns>Returns a FileInfo for the found include file, or null if the file cannot be found.</returns>
1414 private string GetIncludeFile(ProcessingState state, string includePath)
1415 {
1416 string finalIncludePath = null;
1417
1418 includePath = includePath.Trim();
1419
1420 // remove quotes (only if they match)
1421 if ((includePath.StartsWith("\"", StringComparison.Ordinal) && includePath.EndsWith("\"", StringComparison.Ordinal)) ||
1422 (includePath.StartsWith("'", StringComparison.Ordinal) && includePath.EndsWith("'", StringComparison.Ordinal)))
1423 {
1424 includePath = includePath.Substring(1, includePath.Length - 2);
1425 }
1426
1427 // check if the include file is a full path
1428 if (Path.IsPathRooted(includePath))
1429 {
1430 if (File.Exists(includePath))
1431 {
1432 finalIncludePath = includePath;
1433 }
1434 }
1435 else // relative path
1436 {
1437 // build a string to test the directory containing the source file first
1438 var currentFolder = state.CurrentFileStack.Peek();
1439 var includeTestPath = Path.Combine(Path.GetDirectoryName(currentFolder), includePath);
1440
1441 // test the source file directory
1442 if (File.Exists(includeTestPath))
1443 {
1444 finalIncludePath = includeTestPath;
1445 }
1446 else if (state.Context.IncludeSearchPaths != null) // test all search paths in the order specified on the command line
1447 {
1448 foreach (var includeSearchPath in state.Context.IncludeSearchPaths)
1449 {
1450 // if the path exists, we have found the final string
1451 includeTestPath = Path.Combine(includeSearchPath, includePath);
1452 if (File.Exists(includeTestPath))
1453 {
1454 finalIncludePath = includeTestPath;
1455 break;
1456 }
1457 }
1458 }
1459 }
1460
1461 return finalIncludePath;
1462 }
1463
1464 private void PreProcess(ProcessingState state)
1465 {
1466 if (state.Context.Extensions == null)
1467 {
1468 return;
1469 }
1470
1471 foreach (var extension in state.Context.Extensions)
1472 {
1473 if (extension.Prefixes != null)
1474 {
1475 foreach (var prefix in extension.Prefixes)
1476 {
1477 if (!state.ExtensionsByPrefix.TryGetValue(prefix, out var collidingExtension))
1478 {
1479 state.ExtensionsByPrefix.Add(prefix, extension);
1480 }
1481 else
1482 {
1483 this.Messaging.Write(ErrorMessages.DuplicateExtensionPreprocessorType(extension.GetType().ToString(), prefix, collidingExtension.GetType().ToString()));
1484 }
1485 }
1486 }
1487
1488 extension.PrePreprocess(state.Context);
1489 }
1490 }
1491
1492 private void PostProcess(ProcessingState state, IPreprocessResult result)
1493 {
1494 if (state.Context.Extensions == null)
1495 {
1496 return;
1497 }
1498
1499 foreach (var extension in state.Context.Extensions)
1500 {
1501 extension.PostPreprocess(result);
1502 }
1503 }
1504
1505 private class ProcessingState
1506 {
1507 public ProcessingState(IServiceProvider serviceProvider, IPreprocessContext context)
1508 {
1509 var path = Path.GetFullPath(context.SourcePath);
1510
1511 this.Context = context;
1512 this.Context.CurrentSourceLineNumber = new SourceLineNumber(path);
1513 this.Context.Variables = this.Context.Variables == null ? new Dictionary<string, string>() : new Dictionary<string, string>(this.Context.Variables);
1514
1515 this.Helper = serviceProvider.GetService<IPreprocessHelper>();
1516 }
1517
1518 public IPreprocessContext Context { get; }
1519
1520 public IPreprocessHelper Helper { get; }
1521
1522 public List<IIncludedFile> IncludedFiles { get; } = new List<IIncludedFile>();
1523
1524 public XDocument Output { get; } = new XDocument();
1525
1526 public Stack<string> CurrentFileStack { get; } = new Stack<string>();
1527
1528 public Dictionary<string, IPreprocessorExtension> ExtensionsByPrefix { get; } = new Dictionary<string, IPreprocessorExtension>();
1529
1530 public Stack<bool> IncludeNextStack { get; } = new Stack<bool>();
1531
1532 public Stack<SourceLineNumber> SourceStack { get; } = new Stack<SourceLineNumber>();
1533 }
1534 }
1535 }