aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Unbind/UnbindDatabaseCommand.cs11
-rw-r--r--src/WixToolset.Core/Bind/ResolveDelayedFieldsCommand.cs73
-rw-r--r--src/WixToolset.Core/Common.cs271
-rw-r--r--src/WixToolset.Core/Compiler.cs47
-rw-r--r--src/WixToolset.Core/CompilerCore.cs55
-rw-r--r--src/WixToolset.Core/Compiler_2.cs28
-rw-r--r--src/WixToolset.Core/Compiler_Bundle.cs2
-rw-r--r--src/WixToolset.Core/Compiler_UI.cs2
-rw-r--r--src/WixToolset.Core/ExtensibilityServices/ParseHelper.cs132
-rw-r--r--src/WixToolset.Core/ParsedWixVariable.cs19
-rw-r--r--src/WixToolset.Core/VariableResolver.cs188
11 files changed, 454 insertions, 374 deletions
diff --git a/src/WixToolset.Core.WindowsInstaller/Unbind/UnbindDatabaseCommand.cs b/src/WixToolset.Core.WindowsInstaller/Unbind/UnbindDatabaseCommand.cs
index d5601fad..36172b5e 100644
--- a/src/WixToolset.Core.WindowsInstaller/Unbind/UnbindDatabaseCommand.cs
+++ b/src/WixToolset.Core.WindowsInstaller/Unbind/UnbindDatabaseCommand.cs
@@ -226,11 +226,12 @@ namespace WixToolset.Core.WindowsInstaller.Unbind
226 value = value.Replace("$(", "$$("); 226 value = value.Replace("$(", "$$(");
227 227
228 // escape things that look like wix variables 228 // escape things that look like wix variables
229 var matches = Common.WixVariableRegex.Matches(value); 229 // TODO: Evaluate this requirement.
230 for (var j = matches.Count - 1; 0 <= j; j--) 230 //var matches = Common.WixVariableRegex.Matches(value);
231 { 231 //for (var j = matches.Count - 1; 0 <= j; j--)
232 value = value.Insert(matches[j].Index, "!"); 232 //{
233 } 233 // value = value.Insert(matches[j].Index, "!");
234 //}
234 235
235 row[i] = value; 236 row[i] = value;
236 break; 237 break;
diff --git a/src/WixToolset.Core/Bind/ResolveDelayedFieldsCommand.cs b/src/WixToolset.Core/Bind/ResolveDelayedFieldsCommand.cs
index a10b98dc..ebabed47 100644
--- a/src/WixToolset.Core/Bind/ResolveDelayedFieldsCommand.cs
+++ b/src/WixToolset.Core/Bind/ResolveDelayedFieldsCommand.cs
@@ -69,11 +69,11 @@ namespace WixToolset.Core.Bind
69 } 69 }
70 70
71 // add specialization for ProductVersion fields 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)) 73 if (this.VariableCache.TryGetValue(keyProductVersion, out var versionValue) && Version.TryParse(versionValue, out Version productVersion))
74 { 74 {
75 // Don't add the variable if it already exists (developer defined a property with the same name). 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)) 77 if (!this.VariableCache.ContainsKey(fieldKey))
78 { 78 {
79 this.VariableCache[fieldKey] = productVersion.Major.ToString(CultureInfo.InvariantCulture); 79 this.VariableCache[fieldKey] = productVersion.Major.ToString(CultureInfo.InvariantCulture);
@@ -115,68 +115,45 @@ namespace WixToolset.Core.Bind
115 115
116 private static string ResolveDelayedVariables(SourceLineNumber sourceLineNumbers, string value, IDictionary<string, string> resolutionData) 116 private static string ResolveDelayedVariables(SourceLineNumber sourceLineNumbers, string value, IDictionary<string, string> resolutionData)
117 { 117 {
118 var matches = Common.WixVariableRegex.Matches(value); 118 var start = 0;
119 119
120 if (0 < matches.Count) 120 while (Common.TryParseWixVariable(value, start, out var parsed))
121 { 121 {
122 var sb = new StringBuilder(value); 122 if (parsed.Namespace == "bind")
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--)
127 { 123 {
128 string variableNamespace = matches[i].Groups["namespace"].Value; 124 var key = String.Concat(parsed.Name, ".", parsed.Scope);
129 string variableId = matches[i].Groups["fullname"].Value;
130 string variableDefaultValue = null;
131 string variableScope = null;
132 125
133 // get the default value if one was specified 126 if (!resolutionData.TryGetValue(key, out var resolvedValue))
134 if (matches[i].Groups["value"].Success)
135 { 127 {
136 variableDefaultValue = matches[i].Groups["value"].Value; 128 resolvedValue = parsed.DefaultValue;
137 } 129 }
138 130
139 // get the scope if one was specified 131 // insert the resolved value if it was found or display an error
140 if (matches[i].Groups["scope"].Success) 132 if (null != resolvedValue)
141 { 133 {
142 variableScope = matches[i].Groups["scope"].Value; 134 if (parsed.Index == 0 && parsed.Length == value.Length)
143 if ("bind" == variableNamespace)
144 { 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();
146 } 144 }
147 }
148 145
149 // check for an escape sequence of !! indicating the match is not a variable expression 146 start = parsed.Index;
150 if (0 < matches[i].Index && '!' == sb[matches[i].Index - 1])
151 {
152 sb.Remove(matches[i].Index - 1, 1);
153 } 147 }
154 else 148 else
155 { 149 {
156 var key = String.Format(CultureInfo.InvariantCulture, "{0}.{1}", variableId, variableScope).ToLower(CultureInfo.InvariantCulture); 150 throw new WixException(ErrorMessages.UnresolvedBindReference(sourceLineNumbers, value));
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 }
176 } 151 }
177 } 152 }
178 153 else
179 value = sb.ToString(); 154 {
155 start = parsed.Index + parsed.Length;
156 }
180 } 157 }
181 158
182 return value; 159 return value;
diff --git a/src/WixToolset.Core/Common.cs b/src/WixToolset.Core/Common.cs
index a9fc9538..19c77368 100644
--- a/src/WixToolset.Core/Common.cs
+++ b/src/WixToolset.Core/Common.cs
@@ -9,7 +9,6 @@ namespace WixToolset.Core
9 using System.Linq; 9 using System.Linq;
10 using System.Security.Cryptography; 10 using System.Security.Cryptography;
11 using System.Text; 11 using System.Text;
12 using System.Text.RegularExpressions;
13 using System.Xml; 12 using System.Xml;
14 using System.Xml.Linq; 13 using System.Xml.Linq;
15 using WixToolset.Data; 14 using WixToolset.Data;
@@ -102,18 +101,14 @@ namespace WixToolset.Core
102 // TODO: Find a place to put this that it doesn't have to be public and exposed by WixToolset.Core.dll 101 // TODO: Find a place to put this that it doesn't have to be public and exposed by WixToolset.Core.dll
103 public static readonly string[] FilePermissions = { "Read", "Write", "Append", "ReadExtendedAttributes", "WriteExtendedAttributes", "Execute", "FileAllRights", "ReadAttributes", "WriteAttributes" }; 102 public static readonly string[] FilePermissions = { "Read", "Write", "Append", "ReadExtendedAttributes", "WriteExtendedAttributes", "Execute", "FileAllRights", "ReadAttributes", "WriteAttributes" };
104 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 '?'
106 107
107 private static readonly Regex PropertySearch = new Regex(@"\[[#$!]?[a-zA-Z_][a-zA-Z0-9_\.]*]", RegexOptions.Singleline); 108 private static readonly char[] IllegalShortFilenameCharacters = new[] { '\\', '?', '|', '>', '<', ':', '/', '*', '\"', '+', ',', ';', '=', '[', ']', '.', ' ' };
108 private static readonly Regex AddPrefix = new Regex(@"^[^a-zA-Z_]", RegexOptions.Compiled); 109 private static readonly char[] IllegalWildcardShortFilenameCharacters = new[] { '\\', '|', '>', '<', ':', '/', '\"', '+', ',', ';', '=', '[', ']', '.', ' ' };
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
111 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);
114 111
115 private const string LegalWildcardShortFilenameCharacters = @"[^\\|><:/""\+,;=\[\]\. ]"; // illegal: \ | > < : / " + , ; = [ ] . (space)
116 private static readonly Regex LegalWildcardShortFilename = new Regex(String.Concat("^", LegalWildcardShortFilenameCharacters, @"{1,16}(\.", LegalWildcardShortFilenameCharacters, "{0,6})?$"));
117 112
118 /// <summary> 113 /// <summary>
119 /// Gets a valid code page from the given web name or integer value. 114 /// Gets a valid code page from the given web name or integer value.
@@ -197,79 +192,95 @@ namespace WixToolset.Core
197 192
198 if (allowWildcards) 193 if (allowWildcards)
199 { 194 {
200 if (Common.LegalWildcardShortFilename.IsMatch(filename)) 195 var expectedDot = filename.IndexOfAny(IllegalWildcardShortFilenameCharacters);
196 if (expectedDot == -1)
201 { 197 {
202 bool foundPeriod = false; 198 }
203 int beforePeriod = 0; 199 else if (filename[expectedDot] != '.')
204 int afterPeriod = 0; 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 }
205 211
206 // count the number of characters before and after the period 212 var foundPeriod = false;
207 // '*' is not counted because it may represent zero characters 213 var beforePeriod = 0;
208 foreach (char character in filename) 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)
209 { 225 {
210 if ('.' == character) 226 if (foundPeriod)
211 { 227 {
212 foundPeriod = true; 228 afterPeriod++;
213 } 229 }
214 else if ('*' != character) 230 else
215 { 231 {
216 if (foundPeriod) 232 beforePeriod++;
217 {
218 afterPeriod++;
219 }
220 else
221 {
222 beforePeriod++;
223 }
224 } 233 }
225 } 234 }
235 }
226 236
227 if (8 >= beforePeriod && 3 >= afterPeriod) 237 if (8 >= beforePeriod && 3 >= afterPeriod)
228 { 238 {
229 return true; 239 return true;
230 }
231 } 240 }
232 241
233 return false; 242 return false;
234 } 243 }
235 else 244 else
236 { 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;
238 } 263 }
239 } 264 }
240 265
241 /// <summary> 266 /// <summary>
242 /// Verifies if an identifier is a valid binder variable name. 267 /// Verifies if an identifier is a valid binder variable name.
243 /// </summary> 268 /// </summary>
244 /// <param name="name">Binder variable name to verify.</param> 269 /// <param name="variable">Binder variable name to verify.</param>
245 /// <returns>True if the identifier is a valid binder variable name.</returns> 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)
247 { 272 {
248 if (String.IsNullOrEmpty(name)) 273 return TryParseWixVariable(variable, 0, out var parsed) && parsed.Index == 0 && parsed.Length == variable.Length && (parsed.Namespace == "bind" || parsed.Namespace == "wix");
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);
256 } 274 }
257 275
258 /// <summary> 276 /// <summary>
259 /// Verifies if a string contains a valid binder variable name. 277 /// Verifies if a string contains a valid binder variable name.
260 /// </summary> 278 /// </summary>
261 /// <param name="name">String to verify.</param> 279 /// <param name="verify">String to verify.</param>
262 /// <returns>True if the string contains a valid binder variable name.</returns> 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)
264 { 282 {
265 if (String.IsNullOrEmpty(name)) 283 return TryParseWixVariable(verify, 0, out var parsed) && (parsed.Namespace == "bind" || parsed.Namespace == "wix");
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);
273 } 284 }
274 285
275 /// <summary> 286 /// <summary>
@@ -281,7 +292,7 @@ namespace WixToolset.Core
281 { 292 {
282 if (!Common.IsValidBinderVariable(version)) 293 if (!Common.IsValidBinderVariable(version))
283 { 294 {
284 Version ver = null; 295 Version ver;
285 296
286 try 297 try
287 { 298 {
@@ -344,16 +355,31 @@ namespace WixToolset.Core
344 /// <returns>A version of the name that is a legal identifier.</returns> 355 /// <returns>A version of the name that is a legal identifier.</returns>
345 internal static string GetIdentifierFromName(string name) 356 internal static string GetIdentifierFromName(string name)
346 { 357 {
347 string result = IllegalIdentifierCharacters.Replace(name, "_"); // replace illegal characters with "_". 358 StringBuilder sb = null;
359 var offset = 0;
348 360
349 // MSI identifiers must begin with an alphabetic character or an 361 // MSI identifiers must begin with an alphabetic character or an
350 // underscore. Prefix all other values with an underscore. 362 // underscore. Prefix all other values with an underscore.
351 if (AddPrefix.IsMatch(name)) 363 if (!ValidIdentifierChar(name[0], true))
352 { 364 {
353 result = String.Concat("_", result); 365 sb = new StringBuilder("_" + name);
366 offset = 1;
354 } 367 }
355 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;
357 } 383 }
358 384
359 /// <summary> 385 /// <summary>
@@ -363,7 +389,28 @@ namespace WixToolset.Core
363 /// <returns>True if a property is found in the string.</returns> 389 /// <returns>True if a property is found in the string.</returns>
364 internal static bool ContainsProperty(string possibleProperty) 390 internal static bool ContainsProperty(string possibleProperty)
365 { 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;
367 } 414 }
368 415
369 /// <summary> 416 /// <summary>
@@ -436,7 +483,7 @@ namespace WixToolset.Core
436 public static string[] GetNames(string value) 483 public static string[] GetNames(string value)
437 { 484 {
438 string[] names = new string[4]; 485 string[] names = new string[4];
439 int targetSeparator = value.IndexOf(":", StringComparison.Ordinal); 486 int targetSeparator = value.IndexOf(':');
440 487
441 // split source and target 488 // split source and target
442 string sourceName = null; 489 string sourceName = null;
@@ -451,7 +498,7 @@ namespace WixToolset.Core
451 string sourceLongName = null; 498 string sourceLongName = null;
452 if (null != sourceName) 499 if (null != sourceName)
453 { 500 {
454 int sourceLongNameSeparator = sourceName.IndexOf("|", StringComparison.Ordinal); 501 int sourceLongNameSeparator = sourceName.IndexOf('|');
455 if (0 <= sourceLongNameSeparator) 502 if (0 <= sourceLongNameSeparator)
456 { 503 {
457 sourceLongName = sourceName.Substring(sourceLongNameSeparator + 1); 504 sourceLongName = sourceName.Substring(sourceLongNameSeparator + 1);
@@ -460,7 +507,7 @@ namespace WixToolset.Core
460 } 507 }
461 508
462 // split the target short and long names 509 // split the target short and long names
463 int targetLongNameSeparator = targetName.IndexOf("|", StringComparison.Ordinal); 510 int targetLongNameSeparator = targetName.IndexOf('|');
464 string targetLongName = null; 511 string targetLongName = null;
465 if (0 <= targetLongNameSeparator) 512 if (0 <= targetLongNameSeparator)
466 { 513 {
@@ -555,7 +602,7 @@ namespace WixToolset.Core
555 /// <returns>The attribute's value.</returns> 602 /// <returns>The attribute's value.</returns>
556 internal static string GetAttributeValue(IMessaging messaging, SourceLineNumber sourceLineNumbers, XAttribute attribute, EmptyRule emptyRule) 603 internal static string GetAttributeValue(IMessaging messaging, SourceLineNumber sourceLineNumbers, XAttribute attribute, EmptyRule emptyRule)
557 { 604 {
558 string value = attribute.Value; 605 var value = attribute.Value;
559 606
560 if ((emptyRule == EmptyRule.MustHaveNonWhitespaceCharacters && String.IsNullOrEmpty(value.Trim())) || 607 if ((emptyRule == EmptyRule.MustHaveNonWhitespaceCharacters && String.IsNullOrEmpty(value.Trim())) ||
561 (emptyRule == EmptyRule.CanBeWhitespaceOnly && String.IsNullOrEmpty(value))) 608 (emptyRule == EmptyRule.CanBeWhitespaceOnly && String.IsNullOrEmpty(value)))
@@ -574,15 +621,20 @@ namespace WixToolset.Core
574 /// <returns>true if the value is an identifier; false otherwise.</returns> 621 /// <returns>true if the value is an identifier; false otherwise.</returns>
575 public static bool IsIdentifier(string value) 622 public static bool IsIdentifier(string value)
576 { 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)
578 { 630 {
579 if (LegalIdentifierCharacters.IsMatch(value)) 631 if (!ValidIdentifierChar(value[i], i == 0))
580 { 632 {
581 return true; 633 return false;
582 } 634 }
583 } 635 }
584 636
585 return false; 637 return true;
586 } 638 }
587 639
588 /// <summary> 640 /// <summary>
@@ -698,6 +750,85 @@ namespace WixToolset.Core
698 return text?.Value; 750 return text?.Value;
699 } 751 }
700 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
701 /// <summary> 832 /// <summary>
702 /// Display an unexpected attribute error. 833 /// Display an unexpected attribute error.
703 /// </summary> 834 /// </summary>
@@ -727,5 +858,11 @@ namespace WixToolset.Core
727 messaging.Write(ErrorMessages.UnsupportedExtensionAttribute(sourceLineNumbers, extensionAttribute.Parent.Name.LocalName, extensionAttribute.Name.LocalName)); 858 messaging.Write(ErrorMessages.UnsupportedExtensionAttribute(sourceLineNumbers, extensionAttribute.Parent.Name.LocalName, extensionAttribute.Name.LocalName));
728 } 859 }
729 } 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 }
730 } 867 }
731} 868}
diff --git a/src/WixToolset.Core/Compiler.cs b/src/WixToolset.Core/Compiler.cs
index b29821b0..020e35fe 100644
--- a/src/WixToolset.Core/Compiler.cs
+++ b/src/WixToolset.Core/Compiler.cs
@@ -9,7 +9,6 @@ namespace WixToolset.Core
9 using System.Globalization; 9 using System.Globalization;
10 using System.IO; 10 using System.IO;
11 using System.Linq; 11 using System.Linq;
12 using System.Text.RegularExpressions;
13 using System.Xml.Linq; 12 using System.Xml.Linq;
14 using WixToolset.Data; 13 using WixToolset.Data;
15 using WixToolset.Data.Symbols; 14 using WixToolset.Data.Symbols;
@@ -26,8 +25,8 @@ namespace WixToolset.Core
26 private const int MinValueOfMaxCabSizeForLargeFileSplitting = 20; // 20 MB 25 private const int MinValueOfMaxCabSizeForLargeFileSplitting = 20; // 20 MB
27 private const int MaxValueOfMaxCabSizeForLargeFileSplitting = 2 * 1024; // 2048 MB (i.e. 2 GB) 26 private const int MaxValueOfMaxCabSizeForLargeFileSplitting = 2 * 1024; // 2048 MB (i.e. 2 GB)
28 27
29 private const string DefaultComponentIdPlaceholderFormat = "WixComponentIdPlaceholder{0}"; 28 private const string DefaultComponentIdPlaceholderPrefix = "WixComponentIdPlaceholder";
30 private const string DefaultComponentIdPlaceholderWixVariableFormat = "!(wix.{0})"; 29 private const string DefaultComponentIdPlaceholderWixVariablePrefix = "!(wix.";
31 // If these are true you know you are building a module or product 30 // If these are true you know you are building a module or product
32 // but if they are false you cannot not be sure they will not end 31 // but if they are false you cannot not be sure they will not end
33 // up a product or module. Use these flags carefully. 32 // up a product or module. Use these flags carefully.
@@ -342,16 +341,22 @@ namespace WixToolset.Core
342 341
343 if (!String.IsNullOrEmpty(value)) 342 if (!String.IsNullOrEmpty(value))
344 { 343 {
345 var regex = new Regex(@"\[(?<identifier>[a-zA-Z_][a-zA-Z0-9_\.]*)]", RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.ExplicitCapture); 344 var start = value.IndexOf('[');
346 var matches = regex.Matches(value); 345 while (start != -1 && start < value.Length)
347
348 foreach (Match match in matches)
349 { 346 {
350 var group = match.Groups["identifier"]; 347 var end = value.IndexOf(']', start + 1);
351 if (group.Success) 348 if (end == -1)
349 {
350 break;
351 }
352
353 var id = value.Substring(start + 1, end - 1);
354 if (Common.IsIdentifier(id))
352 { 355 {
353 this.Core.Write(WarningMessages.PropertyValueContainsPropertyReference(sourceLineNumbers, propertyId.Id, group.Value)); 356 this.Core.Write(WarningMessages.PropertyValueContainsPropertyReference(sourceLineNumbers, propertyId.Id, id));
354 } 357 }
358
359 start = (end < value.Length) ? value.IndexOf('[', end + 1) : -1;
355 } 360 }
356 } 361 }
357 362
@@ -2091,8 +2096,8 @@ namespace WixToolset.Core
2091 var encounteredODBCDataSource = false; 2096 var encounteredODBCDataSource = false;
2092 var files = 0; 2097 var files = 0;
2093 var guid = "*"; 2098 var guid = "*";
2094 var componentIdPlaceholder = String.Format(Compiler.DefaultComponentIdPlaceholderFormat, this.componentIdPlaceholdersResolver.VariableCount); // placeholder id for defaulting Component/@Id to keypath id. 2099 var componentIdPlaceholder = Compiler.DefaultComponentIdPlaceholderPrefix + this.componentIdPlaceholdersResolver.VariableCount; // placeholder id for defaulting Component/@Id to keypath id.
2095 var componentIdPlaceholderWixVariable = String.Format(Compiler.DefaultComponentIdPlaceholderWixVariableFormat, componentIdPlaceholder); 2100 var componentIdPlaceholderWixVariable = Compiler.DefaultComponentIdPlaceholderWixVariablePrefix + componentIdPlaceholder + ")";
2096 var id = new Identifier(AccessModifier.Private, componentIdPlaceholderWixVariable); 2101 var id = new Identifier(AccessModifier.Private, componentIdPlaceholderWixVariable);
2097 var keyFound = false; 2102 var keyFound = false;
2098 string keyPath = null; 2103 string keyPath = null;
@@ -4080,7 +4085,7 @@ namespace WixToolset.Core
4080 break; 4085 break;
4081 case "Name": 4086 case "Name":
4082 nameHasValue = true; 4087 nameHasValue = true;
4083 if (attrib.Value.Equals(".")) 4088 if (attrib.Value == ".")
4084 { 4089 {
4085 name = attrib.Value; 4090 name = attrib.Value;
4086 } 4091 }
@@ -4225,7 +4230,7 @@ namespace WixToolset.Core
4225 defaultDir = String.Concat(defaultDir, ":", String.IsNullOrEmpty(shortSourceName) ? sourceName : String.Concat(shortSourceName, "|", sourceName)); 4230 defaultDir = String.Concat(defaultDir, ":", String.IsNullOrEmpty(shortSourceName) ? sourceName : String.Concat(shortSourceName, "|", sourceName));
4226 } 4231 }
4227 4232
4228 if ("TARGETDIR".Equals(id.Id) && !"SourceDir".Equals(defaultDir)) 4233 if ("TARGETDIR".Equals(id.Id, StringComparison.Ordinal) && !"SourceDir".Equals(defaultDir, StringComparison.Ordinal))
4229 { 4234 {
4230 this.Core.Write(ErrorMessages.IllegalTargetDirDefaultDir(sourceLineNumbers, defaultDir)); 4235 this.Core.Write(ErrorMessages.IllegalTargetDirDefaultDir(sourceLineNumbers, defaultDir));
4231 } 4236 }
@@ -7147,13 +7152,9 @@ namespace WixToolset.Core
7147 else // external cabinet file 7152 else // external cabinet file
7148 { 7153 {
7149 // external cabinet files must use 8.3 filenames 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))
7151 { 7156 {
7152 // WiX variables in the name will trip the "not a valid 8.3 name" switch, so let them through 7157 this.Core.Write(WarningMessages.MediaExternalCabinetFilenameIllegal(sourceLineNumbers, node.Name.LocalName, "Cabinet", cabinet));
7153 if (!Common.WixVariableRegex.Match(cabinet).Success)
7154 {
7155 this.Core.Write(WarningMessages.MediaExternalCabinetFilenameIllegal(sourceLineNumbers, node.Name.LocalName, "Cabinet", cabinet));
7156 }
7157 } 7158 }
7158 } 7159 }
7159 } 7160 }
@@ -7286,12 +7287,12 @@ namespace WixToolset.Core
7286 if (!this.Core.IsValidLocIdentifier(exampleCabinetName)) 7287 if (!this.Core.IsValidLocIdentifier(exampleCabinetName))
7287 { 7288 {
7288 // The example name should not match the authored template since that would nullify the 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 // reason for having multiple cabinets. External cabinet files must also be valid file names.
7290 if (exampleCabinetName.Equals(authoredCabinetTemplateValue) || !this.Core.IsValidLongFilename(exampleCabinetName, false)) 7291 if (exampleCabinetName.Equals(authoredCabinetTemplateValue, StringComparison.OrdinalIgnoreCase) || !this.Core.IsValidLongFilename(exampleCabinetName, false))
7291 { 7292 {
7292 this.Core.Write(ErrorMessages.InvalidCabinetTemplate(sourceLineNumbers, cabinetTemplate)); 7293 this.Core.Write(ErrorMessages.InvalidCabinetTemplate(sourceLineNumbers, cabinetTemplate));
7293 } 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.
7295 { 7296 {
7296 this.Core.Write(WarningMessages.MediaExternalCabinetFilenameIllegal(sourceLineNumbers, node.Name.LocalName, "CabinetTemplate", cabinetTemplate)); 7297 this.Core.Write(WarningMessages.MediaExternalCabinetFilenameIllegal(sourceLineNumbers, node.Name.LocalName, "CabinetTemplate", cabinetTemplate));
7297 } 7298 }
diff --git a/src/WixToolset.Core/CompilerCore.cs b/src/WixToolset.Core/CompilerCore.cs
index c2724f6b..2f65db7a 100644
--- a/src/WixToolset.Core/CompilerCore.cs
+++ b/src/WixToolset.Core/CompilerCore.cs
@@ -10,7 +10,6 @@ namespace WixToolset.Core
10 using System.Globalization; 10 using System.Globalization;
11 using System.Reflection; 11 using System.Reflection;
12 using System.Text; 12 using System.Text;
13 using System.Text.RegularExpressions;
14 using System.Xml.Linq; 13 using System.Xml.Linq;
15 using WixToolset.Data; 14 using WixToolset.Data;
16 using WixToolset.Data.Symbols; 15 using WixToolset.Data.Symbols;
@@ -45,16 +44,8 @@ namespace WixToolset.Core
45 internal static readonly XNamespace W3SchemaPrefix = "http://www.w3.org/"; 44 internal static readonly XNamespace W3SchemaPrefix = "http://www.w3.org/";
46 internal static readonly XNamespace WixNamespace = "http://wixtoolset.org/schemas/v4/wxs"; 45 internal static readonly XNamespace WixNamespace = "http://wixtoolset.org/schemas/v4/wxs";
47 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
56 // Built-in variables (from burn\engine\variable.cpp, "vrgBuiltInVariables", around line 113) 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>(
58 new string[] { 49 new string[] {
59 "AdminToolsFolder", 50 "AdminToolsFolder",
60 "AppDataFolder", 51 "AppDataFolder",
@@ -221,7 +212,13 @@ namespace WixToolset.Core
221 /// <returns>true if the filename is ambiguous; false otherwise.</returns> 212 /// <returns>true if the filename is ambiguous; false otherwise.</returns>
222 public static bool IsAmbiguousFilename(string filename) 213 public static bool IsAmbiguousFilename(string filename)
223 { 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]);
225 } 222 }
226 223
227 /// <summary> 224 /// <summary>
@@ -273,9 +270,29 @@ namespace WixToolset.Core
273 /// <param name="filename">Filename to make valid.</param> 270 /// <param name="filename">Filename to make valid.</param>
274 /// <param name="replace">Replacement string for invalid characters in filename.</param> 271 /// <param name="replace">Replacement string for invalid characters in filename.</param>
275 /// <returns>Valid filename.</returns> 272 /// <returns>Valid filename.</returns>
276 public static string MakeValidLongFileName(string filename, string replace) 273 public static string MakeValidLongFileName(string filename, char replace)
277 { 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;
279 } 296 }
280 297
281 /// <summary> 298 /// <summary>
@@ -717,7 +734,7 @@ namespace WixToolset.Core
717 throw new ArgumentNullException("attribute"); 734 throw new ArgumentNullException("attribute");
718 } 735 }
719 736
720 string value = this.GetAttributeValue(sourceLineNumbers, attribute); 737 var value = this.GetAttributeValue(sourceLineNumbers, attribute);
721 738
722 if (0 < value.Length) 739 if (0 < value.Length)
723 { 740 {
@@ -1039,16 +1056,6 @@ namespace WixToolset.Core
1039 return this.parseHelper.ScheduleActionSymbol(this.ActiveSection, sourceLineNumbers, access, sequence, actionName, condition, beforeAction, afterAction, overridable); 1056 return this.parseHelper.ScheduleActionSymbol(this.ActiveSection, sourceLineNumbers, access, sequence, actionName, condition, beforeAction, afterAction, overridable);
1040 } 1057 }
1041 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
1052 private static string CreateValueList(ValueListKind kind, IEnumerable<string> values) 1059 private static string CreateValueList(ValueListKind kind, IEnumerable<string> values)
1053 { 1060 {
1054 // Ideally, we could denote the list kind (and the list itself) directly in the 1061 // Ideally, we could denote the list kind (and the list itself) directly in the
diff --git a/src/WixToolset.Core/Compiler_2.cs b/src/WixToolset.Core/Compiler_2.cs
index 7e2485e1..29f240f4 100644
--- a/src/WixToolset.Core/Compiler_2.cs
+++ b/src/WixToolset.Core/Compiler_2.cs
@@ -4182,30 +4182,6 @@ namespace WixToolset.Core
4182 this.Core.Write(ErrorMessages.IllegalAttributeValue(sourceLineNumbers, node.Name.LocalName, "Show", showValue, "normal", "maximized", "minimized")); 4182 this.Core.Write(ErrorMessages.IllegalAttributeValue(sourceLineNumbers, node.Name.LocalName, "Show", showValue, "normal", "maximized", "minimized"));
4183 break; 4183 break;
4184 } 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 //}
4209 break; 4185 break;
4210 case "Target": 4186 case "Target":
4211 target = this.Core.GetAttributeValue(sourceLineNumbers, attrib); 4187 target = this.Core.GetAttributeValue(sourceLineNumbers, attrib);
@@ -4325,11 +4301,11 @@ namespace WixToolset.Core
4325 } 4301 }
4326 else if ("Component" == parentElementLocalName || "CreateFolder" == parentElementLocalName) 4302 else if ("Component" == parentElementLocalName || "CreateFolder" == parentElementLocalName)
4327 { 4303 {
4328 target = String.Format(CultureInfo.InvariantCulture, "[{0}]", defaultTarget); 4304 target = "[" + defaultTarget + "]";
4329 } 4305 }
4330 else if ("File" == parentElementLocalName) 4306 else if ("File" == parentElementLocalName)
4331 { 4307 {
4332 target = String.Format(CultureInfo.InvariantCulture, "[#{0}]", defaultTarget); 4308 target = "[#" + defaultTarget + "]";
4333 } 4309 }
4334 4310
4335 this.Core.AddSymbol(new ShortcutSymbol(sourceLineNumbers, id) 4311 this.Core.AddSymbol(new ShortcutSymbol(sourceLineNumbers, id)
diff --git a/src/WixToolset.Core/Compiler_Bundle.cs b/src/WixToolset.Core/Compiler_Bundle.cs
index 578c7dcd..2089f037 100644
--- a/src/WixToolset.Core/Compiler_Bundle.cs
+++ b/src/WixToolset.Core/Compiler_Bundle.cs
@@ -236,7 +236,7 @@ namespace WixToolset.Core
236 else 236 else
237 { 237 {
238 // Ensure only allowable path characters are in "name" (and change spaces to underscores). 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"); 240 logVariablePrefixAndExtension = String.Concat("WixBundleLog:", fileSystemSafeBundleName, ":log");
241 } 241 }
242 242
diff --git a/src/WixToolset.Core/Compiler_UI.cs b/src/WixToolset.Core/Compiler_UI.cs
index 36d2e4e9..9353d966 100644
--- a/src/WixToolset.Core/Compiler_UI.cs
+++ b/src/WixToolset.Core/Compiler_UI.cs
@@ -1740,7 +1740,7 @@ namespace WixToolset.Core
1740 { 1740 {
1741 // if we're not looking at a standard action or a formatted string then create a reference 1741 // if we're not looking at a standard action or a formatted string then create a reference
1742 // to the custom action. 1742 // to the custom action.
1743 if (!WindowsInstallerStandard.IsStandardAction(argument) && !Common.ContainsProperty(argument)) 1743 if (!WindowsInstallerStandard.IsStandardAction(argument) && !this.Core.ContainsProperty(argument))
1744 { 1744 {
1745 this.Core.CreateSimpleReference(sourceLineNumbers, SymbolDefinitions.CustomAction, argument); 1745 this.Core.CreateSimpleReference(sourceLineNumbers, SymbolDefinitions.CustomAction, argument);
1746 } 1746 }
diff --git a/src/WixToolset.Core/ExtensibilityServices/ParseHelper.cs b/src/WixToolset.Core/ExtensibilityServices/ParseHelper.cs
index db465354..81a18e24 100644
--- a/src/WixToolset.Core/ExtensibilityServices/ParseHelper.cs
+++ b/src/WixToolset.Core/ExtensibilityServices/ParseHelper.cs
@@ -9,7 +9,6 @@ namespace WixToolset.Core.ExtensibilityServices
9 using System.IO; 9 using System.IO;
10 using System.Security.Cryptography; 10 using System.Security.Cryptography;
11 using System.Text; 11 using System.Text;
12 using System.Text.RegularExpressions;
13 using System.Xml.Linq; 12 using System.Xml.Linq;
14 using WixToolset.Data; 13 using WixToolset.Data;
15 using WixToolset.Data.Symbols; 14 using WixToolset.Data.Symbols;
@@ -20,19 +19,6 @@ namespace WixToolset.Core.ExtensibilityServices
20 19
21 internal class ParseHelper : IParseHelper 20 internal class ParseHelper : IParseHelper
22 { 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
36 public ParseHelper(IWixToolsetServiceProvider serviceProvider) 22 public ParseHelper(IWixToolsetServiceProvider serviceProvider)
37 { 23 {
38 this.ServiceProvider = serviceProvider; 24 this.ServiceProvider = serviceProvider;
@@ -119,7 +105,7 @@ namespace WixToolset.Core.ExtensibilityServices
119 id = parentId; 105 id = parentId;
120 106
121 var pathStartsAt = 0; 107 var pathStartsAt = 0;
122 if (inlineSyntax[0].EndsWith(":")) 108 if (inlineSyntax[0].EndsWith(":", StringComparison.Ordinal))
123 { 109 {
124 // TODO: should overriding the parent identifier with a specific id be an error or a warning or just let it slide? 110 // TODO: should overriding the parent identifier with a specific id be an error or a warning or just let it slide?
125 //if (null != parentId) 111 //if (null != parentId)
@@ -415,44 +401,34 @@ namespace WixToolset.Core.ExtensibilityServices
415 var emptyRule = canBeEmpty ? EmptyRule.CanBeEmpty : EmptyRule.CanBeWhitespaceOnly; 401 var emptyRule = canBeEmpty ? EmptyRule.CanBeEmpty : EmptyRule.CanBeWhitespaceOnly;
416 var value = this.GetAttributeValue(sourceLineNumbers, attribute, emptyRule); 402 var value = this.GetAttributeValue(sourceLineNumbers, attribute, emptyRule);
417 403
418 if (String.IsNullOrEmpty(value) && canBeEmpty) 404 if (String.IsNullOrEmpty(value))
419 {
420 return String.Empty;
421 }
422 else if (!String.IsNullOrEmpty(value))
423 { 405 {
424 // If the value starts and ends with braces or parenthesis, accept that and strip them off. 406 if (canBeEmpty)
425 if ((value.StartsWith("{", StringComparison.Ordinal) && value.EndsWith("}", StringComparison.Ordinal))
426 || (value.StartsWith("(", StringComparison.Ordinal) && value.EndsWith(")", StringComparison.Ordinal)))
427 { 407 {
428 value = value.Substring(1, value.Length - 2); 408 return String.Empty;
429 } 409 }
430 410 }
431 if (generatable && "*".Equals(value, StringComparison.Ordinal)) 411 else
412 {
413 if (generatable && value == "*")
432 { 414 {
433 return value; 415 return value;
434 } 416 }
435 417
436 if (ParseHelper.PutGuidHere.IsMatch(value)) 418 if (Guid.TryParse(value, out var guid))
437 { 419 {
438 this.Messaging.Write(ErrorMessages.ExampleGuid(sourceLineNumbers, attribute.Parent.Name.LocalName, attribute.Name.LocalName, value)); 420 return guid.ToString("B").ToUpperInvariant();
439 return CompilerConstants.IllegalGuid;
440 } 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))
442 { 424 {
443 return value; 425 return value;
444 } 426 }
445 else if (Guid.TryParse(value, out var guid))
446 {
447 var uppercaseGuid = guid.ToString().ToUpperInvariant();
448 427
449 // TODO: This used to be a pedantic error, what should it be now? 428 if (value.StartsWith("PUT-GUID-", StringComparison.OrdinalIgnoreCase) ||
450 //if (uppercaseGuid != value) 429 value.StartsWith("{PUT-GUID-", StringComparison.OrdinalIgnoreCase))
451 //{ 430 {
452 // this.Messaging.Write(WixErrors.GuidContainsLowercaseLetters(sourceLineNumbers, attribute.Parent.Name.LocalName, attribute.Name.LocalName, value)); 431 this.Messaging.Write(ErrorMessages.ExampleGuid(sourceLineNumbers, attribute.Parent.Name.LocalName, attribute.Name.LocalName, value));
453 //}
454
455 return String.Concat("{", uppercaseGuid, "}");
456 } 432 }
457 else 433 else
458 { 434 {
@@ -468,19 +444,45 @@ namespace WixToolset.Core.ExtensibilityServices
468 var access = AccessModifier.Public; 444 var access = AccessModifier.Public;
469 var value = Common.GetAttributeValue(this.Messaging, sourceLineNumbers, attribute, EmptyRule.CanBeEmpty); 445 var value = Common.GetAttributeValue(this.Messaging, sourceLineNumbers, attribute, EmptyRule.CanBeEmpty);
470 446
471 var match = ParseHelper.LegalIdentifierWithAccess.Match(value); 447 var separator = value.IndexOf(' ');
472 if (!match.Success) 448 if (separator > 0)
473 { 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();
475 } 478 }
476 else if (match.Groups["access"].Success) 479
480 if (!Common.IsIdentifier(value))
477 { 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;
479 } 484 }
480 485 else if (72 < value.Length)
481 value = match.Groups["id"].Value;
482
483 if (Common.IsIdentifier(value) && 72 < value.Length)
484 { 486 {
485 this.Messaging.Write(WarningMessages.IdentifierTooLong(sourceLineNumbers, attribute.Parent.Name.LocalName, attribute.Name.LocalName, value)); 487 this.Messaging.Write(WarningMessages.IdentifierTooLong(sourceLineNumbers, attribute.Parent.Name.LocalName, attribute.Name.LocalName, value));
486 } 488 }
@@ -520,7 +522,7 @@ namespace WixToolset.Core.ExtensibilityServices
520 } 522 }
521 else if (resultUsedToCreateReference && 1 == result.Length) 523 else if (resultUsedToCreateReference && 1 == result.Length)
522 { 524 {
523 if (value.EndsWith("\\")) 525 if (value.EndsWith("\\", StringComparison.Ordinal))
524 { 526 {
525 if (!this.IsValidLongFilename(result[0], false, false)) 527 if (!this.IsValidLongFilename(result[0], false, false))
526 { 528 {
@@ -547,7 +549,7 @@ namespace WixToolset.Core.ExtensibilityServices
547 } 549 }
548 } 550 }
549 551
550 if (1 < result.Length && !value.EndsWith("\\")) 552 if (1 < result.Length && !value.EndsWith("\\", StringComparison.Ordinal))
551 { 553 {
552 this.Messaging.Write(WarningMessages.BackslashTerminateInlineDirectorySyntax(sourceLineNumbers, attribute.Parent.Name.LocalName, attribute.Name.LocalName, value)); 554 this.Messaging.Write(WarningMessages.BackslashTerminateInlineDirectorySyntax(sourceLineNumbers, attribute.Parent.Name.LocalName, attribute.Name.LocalName, value));
553 } 555 }
@@ -776,14 +778,7 @@ namespace WixToolset.Core.ExtensibilityServices
776 778
777 public bool IsValidLocIdentifier(string identifier) 779 public bool IsValidLocIdentifier(string identifier)
778 { 780 {
779 if (String.IsNullOrEmpty(identifier)) 781 return Common.TryParseWixVariable(identifier, 0, out var parsed) && parsed.Index == 0 && parsed.Length == identifier.Length && parsed.Namespace == "loc";
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);
787 } 782 }
788 783
789 public bool IsValidLongFilename(string filename, bool allowWildcards, bool allowRelative) 784 public bool IsValidLongFilename(string filename, bool allowWildcards, bool allowRelative)
@@ -792,29 +787,38 @@ namespace WixToolset.Core.ExtensibilityServices
792 { 787 {
793 return false; 788 return false;
794 } 789 }
790 else if (filename.Length > 259)
791 {
792 return false;
793 }
795 794
796 // Check for a non-period character (all periods is not legal) 795 // Check for a non-period character (all periods is not legal)
797 var nonPeriodFound = false; 796 var allPeriods = true;
798 foreach (var character in filename) 797 foreach (var character in filename)
799 { 798 {
800 if ('.' != character) 799 if ('.' != character)
801 { 800 {
802 nonPeriodFound = true; 801 allPeriods = false;
803 break; 802 break;
804 } 803 }
805 } 804 }
806 805
806 if (allPeriods)
807 {
808 return false;
809 }
810
807 if (allowWildcards) 811 if (allowWildcards)
808 { 812 {
809 return (nonPeriodFound && ParseHelper.LegalWildcardLongFilename.IsMatch(filename)); 813 return filename.IndexOfAny(Common.IllegalWildcardLongFilenameCharacters) == -1;
810 } 814 }
811 else if (allowRelative) 815 else if (allowRelative)
812 { 816 {
813 return (nonPeriodFound && ParseHelper.LegalRelativeLongFilename.IsMatch(filename)); 817 return filename.IndexOfAny(Common.IllegalRelativeLongFilenameCharacters) == -1;
814 } 818 }
815 else 819 else
816 { 820 {
817 return (nonPeriodFound && ParseHelper.LegalLongFilename.IsMatch(filename)); 821 return filename.IndexOfAny(Common.IllegalLongFilenameCharacters) == -1;
818 } 822 }
819 } 823 }
820 824
diff --git a/src/WixToolset.Core/ParsedWixVariable.cs b/src/WixToolset.Core/ParsedWixVariable.cs
new file mode 100644
index 00000000..9d308b77
--- /dev/null
+++ b/src/WixToolset.Core/ParsedWixVariable.cs
@@ -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
3namespace 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}
diff --git a/src/WixToolset.Core/VariableResolver.cs b/src/WixToolset.Core/VariableResolver.cs
index 88067673..140e7def 100644
--- a/src/WixToolset.Core/VariableResolver.cs
+++ b/src/WixToolset.Core/VariableResolver.cs
@@ -79,150 +79,108 @@ namespace WixToolset.Core
79 79
80 public IVariableResolution ResolveVariables(SourceLineNumber sourceLineNumbers, string value, bool errorOnUnknown) 80 public IVariableResolution ResolveVariables(SourceLineNumber sourceLineNumbers, string value, bool errorOnUnknown)
81 { 81 {
82 var matches = Common.WixVariableRegex.Matches(value); 82 var start = 0;
83 var defaulted = true;
84 var delayed = false;
85 var updated = false;
83 86
84 // the value is the default unless it's substituted further down 87 while (Common.TryParseWixVariable(value, start, out var parsed))
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)
92 { 88 {
93 var updatedResultThisPass = false; 89 var variableNamespace = parsed.Namespace;
94 var sb = new StringBuilder(value); 90 var variableId = parsed.Name;
91 var variableDefaultValue = parsed.DefaultValue;
95 92
96 // notice how this code walks backward through the list 93 // check for an escape sequence of !! indicating the match is not a variable expression
97 // because it modifies the string as we move through it 94 if (0 < parsed.Index && '!' == value[parsed.Index - 1])
98 for (var i = matches.Count - 1; 0 <= i; i--)
99 { 95 {
100 var variableNamespace = matches[i].Groups["namespace"].Value; 96 var sb = new StringBuilder(value);
101 var variableId = matches[i].Groups["fullname"].Value; 97 sb.Remove(parsed.Index - 1, 1);
102 string variableDefaultValue = null; 98 value = sb.ToString();
103 99
104 // get the default value if one was specified 100 updated = true;
105 if (matches[i].Groups["value"].Success) 101 start = parsed.Index + parsed.Length - 1;
106 {
107 variableDefaultValue = matches[i].Groups["value"].Value;
108 102
109 // localization variables do not support inline default values 103 continue;
110 if ("loc" == variableNamespace) 104 }
111 { 105
112 this.Messaging.Write(ErrorMessages.IllegalInlineLocVariable(sourceLineNumbers, variableId, variableDefaultValue)); 106 string resolvedValue = null;
113 } 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;
114 } 115 }
115 116
116 // get the scope if one was specified 117 if (this.locVariables.TryGetValue(variableId, out var bindVariable))
117 if (matches[i].Groups["scope"].Success)
118 { 118 {
119 if ("bind" == variableNamespace) 119 resolvedValue = bindVariable.Value;
120 { 120 }
121 variableId = matches[i].Groups["name"].Value; 121 }
122 } 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;
123 } 128 }
129 else if (null != variableDefaultValue) // default the resolved value to the inline value if one was specified
130 {
131 resolvedValue = variableDefaultValue;
132 }
133 }
124 134
125 // check for an escape sequence of !! indicating the match is not a variable expression 135 if ("bind" == variableNamespace)
126 if (0 < matches[i].Index && '!' == sb[matches[i].Index - 1]) 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)
127 { 145 {
128 if (finalizeEscapes) 146 if (parsed.Index == 0 && parsed.Length == value.Length)
129 { 147 {
130 sb.Remove(matches[i].Index - 1, 1); 148 value = resolvedValue;
131
132 result.UpdatedValue = true;
133 } 149 }
134 else 150 else
135 { 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();
137 } 156 }
157
158 updated = true;
159 start = parsed.Index;
138 } 160 }
139 else 161 else
140 { 162 {
141 string resolvedValue = null; 163 if ("loc" == variableNamespace && errorOnUnknown) // unresolved loc variable
142
143 if ("loc" == variableNamespace)
144 { 164 {
145 // warn about deprecated syntax of $(loc.var) 165 this.Messaging.Write(ErrorMessages.LocalizationVariableUnknown(sourceLineNumbers, variableId));
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 }
155 } 166 }
156 else if ("wix" == variableNamespace) 167 else if ("wix" == variableNamespace && errorOnUnknown) // unresolved wix variable
157 { 168 {
158 // illegal syntax of $(wix.var) 169 this.Messaging.Write(ErrorMessages.WixVariableUnknown(sourceLineNumbers, variableId));
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 }
175 } 170 }
176 171
177 if ("bind" == variableNamespace) 172 start = parsed.Index + parsed.Length;
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 }
202 } 173 }
203 } 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 }
223 } 175 }
224 176
225 return result; 177 return new VariableResolution
178 {
179 DelayedResolve = delayed,
180 IsDefault = defaulted,
181 UpdatedValue = updated,
182 Value = value,
183 };
226 } 184 }
227 185
228 private static bool TryAddWixVariable(IDictionary<string, BindVariable> variables, BindVariable variable) 186 private static bool TryAddWixVariable(IDictionary<string, BindVariable> variables, BindVariable variable)