Allow quoted values in foreach
Fixes 7039
Rob Mensching committed
Dec 6, 2022 at 14:11 UTC
07fbc3561fb66dba1502305ba4aff1db905c84e4
2 files changed
+287
-226
src/wix/WixToolset.Core/Preprocessor.cs
+237
-226
@@ -242,26 +242,26 @@ namespace WixToolset.Core
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;
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
}
@@ -298,97 +298,97 @@ namespace WixToolset.Core
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;
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;
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;
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
- }
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
- }
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;
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
- }
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
- }
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;
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
- }
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;
389
+ ifContext = ifStack.Pop();
390
+ ignore = true;
391
+ break;
392
}
393
394
if (ignore) // ignore this node since we just handled it above
@@ -404,132 +404,132 @@ namespace WixToolset.Core
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())
407
+ case XmlNodeType.XmlDeclaration:
408
+ if (currentContainer is XDocument document)
409
{
413
- switch (reader.LocalName)
410
+ document.Declaration = new XDeclaration(null, null, null);
411
+ while (reader.MoveToNextAttribute())
412
{
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;
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
}
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);
429
+ //else
430
+ //{
431
+ // display an error? Can this happen?
432
+ //}
433
break;
434
442
- case "error":
443
- this.PreprocessError(state, reader.Value);
444
- break;
435
+ case XmlNodeType.ProcessingInstruction:
436
+ switch (reader.LocalName)
437
+ {
438
+ case "define":
439
+ this.PreprocessDefine(state, reader.Value);
440
+ break;
441
446
- case "warning":
447
- this.PreprocessWarning(state, reader.Value);
448
- break;
442
+ case "error":
443
+ this.PreprocessError(state, reader.Value);
444
+ break;
445
450
- case "undef":
451
- this.PreprocessUndef(state, reader.Value);
452
- break;
446
+ case "warning":
447
+ this.PreprocessWarning(state, reader.Value);
448
+ break;
449
454
- case "include":
455
- this.UpdateCurrentLineNumber(state, reader, offset);
456
- this.PreprocessInclude(state, reader.Value, currentContainer);
457
- break;
450
+ case "undef":
451
+ this.PreprocessUndef(state, reader.Value);
452
+ break;
453
459
- case "foreach":
460
- this.PreprocessForeach(state, reader, currentContainer, offset);
461
- break;
454
+ case "include":
455
+ this.UpdateCurrentLineNumber(state, reader, offset);
456
+ this.PreprocessInclude(state, reader.Value, currentContainer);
457
+ break;
458
463
- case "endforeach": // endforeach is handled in PreprocessForeach, so seeing it here is an error
464
- throw new WixException(ErrorMessages.UnmatchedPreprocessorInstruction(sourceLineNumbers, "foreach", "endforeach"));
459
+ case "foreach":
460
+ this.PreprocessForeach(state, reader, currentContainer, offset);
461
+ break;
462
466
- case "pragma":
467
- this.PreprocessPragma(state, reader.Value, currentContainer);
468
- break;
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
470
- default:
471
- // unknown processing instructions are currently ignored
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;
473
- }
474
- break;
475
476
- case XmlNodeType.Element:
477
- if (0 < state.IncludeNextStack.Count && state.IncludeNextStack.Peek())
478
- {
479
- if ("Include" != reader.LocalName)
476
+ case XmlNodeType.Element:
477
+ if (0 < state.IncludeNextStack.Count && state.IncludeNextStack.Peek())
478
{
481
- this.Messaging.Write(ErrorMessages.InvalidDocumentElement(sourceLineNumbers, reader.Name, "include", "Include"));
482
- }
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
- }
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);
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);
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);
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;
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
- }
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;
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;
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;
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;
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;
531
+ default:
532
+ break;
533
}
534
}
535
@@ -695,6 +695,17 @@ namespace WixToolset.Core
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
@@ -725,23 +736,23 @@ namespace WixToolset.Core
736
{
737
switch (reader.LocalName)
738
{
728
- case "foreach":
729
- ++nestedForeachCount;
730
- // Output the foreach statement
731
- fragmentBuilder.AppendFormat("<?foreach {0}?>", reader.Value);
732
- break;
739
+ case "foreach":
740
+ ++nestedForeachCount;
741
+ // Output the foreach statement
742
+ fragmentBuilder.AppendFormat("<?foreach {0}?>", reader.Value);
743
+ break;
744
734
- case "endforeach":
735
- --nestedForeachCount;
736
- if (0 != nestedForeachCount)
737
- {
738
- fragmentBuilder.Append("<?endforeach ?>");
739
- }
740
- break;
745
+ case "endforeach":
746
+ --nestedForeachCount;
747
+ if (0 != nestedForeachCount)
748
+ {
749
+ fragmentBuilder.Append("<?endforeach ?>");
750
+ }
751
+ break;
752
742
- default:
743
- fragmentBuilder.AppendFormat("<?{0} {1}?>", reader.LocalName, reader.Value);
744
- break;
753
+ default:
754
+ fragmentBuilder.AppendFormat("<?{0} {1}?>", reader.LocalName, reader.Value);
755
+ break;
756
}
757
}
758
else if (reader.NodeType == XmlNodeType.Element)
@@ -1224,17 +1235,17 @@ namespace WixToolset.Core
1235
{
1236
switch (operation)
1237
{
1227
- case PreprocessorOperation.And:
1228
- currentValue = currentValue && prevResult;
1229
- break;
1230
- case PreprocessorOperation.Or:
1231
- currentValue = currentValue || prevResult;
1232
- break;
1233
- case PreprocessorOperation.Not:
1234
- currentValue = !currentValue;
1235
- break;
1236
- default:
1237
- throw new WixException(ErrorMessages.UnexpectedPreprocessorOperator(state.Context.CurrentSourceLineNumber, operation.ToString()));
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
src/wix/test/WixToolsetTest.Core/PreprocessorFixture.cs
+50
@@ -266,6 +266,56 @@ namespace WixToolsetTest.Core
266
WixAssert.CompareLineByLine(expected, actual);
267
}
268
269
+ [Fact]
270
+ public void CanPreprocessForeach()
271
+ {
272
+ var input = String.Join(Environment.NewLine,
273
+ "<Wix>",
274
+ "<?foreach value in A ; B ; C ?>",
275
+ " <Fragment Id='$(value)' />",
276
+ "<?endforeach?>",
277
+ "</Wix>"
278
+ );
279
+ var expected = new[]
280
+ {
281
+ "<Wix>",
282
+ " <Fragment Id=\"A \" />",
283
+ " <Fragment Id=\" B \" />",
284
+ " <Fragment Id=\" C\" />",
285
+ "</Wix>"
286
+ };
287
+
288
+ var result = PreprocessFromString(input);
289
+
290
+ var actual = result.Document.ToString().Split("\r\n");
291
+ WixAssert.CompareLineByLine(expected, actual);
292
+ }
293
+
294
+ [Fact]
295
+ public void CanPreprocessForeachWithQuotes()
296
+ {
297
+ var input = String.Join(Environment.NewLine,
298
+ "<Wix>",
299
+ "<?foreach value in \" A ; B ; C \" ?>",
300
+ " <Fragment Id='$(value)' />",
301
+ "<?endforeach?>",
302
+ "</Wix>"
303
+ );
304
+ var expected = new[]
305
+ {
306
+ "<Wix>",
307
+ " <Fragment Id=\" A \" />",
308
+ " <Fragment Id=\" B \" />",
309
+ " <Fragment Id=\" C \" />",
310
+ "</Wix>"
311
+ };
312
+
313
+ var result = PreprocessFromString(input);
314
+
315
+ var actual = result.Document.ToString().Split("\r\n");
316
+ WixAssert.CompareLineByLine(expected, actual);
317
+ }
318
+
319
private static IPreprocessResult PreprocessFromString(string xml)
320
{
321
using var stringReader = new StringReader(xml);