@joebigelow / wix-1 / commits / b8bd0396

Improve compiler performance by removing regex and other string fixes

Rob Mensching committed Jul 5, 2020 at 23:26 UTC b8bd03960b79e92d38ee7094a88e246253dad800
11 files changed +454 -374
src/WixToolset.Core.WindowsInstaller/Unbind/UnbindDatabaseCommand.cs
+6 -5
@@ -226,11 +226,12 @@ namespace WixToolset.Core.WindowsInstaller.Unbind
226 value = value.Replace("$(", "$$(");
227
228 // escape things that look like wix variables
229 - var matches = Common.WixVariableRegex.Matches(value);
230 - for (var j = matches.Count - 1; 0 <= j; j--)
231 - {
232 - value = value.Insert(matches[j].Index, "!");
233 - }
229 + // TODO: Evaluate this requirement.
230 + //var matches = Common.WixVariableRegex.Matches(value);
231 + //for (var j = matches.Count - 1; 0 <= j; j--)
232 + //{
233 + // value = value.Insert(matches[j].Index, "!");
234 + //}
235
236 row[i] = value;
237 break;
src/WixToolset.Core/Bind/ResolveDelayedFieldsCommand.cs
+25 -48
@@ -69,11 +69,11 @@ namespace WixToolset.Core.Bind
69 }
70
71 // add specialization for ProductVersion fields
72 - string keyProductVersion = "property.ProductVersion";
72 + var keyProductVersion = "property.ProductVersion";
73 if (this.VariableCache.TryGetValue(keyProductVersion, out var versionValue) && Version.TryParse(versionValue, out Version productVersion))
74 {
75 // Don't add the variable if it already exists (developer defined a property with the same name).
76 - string fieldKey = String.Concat(keyProductVersion, ".Major");
76 + var fieldKey = String.Concat(keyProductVersion, ".Major");
77 if (!this.VariableCache.ContainsKey(fieldKey))
78 {
79 this.VariableCache[fieldKey] = productVersion.Major.ToString(CultureInfo.InvariantCulture);
@@ -115,68 +115,45 @@ namespace WixToolset.Core.Bind
115
116 private static string ResolveDelayedVariables(SourceLineNumber sourceLineNumbers, string value, IDictionary<string, string> resolutionData)
117 {
118 - var matches = Common.WixVariableRegex.Matches(value);
118 + var start = 0;
119
120 - if (0 < matches.Count)
120 + while (Common.TryParseWixVariable(value, start, out var parsed))
121 {
122 - var sb = new StringBuilder(value);
123 -
124 - // notice how this code walks backward through the list
125 - // because it modifies the string as we go through it
126 - for (int i = matches.Count - 1; 0 <= i; i--)
122 + if (parsed.Namespace == "bind")
123 {
128 - string variableNamespace = matches[i].Groups["namespace"].Value;
129 - string variableId = matches[i].Groups["fullname"].Value;
130 - string variableDefaultValue = null;
131 - string variableScope = null;
124 + var key = String.Concat(parsed.Name, ".", parsed.Scope);
125
133 - // get the default value if one was specified
134 - if (matches[i].Groups["value"].Success)
126 + if (!resolutionData.TryGetValue(key, out var resolvedValue))
127 {
136 - variableDefaultValue = matches[i].Groups["value"].Value;
128 + resolvedValue = parsed.DefaultValue;
129 }
130
139 - // get the scope if one was specified
140 - if (matches[i].Groups["scope"].Success)
131 + // insert the resolved value if it was found or display an error
132 + if (null != resolvedValue)
133 {
142 - variableScope = matches[i].Groups["scope"].Value;
143 - if ("bind" == variableNamespace)
134 + if (parsed.Index == 0 && parsed.Length == value.Length)
135 {
145 - variableId = matches[i].Groups["name"].Value;
136 + value = resolvedValue;
137 + }
138 + else
139 + {
140 + var sb = new StringBuilder(value);
141 + sb.Remove(parsed.Index, parsed.Length);
142 + sb.Insert(parsed.Index, resolvedValue);
143 + value = sb.ToString();
144 }
147 - }
145
149 - // check for an escape sequence of !! indicating the match is not a variable expression
150 - if (0 < matches[i].Index && '!' == sb[matches[i].Index - 1])
151 - {
152 - sb.Remove(matches[i].Index - 1, 1);
146 + start = parsed.Index;
147 }
148 else
149 {
156 - var key = String.Format(CultureInfo.InvariantCulture, "{0}.{1}", variableId, variableScope).ToLower(CultureInfo.InvariantCulture);
157 -
158 - if (!resolutionData.TryGetValue(key, out var resolvedValue))
159 - {
160 - resolvedValue = variableDefaultValue;
161 - }
162 -
163 - if ("bind" == variableNamespace)
164 - {
165 - // insert the resolved value if it was found or display an error
166 - if (null != resolvedValue)
167 - {
168 - sb.Remove(matches[i].Index, matches[i].Length);
169 - sb.Insert(matches[i].Index, resolvedValue);
170 - }
171 - else
172 - {
173 - throw new WixException(ErrorMessages.UnresolvedBindReference(sourceLineNumbers, value));
174 - }
175 - }
150 + throw new WixException(ErrorMessages.UnresolvedBindReference(sourceLineNumbers, value));
151 }
152 }
178 -
179 - value = sb.ToString();
153 + else
154 + {
155 + start = parsed.Index + parsed.Length;
156 + }
157 }
158
159 return value;
src/WixToolset.Core/Common.cs
+204 -67
@@ -9,7 +9,6 @@ namespace WixToolset.Core
9 using System.Linq;
10 using System.Security.Cryptography;
11 using System.Text;
12 - using System.Text.RegularExpressions;
12 using System.Xml;
13 using System.Xml.Linq;
14 using WixToolset.Data;
@@ -102,18 +101,14 @@ namespace WixToolset.Core
101 // TODO: Find a place to put this that it doesn't have to be public and exposed by WixToolset.Core.dll
102 public static readonly string[] FilePermissions = { "Read", "Write", "Append", "ReadExtendedAttributes", "WriteExtendedAttributes", "Execute", "FileAllRights", "ReadAttributes", "WriteAttributes" };
103
105 - public static readonly Regex WixVariableRegex = new Regex(@"(\!|\$)\((?<namespace>loc|wix|bind|bindpath)\.(?<fullname>(?<name>[_A-Za-z][0-9A-Za-z_]+)(\.(?<scope>[_A-Za-z][0-9A-Za-z_\.]*))?)(\=(?<value>.+?))?\)", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.ExplicitCapture);
104 + internal static readonly char[] IllegalLongFilenameCharacters = new[] { '\\', '/', '?', '*', '|', '>', '<', ':', '\"' }; // illegal: \ / ? | > < : / * "
105 + internal static readonly char[] IllegalRelativeLongFilenameCharacters = new[] { '?', '*', '|', '>', '<', ':', '\"' }; // like illegal, but we allow '\' and '/'
106 + internal static readonly char[] IllegalWildcardLongFilenameCharacters = new[] { '\\', '/', '|', '>', '<', ':', '\"' }; // like illegal: but we allow '*' and '?'
107
107 - private static readonly Regex PropertySearch = new Regex(@"\[[#$!]?[a-zA-Z_][a-zA-Z0-9_\.]*]", RegexOptions.Singleline);
108 - private static readonly Regex AddPrefix = new Regex(@"^[^a-zA-Z_]", RegexOptions.Compiled);
109 - private static readonly Regex LegalIdentifierCharacters = new Regex(@"^[_A-Za-z][0-9A-Za-z_\.]*$", RegexOptions.Compiled);
110 - private static readonly Regex IllegalIdentifierCharacters = new Regex(@"[^A-Za-z0-9_\.]|\.{2,}", RegexOptions.Compiled); // non 'words' and assorted valid characters
108 + private static readonly char[] IllegalShortFilenameCharacters = new[] { '\\', '?', '|', '>', '<', ':', '/', '*', '\"', '+', ',', ';', '=', '[', ']', '.', ' ' };
109 + private static readonly char[] IllegalWildcardShortFilenameCharacters = new[] { '\\', '|', '>', '<', ':', '/', '\"', '+', ',', ';', '=', '[', ']', '.', ' ' };
110
112 - private const string LegalShortFilenameCharacters = @"[^\\\?|><:/\*""\+,;=\[\]\. ]"; // illegal: \ ? | > < : / * " + , ; = [ ] . (space)
113 - private static readonly Regex LegalShortFilename = new Regex(String.Concat("^", LegalShortFilenameCharacters, @"{1,8}(\.", LegalShortFilenameCharacters, "{0,3})?$"), RegexOptions.Compiled);
111
115 - private const string LegalWildcardShortFilenameCharacters = @"[^\\|><:/""\+,;=\[\]\. ]"; // illegal: \ | > < : / " + , ; = [ ] . (space)
116 - private static readonly Regex LegalWildcardShortFilename = new Regex(String.Concat("^", LegalWildcardShortFilenameCharacters, @"{1,16}(\.", LegalWildcardShortFilenameCharacters, "{0,6})?$"));
112
113 /// <summary>
114 /// Gets a valid code page from the given web name or integer value.
@@ -197,79 +192,95 @@ namespace WixToolset.Core
192
193 if (allowWildcards)
194 {
200 - if (Common.LegalWildcardShortFilename.IsMatch(filename))
195 + var expectedDot = filename.IndexOfAny(IllegalWildcardShortFilenameCharacters);
196 + if (expectedDot == -1)
197 {
202 - bool foundPeriod = false;
203 - int beforePeriod = 0;
204 - int afterPeriod = 0;
198 + }
199 + else if (filename[expectedDot] != '.')
200 + {
201 + return false;
202 + }
203 + else if (expectedDot < filename.Length)
204 + {
205 + var extensionInvalids = filename.IndexOfAny(IllegalShortFilenameCharacters, expectedDot + 1);
206 + if (extensionInvalids != -1)
207 + {
208 + return false;
209 + }
210 + }
211
206 - // count the number of characters before and after the period
207 - // '*' is not counted because it may represent zero characters
208 - foreach (char character in filename)
212 + var foundPeriod = false;
213 + var beforePeriod = 0;
214 + var afterPeriod = 0;
215 +
216 + // count the number of characters before and after the period
217 + // '*' is not counted because it may represent zero characters
218 + foreach (var character in filename)
219 + {
220 + if ('.' == character)
221 + {
222 + foundPeriod = true;
223 + }
224 + else if ('*' != character)
225 {
210 - if ('.' == character)
226 + if (foundPeriod)
227 {
212 - foundPeriod = true;
228 + afterPeriod++;
229 }
214 - else if ('*' != character)
230 + else
231 {
216 - if (foundPeriod)
217 - {
218 - afterPeriod++;
219 - }
220 - else
221 - {
222 - beforePeriod++;
223 - }
232 + beforePeriod++;
233 }
234 }
235 + }
236
227 - if (8 >= beforePeriod && 3 >= afterPeriod)
228 - {
229 - return true;
230 - }
237 + if (8 >= beforePeriod && 3 >= afterPeriod)
238 + {
239 + return true;
240 }
241
242 return false;
243 }
244 else
245 {
237 - return Common.LegalShortFilename.IsMatch(filename);
246 + if (filename.Length > 12)
247 + {
248 + return false;
249 + }
250 +
251 + var expectedDot = filename.IndexOfAny(IllegalShortFilenameCharacters);
252 + if (expectedDot == -1)
253 + {
254 + return filename.Length < 9;
255 + }
256 + else if (expectedDot > 8 || filename[expectedDot] != '.')
257 + {
258 + return false;
259 + }
260 +
261 + var validExtension = filename.IndexOfAny(IllegalShortFilenameCharacters, expectedDot + 1);
262 + return validExtension == -1;
263 }
264 }
265
266 /// <summary>
267 /// Verifies if an identifier is a valid binder variable name.
268 /// </summary>
244 - /// <param name="name">Binder variable name to verify.</param>
269 + /// <param name="variable">Binder variable name to verify.</param>
270 /// <returns>True if the identifier is a valid binder variable name.</returns>
246 - public static bool IsValidBinderVariable(string name)
271 + public static bool IsValidBinderVariable(string variable)
272 {
248 - if (String.IsNullOrEmpty(name))
249 - {
250 - return false;
251 - }
252 -
253 - Match match = Common.WixVariableRegex.Match(name);
254 -
255 - return (match.Success && ("bind" == match.Groups["namespace"].Value || "wix" == match.Groups["namespace"].Value) && 0 == match.Index && name.Length == match.Length);
273 + return TryParseWixVariable(variable, 0, out var parsed) && parsed.Index == 0 && parsed.Length == variable.Length && (parsed.Namespace == "bind" || parsed.Namespace == "wix");
274 }
275
276 /// <summary>
277 /// Verifies if a string contains a valid binder variable name.
278 /// </summary>
261 - /// <param name="name">String to verify.</param>
279 + /// <param name="verify">String to verify.</param>
280 /// <returns>True if the string contains a valid binder variable name.</returns>
263 - public static bool ContainsValidBinderVariable(string name)
281 + public static bool ContainsValidBinderVariable(string verify)
282 {
265 - if (String.IsNullOrEmpty(name))
266 - {
267 - return false;
268 - }
269 -
270 - Match match = Common.WixVariableRegex.Match(name);
271 -
272 - return match.Success && ("bind" == match.Groups["namespace"].Value || "wix" == match.Groups["namespace"].Value);
283 + return TryParseWixVariable(verify, 0, out var parsed) && (parsed.Namespace == "bind" || parsed.Namespace == "wix");
284 }
285
286 /// <summary>
@@ -281,7 +292,7 @@ namespace WixToolset.Core
292 {
293 if (!Common.IsValidBinderVariable(version))
294 {
284 - Version ver = null;
295 + Version ver;
296
297 try
298 {
@@ -344,16 +355,31 @@ namespace WixToolset.Core
355 /// <returns>A version of the name that is a legal identifier.</returns>
356 internal static string GetIdentifierFromName(string name)
357 {
347 - string result = IllegalIdentifierCharacters.Replace(name, "_"); // replace illegal characters with "_".
358 + StringBuilder sb = null;
359 + var offset = 0;
360
361 // MSI identifiers must begin with an alphabetic character or an
362 // underscore. Prefix all other values with an underscore.
351 - if (AddPrefix.IsMatch(name))
363 + if (!ValidIdentifierChar(name[0], true))
364 {
353 - result = String.Concat("_", result);
365 + sb = new StringBuilder("_" + name);
366 + offset = 1;
367 }
368
356 - return result;
369 + for (var i = 0; i < name.Length; ++i)
370 + {
371 + if (!ValidIdentifierChar(name[i], false))
372 + {
373 + if (sb == null)
374 + {
375 + sb = new StringBuilder(name);
376 + }
377 +
378 + sb[i + offset] = '_';
379 + }
380 + }
381 +
382 + return sb?.ToString() ?? name;
383 }
384
385 /// <summary>
@@ -363,7 +389,28 @@ namespace WixToolset.Core
389 /// <returns>True if a property is found in the string.</returns>
390 internal static bool ContainsProperty(string possibleProperty)
391 {
366 - return PropertySearch.IsMatch(possibleProperty);
392 + var start = possibleProperty.IndexOf('[');
393 + if (start != -1 && start < possibleProperty.Length - 2)
394 + {
395 + var end = possibleProperty.IndexOf(']', start + 1);
396 + if (end > start + 1)
397 + {
398 + // Skip supported property modifiers.
399 + if (possibleProperty[start + 1] == '#' || possibleProperty[start + 1] == '$' || possibleProperty[start + 1] == '!')
400 + {
401 + ++start;
402 + }
403 +
404 + var id = possibleProperty.Substring(start + 1, end - 1);
405 +
406 + if (Common.IsIdentifier(id))
407 + {
408 + return true;
409 + }
410 + }
411 + }
412 +
413 + return false;
414 }
415
416 /// <summary>
@@ -436,7 +483,7 @@ namespace WixToolset.Core
483 public static string[] GetNames(string value)
484 {
485 string[] names = new string[4];
439 - int targetSeparator = value.IndexOf(":", StringComparison.Ordinal);
486 + int targetSeparator = value.IndexOf(':');
487
488 // split source and target
489 string sourceName = null;
@@ -451,7 +498,7 @@ namespace WixToolset.Core
498 string sourceLongName = null;
499 if (null != sourceName)
500 {
454 - int sourceLongNameSeparator = sourceName.IndexOf("|", StringComparison.Ordinal);
501 + int sourceLongNameSeparator = sourceName.IndexOf('|');
502 if (0 <= sourceLongNameSeparator)
503 {
504 sourceLongName = sourceName.Substring(sourceLongNameSeparator + 1);
@@ -460,7 +507,7 @@ namespace WixToolset.Core
507 }
508
509 // split the target short and long names
463 - int targetLongNameSeparator = targetName.IndexOf("|", StringComparison.Ordinal);
510 + int targetLongNameSeparator = targetName.IndexOf('|');
511 string targetLongName = null;
512 if (0 <= targetLongNameSeparator)
513 {
@@ -555,7 +602,7 @@ namespace WixToolset.Core
602 /// <returns>The attribute's value.</returns>
603 internal static string GetAttributeValue(IMessaging messaging, SourceLineNumber sourceLineNumbers, XAttribute attribute, EmptyRule emptyRule)
604 {
558 - string value = attribute.Value;
605 + var value = attribute.Value;
606
607 if ((emptyRule == EmptyRule.MustHaveNonWhitespaceCharacters && String.IsNullOrEmpty(value.Trim())) ||
608 (emptyRule == EmptyRule.CanBeWhitespaceOnly && String.IsNullOrEmpty(value)))
@@ -574,15 +621,20 @@ namespace WixToolset.Core
621 /// <returns>true if the value is an identifier; false otherwise.</returns>
622 public static bool IsIdentifier(string value)
623 {
577 - if (!String.IsNullOrEmpty(value))
624 + if (String.IsNullOrEmpty(value))
625 + {
626 + return false;
627 + }
628 +
629 + for (var i = 0; i < value.Length; ++i)
630 {
579 - if (LegalIdentifierCharacters.IsMatch(value))
631 + if (!ValidIdentifierChar(value[i], i == 0))
632 {
581 - return true;
633 + return false;
634 }
635 }
636
585 - return false;
637 + return true;
638 }
639
640 /// <summary>
@@ -698,6 +750,85 @@ namespace WixToolset.Core
750 return text?.Value;
751 }
752
753 + internal static bool TryParseWixVariable(string value, int start, out ParsedWixVariable parsedVariable)
754 + {
755 + parsedVariable = null;
756 +
757 + if (String.IsNullOrEmpty(value) || start >= value.Length)
758 + {
759 + return false;
760 + }
761 +
762 + var startWixVariable = value.IndexOf("!(", start, StringComparison.Ordinal);
763 + if (startWixVariable == -1)
764 + {
765 + return false;
766 + }
767 +
768 + var firstDot = value.IndexOf('.', startWixVariable + 1);
769 + if (firstDot == -1)
770 + {
771 + return false;
772 + }
773 +
774 + var ns = value.Substring(startWixVariable + 2, firstDot - startWixVariable - 2);
775 + if (ns != "loc" && ns != "bind" && ns != "wix")
776 + {
777 + return false;
778 + }
779 +
780 + var closeParen = value.IndexOf(')', firstDot);
781 + if (closeParen == -1)
782 + {
783 + return false;
784 + }
785 +
786 + string name;
787 + string scope = null;
788 + string defaultValue = null;
789 +
790 + var secondDot = value.IndexOf('.', firstDot + 1, closeParen - firstDot);
791 + var equalsDefaultValue = value.IndexOf('=', firstDot + 1, closeParen - firstDot);
792 + var end = equalsDefaultValue == -1 ? closeParen : equalsDefaultValue;
793 +
794 + if (secondDot == -1)
795 + {
796 + name = value.Substring(firstDot + 1, end - firstDot - 1);
797 + }
798 + else
799 + {
800 + name = value.Substring(firstDot + 1, secondDot - firstDot - 1);
801 + scope = value.Substring(secondDot + 1, end - secondDot - 1);
802 +
803 + if (!Common.IsIdentifier(scope))
804 + {
805 + return false;
806 + }
807 + }
808 +
809 + if (!Common.IsIdentifier(name))
810 + {
811 + return false;
812 + }
813 +
814 + if (equalsDefaultValue != -1 && equalsDefaultValue < closeParen)
815 + {
816 + defaultValue = value.Substring(equalsDefaultValue + 1, end - equalsDefaultValue - 1);
817 + }
818 +
819 + parsedVariable = new ParsedWixVariable
820 + {
821 + Index = startWixVariable,
822 + Length = closeParen - startWixVariable + 1,
823 + Namespace = ns,
824 + Name = name,
825 + Scope = scope,
826 + DefaultValue = defaultValue
827 + };
828 +
829 + return true;
830 + }
831 +
832 /// <summary>
833 /// Display an unexpected attribute error.
834 /// </summary>
@@ -727,5 +858,11 @@ namespace WixToolset.Core
858 messaging.Write(ErrorMessages.UnsupportedExtensionAttribute(sourceLineNumbers, extensionAttribute.Parent.Name.LocalName, extensionAttribute.Name.LocalName));
859 }
860 }
861 +
862 + private static bool ValidIdentifierChar(char c, bool firstChar)
863 + {
864 + return ('A' <= c && 'Z' >= c) || ('a' <= c && 'z' >= c) || '_' == c ||
865 + (!firstChar && (Char.IsDigit(c) || '.' == c));
866 + }
867 }
868 }
src/WixToolset.Core/Compiler.cs
+24 -23
@@ -9,7 +9,6 @@ namespace WixToolset.Core
9 using System.Globalization;
10 using System.IO;
11 using System.Linq;
12 - using System.Text.RegularExpressions;
12 using System.Xml.Linq;
13 using WixToolset.Data;
14 using WixToolset.Data.Symbols;
@@ -26,8 +25,8 @@ namespace WixToolset.Core
25 private const int MinValueOfMaxCabSizeForLargeFileSplitting = 20; // 20 MB
26 private const int MaxValueOfMaxCabSizeForLargeFileSplitting = 2 * 1024; // 2048 MB (i.e. 2 GB)
27
29 - private const string DefaultComponentIdPlaceholderFormat = "WixComponentIdPlaceholder{0}";
30 - private const string DefaultComponentIdPlaceholderWixVariableFormat = "!(wix.{0})";
28 + private const string DefaultComponentIdPlaceholderPrefix = "WixComponentIdPlaceholder";
29 + private const string DefaultComponentIdPlaceholderWixVariablePrefix = "!(wix.";
30 // If these are true you know you are building a module or product
31 // but if they are false you cannot not be sure they will not end
32 // up a product or module. Use these flags carefully.
@@ -342,16 +341,22 @@ namespace WixToolset.Core
341
342 if (!String.IsNullOrEmpty(value))
343 {
345 - var regex = new Regex(@"\[(?<identifier>[a-zA-Z_][a-zA-Z0-9_\.]*)]", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.ExplicitCapture);
346 - var matches = regex.Matches(value);
347 -
348 - foreach (Match match in matches)
344 + var start = value.IndexOf('[');
345 + while (start != -1 && start < value.Length)
346 {
350 - var group = match.Groups["identifier"];
351 - if (group.Success)
347 + var end = value.IndexOf(']', start + 1);
348 + if (end == -1)
349 + {
350 + break;
351 + }
352 +
353 + var id = value.Substring(start + 1, end - 1);
354 + if (Common.IsIdentifier(id))
355 {
353 - this.Core.Write(WarningMessages.PropertyValueContainsPropertyReference(sourceLineNumbers, propertyId.Id, group.Value));
356 + this.Core.Write(WarningMessages.PropertyValueContainsPropertyReference(sourceLineNumbers, propertyId.Id, id));
357 }
358 +
359 + start = (end < value.Length) ? value.IndexOf('[', end + 1) : -1;
360 }
361 }
362
@@ -2091,8 +2096,8 @@ namespace WixToolset.Core
2096 var encounteredODBCDataSource = false;
2097 var files = 0;
2098 var guid = "*";
2094 - var componentIdPlaceholder = String.Format(Compiler.DefaultComponentIdPlaceholderFormat, this.componentIdPlaceholdersResolver.VariableCount); // placeholder id for defaulting Component/@Id to keypath id.
2095 - var componentIdPlaceholderWixVariable = String.Format(Compiler.DefaultComponentIdPlaceholderWixVariableFormat, componentIdPlaceholder);
2099 + var componentIdPlaceholder = Compiler.DefaultComponentIdPlaceholderPrefix + this.componentIdPlaceholdersResolver.VariableCount; // placeholder id for defaulting Component/@Id to keypath id.
2100 + var componentIdPlaceholderWixVariable = Compiler.DefaultComponentIdPlaceholderWixVariablePrefix + componentIdPlaceholder + ")";
2101 var id = new Identifier(AccessModifier.Private, componentIdPlaceholderWixVariable);
2102 var keyFound = false;
2103 string keyPath = null;
@@ -4080,7 +4085,7 @@ namespace WixToolset.Core
4085 break;
4086 case "Name":
4087 nameHasValue = true;
4083 - if (attrib.Value.Equals("."))
4088 + if (attrib.Value == ".")
4089 {
4090 name = attrib.Value;
4091 }
@@ -4225,7 +4230,7 @@ namespace WixToolset.Core
4230 defaultDir = String.Concat(defaultDir, ":", String.IsNullOrEmpty(shortSourceName) ? sourceName : String.Concat(shortSourceName, "|", sourceName));
4231 }
4232
4228 - if ("TARGETDIR".Equals(id.Id) && !"SourceDir".Equals(defaultDir))
4233 + if ("TARGETDIR".Equals(id.Id, StringComparison.Ordinal) && !"SourceDir".Equals(defaultDir, StringComparison.Ordinal))
4234 {
4235 this.Core.Write(ErrorMessages.IllegalTargetDirDefaultDir(sourceLineNumbers, defaultDir));
4236 }
@@ -7147,13 +7152,9 @@ namespace WixToolset.Core
7152 else // external cabinet file
7153 {
7154 // external cabinet files must use 8.3 filenames
7150 - if (!String.IsNullOrEmpty(cabinet) && !this.Core.IsValidShortFilename(cabinet, false))
7155 + if (!String.IsNullOrEmpty(cabinet) && !this.Core.IsValidLongFilename(cabinet) && !Common.ContainsValidBinderVariable(cabinet))
7156 {
7152 - // WiX variables in the name will trip the "not a valid 8.3 name" switch, so let them through
7153 - if (!Common.WixVariableRegex.Match(cabinet).Success)
7154 - {
7155 - this.Core.Write(WarningMessages.MediaExternalCabinetFilenameIllegal(sourceLineNumbers, node.Name.LocalName, "Cabinet", cabinet));
7156 - }
7157 + this.Core.Write(WarningMessages.MediaExternalCabinetFilenameIllegal(sourceLineNumbers, node.Name.LocalName, "Cabinet", cabinet));
7158 }
7159 }
7160 }
@@ -7286,12 +7287,12 @@ namespace WixToolset.Core
7287 if (!this.Core.IsValidLocIdentifier(exampleCabinetName))
7288 {
7289 // The example name should not match the authored template since that would nullify the
7289 - // reason for having multiple cabients. External cabinet files must also be valid file names.
7290 - if (exampleCabinetName.Equals(authoredCabinetTemplateValue) || !this.Core.IsValidLongFilename(exampleCabinetName, false))
7290 + // reason for having multiple cabinets. External cabinet files must also be valid file names.
7291 + if (exampleCabinetName.Equals(authoredCabinetTemplateValue, StringComparison.OrdinalIgnoreCase) || !this.Core.IsValidLongFilename(exampleCabinetName, false))
7292 {
7293 this.Core.Write(ErrorMessages.InvalidCabinetTemplate(sourceLineNumbers, cabinetTemplate));
7294 }
7294 - else if (!this.Core.IsValidShortFilename(exampleCabinetName, false) && !Common.WixVariableRegex.Match(exampleCabinetName).Success) // ignore short names with wix variables because it rarely works out.
7295 + else if (!this.Core.IsValidLongFilename(exampleCabinetName) && !Common.ContainsValidBinderVariable(exampleCabinetName)) // ignore short names with wix variables because it rarely works out.
7296 {
7297 this.Core.Write(WarningMessages.MediaExternalCabinetFilenameIllegal(sourceLineNumbers, node.Name.LocalName, "CabinetTemplate", cabinetTemplate));
7298 }
src/WixToolset.Core/CompilerCore.cs
+31 -24
@@ -10,7 +10,6 @@ namespace WixToolset.Core
10 using System.Globalization;
11 using System.Reflection;
12 using System.Text;
13 - using System.Text.RegularExpressions;
13 using System.Xml.Linq;
14 using WixToolset.Data;
15 using WixToolset.Data.Symbols;
@@ -45,16 +44,8 @@ namespace WixToolset.Core
44 internal static readonly XNamespace W3SchemaPrefix = "http://www.w3.org/";
45 internal static readonly XNamespace WixNamespace = "http://wixtoolset.org/schemas/v4/wxs";
46
48 - private static readonly Regex AmbiguousFilename = new Regex(@"^.{6}\~\d", RegexOptions.Compiled);
49 -
50 - private const string IllegalLongFilenameCharacters = @"[\\\?|><:/\*""]"; // illegal: \ ? | > < : / * "
51 - private static readonly Regex IllegalLongFilename = new Regex(IllegalLongFilenameCharacters, RegexOptions.Compiled);
52 -
53 - //public const int DefaultMaximumUncompressedMediaSize = 200; // Default value is 200 MB
54 -
55 -
47 // Built-in variables (from burn\engine\variable.cpp, "vrgBuiltInVariables", around line 113)
57 - private static readonly List<String> BuiltinBundleVariables = new List<string>(
48 + private static readonly List<string> BuiltinBundleVariables = new List<string>(
49 new string[] {
50 "AdminToolsFolder",
51 "AppDataFolder",
@@ -221,7 +212,13 @@ namespace WixToolset.Core
212 /// <returns>true if the filename is ambiguous; false otherwise.</returns>
213 public static bool IsAmbiguousFilename(string filename)
214 {
224 - return String.IsNullOrEmpty(filename) ? false : CompilerCore.AmbiguousFilename.IsMatch(filename);
215 + if (String.IsNullOrEmpty(filename))
216 + {
217 + return false;
218 + }
219 +
220 + var tilde = filename.IndexOf('~');
221 + return (tilde > 0 && tilde < filename.Length) && Char.IsNumber(filename[tilde + 1]);
222 }
223
224 /// <summary>
@@ -273,9 +270,29 @@ namespace WixToolset.Core
270 /// <param name="filename">Filename to make valid.</param>
271 /// <param name="replace">Replacement string for invalid characters in filename.</param>
272 /// <returns>Valid filename.</returns>
276 - public static string MakeValidLongFileName(string filename, string replace)
273 + public static string MakeValidLongFileName(string filename, char replace)
274 {
278 - return CompilerCore.IllegalLongFilename.Replace(filename, replace);
275 + if (String.IsNullOrEmpty(filename))
276 + {
277 + return filename;
278 + }
279 +
280 + StringBuilder sb = null;
281 +
282 + var found = filename.IndexOfAny(Common.IllegalLongFilenameCharacters);
283 + while (found != -1)
284 + {
285 + if (sb == null)
286 + {
287 + sb = new StringBuilder(filename);
288 + }
289 +
290 + sb[found] = replace;
291 +
292 + found = (found + 1 < filename.Length) ? filename.IndexOfAny(Common.IllegalLongFilenameCharacters, found + 1) : -1;
293 + }
294 +
295 + return sb?.ToString() ?? filename;
296 }
297
298 /// <summary>
@@ -717,7 +734,7 @@ namespace WixToolset.Core
734 throw new ArgumentNullException("attribute");
735 }
736
720 - string value = this.GetAttributeValue(sourceLineNumbers, attribute);
737 + var value = this.GetAttributeValue(sourceLineNumbers, attribute);
738
739 if (0 < value.Length)
740 {
@@ -1039,16 +1056,6 @@ namespace WixToolset.Core
1056 return this.parseHelper.ScheduleActionSymbol(this.ActiveSection, sourceLineNumbers, access, sequence, actionName, condition, beforeAction, afterAction, overridable);
1057 }
1058
1042 - /// <summary>
1043 - /// Finds a compiler extension by namespace URI.
1044 - /// </summary>
1045 - /// <param name="ns">Namespace the extension supports.</param>
1046 - /// <returns>True if found compiler extension or false if nothing matches namespace URI.</returns>
1047 - private bool TryFindExtension(XNamespace ns, out ICompilerExtension extension)
1048 - {
1049 - return this.extensions.TryGetValue(ns, out extension);
1050 - }
1051 -
1059 private static string CreateValueList(ValueListKind kind, IEnumerable<string> values)
1060 {
1061 // Ideally, we could denote the list kind (and the list itself) directly in the
src/WixToolset.Core/Compiler_2.cs
+2 -26
@@ -4182,30 +4182,6 @@ namespace WixToolset.Core
4182 this.Core.Write(ErrorMessages.IllegalAttributeValue(sourceLineNumbers, node.Name.LocalName, "Show", showValue, "normal", "maximized", "minimized"));
4183 break;
4184 }
4185 - //if (showValue.Length == 0)
4186 - //{
4187 - // show = CompilerConstants.IllegalInteger;
4188 - //}
4189 - //else
4190 - //{
4191 - // var showType = Wix.Shortcut.ParseShowType(showValue);
4192 - // switch (showType)
4193 - // {
4194 - // case Wix.Shortcut.ShowType.normal:
4195 - // show = 1;
4196 - // break;
4197 - // case Wix.Shortcut.ShowType.maximized:
4198 - // show = 3;
4199 - // break;
4200 - // case Wix.Shortcut.ShowType.minimized:
4201 - // show = 7;
4202 - // break;
4203 - // default:
4204 - // this.Core.Write(ErrorMessages.IllegalAttributeValue(sourceLineNumbers, node.Name.LocalName, "Show", showValue, "normal", "maximized", "minimized"));
4205 - // show = CompilerConstants.IllegalInteger;
4206 - // break;
4207 - // }
4208 - //}
4185 break;
4186 case "Target":
4187 target = this.Core.GetAttributeValue(sourceLineNumbers, attrib);
@@ -4325,11 +4301,11 @@ namespace WixToolset.Core
4301 }
4302 else if ("Component" == parentElementLocalName || "CreateFolder" == parentElementLocalName)
4303 {
4328 - target = String.Format(CultureInfo.InvariantCulture, "[{0}]", defaultTarget);
4304 + target = "[" + defaultTarget + "]";
4305 }
4306 else if ("File" == parentElementLocalName)
4307 {
4332 - target = String.Format(CultureInfo.InvariantCulture, "[#{0}]", defaultTarget);
4308 + target = "[#" + defaultTarget + "]";
4309 }
4310
4311 this.Core.AddSymbol(new ShortcutSymbol(sourceLineNumbers, id)
src/WixToolset.Core/Compiler_Bundle.cs
+1 -1
@@ -236,7 +236,7 @@ namespace WixToolset.Core
236 else
237 {
238 // Ensure only allowable path characters are in "name" (and change spaces to underscores).
239 - fileSystemSafeBundleName = CompilerCore.MakeValidLongFileName(name.Replace(' ', '_'), "_");
239 + fileSystemSafeBundleName = CompilerCore.MakeValidLongFileName(name.Replace(' ', '_'), '_');
240 logVariablePrefixAndExtension = String.Concat("WixBundleLog:", fileSystemSafeBundleName, ":log");
241 }
242
src/WixToolset.Core/Compiler_UI.cs
+1 -1
@@ -1740,7 +1740,7 @@ namespace WixToolset.Core
1740 {
1741 // if we're not looking at a standard action or a formatted string then create a reference
1742 // to the custom action.
1743 - if (!WindowsInstallerStandard.IsStandardAction(argument) && !Common.ContainsProperty(argument))
1743 + if (!WindowsInstallerStandard.IsStandardAction(argument) && !this.Core.ContainsProperty(argument))
1744 {
1745 this.Core.CreateSimpleReference(sourceLineNumbers, SymbolDefinitions.CustomAction, argument);
1746 }
src/WixToolset.Core/ExtensibilityServices/ParseHelper.cs
+68 -64
@@ -9,7 +9,6 @@ namespace WixToolset.Core.ExtensibilityServices
9 using System.IO;
10 using System.Security.Cryptography;
11 using System.Text;
12 - using System.Text.RegularExpressions;
12 using System.Xml.Linq;
13 using WixToolset.Data;
14 using WixToolset.Data.Symbols;
@@ -20,19 +19,6 @@ namespace WixToolset.Core.ExtensibilityServices
19
20 internal class ParseHelper : IParseHelper
21 {
23 - private const string LegalLongFilenameCharacters = @"[^\\\?|><:/\*""]"; // opposite of illegal above.
24 - private static readonly Regex LegalLongFilename = new Regex(String.Concat("^", LegalLongFilenameCharacters, @"{1,259}$"), RegexOptions.Compiled);
25 -
26 - private const string LegalRelativeLongFilenameCharacters = @"[^\?|><:/\*""]"; // (like legal long, but we allow '\') illegal: ? | > < : / * "
27 - private static readonly Regex LegalRelativeLongFilename = new Regex(String.Concat("^", LegalRelativeLongFilenameCharacters, @"{1,259}$"), RegexOptions.Compiled);
28 -
29 - private const string LegalWildcardLongFilenameCharacters = @"[^\\|><:/""]"; // illegal: \ | > < : / "
30 - private static readonly Regex LegalWildcardLongFilename = new Regex(String.Concat("^", LegalWildcardLongFilenameCharacters, @"{1,259}$"));
31 -
32 - private static readonly Regex LegalIdentifierWithAccess = new Regex(@"^((?<access>public|internal|protected|private)\s+)?(?<id>[_A-Za-z][0-9A-Za-z_\.]*)$", RegexOptions.Compiled | RegexOptions.ExplicitCapture);
33 -
34 - private static readonly Regex PutGuidHere = new Regex(@"PUT\-GUID\-(?:\d+\-)?HERE", RegexOptions.Singleline);
35 -
22 public ParseHelper(IWixToolsetServiceProvider serviceProvider)
23 {
24 this.ServiceProvider = serviceProvider;
@@ -119,7 +105,7 @@ namespace WixToolset.Core.ExtensibilityServices
105 id = parentId;
106
107 var pathStartsAt = 0;
122 - if (inlineSyntax[0].EndsWith(":"))
108 + if (inlineSyntax[0].EndsWith(":", StringComparison.Ordinal))
109 {
110 // TODO: should overriding the parent identifier with a specific id be an error or a warning or just let it slide?
111 //if (null != parentId)
@@ -415,44 +401,34 @@ namespace WixToolset.Core.ExtensibilityServices
401 var emptyRule = canBeEmpty ? EmptyRule.CanBeEmpty : EmptyRule.CanBeWhitespaceOnly;
402 var value = this.GetAttributeValue(sourceLineNumbers, attribute, emptyRule);
403
418 - if (String.IsNullOrEmpty(value) && canBeEmpty)
419 - {
420 - return String.Empty;
421 - }
422 - else if (!String.IsNullOrEmpty(value))
404 + if (String.IsNullOrEmpty(value))
405 {
424 - // If the value starts and ends with braces or parenthesis, accept that and strip them off.
425 - if ((value.StartsWith("{", StringComparison.Ordinal) && value.EndsWith("}", StringComparison.Ordinal))
426 - || (value.StartsWith("(", StringComparison.Ordinal) && value.EndsWith(")", StringComparison.Ordinal)))
406 + if (canBeEmpty)
407 {
428 - value = value.Substring(1, value.Length - 2);
408 + return String.Empty;
409 }
430 -
431 - if (generatable && "*".Equals(value, StringComparison.Ordinal))
410 + }
411 + else
412 + {
413 + if (generatable && value == "*")
414 {
415 return value;
416 }
417
436 - if (ParseHelper.PutGuidHere.IsMatch(value))
418 + if (Guid.TryParse(value, out var guid))
419 {
438 - this.Messaging.Write(ErrorMessages.ExampleGuid(sourceLineNumbers, attribute.Parent.Name.LocalName, attribute.Name.LocalName, value));
439 - return CompilerConstants.IllegalGuid;
420 + return guid.ToString("B").ToUpperInvariant();
421 }
441 - else if (value.StartsWith("!(loc", StringComparison.Ordinal) || value.StartsWith("$(loc", StringComparison.Ordinal) || value.StartsWith("!(wix", StringComparison.Ordinal))
422 +
423 + if (value.StartsWith("!(loc", StringComparison.Ordinal) || value.StartsWith("$(loc", StringComparison.Ordinal) || value.StartsWith("!(wix", StringComparison.Ordinal))
424 {
425 return value;
426 }
445 - else if (Guid.TryParse(value, out var guid))
446 - {
447 - var uppercaseGuid = guid.ToString().ToUpperInvariant();
427
449 - // TODO: This used to be a pedantic error, what should it be now?
450 - //if (uppercaseGuid != value)
451 - //{
452 - // this.Messaging.Write(WixErrors.GuidContainsLowercaseLetters(sourceLineNumbers, attribute.Parent.Name.LocalName, attribute.Name.LocalName, value));
453 - //}
454 -
455 - return String.Concat("{", uppercaseGuid, "}");
428 + if (value.StartsWith("PUT-GUID-", StringComparison.OrdinalIgnoreCase) ||
429 + value.StartsWith("{PUT-GUID-", StringComparison.OrdinalIgnoreCase))
430 + {
431 + this.Messaging.Write(ErrorMessages.ExampleGuid(sourceLineNumbers, attribute.Parent.Name.LocalName, attribute.Name.LocalName, value));
432 }
433 else
434 {
@@ -468,19 +444,45 @@ namespace WixToolset.Core.ExtensibilityServices
444 var access = AccessModifier.Public;
445 var value = Common.GetAttributeValue(this.Messaging, sourceLineNumbers, attribute, EmptyRule.CanBeEmpty);
446
471 - var match = ParseHelper.LegalIdentifierWithAccess.Match(value);
472 - if (!match.Success)
447 + var separator = value.IndexOf(' ');
448 + if (separator > 0)
449 {
474 - return null;
450 + var prefix = value.Substring(0, separator);
451 + switch (prefix)
452 + {
453 + case "public":
454 + case "package":
455 + access = AccessModifier.Public;
456 + break;
457 +
458 + case "internal":
459 + case "library":
460 + access = AccessModifier.Internal;
461 + break;
462 +
463 + case "protected":
464 + case "file":
465 + access = AccessModifier.Protected;
466 + break;
467 +
468 + case "private":
469 + case "fragment":
470 + access = AccessModifier.Private;
471 + break;
472 +
473 + default:
474 + return null;
475 + }
476 +
477 + value = value.Substring(separator + 1).Trim();
478 }
476 - else if (match.Groups["access"].Success)
479 +
480 + if (!Common.IsIdentifier(value))
481 {
478 - access = (AccessModifier)Enum.Parse(typeof(AccessModifier), match.Groups["access"].Value, true);
482 + this.Messaging.Write(ErrorMessages.IllegalIdentifier(sourceLineNumbers, attribute.Parent.Name.LocalName, attribute.Name.LocalName, value));
483 + return null;
484 }
480 -
481 - value = match.Groups["id"].Value;
482 -
483 - if (Common.IsIdentifier(value) && 72 < value.Length)
485 + else if (72 < value.Length)
486 {
487 this.Messaging.Write(WarningMessages.IdentifierTooLong(sourceLineNumbers, attribute.Parent.Name.LocalName, attribute.Name.LocalName, value));
488 }
@@ -520,7 +522,7 @@ namespace WixToolset.Core.ExtensibilityServices
522 }
523 else if (resultUsedToCreateReference && 1 == result.Length)
524 {
523 - if (value.EndsWith("\\"))
525 + if (value.EndsWith("\\", StringComparison.Ordinal))
526 {
527 if (!this.IsValidLongFilename(result[0], false, false))
528 {
@@ -547,7 +549,7 @@ namespace WixToolset.Core.ExtensibilityServices
549 }
550 }
551
550 - if (1 < result.Length && !value.EndsWith("\\"))
552 + if (1 < result.Length && !value.EndsWith("\\", StringComparison.Ordinal))
553 {
554 this.Messaging.Write(WarningMessages.BackslashTerminateInlineDirectorySyntax(sourceLineNumbers, attribute.Parent.Name.LocalName, attribute.Name.LocalName, value));
555 }
@@ -776,14 +778,7 @@ namespace WixToolset.Core.ExtensibilityServices
778
779 public bool IsValidLocIdentifier(string identifier)
780 {
779 - if (String.IsNullOrEmpty(identifier))
780 - {
781 - return false;
782 - }
783 -
784 - var match = Common.WixVariableRegex.Match(identifier);
785 -
786 - return (match.Success && "loc" == match.Groups["namespace"].Value && 0 == match.Index && identifier.Length == match.Length);
781 + return Common.TryParseWixVariable(identifier, 0, out var parsed) && parsed.Index == 0 && parsed.Length == identifier.Length && parsed.Namespace == "loc";
782 }
783
784 public bool IsValidLongFilename(string filename, bool allowWildcards, bool allowRelative)
@@ -792,29 +787,38 @@ namespace WixToolset.Core.ExtensibilityServices
787 {
788 return false;
789 }
790 + else if (filename.Length > 259)
791 + {
792 + return false;
793 + }
794
795 // Check for a non-period character (all periods is not legal)
797 - var nonPeriodFound = false;
796 + var allPeriods = true;
797 foreach (var character in filename)
798 {
799 if ('.' != character)
800 {
802 - nonPeriodFound = true;
801 + allPeriods = false;
802 break;
803 }
804 }
805
806 + if (allPeriods)
807 + {
808 + return false;
809 + }
810 +
811 if (allowWildcards)
812 {
809 - return (nonPeriodFound && ParseHelper.LegalWildcardLongFilename.IsMatch(filename));
813 + return filename.IndexOfAny(Common.IllegalWildcardLongFilenameCharacters) == -1;
814 }
815 else if (allowRelative)
816 {
813 - return (nonPeriodFound && ParseHelper.LegalRelativeLongFilename.IsMatch(filename));
817 + return filename.IndexOfAny(Common.IllegalRelativeLongFilenameCharacters) == -1;
818 }
819 else
820 {
817 - return (nonPeriodFound && ParseHelper.LegalLongFilename.IsMatch(filename));
821 + return filename.IndexOfAny(Common.IllegalLongFilenameCharacters) == -1;
822 }
823 }
824
src/WixToolset.Core/ParsedWixVariable.cs new
+19
@@ -0,0 +1,19 @@
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 + internal class ParsedWixVariable
6 + {
7 + public int Index { get; set; }
8 +
9 + public int Length { get; set; }
10 +
11 + public string Namespace { get; set; }
12 +
13 + public string Name { get; set; }
14 +
15 + public string Scope { get; set; }
16 +
17 + public string DefaultValue { get; set; }
18 + }
19 +}
src/WixToolset.Core/VariableResolver.cs
+73 -115
@@ -79,150 +79,108 @@ namespace WixToolset.Core
79
80 public IVariableResolution ResolveVariables(SourceLineNumber sourceLineNumbers, string value, bool errorOnUnknown)
81 {
82 - var matches = Common.WixVariableRegex.Matches(value);
82 + var start = 0;
83 + var defaulted = true;
84 + var delayed = false;
85 + var updated = false;
86
84 - // the value is the default unless it's substituted further down
85 - var result = this.ServiceProvider.GetService<IVariableResolution>();
86 - result.IsDefault = true;
87 - result.Value = value;
88 -
89 - var finalizeEscapes = false;
90 -
91 - while (matches.Count > 0)
87 + while (Common.TryParseWixVariable(value, start, out var parsed))
88 {
93 - var updatedResultThisPass = false;
94 - var sb = new StringBuilder(value);
89 + var variableNamespace = parsed.Namespace;
90 + var variableId = parsed.Name;
91 + var variableDefaultValue = parsed.DefaultValue;
92
96 - // notice how this code walks backward through the list
97 - // because it modifies the string as we move through it
98 - for (var i = matches.Count - 1; 0 <= i; i--)
93 + // check for an escape sequence of !! indicating the match is not a variable expression
94 + if (0 < parsed.Index && '!' == value[parsed.Index - 1])
95 {
100 - var variableNamespace = matches[i].Groups["namespace"].Value;
101 - var variableId = matches[i].Groups["fullname"].Value;
102 - string variableDefaultValue = null;
96 + var sb = new StringBuilder(value);
97 + sb.Remove(parsed.Index - 1, 1);
98 + value = sb.ToString();
99
104 - // get the default value if one was specified
105 - if (matches[i].Groups["value"].Success)
106 - {
107 - variableDefaultValue = matches[i].Groups["value"].Value;
100 + updated = true;
101 + start = parsed.Index + parsed.Length - 1;
102
109 - // localization variables do not support inline default values
110 - if ("loc" == variableNamespace)
111 - {
112 - this.Messaging.Write(ErrorMessages.IllegalInlineLocVariable(sourceLineNumbers, variableId, variableDefaultValue));
113 - }
103 + continue;
104 + }
105 +
106 + string resolvedValue = null;
107 +
108 + if ("loc" == variableNamespace)
109 + {
110 + // localization variables do not support inline default values
111 + if (variableDefaultValue != null)
112 + {
113 + this.Messaging.Write(ErrorMessages.IllegalInlineLocVariable(sourceLineNumbers, variableId, variableDefaultValue));
114 + continue;
115 }
116
116 - // get the scope if one was specified
117 - if (matches[i].Groups["scope"].Success)
117 + if (this.locVariables.TryGetValue(variableId, out var bindVariable))
118 {
119 - if ("bind" == variableNamespace)
120 - {
121 - variableId = matches[i].Groups["name"].Value;
122 - }
119 + resolvedValue = bindVariable.Value;
120 + }
121 + }
122 + else if ("wix" == variableNamespace)
123 + {
124 + if (this.wixVariables.TryGetValue(variableId, out var bindVariable))
125 + {
126 + resolvedValue = bindVariable.Value ?? String.Empty;
127 + defaulted = false;
128 }
129 + else if (null != variableDefaultValue) // default the resolved value to the inline value if one was specified
130 + {
131 + resolvedValue = variableDefaultValue;
132 + }
133 + }
134
125 - // check for an escape sequence of !! indicating the match is not a variable expression
126 - if (0 < matches[i].Index && '!' == sb[matches[i].Index - 1])
135 + if ("bind" == variableNamespace)
136 + {
137 + // Can't resolve these yet, but keep track of where we find them so they can be resolved later with less effort.
138 + delayed = true;
139 + start = parsed.Index + parsed.Length - 1;
140 + }
141 + else
142 + {
143 + // insert the resolved value if it was found or display an error
144 + if (null != resolvedValue)
145 {
128 - if (finalizeEscapes)
146 + if (parsed.Index == 0 && parsed.Length == value.Length)
147 {
130 - sb.Remove(matches[i].Index - 1, 1);
131 -
132 - result.UpdatedValue = true;
148 + value = resolvedValue;
149 }
150 else
151 {
136 - continue;
152 + var sb = new StringBuilder(value);
153 + sb.Remove(parsed.Index, parsed.Length);
154 + sb.Insert(parsed.Index, resolvedValue);
155 + value = sb.ToString();
156 }
157 +
158 + updated = true;
159 + start = parsed.Index;
160 }
161 else
162 {
141 - string resolvedValue = null;
142 -
143 - if ("loc" == variableNamespace)
163 + if ("loc" == variableNamespace && errorOnUnknown) // unresolved loc variable
164 {
145 - // warn about deprecated syntax of $(loc.var)
146 - if ('$' == sb[matches[i].Index])
147 - {
148 - this.Messaging.Write(WarningMessages.DeprecatedLocalizationVariablePrefix(sourceLineNumbers, variableId));
149 - }
150 -
151 - if (this.locVariables.TryGetValue(variableId, out var bindVariable))
152 - {
153 - resolvedValue = bindVariable.Value;
154 - }
165 + this.Messaging.Write(ErrorMessages.LocalizationVariableUnknown(sourceLineNumbers, variableId));
166 }
156 - else if ("wix" == variableNamespace)
167 + else if ("wix" == variableNamespace && errorOnUnknown) // unresolved wix variable
168 {
158 - // illegal syntax of $(wix.var)
159 - if ('$' == sb[matches[i].Index])
160 - {
161 - this.Messaging.Write(ErrorMessages.IllegalWixVariablePrefix(sourceLineNumbers, variableId));
162 - }
163 - else
164 - {
165 - if (this.wixVariables.TryGetValue(variableId, out var bindVariable))
166 - {
167 - resolvedValue = bindVariable.Value ?? String.Empty;
168 - result.IsDefault = false;
169 - }
170 - else if (null != variableDefaultValue) // default the resolved value to the inline value if one was specified
171 - {
172 - resolvedValue = variableDefaultValue;
173 - }
174 - }
169 + this.Messaging.Write(ErrorMessages.WixVariableUnknown(sourceLineNumbers, variableId));
170 }
171
177 - if ("bind" == variableNamespace)
178 - {
179 - // can't resolve these yet, but keep track of where we find them so they can be resolved later with less effort
180 - result.DelayedResolve = true;
181 - }
182 - else
183 - {
184 - // insert the resolved value if it was found or display an error
185 - if (null != resolvedValue)
186 - {
187 - sb.Remove(matches[i].Index, matches[i].Length);
188 - sb.Insert(matches[i].Index, resolvedValue);
189 -
190 - result.UpdatedValue = true;
191 - updatedResultThisPass = true;
192 - }
193 - else if ("loc" == variableNamespace && errorOnUnknown) // unresolved loc variable
194 - {
195 - this.Messaging.Write(ErrorMessages.LocalizationVariableUnknown(sourceLineNumbers, variableId));
196 - }
197 - else if ("wix" == variableNamespace && errorOnUnknown) // unresolved wix variable
198 - {
199 - this.Messaging.Write(ErrorMessages.WixVariableUnknown(sourceLineNumbers, variableId));
200 - }
201 - }
172 + start = parsed.Index + parsed.Length;
173 }
174 }
204 -
205 - result.Value = sb.ToString();
206 - value = result.Value;
207 -
208 - if (finalizeEscapes)
209 - {
210 - // escaped references have been un-escaped, so we're done
211 - break;
212 - }
213 - else if (updatedResultThisPass)
214 - {
215 - // we substituted loc strings, so make another pass to see if that brought in more loc strings
216 - matches = Common.WixVariableRegex.Matches(value);
217 - }
218 - else
219 - {
220 - // make one final pass to un-escape any escaped references
221 - finalizeEscapes = true;
222 - }
175 }
176
225 - return result;
177 + return new VariableResolution
178 + {
179 + DelayedResolve = delayed,
180 + IsDefault = defaulted,
181 + UpdatedValue = updated,
182 + Value = value,
183 + };
184 }
185
186 private static bool TryAddWixVariable(IDictionary<string, BindVariable> variables, BindVariable variable)