main
cs 853 lines 34.2 KB
Raw
1 // Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3 namespace WixToolset.Core
4 {
5 using System;
6 using System.Diagnostics;
7 using System.Globalization;
8 using System.IO;
9 using System.Linq;
10 using System.Security.Cryptography;
11 using System.Text;
12 using System.Xml;
13 using System.Xml.Linq;
14 using WixToolset.Data;
15 using WixToolset.Extensibility;
16 using WixToolset.Extensibility.Services;
17 using WixToolset.Versioning;
18
19 /// <summary>
20 /// Common Wix utility methods and types.
21 /// </summary>
22 internal static class Common
23 {
24 private static readonly char[] IllegalShortFilenameCharacters = new[] { '\\', '?', '|', '>', '<', ':', '/', '*', '\"', '+', ',', ';', '=', '[', ']', '.', ' ' };
25 private static readonly char[] IllegalWildcardShortFilenameCharacters = new[] { '\\', '|', '>', '<', ':', '/', '\"', '+', ',', ';', '=', '[', ']', '.', ' ' };
26
27 internal static readonly char[] IllegalLongFilenameCharacters = new[] { '\\', '/', '?', '*', '|', '>', '<', ':', '\"' }; // illegal: \ / ? | > < : / * "
28 internal static readonly char[] IllegalRelativeLongFilenameCharacters = new[] { '?', '*', '|', '>', '<', ':', '\"' }; // like illegal, but we allow '\' and '/'
29 internal static readonly char[] IllegalWildcardLongFilenameCharacters = new[] { '\\', '/', '|', '>', '<', ':', '\"' }; // like illegal: but we allow '*' and '?'
30
31 /// <summary>
32 /// Gets a valid code page from the given web name or integer value.
33 /// </summary>
34 /// <param name="value">A code page web name or integer value as a string.</param>
35 /// <param name="allowNoChange">Whether to allow -1 which does not change the database code pages. This may be the case with wxl files.</param>
36 /// <param name="onlyAnsi">Whether to allow Unicode (UCS) or UTF code pages.</param>
37 /// <param name="sourceLineNumbers">Source line information for the current authoring.</param>
38 /// <returns>A valid code page number.</returns>
39 /// <exception cref="ArgumentOutOfRangeException">The value is an integer less than 0 or greater than 65535.</exception>
40 /// <exception cref="ArgumentNullException"><paramref name="value"/> is null.</exception>
41 /// <exception cref="NotSupportedException">The value doesn't not represent a valid code page name or integer value.</exception>
42 /// <exception cref="WixException">The code page is invalid for summary information.</exception>
43 public static int GetValidCodePage(string value, bool allowNoChange = false, bool onlyAnsi = false, SourceLineNumber sourceLineNumbers = null)
44 {
45 Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
46
47 try
48 {
49 Encoding encoding;
50
51 // Check if a integer as a string was passed.
52 if (Int32.TryParse(value, out var codePage))
53 {
54 if (0 == codePage)
55 {
56 // 0 represents a neutral database
57 return 0;
58 }
59 else if (allowNoChange && -1 == codePage)
60 {
61 // -1 means no change to the database code page
62 return -1;
63 }
64
65 encoding = Encoding.GetEncoding(codePage);
66 }
67 else
68 {
69 encoding = Encoding.GetEncoding(value);
70 }
71
72 // Windows Installer parses some code page references
73 // as unsigned shorts which fail to open the database.
74 if (onlyAnsi)
75 {
76 codePage = encoding.CodePage;
77 if (0 > codePage || Int16.MaxValue < codePage)
78 {
79 throw new WixException(ErrorMessages.InvalidSummaryInfoCodePage(sourceLineNumbers, codePage));
80 }
81 }
82
83 if (encoding == null)
84 {
85 throw new WixException(ErrorMessages.IllegalCodepage(sourceLineNumbers, codePage));
86 }
87
88 return encoding.CodePage;
89 }
90 catch (ArgumentException ex)
91 {
92 // Rethrow as NotSupportedException since either can be thrown
93 // if the system does not support the specified code page.
94 throw new NotSupportedException(ex.Message, ex);
95 }
96 }
97
98 /// <summary>
99 /// Verifies if an identifier is a valid binder variable name.
100 /// </summary>
101 /// <param name="variable">Binder variable name to verify.</param>
102 /// <returns>True if the identifier is a valid binder variable name.</returns>
103 public static bool IsValidBinderVariable(string variable)
104 {
105 return TryParseWixVariable(variable, 0, out var parsed) && parsed.Index == 0 && parsed.Length == variable.Length && (parsed.Namespace == "bind" || parsed.Namespace == "wix");
106 }
107
108 /// <summary>
109 /// Verifies if a string contains a valid binder variable name.
110 /// </summary>
111 /// <param name="verify">String to verify.</param>
112 /// <returns>True if the string contains a valid binder variable name.</returns>
113 public static bool ContainsValidBinderVariable(string verify)
114 {
115 return TryParseWixVariable(verify, 0, out var parsed) && (parsed.Namespace == "bind" || parsed.Namespace == "wix");
116 }
117
118 /// <summary>
119 /// Verifies the given string is a valid 4-part version module or bundle version.
120 /// </summary>
121 /// <param name="version">The version to verify.</param>
122 /// <returns>True if version is a valid module or bundle version.</returns>
123 public static bool IsValidFourPartVersion(string version)
124 {
125 if (!Common.IsValidBinderVariable(version))
126 {
127 if (!Version.TryParse(version, out var ver) || 65535 < ver.Major || 65535 < ver.Minor || 65535 < ver.Build || 65535 < ver.Revision)
128 {
129 return false;
130 }
131 }
132
133 return true;
134 }
135
136 public static bool IsValidMsiProductVersion(string version)
137 {
138 return WixVersion.TryParse(version, out var wixVersion) && wixVersion.HasMajor && wixVersion.Major < 256 && wixVersion.Minor < 256 && wixVersion.Patch < 65536 && wixVersion.Labels == null && String.IsNullOrEmpty(wixVersion.Metadata);
139 }
140
141 public static bool IsValidLongFilename(string filename, bool allowWildcards, bool allowRelative)
142 {
143 if (String.IsNullOrEmpty(filename))
144 {
145 return false;
146 }
147 else if (filename.Length > 259)
148 {
149 return false;
150 }
151
152 // Check for a non-period character (all periods is not legal)
153 var allPeriods = true;
154 foreach (var character in filename)
155 {
156 if ('.' != character)
157 {
158 allPeriods = false;
159 break;
160 }
161 }
162
163 if (allPeriods)
164 {
165 return false;
166 }
167
168 if (allowWildcards)
169 {
170 return filename.IndexOfAny(Common.IllegalWildcardLongFilenameCharacters) == -1;
171 }
172 else if (allowRelative)
173 {
174 return filename.IndexOfAny(Common.IllegalRelativeLongFilenameCharacters) == -1;
175 }
176 else
177 {
178 return filename.IndexOfAny(Common.IllegalLongFilenameCharacters) == -1;
179 }
180 }
181
182 public static bool IsValidShortFilename(string filename, bool allowWildcards)
183 {
184 if (String.IsNullOrEmpty(filename))
185 {
186 return false;
187 }
188
189 if (allowWildcards)
190 {
191 var expectedDot = filename.IndexOfAny(IllegalWildcardShortFilenameCharacters);
192 if (expectedDot == -1)
193 {
194 }
195 else if (filename[expectedDot] != '.')
196 {
197 return false;
198 }
199 else if (expectedDot < filename.Length)
200 {
201 var extensionInvalids = filename.IndexOfAny(IllegalWildcardShortFilenameCharacters, expectedDot + 1);
202 if (extensionInvalids != -1)
203 {
204 return false;
205 }
206 }
207
208 var foundPeriod = false;
209 var beforePeriod = 0;
210 var afterPeriod = 0;
211
212 // count the number of characters before and after the period
213 // '*' is not counted because it may represent zero characters
214 foreach (var character in filename)
215 {
216 if ('.' == character)
217 {
218 foundPeriod = true;
219 }
220 else if ('*' != character)
221 {
222 if (foundPeriod)
223 {
224 afterPeriod++;
225 }
226 else
227 {
228 beforePeriod++;
229 }
230 }
231 }
232
233 if (8 >= beforePeriod && 3 >= afterPeriod)
234 {
235 return true;
236 }
237
238 return false;
239 }
240 else
241 {
242 if (filename.Length > 12)
243 {
244 return false;
245 }
246
247 var expectedDot = filename.IndexOfAny(IllegalShortFilenameCharacters);
248 if (expectedDot == -1)
249 {
250 return filename.Length < 9;
251 }
252 else if (expectedDot == 0 || expectedDot > 8 || filename[expectedDot] != '.' || expectedDot + 4 < filename.Length)
253 {
254 return false;
255 }
256
257 var validExtension = filename.IndexOfAny(IllegalShortFilenameCharacters, expectedDot + 1);
258 return validExtension == -1;
259 }
260 }
261
262 /// <summary>
263 /// Generate a new Windows Installer-friendly guid.
264 /// </summary>
265 /// <returns>A new guid.</returns>
266 public static string GenerateGuid()
267 {
268 return Guid.NewGuid().ToString("B").ToUpperInvariant();
269 }
270
271 /// <summary>
272 /// Generate an identifier by hashing data from the row.
273 /// </summary>
274 /// <param name="prefix">Three letter or less prefix for generated row identifier.</param>
275 /// <param name="args">Information to hash.</param>
276 /// <returns>The generated identifier.</returns>
277 public static string GenerateIdentifier(string prefix, params string[] args)
278 {
279 string base64;
280
281 using (var sha1 = new SHA1CryptoServiceProvider())
282 {
283 var combined = String.Join("|", args);
284 var data = Encoding.UTF8.GetBytes(combined);
285 var hash = sha1.ComputeHash(data);
286 base64 = Convert.ToBase64String(hash);
287 }
288
289 var identifier = new StringBuilder(32);
290 identifier.Append(prefix);
291 identifier.Append(base64);
292 identifier.Length -= 1; // removes the trailing '=' from base64
293 identifier.Replace('+', '.');
294 identifier.Replace('/', '_');
295
296 return identifier.ToString();
297 }
298
299 /// <summary>
300 /// Return an identifier based on provided file or directory name
301 /// </summary>
302 /// <param name="name">File/directory name to generate identifer from</param>
303 /// <returns>A version of the name that is a legal identifier.</returns>
304 internal static string GetIdentifierFromName(string name)
305 {
306 StringBuilder sb = null;
307 var offset = 0;
308
309 // MSI identifiers must begin with an alphabetic character or an
310 // underscore. Prefix all other values with an underscore.
311 if (!ValidIdentifierChar(name[0], true))
312 {
313 sb = new StringBuilder("_" + name);
314 offset = 1;
315 }
316
317 for (var i = 0; i < name.Length; ++i)
318 {
319 if (!ValidIdentifierChar(name[i], false))
320 {
321 if (sb == null)
322 {
323 sb = new StringBuilder(name);
324 }
325
326 sb[i + offset] = '_';
327 }
328 }
329
330 return sb?.ToString() ?? name;
331 }
332
333 /// <summary>
334 /// Checks if the string contains a property (i.e. "foo[Property]bar")
335 /// </summary>
336 /// <param name="possibleProperty">String to evaluate for properties.</param>
337 /// <returns>True if a property is found in the string.</returns>
338 internal static bool ContainsProperty(string possibleProperty)
339 {
340 var start = possibleProperty.IndexOf('[');
341 if (start != -1 && start < possibleProperty.Length - 2)
342 {
343 var end = possibleProperty.IndexOf(']', start + 1);
344 if (end > start + 1)
345 {
346 // Skip supported property modifiers.
347 if (possibleProperty[start + 1] == '#' || possibleProperty[start + 1] == '$' || possibleProperty[start + 1] == '!')
348 {
349 ++start;
350 }
351
352 var id = possibleProperty.Substring(start + 1, end - 1);
353
354 if (Common.IsIdentifier(id))
355 {
356 return true;
357 }
358 }
359 }
360
361 return false;
362 }
363
364 /// <summary>
365 /// Takes an id, and demodularizes it (if possible).
366 /// </summary>
367 /// <remarks>
368 /// If the output type is a module, returns a demodularized version of an id. Otherwise, returns the id.
369 /// </remarks>
370 /// <param name="outputType">The type of the output to bind.</param>
371 /// <param name="modularizationGuid">The modularization GUID.</param>
372 /// <param name="id">The id to demodularize.</param>
373 /// <returns>The demodularized id.</returns>
374 public static string Demodularize(OutputType outputType, string modularizationGuid, string id)
375 {
376 if (OutputType.Module == outputType && id.EndsWith(String.Concat(".", modularizationGuid), StringComparison.Ordinal))
377 {
378 id = id.Substring(0, id.Length - 37);
379 }
380
381 return id;
382 }
383
384 /// <summary>
385 /// Get the source/target and short/long file names from an MSI Filename column.
386 /// </summary>
387 /// <param name="value">The Filename value.</param>
388 /// <returns>An array of strings of length 4. The contents are: short target, long target, short source, and long source.</returns>
389 /// <remarks>
390 /// If any particular file name part is not parsed, its set to null in the appropriate location of the returned array of strings.
391 /// Thus the returned array will always be of length 4.
392 /// </remarks>
393 public static string[] GetNames(string value)
394 {
395 var targetSeparator = value.IndexOf(':');
396
397 // split source and target
398 string sourceName = null;
399 var targetName = value;
400 if (0 <= targetSeparator)
401 {
402 sourceName = value.Substring(targetSeparator + 1);
403 targetName = value.Substring(0, targetSeparator);
404 }
405
406 // split the source short and long names
407 string sourceLongName = null;
408 if (null != sourceName)
409 {
410 var sourceLongNameSeparator = sourceName.IndexOf('|');
411 if (0 <= sourceLongNameSeparator)
412 {
413 sourceLongName = sourceName.Substring(sourceLongNameSeparator + 1);
414 sourceName = sourceName.Substring(0, sourceLongNameSeparator);
415 }
416 }
417
418 // split the target short and long names
419 var targetLongNameSeparator = targetName.IndexOf('|');
420 string targetLongName = null;
421 if (0 <= targetLongNameSeparator)
422 {
423 targetLongName = targetName.Substring(targetLongNameSeparator + 1);
424 targetName = targetName.Substring(0, targetLongNameSeparator);
425 }
426
427 // Remove the long source name when its identical to the short source name.
428 if (null != sourceName && sourceName == sourceLongName)
429 {
430 sourceLongName = null;
431 }
432
433 // Remove the long target name when its identical to the long target name.
434 if (null != targetName && targetName == targetLongName)
435 {
436 targetLongName = null;
437 }
438
439 // Remove the source names when they are identical to the target names.
440 if (sourceName == targetName && sourceLongName == targetLongName)
441 {
442 sourceName = null;
443 sourceLongName = null;
444 }
445
446 // target name(s)
447 if ("." == targetName)
448 {
449 targetName = null;
450 }
451
452 if ("." == targetLongName)
453 {
454 targetLongName = null;
455 }
456
457 // source name(s)
458 if ("." == sourceName)
459 {
460 sourceName = null;
461 }
462
463 if ("." == sourceLongName)
464 {
465 sourceLongName = null;
466 }
467
468 return new[] { targetName, targetLongName, sourceName, sourceLongName };
469 }
470
471 /// <summary>
472 /// Get a source/target and short/long file name from an MSI Filename column.
473 /// </summary>
474 /// <param name="value">The Filename value.</param>
475 /// <param name="source">true to get a source name; false to get a target name</param>
476 /// <param name="longName">true to get a long name; false to get a short name</param>
477 /// <returns>The name.</returns>
478 public static string GetName(string value, bool source, bool longName)
479 {
480 var names = GetNames(value);
481
482 if (source)
483 {
484 if (longName && null != names[3])
485 {
486 return names[3];
487 }
488 else if (null != names[2])
489 {
490 return names[2];
491 }
492 }
493
494 if (longName && null != names[1])
495 {
496 return names[1];
497 }
498 else
499 {
500 return names[0];
501 }
502 }
503
504 /// <summary>
505 /// Get an attribute value.
506 /// </summary>
507 /// <param name="messaging"></param>
508 /// <param name="sourceLineNumbers">Source line information about the owner element.</param>
509 /// <param name="attribute">The attribute containing the value to get.</param>
510 /// <param name="emptyRule">A rule for the contents of the value. If the contents do not follow the rule, an error is thrown.</param>
511 /// <returns>The attribute's value.</returns>
512 internal static string GetAttributeValue(IMessaging messaging, SourceLineNumber sourceLineNumbers, XAttribute attribute, EmptyRule emptyRule)
513 {
514 var value = attribute.Value;
515
516 if ((emptyRule == EmptyRule.MustHaveNonWhitespaceCharacters && String.IsNullOrEmpty(value.Trim())) ||
517 (emptyRule == EmptyRule.CanBeWhitespaceOnly && String.IsNullOrEmpty(value)))
518 {
519 messaging.Write(ErrorMessages.IllegalEmptyAttributeValue(sourceLineNumbers, attribute.Parent.Name.LocalName, attribute.Name.LocalName));
520 return String.Empty;
521 }
522
523 return value;
524 }
525
526 /// <summary>
527 /// Verifies that a value is a legal identifier.
528 /// </summary>
529 /// <param name="value">The value to verify.</param>
530 /// <returns>true if the value is an identifier; false otherwise.</returns>
531 public static bool IsIdentifier(string value)
532 {
533 if (String.IsNullOrEmpty(value))
534 {
535 return false;
536 }
537
538 for (var i = 0; i < value.Length; ++i)
539 {
540 if (!ValidIdentifierChar(value[i], i == 0))
541 {
542 return false;
543 }
544 }
545
546 return true;
547 }
548
549 /// <summary>
550 /// Verifies that a value is a legal Bundle Variable/@Name.
551 /// </summary>
552 /// <param name="value">The value to verify.</param>
553 /// <returns>true if the value is an valid Variable/@Name; false otherwise.</returns>
554 public static bool IsBundleVariableName(string value)
555 {
556 if (String.IsNullOrEmpty(value))
557 {
558 return false;
559 }
560
561 for (var i = 0; i < value.Length; ++i)
562 {
563 if (!ValidBundleVariableNameChar(value[i], i == 0))
564 {
565 return false;
566 }
567 }
568
569 return true;
570 }
571
572 /// <summary>
573 /// Get an identifier attribute value and displays an error for an illegal identifier value.
574 /// </summary>
575 /// <param name="messaging"></param>
576 /// <param name="sourceLineNumbers">Source line information about the owner element.</param>
577 /// <param name="attribute">The attribute containing the value to get.</param>
578 /// <returns>The attribute's identifier value or a special value if an error occurred.</returns>
579 internal static string GetAttributeIdentifierValue(IMessaging messaging, SourceLineNumber sourceLineNumbers, XAttribute attribute)
580 {
581 var value = Common.GetAttributeValue(messaging, sourceLineNumbers, attribute, EmptyRule.CanBeWhitespaceOnly);
582
583 if (Common.IsIdentifier(value))
584 {
585 if (72 < value.Length)
586 {
587 messaging.Write(WarningMessages.IdentifierTooLong(sourceLineNumbers, attribute.Parent.Name.LocalName, attribute.Name.LocalName, value));
588 }
589
590 return value;
591 }
592 else
593 {
594 if (value.StartsWith("[", StringComparison.Ordinal) && value.EndsWith("]", StringComparison.Ordinal))
595 {
596 messaging.Write(ErrorMessages.IllegalIdentifierLooksLikeFormatted(sourceLineNumbers, attribute.Parent.Name.LocalName, attribute.Name.LocalName, value));
597 }
598 else
599 {
600 messaging.Write(ErrorMessages.IllegalIdentifier(sourceLineNumbers, attribute.Parent.Name.LocalName, attribute.Name.LocalName, value));
601 }
602
603 return String.Empty;
604 }
605 }
606
607 /// <summary>
608 /// Get an integer attribute value and displays an error for an illegal integer value.
609 /// </summary>
610 /// <param name="messaging"></param>
611 /// <param name="sourceLineNumbers">Source line information about the owner element.</param>
612 /// <param name="attribute">The attribute containing the value to get.</param>
613 /// <param name="minimum">The minimum legal value.</param>
614 /// <param name="maximum">The maximum legal value.</param>
615 /// <returns>The attribute's integer value or a special value if an error occurred during conversion.</returns>
616 public static int GetAttributeIntegerValue(IMessaging messaging, SourceLineNumber sourceLineNumbers, XAttribute attribute, int minimum, int maximum)
617 {
618 Debug.Assert(minimum > CompilerConstants.IntegerNotSet && minimum > CompilerConstants.IllegalInteger, "The legal values for this attribute collide with at least one sentinel used during parsing.");
619
620 var value = Common.GetAttributeValue(messaging, sourceLineNumbers, attribute, EmptyRule.CanBeWhitespaceOnly);
621 var integer = CompilerConstants.IllegalInteger;
622
623 if (0 < value.Length)
624 {
625 if (Int32.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture.NumberFormat, out integer))
626 {
627 if (CompilerConstants.IntegerNotSet == integer || CompilerConstants.IllegalInteger == integer)
628 {
629 messaging.Write(ErrorMessages.IntegralValueSentinelCollision(sourceLineNumbers, integer));
630 }
631 else if (minimum > integer || maximum < integer)
632 {
633 messaging.Write(ErrorMessages.IntegralValueOutOfRange(sourceLineNumbers, attribute.Parent.Name.LocalName, attribute.Name.LocalName, integer, minimum, maximum));
634 integer = CompilerConstants.IllegalInteger;
635 }
636 }
637 else
638 {
639 messaging.Write(ErrorMessages.IllegalIntegerValue(sourceLineNumbers, attribute.Parent.Name.LocalName, attribute.Name.LocalName, value));
640 }
641 }
642
643 return integer;
644 }
645
646 /// <summary>
647 /// Get an integer attribute value and displays an error for an illegal integer value.
648 /// </summary>
649 /// <param name="messaging"></param>
650 /// <param name="sourceLineNumbers">Source line information about the owner element.</param>
651 /// <param name="attribute">The attribute containing the value to get.</param>
652 /// <returns>The attribute's integer value or null if an error occurred during conversion.</returns>
653 public static int? GetAttributeRawIntegerValue(IMessaging messaging, SourceLineNumber sourceLineNumbers, XAttribute attribute)
654 {
655 var value = Common.GetAttributeValue(messaging, sourceLineNumbers, attribute, EmptyRule.MustHaveNonWhitespaceCharacters);
656 int? integer = null;
657
658 if (0 < value.Length)
659 {
660 if (Int32.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture.NumberFormat, out var integerValue))
661 {
662 integer = integerValue;
663 }
664 else
665 {
666 messaging.Write(ErrorMessages.IllegalIntegerValue(sourceLineNumbers, attribute.Parent.Name.LocalName, attribute.Name.LocalName, value));
667 }
668 }
669
670 return integer;
671 }
672
673 /// <summary>
674 /// Gets a yes/no value and displays an error for an illegal yes/no value.
675 /// </summary>
676 /// <param name="messaging"></param>
677 /// <param name="sourceLineNumbers">Source line information about the owner element.</param>
678 /// <param name="attribute">The attribute containing the value to get.</param>
679 /// <returns>The attribute's YesNoType value.</returns>
680 internal static YesNoType GetAttributeYesNoValue(IMessaging messaging, SourceLineNumber sourceLineNumbers, XAttribute attribute)
681 {
682 var value = Common.GetAttributeValue(messaging, sourceLineNumbers, attribute, EmptyRule.CanBeWhitespaceOnly);
683 var yesNo = YesNoType.IllegalValue;
684
685 if ("yes".Equals(value) || "true".Equals(value))
686 {
687 yesNo = YesNoType.Yes;
688 }
689 else if ("no".Equals(value) || "false".Equals(value))
690 {
691 yesNo = YesNoType.No;
692 }
693 else
694 {
695 messaging.Write(ErrorMessages.IllegalYesNoValue(sourceLineNumbers, attribute.Parent.Name.LocalName, attribute.Name.LocalName, value));
696 }
697
698 return yesNo;
699 }
700
701 /// <summary>
702 /// Gets the text of an XElement.
703 /// </summary>
704 /// <param name="node">Element to get text.</param>
705 /// <returns>The element's text.</returns>
706 internal static string GetInnerText(XElement node)
707 {
708 var text = node.Nodes().Where(n => XmlNodeType.Text == n.NodeType || XmlNodeType.CDATA == n.NodeType).Cast<XText>().FirstOrDefault();
709 return text?.Value;
710 }
711
712 internal static void InnerTextDisallowed(IMessaging messaging, XElement element, string attributeName)
713 {
714 var innerText = Common.GetInnerText(element);
715 if (!String.IsNullOrWhiteSpace(innerText))
716 {
717 var sourceLineNumbers = Preprocessor.GetSourceLineNumbers(element);
718 if (attributeName == null)
719 {
720 messaging.Write(ErrorMessages.IllegalInnerText(sourceLineNumbers, element.Name.LocalName, innerText));
721 }
722 else
723 {
724 messaging.Write(ErrorMessages.IllegalInnerText(sourceLineNumbers, element.Name.LocalName, innerText, attributeName));
725 }
726 }
727 }
728
729 internal static bool TryParseWixVariable(string value, int start, out ParsedWixVariable parsedVariable)
730 {
731 parsedVariable = null;
732
733 if (String.IsNullOrEmpty(value) || start >= value.Length)
734 {
735 return false;
736 }
737
738 var startWixVariable = value.IndexOf("!(", start, StringComparison.Ordinal);
739 if (startWixVariable == -1)
740 {
741 return false;
742 }
743
744 var firstDot = value.IndexOf('.', startWixVariable + 1);
745 if (firstDot == -1)
746 {
747 return false;
748 }
749
750 var ns = value.Substring(startWixVariable + 2, firstDot - startWixVariable - 2);
751 if (ns != "loc" && ns != "bind" && ns != "wix")
752 {
753 return false;
754 }
755
756 var closeParen = value.IndexOf(')', firstDot);
757 if (closeParen == -1)
758 {
759 return false;
760 }
761
762 string name;
763 string scope = null;
764 string defaultValue = null;
765
766 var equalsDefaultValue = value.IndexOf('=', firstDot + 1, closeParen - firstDot);
767 var end = equalsDefaultValue == -1 ? closeParen : equalsDefaultValue;
768 // bind variables may have a second dot to define their scope, other variables do not have scope and ignore additional dots.
769 var secondDot = ns == "bind" ? value.IndexOf('.', firstDot + 1, end - firstDot) : -1;
770
771 if (secondDot == -1)
772 {
773 name = value.Substring(firstDot + 1, end - firstDot - 1);
774 }
775 else
776 {
777 name = value.Substring(firstDot + 1, secondDot - firstDot - 1);
778 scope = value.Substring(secondDot + 1, end - secondDot - 1);
779
780 if (!Common.IsIdentifier(scope))
781 {
782 return false;
783 }
784 }
785
786 if (!Common.IsIdentifier(name))
787 {
788 return false;
789 }
790
791 if (equalsDefaultValue != -1 && equalsDefaultValue < closeParen)
792 {
793 defaultValue = value.Substring(equalsDefaultValue + 1, closeParen - equalsDefaultValue - 1);
794 }
795
796 parsedVariable = new ParsedWixVariable
797 {
798 Index = startWixVariable,
799 Length = closeParen - startWixVariable + 1,
800 Namespace = ns,
801 Name = name,
802 Scope = scope,
803 DefaultValue = defaultValue
804 };
805
806 return true;
807 }
808
809 /// <summary>
810 /// Display an unexpected attribute error.
811 /// </summary>
812 /// <param name="messaging"></param>
813 /// <param name="sourceLineNumbers">Source line information about the owner element.</param>
814 /// <param name="attribute">The attribute.</param>
815 public static void UnexpectedAttribute(IMessaging messaging, SourceLineNumber sourceLineNumbers, XAttribute attribute)
816 {
817 // Ignore elements defined by the W3C because we'll assume they are always right.
818 if (!((String.IsNullOrEmpty(attribute.Name.NamespaceName) && attribute.Name.LocalName.Equals("xmlns", StringComparison.Ordinal)) ||
819 attribute.Name.NamespaceName.StartsWith(CompilerCore.W3SchemaPrefix.NamespaceName, StringComparison.Ordinal)))
820 {
821 messaging.Write(ErrorMessages.UnexpectedAttribute(sourceLineNumbers, attribute.Parent.Name.LocalName, attribute.Name.LocalName));
822 }
823 }
824
825 /// <summary>
826 /// Display an unsupported extension attribute error.
827 /// </summary>
828 /// <param name="messaging"></param>
829 /// <param name="sourceLineNumbers">Source line information about the owner element.</param>
830 /// <param name="extensionAttribute">The extension attribute.</param>
831 internal static void UnsupportedExtensionAttribute(IMessaging messaging, SourceLineNumber sourceLineNumbers, XAttribute extensionAttribute)
832 {
833 // Ignore elements defined by the W3C because we'll assume they are always right.
834 if (!((String.IsNullOrEmpty(extensionAttribute.Name.NamespaceName) && extensionAttribute.Name.LocalName.Equals("xmlns", StringComparison.Ordinal)) ||
835 extensionAttribute.Name.NamespaceName.StartsWith(CompilerCore.W3SchemaPrefix.NamespaceName, StringComparison.Ordinal)))
836 {
837 messaging.Write(ErrorMessages.UnsupportedExtensionAttribute(sourceLineNumbers, extensionAttribute.Parent.Name.LocalName, extensionAttribute.Name.LocalName));
838 }
839 }
840
841 private static bool ValidIdentifierChar(char c, bool firstChar)
842 {
843 return ('A' <= c && 'Z' >= c) || ('a' <= c && 'z' >= c) || '_' == c ||
844 (!firstChar && (Char.IsDigit(c) || '.' == c));
845 }
846
847 private static bool ValidBundleVariableNameChar(char c, bool firstChar)
848 {
849 return ('A' <= c && 'Z' >= c) || ('a' <= c && 'z' >= c) || '_' == c ||
850 (!firstChar && Char.IsDigit(c));
851 }
852 }
853 }