diff options
Diffstat (limited to 'src/WixToolset.Converters/WixConverter.cs')
-rw-r--r-- | src/WixToolset.Converters/WixConverter.cs | 1098 |
1 files changed, 1098 insertions, 0 deletions
diff --git a/src/WixToolset.Converters/WixConverter.cs b/src/WixToolset.Converters/WixConverter.cs new file mode 100644 index 00000000..a89d44ce --- /dev/null +++ b/src/WixToolset.Converters/WixConverter.cs | |||
@@ -0,0 +1,1098 @@ | |||
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.Converters | ||
4 | { | ||
5 | using System; | ||
6 | using System.Collections.Generic; | ||
7 | using System.Globalization; | ||
8 | using System.IO; | ||
9 | using System.Linq; | ||
10 | using System.Text; | ||
11 | using System.Text.RegularExpressions; | ||
12 | using System.Xml; | ||
13 | using System.Xml.Linq; | ||
14 | using WixToolset.Data; | ||
15 | using WixToolset.Extensibility.Services; | ||
16 | |||
17 | /// <summary> | ||
18 | /// WiX source code converter. | ||
19 | /// </summary> | ||
20 | public class WixConverter | ||
21 | { | ||
22 | private static readonly Regex AddPrefix = new Regex(@"^[^a-zA-Z_]", RegexOptions.Compiled); | ||
23 | private static readonly Regex IllegalIdentifierCharacters = new Regex(@"[^A-Za-z0-9_\.]|\.{2,}", RegexOptions.Compiled); // non 'words' and assorted valid characters | ||
24 | |||
25 | private const char XDocumentNewLine = '\n'; // XDocument normalizes "\r\n" to just "\n". | ||
26 | private static readonly XNamespace WixNamespace = "http://wixtoolset.org/schemas/v4/wxs"; | ||
27 | private static readonly XNamespace WixUtilNamespace = "http://wixtoolset.org/schemas/v4/wxs/util"; | ||
28 | |||
29 | private static readonly XName AdminExecuteSequenceElementName = WixNamespace + "AdminExecuteSequence"; | ||
30 | private static readonly XName AdminUISequenceSequenceElementName = WixNamespace + "AdminUISequence"; | ||
31 | private static readonly XName AdvertiseExecuteSequenceElementName = WixNamespace + "AdvertiseExecuteSequence"; | ||
32 | private static readonly XName InstallExecuteSequenceElementName = WixNamespace + "InstallExecuteSequence"; | ||
33 | private static readonly XName InstallUISequenceSequenceElementName = WixNamespace + "InstallUISequence"; | ||
34 | private static readonly XName EmbeddedChainerElementName = WixNamespace + "EmbeddedChainer"; | ||
35 | private static readonly XName ColumnElementName = WixNamespace + "Column"; | ||
36 | private static readonly XName ComponentElementName = WixNamespace + "Component"; | ||
37 | private static readonly XName ControlElementName = WixNamespace + "Control"; | ||
38 | private static readonly XName ConditionElementName = WixNamespace + "Condition"; | ||
39 | private static readonly XName CreateFolderElementName = WixNamespace + "CreateFolder"; | ||
40 | private static readonly XName CustomTableElementName = WixNamespace + "CustomTable"; | ||
41 | private static readonly XName DirectoryElementName = WixNamespace + "Directory"; | ||
42 | private static readonly XName FeatureElementName = WixNamespace + "Feature"; | ||
43 | private static readonly XName FileElementName = WixNamespace + "File"; | ||
44 | private static readonly XName FragmentElementName = WixNamespace + "Fragment"; | ||
45 | private static readonly XName ErrorElementName = WixNamespace + "Error"; | ||
46 | private static readonly XName LaunchElementName = WixNamespace + "Launch"; | ||
47 | private static readonly XName LevelElementName = WixNamespace + "Level"; | ||
48 | private static readonly XName ExePackageElementName = WixNamespace + "ExePackage"; | ||
49 | private static readonly XName MsiPackageElementName = WixNamespace + "MsiPackage"; | ||
50 | private static readonly XName MspPackageElementName = WixNamespace + "MspPackage"; | ||
51 | private static readonly XName MsuPackageElementName = WixNamespace + "MsuPackage"; | ||
52 | private static readonly XName PayloadElementName = WixNamespace + "Payload"; | ||
53 | private static readonly XName PermissionExElementName = WixNamespace + "PermissionEx"; | ||
54 | private static readonly XName ProductElementName = WixNamespace + "Product"; | ||
55 | private static readonly XName ProgressTextElementName = WixNamespace + "ProgressText"; | ||
56 | private static readonly XName PublishElementName = WixNamespace + "Publish"; | ||
57 | private static readonly XName MultiStringValueElementName = WixNamespace + "MultiStringValue"; | ||
58 | private static readonly XName RequiredPrivilegeElementName = WixNamespace + "RequiredPrivilege"; | ||
59 | private static readonly XName RowElementName = WixNamespace + "Row"; | ||
60 | private static readonly XName ServiceArgumentElementName = WixNamespace + "ServiceArgument"; | ||
61 | private static readonly XName SetDirectoryElementName = WixNamespace + "SetDirectory"; | ||
62 | private static readonly XName SetPropertyElementName = WixNamespace + "SetProperty"; | ||
63 | private static readonly XName ShortcutPropertyElementName = WixNamespace + "ShortcutProperty"; | ||
64 | private static readonly XName TextElementName = WixNamespace + "Text"; | ||
65 | private static readonly XName UITextElementName = WixNamespace + "UIText"; | ||
66 | private static readonly XName UtilPermissionExElementName = WixUtilNamespace + "PermissionEx"; | ||
67 | private static readonly XName CustomActionElementName = WixNamespace + "CustomAction"; | ||
68 | private static readonly XName PropertyElementName = WixNamespace + "Property"; | ||
69 | private static readonly XName WixElementWithoutNamespaceName = XNamespace.None + "Wix"; | ||
70 | private static readonly XName IncludeElementWithoutNamespaceName = XNamespace.None + "Include"; | ||
71 | |||
72 | private static readonly Dictionary<string, XNamespace> OldToNewNamespaceMapping = new Dictionary<string, XNamespace>() | ||
73 | { | ||
74 | { "http://schemas.microsoft.com/wix/BalExtension", "http://wixtoolset.org/schemas/v4/wxs/bal" }, | ||
75 | { "http://schemas.microsoft.com/wix/ComPlusExtension", "http://wixtoolset.org/schemas/v4/wxs/complus" }, | ||
76 | { "http://schemas.microsoft.com/wix/DependencyExtension", "http://wixtoolset.org/schemas/v4/wxs/dependency" }, | ||
77 | { "http://schemas.microsoft.com/wix/DifxAppExtension", "http://wixtoolset.org/schemas/v4/wxs/difxapp" }, | ||
78 | { "http://schemas.microsoft.com/wix/FirewallExtension", "http://wixtoolset.org/schemas/v4/wxs/firewall" }, | ||
79 | { "http://schemas.microsoft.com/wix/HttpExtension", "http://wixtoolset.org/schemas/v4/wxs/http" }, | ||
80 | { "http://schemas.microsoft.com/wix/IIsExtension", "http://wixtoolset.org/schemas/v4/wxs/iis" }, | ||
81 | { "http://schemas.microsoft.com/wix/MsmqExtension", "http://wixtoolset.org/schemas/v4/wxs/msmq" }, | ||
82 | { "http://schemas.microsoft.com/wix/NetFxExtension", "http://wixtoolset.org/schemas/v4/wxs/netfx" }, | ||
83 | { "http://schemas.microsoft.com/wix/PSExtension", "http://wixtoolset.org/schemas/v4/wxs/powershell" }, | ||
84 | { "http://schemas.microsoft.com/wix/SqlExtension", "http://wixtoolset.org/schemas/v4/wxs/sql" }, | ||
85 | { "http://schemas.microsoft.com/wix/TagExtension", "http://wixtoolset.org/schemas/v4/wxs/tag" }, | ||
86 | { "http://schemas.microsoft.com/wix/UtilExtension", WixUtilNamespace }, | ||
87 | { "http://schemas.microsoft.com/wix/VSExtension", "http://wixtoolset.org/schemas/v4/wxs/vs" }, | ||
88 | { "http://wixtoolset.org/schemas/thmutil/2010", "http://wixtoolset.org/schemas/v4/thmutil" }, | ||
89 | { "http://schemas.microsoft.com/wix/2009/Lux", "http://wixtoolset.org/schemas/v4/lux" }, | ||
90 | { "http://schemas.microsoft.com/wix/2006/wi", "http://wixtoolset.org/schemas/v4/wxs" }, | ||
91 | { "http://schemas.microsoft.com/wix/2006/localization", "http://wixtoolset.org/schemas/v4/wxl" }, | ||
92 | { "http://schemas.microsoft.com/wix/2006/libraries", "http://wixtoolset.org/schemas/v4/wixlib" }, | ||
93 | { "http://schemas.microsoft.com/wix/2006/objects", "http://wixtoolset.org/schemas/v4/wixobj" }, | ||
94 | { "http://schemas.microsoft.com/wix/2006/outputs", "http://wixtoolset.org/schemas/v4/wixout" }, | ||
95 | { "http://schemas.microsoft.com/wix/2007/pdbs", "http://wixtoolset.org/schemas/v4/wixpdb" }, | ||
96 | { "http://schemas.microsoft.com/wix/2003/04/actions", "http://wixtoolset.org/schemas/v4/wi/actions" }, | ||
97 | { "http://schemas.microsoft.com/wix/2006/tables", "http://wixtoolset.org/schemas/v4/wi/tables" }, | ||
98 | { "http://schemas.microsoft.com/wix/2006/WixUnit", "http://wixtoolset.org/schemas/v4/wixunit" }, | ||
99 | }; | ||
100 | |||
101 | private readonly static SortedSet<string> Wix3Namespaces = new SortedSet<string> | ||
102 | { | ||
103 | "http://schemas.microsoft.com/wix/2006/wi", | ||
104 | "http://schemas.microsoft.com/wix/2006/localization", | ||
105 | }; | ||
106 | |||
107 | private readonly static SortedSet<string> Wix4Namespaces = new SortedSet<string> | ||
108 | { | ||
109 | "http://wixtoolset.org/schemas/v4/wxs", | ||
110 | "http://wixtoolset.org/schemas/v4/wxl", | ||
111 | }; | ||
112 | |||
113 | private readonly Dictionary<XName, Action<XElement>> ConvertElementMapping; | ||
114 | |||
115 | /// <summary> | ||
116 | /// Instantiate a new Converter class. | ||
117 | /// </summary> | ||
118 | /// <param name="indentationAmount">Indentation value to use when validating leading whitespace.</param> | ||
119 | /// <param name="errorsAsWarnings">Test errors to display as warnings.</param> | ||
120 | /// <param name="ignoreErrors">Test errors to ignore.</param> | ||
121 | public WixConverter(IMessaging messaging, int indentationAmount, IEnumerable<string> errorsAsWarnings = null, IEnumerable<string> ignoreErrors = null) | ||
122 | { | ||
123 | this.ConvertElementMapping = new Dictionary<XName, Action<XElement>> | ||
124 | { | ||
125 | { WixConverter.AdminExecuteSequenceElementName, this.ConvertSequenceElement }, | ||
126 | { WixConverter.AdminUISequenceSequenceElementName, this.ConvertSequenceElement }, | ||
127 | { WixConverter.AdvertiseExecuteSequenceElementName, this.ConvertSequenceElement }, | ||
128 | { WixConverter.InstallUISequenceSequenceElementName, this.ConvertSequenceElement }, | ||
129 | { WixConverter.InstallExecuteSequenceElementName, this.ConvertSequenceElement }, | ||
130 | { WixConverter.ColumnElementName, this.ConvertColumnElement }, | ||
131 | { WixConverter.CustomTableElementName, this.ConvertCustomTableElement }, | ||
132 | { WixConverter.ControlElementName, this.ConvertControlElement }, | ||
133 | { WixConverter.ComponentElementName, this.ConvertComponentElement }, | ||
134 | { WixConverter.DirectoryElementName, this.ConvertDirectoryElement }, | ||
135 | { WixConverter.FeatureElementName, this.ConvertFeatureElement }, | ||
136 | { WixConverter.FileElementName, this.ConvertFileElement }, | ||
137 | { WixConverter.FragmentElementName, this.ConvertFragmentElement }, | ||
138 | { WixConverter.EmbeddedChainerElementName, this.ConvertEmbeddedChainerElement }, | ||
139 | { WixConverter.ErrorElementName, this.ConvertErrorElement }, | ||
140 | { WixConverter.ExePackageElementName, this.ConvertSuppressSignatureValidation }, | ||
141 | { WixConverter.MsiPackageElementName, this.ConvertSuppressSignatureValidation }, | ||
142 | { WixConverter.MspPackageElementName, this.ConvertSuppressSignatureValidation }, | ||
143 | { WixConverter.MsuPackageElementName, this.ConvertSuppressSignatureValidation }, | ||
144 | { WixConverter.PayloadElementName, this.ConvertSuppressSignatureValidation }, | ||
145 | { WixConverter.PermissionExElementName, this.ConvertPermissionExElement }, | ||
146 | { WixConverter.ProductElementName, this.ConvertProductElement }, | ||
147 | { WixConverter.ProgressTextElementName, this.ConvertProgressTextElement }, | ||
148 | { WixConverter.PublishElementName, this.ConvertPublishElement }, | ||
149 | { WixConverter.MultiStringValueElementName, this.ConvertMultiStringValueElement }, | ||
150 | { WixConverter.RequiredPrivilegeElementName, this.ConvertRequiredPrivilegeElement }, | ||
151 | { WixConverter.RowElementName, this.ConvertRowElement }, | ||
152 | { WixConverter.CustomActionElementName, this.ConvertCustomActionElement }, | ||
153 | { WixConverter.ServiceArgumentElementName, this.ConvertServiceArgumentElement }, | ||
154 | { WixConverter.SetDirectoryElementName, this.ConvertSetDirectoryElement }, | ||
155 | { WixConverter.SetPropertyElementName, this.ConvertSetPropertyElement }, | ||
156 | { WixConverter.ShortcutPropertyElementName, this.ConvertShortcutPropertyElement }, | ||
157 | { WixConverter.TextElementName, this.ConvertTextElement }, | ||
158 | { WixConverter.UITextElementName, this.ConvertUITextElement }, | ||
159 | { WixConverter.UtilPermissionExElementName, this.ConvertUtilPermissionExElement }, | ||
160 | { WixConverter.PropertyElementName, this.ConvertPropertyElement }, | ||
161 | { WixConverter.WixElementWithoutNamespaceName, this.ConvertElementWithoutNamespace }, | ||
162 | { WixConverter.IncludeElementWithoutNamespaceName, this.ConvertElementWithoutNamespace }, | ||
163 | }; | ||
164 | |||
165 | this.Messaging = messaging; | ||
166 | |||
167 | this.IndentationAmount = indentationAmount; | ||
168 | |||
169 | this.ErrorsAsWarnings = new HashSet<ConverterTestType>(this.YieldConverterTypes(errorsAsWarnings)); | ||
170 | |||
171 | this.IgnoreErrors = new HashSet<ConverterTestType>(this.YieldConverterTypes(ignoreErrors)); | ||
172 | } | ||
173 | |||
174 | private int Errors { get; set; } | ||
175 | |||
176 | private HashSet<ConverterTestType> ErrorsAsWarnings { get; set; } | ||
177 | |||
178 | private HashSet<ConverterTestType> IgnoreErrors { get; set; } | ||
179 | |||
180 | private IMessaging Messaging { get; } | ||
181 | |||
182 | private int IndentationAmount { get; set; } | ||
183 | |||
184 | private string SourceFile { get; set; } | ||
185 | |||
186 | private int SourceVersion { get; set; } | ||
187 | |||
188 | /// <summary> | ||
189 | /// Convert a file. | ||
190 | /// </summary> | ||
191 | /// <param name="sourceFile">The file to convert.</param> | ||
192 | /// <param name="saveConvertedFile">Option to save the converted errors that are found.</param> | ||
193 | /// <returns>The number of errors found.</returns> | ||
194 | public int ConvertFile(string sourceFile, bool saveConvertedFile) | ||
195 | { | ||
196 | XDocument document; | ||
197 | |||
198 | // Set the instance info. | ||
199 | this.Errors = 0; | ||
200 | this.SourceFile = sourceFile; | ||
201 | this.SourceVersion = 0; | ||
202 | |||
203 | try | ||
204 | { | ||
205 | document = XDocument.Load(this.SourceFile, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo); | ||
206 | } | ||
207 | catch (XmlException e) | ||
208 | { | ||
209 | this.OnError(ConverterTestType.XmlException, null, "The xml is invalid. Detail: '{0}'", e.Message); | ||
210 | |||
211 | return this.Errors; | ||
212 | } | ||
213 | |||
214 | this.ConvertDocument(document); | ||
215 | |||
216 | // Fix errors if requested and necessary. | ||
217 | if (saveConvertedFile && 0 < this.Errors) | ||
218 | { | ||
219 | try | ||
220 | { | ||
221 | using (var writer = XmlWriter.Create(this.SourceFile, new XmlWriterSettings { OmitXmlDeclaration = true })) | ||
222 | { | ||
223 | document.Save(writer); | ||
224 | } | ||
225 | } | ||
226 | catch (UnauthorizedAccessException) | ||
227 | { | ||
228 | this.OnError(ConverterTestType.UnauthorizedAccessException, null, "Could not write to file."); | ||
229 | } | ||
230 | } | ||
231 | |||
232 | return this.Errors; | ||
233 | } | ||
234 | |||
235 | /// <summary> | ||
236 | /// Convert a document. | ||
237 | /// </summary> | ||
238 | /// <param name="document">The document to convert.</param> | ||
239 | /// <returns>The number of errors found.</returns> | ||
240 | public int ConvertDocument(XDocument document) | ||
241 | { | ||
242 | this.Errors = 0; | ||
243 | this.SourceVersion = 0; | ||
244 | |||
245 | var declaration = document.Declaration; | ||
246 | |||
247 | // Remove the declaration. | ||
248 | if (null != declaration) | ||
249 | { | ||
250 | if (this.OnError(ConverterTestType.DeclarationPresent, null, "This file contains an XML declaration on the first line.")) | ||
251 | { | ||
252 | document.Declaration = null; | ||
253 | } | ||
254 | } | ||
255 | |||
256 | TrimLeadingText(document); | ||
257 | |||
258 | // Start converting the nodes at the top. | ||
259 | this.ConvertNodes(document.Nodes(), 0); | ||
260 | |||
261 | return this.Errors; | ||
262 | } | ||
263 | |||
264 | private void ConvertNodes(IEnumerable<XNode> nodes, int level) | ||
265 | { | ||
266 | // Note we operate on a copy of the node list since we may | ||
267 | // remove some whitespace nodes during this processing. | ||
268 | foreach (var node in nodes.ToList()) | ||
269 | { | ||
270 | if (node is XText text) | ||
271 | { | ||
272 | if (!String.IsNullOrWhiteSpace(text.Value)) | ||
273 | { | ||
274 | text.Value = text.Value.Trim(); | ||
275 | } | ||
276 | else if (node.NextNode is XCData cdata) | ||
277 | { | ||
278 | this.EnsurePrecedingWhitespaceRemoved(text, node, ConverterTestType.WhitespacePrecedingNodeWrong); | ||
279 | } | ||
280 | else if (node.NextNode is XElement element) | ||
281 | { | ||
282 | this.EnsurePrecedingWhitespaceCorrect(text, node, level, ConverterTestType.WhitespacePrecedingNodeWrong); | ||
283 | } | ||
284 | else if (node.NextNode is null) // this is the space before the close element | ||
285 | { | ||
286 | if (node.PreviousNode is null || node.PreviousNode is XCData) | ||
287 | { | ||
288 | this.EnsurePrecedingWhitespaceRemoved(text, node.Parent, ConverterTestType.WhitespacePrecedingEndElementWrong); | ||
289 | } | ||
290 | else if (level == 0) // root element's close tag | ||
291 | { | ||
292 | this.EnsurePrecedingWhitespaceCorrect(text, node, 0, ConverterTestType.WhitespacePrecedingEndElementWrong); | ||
293 | } | ||
294 | else | ||
295 | { | ||
296 | this.EnsurePrecedingWhitespaceCorrect(text, node, level - 1, ConverterTestType.WhitespacePrecedingEndElementWrong); | ||
297 | } | ||
298 | } | ||
299 | } | ||
300 | else if (node is XElement element) | ||
301 | { | ||
302 | this.ConvertElement(element); | ||
303 | |||
304 | this.ConvertNodes(element.Nodes(), level + 1); | ||
305 | } | ||
306 | } | ||
307 | } | ||
308 | |||
309 | private void EnsurePrecedingWhitespaceCorrect(XText whitespace, XNode node, int level, ConverterTestType testType) | ||
310 | { | ||
311 | if (!WixConverter.LeadingWhitespaceValid(this.IndentationAmount, level, whitespace.Value)) | ||
312 | { | ||
313 | var message = testType == ConverterTestType.WhitespacePrecedingEndElementWrong ? "The whitespace preceding this end element is incorrect." : "The whitespace preceding this node is incorrect."; | ||
314 | |||
315 | if (this.OnError(testType, node, message)) | ||
316 | { | ||
317 | WixConverter.FixupWhitespace(this.IndentationAmount, level, whitespace); | ||
318 | } | ||
319 | } | ||
320 | } | ||
321 | |||
322 | private void EnsurePrecedingWhitespaceRemoved(XText whitespace, XNode node, ConverterTestType testType) | ||
323 | { | ||
324 | if (!String.IsNullOrEmpty(whitespace.Value) && whitespace.NodeType != XmlNodeType.CDATA) | ||
325 | { | ||
326 | var message = testType == ConverterTestType.WhitespacePrecedingEndElementWrong ? "The whitespace preceding this end element is incorrect." : "The whitespace preceding this node is incorrect."; | ||
327 | |||
328 | if (this.OnError(testType, node, message)) | ||
329 | { | ||
330 | whitespace.Remove(); | ||
331 | } | ||
332 | } | ||
333 | } | ||
334 | |||
335 | private void ConvertElement(XElement element) | ||
336 | { | ||
337 | // Gather any deprecated namespaces, then update this element tree based on those deprecations. | ||
338 | var deprecatedToUpdatedNamespaces = new Dictionary<XNamespace, XNamespace>(); | ||
339 | |||
340 | foreach (var declaration in element.Attributes().Where(a => a.IsNamespaceDeclaration)) | ||
341 | { | ||
342 | if (WixConverter.OldToNewNamespaceMapping.TryGetValue(declaration.Value, out var ns)) | ||
343 | { | ||
344 | if (Wix3Namespaces.Contains(declaration.Value)) | ||
345 | { | ||
346 | this.SourceVersion = 3; | ||
347 | } | ||
348 | else if (Wix4Namespaces.Contains(declaration.Value)) | ||
349 | { | ||
350 | this.SourceVersion = 4; | ||
351 | } | ||
352 | |||
353 | if (this.OnError(ConverterTestType.XmlnsValueWrong, declaration, "The namespace '{0}' is out of date. It must be '{1}'.", declaration.Value, ns.NamespaceName)) | ||
354 | { | ||
355 | deprecatedToUpdatedNamespaces.Add(declaration.Value, ns); | ||
356 | } | ||
357 | } | ||
358 | } | ||
359 | |||
360 | if (deprecatedToUpdatedNamespaces.Any()) | ||
361 | { | ||
362 | WixConverter.UpdateElementsWithDeprecatedNamespaces(element.DescendantsAndSelf(), deprecatedToUpdatedNamespaces); | ||
363 | } | ||
364 | |||
365 | // Apply any specialized conversion actions. | ||
366 | if (this.ConvertElementMapping.TryGetValue(element.Name, out var convert)) | ||
367 | { | ||
368 | convert(element); | ||
369 | } | ||
370 | } | ||
371 | |||
372 | private void ConvertColumnElement(XElement element) | ||
373 | { | ||
374 | var category = element.Attribute("Category"); | ||
375 | if (category != null) | ||
376 | { | ||
377 | var camelCaseValue = LowercaseFirstChar(category.Value); | ||
378 | if (category.Value != camelCaseValue && | ||
379 | this.OnError(ConverterTestType.ColumnCategoryCamelCase, element, "The CustomTable Category attribute contains an incorrectly cased '{0}' value. Lowercase the first character instead.", category.Name)) | ||
380 | { | ||
381 | category.Value = camelCaseValue; | ||
382 | } | ||
383 | } | ||
384 | |||
385 | var modularization = element.Attribute("Modularize"); | ||
386 | if (modularization != null) | ||
387 | { | ||
388 | var camelCaseValue = LowercaseFirstChar(modularization.Value); | ||
389 | if (category.Value != camelCaseValue && | ||
390 | this.OnError(ConverterTestType.ColumnModularizeCamelCase, element, "The CustomTable Modularize attribute contains an incorrectly cased '{0}' value. Lowercase the first character instead.", modularization.Name)) | ||
391 | { | ||
392 | modularization.Value = camelCaseValue; | ||
393 | } | ||
394 | } | ||
395 | } | ||
396 | |||
397 | private void ConvertCustomTableElement(XElement element) | ||
398 | { | ||
399 | var bootstrapperApplicationData = element.Attribute("BootstrapperApplicationData"); | ||
400 | if (bootstrapperApplicationData != null | ||
401 | && this.OnError(ConverterTestType.BootstrapperApplicationDataDeprecated, element, "The CustomTable element contains deprecated '{0}' attribute. Use the 'Unreal' attribute instead.", bootstrapperApplicationData.Name)) | ||
402 | { | ||
403 | element.Add(new XAttribute("Unreal", bootstrapperApplicationData.Value)); | ||
404 | bootstrapperApplicationData.Remove(); | ||
405 | } | ||
406 | } | ||
407 | |||
408 | private void ConvertControlElement(XElement element) | ||
409 | { | ||
410 | var xCondition = element.Element(ConditionElementName); | ||
411 | if (xCondition != null) | ||
412 | { | ||
413 | var action = UppercaseFirstChar(xCondition.Attribute("Action")?.Value); | ||
414 | if (!String.IsNullOrEmpty(action) && | ||
415 | TryGetInnerText(xCondition, out var text) && | ||
416 | this.OnError(ConverterTestType.InnerTextDeprecated, element, "Using {0} element text is deprecated. Use the '{1}Condition' attribute instead.", xCondition.Name.LocalName, action)) | ||
417 | { | ||
418 | element.Add(new XAttribute(action + "Condition", text)); | ||
419 | xCondition.Remove(); | ||
420 | } | ||
421 | } | ||
422 | } | ||
423 | |||
424 | private void ConvertComponentElement(XElement element) | ||
425 | { | ||
426 | var guid = element.Attribute("Guid"); | ||
427 | if (guid != null && guid.Value == "*") | ||
428 | { | ||
429 | if (this.OnError(ConverterTestType.AutoGuidUnnecessary, element, "Using '*' for the Component Guid attribute is unnecessary. Remove the attribute to remove the redundancy.")) | ||
430 | { | ||
431 | guid.Remove(); | ||
432 | } | ||
433 | } | ||
434 | |||
435 | var xCondition = element.Element(ConditionElementName); | ||
436 | if (xCondition != null) | ||
437 | { | ||
438 | if (TryGetInnerText(xCondition, out var text) && | ||
439 | this.OnError(ConverterTestType.InnerTextDeprecated, element, "Using {0} element text is deprecated. Use the 'Condition' attribute instead.", xCondition.Name.LocalName)) | ||
440 | { | ||
441 | element.Add(new XAttribute("Condition", text)); | ||
442 | xCondition.Remove(); | ||
443 | } | ||
444 | } | ||
445 | } | ||
446 | |||
447 | private void ConvertDirectoryElement(XElement element) | ||
448 | { | ||
449 | if (null == element.Attribute("Name")) | ||
450 | { | ||
451 | var attribute = element.Attribute("ShortName"); | ||
452 | if (null != attribute) | ||
453 | { | ||
454 | var shortName = attribute.Value; | ||
455 | if (this.OnError(ConverterTestType.AssignDirectoryNameFromShortName, element, "The directory ShortName attribute is being renamed to Name since Name wasn't specified for value '{0}'", shortName)) | ||
456 | { | ||
457 | element.Add(new XAttribute("Name", shortName)); | ||
458 | attribute.Remove(); | ||
459 | } | ||
460 | } | ||
461 | } | ||
462 | } | ||
463 | |||
464 | private void ConvertFeatureElement(XElement element) | ||
465 | { | ||
466 | var xCondition = element.Element(ConditionElementName); | ||
467 | if (xCondition != null) | ||
468 | { | ||
469 | var level = xCondition.Attribute("Level")?.Value; | ||
470 | if (!String.IsNullOrEmpty(level) && | ||
471 | TryGetInnerText(xCondition, out var text) && | ||
472 | this.OnError(ConverterTestType.InnerTextDeprecated, element, "Using {0} element text is deprecated. Use the 'Level' element instead.", xCondition.Name.LocalName)) | ||
473 | { | ||
474 | xCondition.AddAfterSelf(new XElement(LevelElementName, | ||
475 | new XAttribute("Value", level), | ||
476 | new XAttribute("Condition", text) | ||
477 | )); | ||
478 | xCondition.Remove(); | ||
479 | } | ||
480 | } | ||
481 | } | ||
482 | |||
483 | private void ConvertFileElement(XElement element) | ||
484 | { | ||
485 | if (this.SourceVersion < 4 && null == element.Attribute("Id")) | ||
486 | { | ||
487 | var attribute = element.Attribute("Name"); | ||
488 | |||
489 | if (null == attribute) | ||
490 | { | ||
491 | attribute = element.Attribute("Source"); | ||
492 | } | ||
493 | |||
494 | if (null != attribute) | ||
495 | { | ||
496 | var name = Path.GetFileName(attribute.Value); | ||
497 | |||
498 | if (this.OnError(ConverterTestType.AssignAnonymousFileId, element, "The file id is being updated to '{0}' to ensure it remains the same as the v3 default", name)) | ||
499 | { | ||
500 | IEnumerable<XAttribute> attributes = element.Attributes().ToList(); | ||
501 | element.RemoveAttributes(); | ||
502 | element.Add(new XAttribute("Id", GetIdentifierFromName(name))); | ||
503 | element.Add(attributes); | ||
504 | } | ||
505 | } | ||
506 | } | ||
507 | } | ||
508 | |||
509 | private void ConvertFragmentElement(XElement element) | ||
510 | { | ||
511 | var xCondition = element.Element(ConditionElementName); | ||
512 | if (xCondition != null) | ||
513 | { | ||
514 | var message = xCondition.Attribute("Message")?.Value; | ||
515 | |||
516 | if (!String.IsNullOrEmpty(message) && | ||
517 | TryGetInnerText(xCondition, out var text) && | ||
518 | this.OnError(ConverterTestType.InnerTextDeprecated, element, "Using {0} element text is deprecated. Use the 'Launch' element instead.", xCondition.Name.LocalName)) | ||
519 | { | ||
520 | xCondition.AddAfterSelf(new XElement(LaunchElementName, | ||
521 | new XAttribute("Condition", text), | ||
522 | new XAttribute("Message", message) | ||
523 | )); | ||
524 | xCondition.Remove(); | ||
525 | } | ||
526 | } | ||
527 | } | ||
528 | |||
529 | private void ConvertEmbeddedChainerElement(XElement element) => this.ConvertInnerTextToAttribute(element, "Condition"); | ||
530 | |||
531 | private void ConvertErrorElement(XElement element) => this.ConvertInnerTextToAttribute(element, "Message"); | ||
532 | |||
533 | private void ConvertPermissionExElement(XElement element) | ||
534 | { | ||
535 | var xCondition = element.Element(ConditionElementName); | ||
536 | if (xCondition != null) | ||
537 | { | ||
538 | if (TryGetInnerText(xCondition, out var text) && | ||
539 | this.OnError(ConverterTestType.InnerTextDeprecated, element, "Using {0} element text is deprecated. Use the 'Condition' attribute instead.", xCondition.Name.LocalName)) | ||
540 | { | ||
541 | element.Add(new XAttribute("Condition", text)); | ||
542 | xCondition.Remove(); | ||
543 | } | ||
544 | } | ||
545 | } | ||
546 | |||
547 | private void ConvertProgressTextElement(XElement element) => this.ConvertInnerTextToAttribute(element, "Message"); | ||
548 | |||
549 | private void ConvertProductElement(XElement element) | ||
550 | { | ||
551 | var id = element.Attribute("Id"); | ||
552 | if (id != null && id.Value == "*") | ||
553 | { | ||
554 | if (this.OnError(ConverterTestType.AutoGuidUnnecessary, element, "Using '*' for the Product Id attribute is unnecessary. Remove the attribute to remove the redundancy.")) | ||
555 | { | ||
556 | id.Remove(); | ||
557 | } | ||
558 | } | ||
559 | |||
560 | var xCondition = element.Element(ConditionElementName); | ||
561 | if (xCondition != null) | ||
562 | { | ||
563 | var message = element.Attribute("Message")?.Value; | ||
564 | |||
565 | if (!String.IsNullOrEmpty(message) && | ||
566 | TryGetInnerText(xCondition, out var text) && | ||
567 | this.OnError(ConverterTestType.InnerTextDeprecated, element, "Using {0} element text is deprecated. Use the 'Launch' element instead.", xCondition.Name.LocalName)) | ||
568 | { | ||
569 | xCondition.AddAfterSelf(new XElement(LaunchElementName, | ||
570 | new XAttribute("Condition", text), | ||
571 | new XAttribute("Message", message) | ||
572 | )); | ||
573 | xCondition.Remove(); | ||
574 | } | ||
575 | } | ||
576 | } | ||
577 | |||
578 | private void ConvertPublishElement(XElement element) => this.ConvertInnerTextToAttribute(element, "Condition"); | ||
579 | |||
580 | private void ConvertMultiStringValueElement(XElement element) => this.ConvertInnerTextToAttribute(element, "Value"); | ||
581 | |||
582 | private void ConvertRequiredPrivilegeElement(XElement element) => this.ConvertInnerTextToAttribute(element, "Name"); | ||
583 | |||
584 | private void ConvertRowElement(XElement element) => this.ConvertInnerTextToAttribute(element, "Value"); | ||
585 | |||
586 | private void ConvertSequenceElement(XElement element) | ||
587 | { | ||
588 | foreach (var child in element.Elements()) | ||
589 | { | ||
590 | this.ConvertInnerTextToAttribute(child, "Condition"); | ||
591 | } | ||
592 | } | ||
593 | |||
594 | private void ConvertServiceArgumentElement(XElement element) => this.ConvertInnerTextToAttribute(element, "Value"); | ||
595 | |||
596 | private void ConvertSetDirectoryElement(XElement element) => this.ConvertInnerTextToAttribute(element, "Condition"); | ||
597 | |||
598 | private void ConvertSetPropertyElement(XElement element) => this.ConvertInnerTextToAttribute(element, "Condition"); | ||
599 | |||
600 | private void ConvertShortcutPropertyElement(XElement element) => this.ConvertInnerTextToAttribute(element, "Value"); | ||
601 | |||
602 | private void ConvertSuppressSignatureValidation(XElement element) | ||
603 | { | ||
604 | var suppressSignatureValidation = element.Attribute("SuppressSignatureValidation"); | ||
605 | |||
606 | if (null != suppressSignatureValidation) | ||
607 | { | ||
608 | if (this.OnError(ConverterTestType.SuppressSignatureValidationDeprecated, element, "The chain package element contains deprecated '{0}' attribute. Use the 'EnableSignatureValidation' attribute instead.", suppressSignatureValidation.Name)) | ||
609 | { | ||
610 | if ("no" == suppressSignatureValidation.Value) | ||
611 | { | ||
612 | element.Add(new XAttribute("EnableSignatureValidation", "yes")); | ||
613 | } | ||
614 | } | ||
615 | |||
616 | suppressSignatureValidation.Remove(); | ||
617 | } | ||
618 | } | ||
619 | |||
620 | private void ConvertTextElement(XElement element) => this.ConvertInnerTextToAttribute(element, "Value"); | ||
621 | |||
622 | private void ConvertUITextElement(XElement element) => this.ConvertInnerTextToAttribute(element, "Value"); | ||
623 | |||
624 | private void ConvertCustomActionElement(XElement xCustomAction) | ||
625 | { | ||
626 | var xBinaryKey = xCustomAction.Attribute("BinaryKey"); | ||
627 | |||
628 | if (xBinaryKey?.Value == "WixCA" || xBinaryKey?.Value == "UtilCA") | ||
629 | { | ||
630 | if (this.OnError(ConverterTestType.WixCABinaryIdRenamed, xCustomAction, "The WixCA custom action DLL Binary table id has been renamed. Use the id 'Wix4UtilCA_X86' instead.")) | ||
631 | { | ||
632 | xBinaryKey.Value = "Wix4UtilCA_X86"; | ||
633 | } | ||
634 | } | ||
635 | |||
636 | if (xBinaryKey?.Value == "WixCA_x64" || xBinaryKey?.Value == "UtilCA_x64") | ||
637 | { | ||
638 | if (this.OnError(ConverterTestType.WixCABinaryIdRenamed, xCustomAction, "The WixCA_x64 custom action DLL Binary table id has been renamed. Use the id 'Wix4UtilCA_X64' instead.")) | ||
639 | { | ||
640 | xBinaryKey.Value = "Wix4UtilCA_X64"; | ||
641 | } | ||
642 | } | ||
643 | |||
644 | var xDllEntry = xCustomAction.Attribute("DllEntry"); | ||
645 | |||
646 | if (xDllEntry?.Value == "CAQuietExec" || xDllEntry?.Value == "CAQuietExec64") | ||
647 | { | ||
648 | if (this.OnError(ConverterTestType.QuietExecCustomActionsRenamed, xCustomAction, "The CAQuietExec and CAQuietExec64 custom action ids have been renamed. Use the ids 'WixQuietExec' and 'WixQuietExec64' instead.")) | ||
649 | { | ||
650 | xDllEntry.Value = xDllEntry.Value.Replace("CAQuietExec", "WixQuietExec"); | ||
651 | } | ||
652 | } | ||
653 | |||
654 | var xProperty = xCustomAction.Attribute("Property"); | ||
655 | |||
656 | if (xProperty?.Value == "QtExecCmdLine" || xProperty?.Value == "QtExec64CmdLine") | ||
657 | { | ||
658 | if (this.OnError(ConverterTestType.QuietExecCustomActionsRenamed, xCustomAction, "The QtExecCmdLine and QtExec64CmdLine property ids have been renamed. Use the ids 'WixQuietExecCmdLine' and 'WixQuietExec64CmdLine' instead.")) | ||
659 | { | ||
660 | xProperty.Value = xProperty.Value.Replace("QtExec", "WixQuietExec"); | ||
661 | } | ||
662 | } | ||
663 | |||
664 | var xScript = xCustomAction.Attribute("Script"); | ||
665 | |||
666 | if (xScript != null && TryGetInnerText(xCustomAction, out var scriptText)) | ||
667 | { | ||
668 | if (this.OnError(ConverterTestType.InnerTextDeprecated, xCustomAction, "Using {0} element text is deprecated. Extract the text to a file and use the 'ScriptFile' attribute to reference it.", xCustomAction.Name.LocalName)) | ||
669 | { | ||
670 | var scriptFolder = Path.GetDirectoryName(this.SourceFile) ?? String.Empty; | ||
671 | var id = xCustomAction.Attribute("Id")?.Value ?? Guid.NewGuid().ToString("N"); | ||
672 | var ext = (xScript.Value == "jscript") ? ".js" : (xScript.Value == "vbscript") ? ".vbs" : ".txt"; | ||
673 | |||
674 | var scriptFile = Path.Combine(scriptFolder, id + ext); | ||
675 | File.WriteAllText(scriptFile, scriptText); | ||
676 | |||
677 | RemoveChildren(xCustomAction); | ||
678 | xCustomAction.Add(new XAttribute("ScriptFile", scriptFile)); | ||
679 | } | ||
680 | } | ||
681 | } | ||
682 | |||
683 | private void ConvertPropertyElement(XElement xProperty) | ||
684 | { | ||
685 | var xId = xProperty.Attribute("Id"); | ||
686 | |||
687 | if (xId.Value == "QtExecCmdTimeout") | ||
688 | { | ||
689 | this.OnError(ConverterTestType.QtExecCmdTimeoutAmbiguous, xProperty, "QtExecCmdTimeout was previously used for both CAQuietExec and CAQuietExec64. For WixQuietExec, use WixQuietExecCmdTimeout. For WixQuietExec64, use WixQuietExec64CmdTimeout."); | ||
690 | } | ||
691 | |||
692 | this.ConvertInnerTextToAttribute(xProperty, "Value"); | ||
693 | } | ||
694 | |||
695 | private void ConvertUtilPermissionExElement(XElement element) | ||
696 | { | ||
697 | if (this.SourceVersion < 4 && null == element.Attribute("Inheritable")) | ||
698 | { | ||
699 | var inheritable = element.Parent.Name == CreateFolderElementName; | ||
700 | if (!inheritable) | ||
701 | { | ||
702 | if (this.OnError(ConverterTestType.AssignPermissionExInheritable, element, "The PermissionEx Inheritable attribute is being set to 'no' to ensure it remains the same as the v3 default")) | ||
703 | { | ||
704 | element.Add(new XAttribute("Inheritable", "no")); | ||
705 | } | ||
706 | } | ||
707 | } | ||
708 | } | ||
709 | |||
710 | /// <summary> | ||
711 | /// Converts a Wix element. | ||
712 | /// </summary> | ||
713 | /// <param name="element">The Wix element to convert.</param> | ||
714 | /// <returns>The converted element.</returns> | ||
715 | private void ConvertElementWithoutNamespace(XElement element) | ||
716 | { | ||
717 | if (this.OnError(ConverterTestType.XmlnsMissing, element, "The xmlns attribute is missing. It must be present with a value of '{0}'.", WixNamespace.NamespaceName)) | ||
718 | { | ||
719 | element.Name = WixNamespace.GetName(element.Name.LocalName); | ||
720 | |||
721 | element.Add(new XAttribute("xmlns", WixNamespace.NamespaceName)); // set the default namespace. | ||
722 | |||
723 | foreach (var elementWithoutNamespace in element.DescendantsAndSelf().Where(e => XNamespace.None == e.Name.Namespace)) | ||
724 | { | ||
725 | elementWithoutNamespace.Name = WixNamespace.GetName(elementWithoutNamespace.Name.LocalName); | ||
726 | } | ||
727 | } | ||
728 | } | ||
729 | |||
730 | private void ConvertInnerTextToAttribute(XElement element, string attributeName) | ||
731 | { | ||
732 | if (TryGetInnerText(element, out var text) && | ||
733 | this.OnError(ConverterTestType.InnerTextDeprecated, element, "Using {0} element text is deprecated. Use the '{1}' attribute instead.", element.Name.LocalName, attributeName)) | ||
734 | { | ||
735 | element.Add(new XAttribute(attributeName, text)); | ||
736 | RemoveChildren(element); | ||
737 | } | ||
738 | } | ||
739 | |||
740 | private IEnumerable<ConverterTestType> YieldConverterTypes(IEnumerable<string> types) | ||
741 | { | ||
742 | if (null != types) | ||
743 | { | ||
744 | foreach (var type in types) | ||
745 | { | ||
746 | if (Enum.TryParse<ConverterTestType>(type, true, out var itt)) | ||
747 | { | ||
748 | yield return itt; | ||
749 | } | ||
750 | else // not a known ConverterTestType | ||
751 | { | ||
752 | this.OnError(ConverterTestType.ConverterTestTypeUnknown, null, "Unknown error type: '{0}'.", type); | ||
753 | } | ||
754 | } | ||
755 | } | ||
756 | } | ||
757 | |||
758 | private static void UpdateElementsWithDeprecatedNamespaces(IEnumerable<XElement> elements, Dictionary<XNamespace, XNamespace> deprecatedToUpdatedNamespaces) | ||
759 | { | ||
760 | foreach (var element in elements) | ||
761 | { | ||
762 | |||
763 | if (deprecatedToUpdatedNamespaces.TryGetValue(element.Name.Namespace, out var ns)) | ||
764 | { | ||
765 | element.Name = ns.GetName(element.Name.LocalName); | ||
766 | } | ||
767 | |||
768 | // Remove all the attributes and add them back to with their namespace updated (as necessary). | ||
769 | IEnumerable<XAttribute> attributes = element.Attributes().ToList(); | ||
770 | element.RemoveAttributes(); | ||
771 | |||
772 | foreach (var attribute in attributes) | ||
773 | { | ||
774 | var convertedAttribute = attribute; | ||
775 | |||
776 | if (attribute.IsNamespaceDeclaration) | ||
777 | { | ||
778 | if (deprecatedToUpdatedNamespaces.TryGetValue(attribute.Value, out ns)) | ||
779 | { | ||
780 | convertedAttribute = ("xmlns" == attribute.Name.LocalName) ? new XAttribute(attribute.Name.LocalName, ns.NamespaceName) : new XAttribute(XNamespace.Xmlns + attribute.Name.LocalName, ns.NamespaceName); | ||
781 | } | ||
782 | } | ||
783 | else if (deprecatedToUpdatedNamespaces.TryGetValue(attribute.Name.Namespace, out ns)) | ||
784 | { | ||
785 | convertedAttribute = new XAttribute(ns.GetName(attribute.Name.LocalName), attribute.Value); | ||
786 | } | ||
787 | |||
788 | element.Add(convertedAttribute); | ||
789 | } | ||
790 | } | ||
791 | } | ||
792 | |||
793 | /// <summary> | ||
794 | /// Determine if the whitespace preceding a node is appropriate for its depth level. | ||
795 | /// </summary> | ||
796 | /// <param name="indentationAmount">Indentation value to use when validating leading whitespace.</param> | ||
797 | /// <param name="level">The depth level that should match this whitespace.</param> | ||
798 | /// <param name="whitespace">The whitespace to validate.</param> | ||
799 | /// <returns>true if the whitespace is legal; false otherwise.</returns> | ||
800 | private static bool LeadingWhitespaceValid(int indentationAmount, int level, string whitespace) | ||
801 | { | ||
802 | // Strip off leading newlines; there can be an arbitrary number of these. | ||
803 | whitespace = whitespace.TrimStart(XDocumentNewLine); | ||
804 | |||
805 | var indentation = new string(' ', level * indentationAmount); | ||
806 | |||
807 | return whitespace == indentation; | ||
808 | } | ||
809 | |||
810 | /// <summary> | ||
811 | /// Fix the whitespace in a whitespace node. | ||
812 | /// </summary> | ||
813 | /// <param name="indentationAmount">Indentation value to use when validating leading whitespace.</param> | ||
814 | /// <param name="level">The depth level of the desired whitespace.</param> | ||
815 | /// <param name="whitespace">The whitespace node to fix.</param> | ||
816 | private static void FixupWhitespace(int indentationAmount, int level, XText whitespace) | ||
817 | { | ||
818 | var value = new StringBuilder(whitespace.Value.Length); | ||
819 | |||
820 | // Keep any previous preceeding new lines. | ||
821 | var newlines = whitespace.Value.TakeWhile(c => c == XDocumentNewLine).Count(); | ||
822 | |||
823 | // Ensure there is always at least one new line before the indentation. | ||
824 | value.Append(XDocumentNewLine, newlines == 0 ? 1 : newlines); | ||
825 | |||
826 | whitespace.Value = value.Append(' ', level * indentationAmount).ToString(); | ||
827 | } | ||
828 | |||
829 | /// <summary> | ||
830 | /// Output an error message to the console. | ||
831 | /// </summary> | ||
832 | /// <param name="converterTestType">The type of converter test.</param> | ||
833 | /// <param name="node">The node that caused the error.</param> | ||
834 | /// <param name="message">Detailed error message.</param> | ||
835 | /// <param name="args">Additional formatted string arguments.</param> | ||
836 | /// <returns>Returns true indicating that action should be taken on this error, and false if it should be ignored.</returns> | ||
837 | private bool OnError(ConverterTestType converterTestType, XObject node, string message, params object[] args) | ||
838 | { | ||
839 | if (this.IgnoreErrors.Contains(converterTestType)) // ignore the error | ||
840 | { | ||
841 | return false; | ||
842 | } | ||
843 | |||
844 | // Increase the error count. | ||
845 | this.Errors++; | ||
846 | |||
847 | var sourceLine = (null == node) ? new SourceLineNumber(this.SourceFile ?? "wixcop.exe") : new SourceLineNumber(this.SourceFile, ((IXmlLineInfo)node).LineNumber); | ||
848 | var warning = this.ErrorsAsWarnings.Contains(converterTestType); | ||
849 | var display = String.Format(CultureInfo.CurrentCulture, message, args); | ||
850 | |||
851 | var msg = new Message(sourceLine, warning ? MessageLevel.Warning : MessageLevel.Error, (int)converterTestType, "{0} ({1})", display, converterTestType.ToString()); | ||
852 | |||
853 | this.Messaging.Write(msg); | ||
854 | |||
855 | return true; | ||
856 | } | ||
857 | |||
858 | /// <summary> | ||
859 | /// Return an identifier based on passed file/directory name | ||
860 | /// </summary> | ||
861 | /// <param name="name">File/directory name to generate identifer from</param> | ||
862 | /// <returns>A version of the name that is a legal identifier.</returns> | ||
863 | /// <remarks>This is duplicated from WiX's Common class.</remarks> | ||
864 | private static string GetIdentifierFromName(string name) | ||
865 | { | ||
866 | var result = IllegalIdentifierCharacters.Replace(name, "_"); // replace illegal characters with "_". | ||
867 | |||
868 | // MSI identifiers must begin with an alphabetic character or an | ||
869 | // underscore. Prefix all other values with an underscore. | ||
870 | if (AddPrefix.IsMatch(name)) | ||
871 | { | ||
872 | result = String.Concat("_", result); | ||
873 | } | ||
874 | |||
875 | return result; | ||
876 | } | ||
877 | |||
878 | private static string LowercaseFirstChar(string value) | ||
879 | { | ||
880 | if (!String.IsNullOrEmpty(value)) | ||
881 | { | ||
882 | var c = Char.ToLowerInvariant(value[0]); | ||
883 | if (c != value[0]) | ||
884 | { | ||
885 | var remainder = value.Length > 1 ? value.Substring(1) : String.Empty; | ||
886 | return c + remainder; | ||
887 | } | ||
888 | } | ||
889 | |||
890 | return value; | ||
891 | } | ||
892 | |||
893 | private static string UppercaseFirstChar(string value) | ||
894 | { | ||
895 | if (!String.IsNullOrEmpty(value)) | ||
896 | { | ||
897 | var c = Char.ToUpperInvariant(value[0]); | ||
898 | if (c != value[0]) | ||
899 | { | ||
900 | var remainder = value.Length > 1 ? value.Substring(1) : String.Empty; | ||
901 | return c + remainder; | ||
902 | } | ||
903 | } | ||
904 | |||
905 | return value; | ||
906 | } | ||
907 | |||
908 | private static bool TryGetInnerText(XElement element, out string value) | ||
909 | { | ||
910 | value = null; | ||
911 | |||
912 | var nodes = element.Nodes(); | ||
913 | |||
914 | if (nodes.All(e => e.NodeType == XmlNodeType.Text || e.NodeType == XmlNodeType.CDATA)) | ||
915 | { | ||
916 | value = String.Join(String.Empty, nodes.Cast<XText>().Select(TrimTextValue)); | ||
917 | } | ||
918 | |||
919 | return !String.IsNullOrEmpty(value); | ||
920 | } | ||
921 | |||
922 | private static bool IsTextNode(XNode node, out XText text) | ||
923 | { | ||
924 | text = null; | ||
925 | |||
926 | if (node.NodeType == XmlNodeType.Text || node.NodeType == XmlNodeType.CDATA) | ||
927 | { | ||
928 | text = (XText)node; | ||
929 | } | ||
930 | |||
931 | return text != null; | ||
932 | } | ||
933 | |||
934 | private static void TrimLeadingText(XDocument document) | ||
935 | { | ||
936 | while (IsTextNode(document.Nodes().FirstOrDefault(), out var text)) | ||
937 | { | ||
938 | text.Remove(); | ||
939 | } | ||
940 | } | ||
941 | |||
942 | private static string TrimTextValue(XText text) | ||
943 | { | ||
944 | var value = text.Value; | ||
945 | |||
946 | if (String.IsNullOrEmpty(value)) | ||
947 | { | ||
948 | return String.Empty; | ||
949 | } | ||
950 | else if (text.NodeType == XmlNodeType.CDATA && String.IsNullOrWhiteSpace(value)) | ||
951 | { | ||
952 | return " "; | ||
953 | } | ||
954 | |||
955 | return value.Trim(); | ||
956 | } | ||
957 | |||
958 | private static void RemoveChildren(XElement element) | ||
959 | { | ||
960 | var nodes = element.Nodes().ToList(); | ||
961 | foreach (var node in nodes) | ||
962 | { | ||
963 | node.Remove(); | ||
964 | } | ||
965 | } | ||
966 | |||
967 | /// <summary> | ||
968 | /// Converter test types. These are used to condition error messages down to warnings. | ||
969 | /// </summary> | ||
970 | private enum ConverterTestType | ||
971 | { | ||
972 | /// <summary> | ||
973 | /// Internal-only: displayed when a string cannot be converted to an ConverterTestType. | ||
974 | /// </summary> | ||
975 | ConverterTestTypeUnknown, | ||
976 | |||
977 | /// <summary> | ||
978 | /// Displayed when an XML loading exception has occurred. | ||
979 | /// </summary> | ||
980 | XmlException, | ||
981 | |||
982 | /// <summary> | ||
983 | /// Displayed when a file cannot be accessed; typically when trying to save back a fixed file. | ||
984 | /// </summary> | ||
985 | UnauthorizedAccessException, | ||
986 | |||
987 | /// <summary> | ||
988 | /// Displayed when the encoding attribute in the XML declaration is not 'UTF-8'. | ||
989 | /// </summary> | ||
990 | DeclarationEncodingWrong, | ||
991 | |||
992 | /// <summary> | ||
993 | /// Displayed when the XML declaration is missing from the source file. | ||
994 | /// </summary> | ||
995 | DeclarationMissing, | ||
996 | |||
997 | /// <summary> | ||
998 | /// Displayed when the whitespace preceding a CDATA node is wrong. | ||
999 | /// </summary> | ||
1000 | WhitespacePrecedingCDATAWrong, | ||
1001 | |||
1002 | /// <summary> | ||
1003 | /// Displayed when the whitespace preceding a node is wrong. | ||
1004 | /// </summary> | ||
1005 | WhitespacePrecedingNodeWrong, | ||
1006 | |||
1007 | /// <summary> | ||
1008 | /// Displayed when an element is not empty as it should be. | ||
1009 | /// </summary> | ||
1010 | NotEmptyElement, | ||
1011 | |||
1012 | /// <summary> | ||
1013 | /// Displayed when the whitespace following a CDATA node is wrong. | ||
1014 | /// </summary> | ||
1015 | WhitespaceFollowingCDATAWrong, | ||
1016 | |||
1017 | /// <summary> | ||
1018 | /// Displayed when the whitespace preceding an end element is wrong. | ||
1019 | /// </summary> | ||
1020 | WhitespacePrecedingEndElementWrong, | ||
1021 | |||
1022 | /// <summary> | ||
1023 | /// Displayed when the xmlns attribute is missing from the document element. | ||
1024 | /// </summary> | ||
1025 | XmlnsMissing, | ||
1026 | |||
1027 | /// <summary> | ||
1028 | /// Displayed when the xmlns attribute on the document element is wrong. | ||
1029 | /// </summary> | ||
1030 | XmlnsValueWrong, | ||
1031 | |||
1032 | /// <summary> | ||
1033 | /// Assign an identifier to a File element when on Id attribute is specified. | ||
1034 | /// </summary> | ||
1035 | AssignAnonymousFileId, | ||
1036 | |||
1037 | /// <summary> | ||
1038 | /// SuppressSignatureValidation attribute is deprecated and replaced with EnableSignatureValidation. | ||
1039 | /// </summary> | ||
1040 | SuppressSignatureValidationDeprecated, | ||
1041 | |||
1042 | /// <summary> | ||
1043 | /// WixCA Binary/@Id has been renamed to UtilCA. | ||
1044 | /// </summary> | ||
1045 | WixCABinaryIdRenamed, | ||
1046 | |||
1047 | /// <summary> | ||
1048 | /// QtExec custom actions have been renamed. | ||
1049 | /// </summary> | ||
1050 | QuietExecCustomActionsRenamed, | ||
1051 | |||
1052 | /// <summary> | ||
1053 | /// QtExecCmdTimeout was previously used for both CAQuietExec and CAQuietExec64. For WixQuietExec, use WixQuietExecCmdTimeout. For WixQuietExec64, use WixQuietExec64CmdTimeout. | ||
1054 | /// </summary> | ||
1055 | QtExecCmdTimeoutAmbiguous, | ||
1056 | |||
1057 | /// <summary> | ||
1058 | /// Directory/@ShortName may only be specified with Directory/@Name. | ||
1059 | /// </summary> | ||
1060 | AssignDirectoryNameFromShortName, | ||
1061 | |||
1062 | /// <summary> | ||
1063 | /// BootstrapperApplicationData attribute is deprecated and replaced with Unreal. | ||
1064 | /// </summary> | ||
1065 | BootstrapperApplicationDataDeprecated, | ||
1066 | |||
1067 | /// <summary> | ||
1068 | /// Inheritable is new and is now defaulted to 'yes' which is a change in behavior for all but children of CreateFolder. | ||
1069 | /// </summary> | ||
1070 | AssignPermissionExInheritable, | ||
1071 | |||
1072 | /// <summary> | ||
1073 | /// Column element's Category attribute is camel-case. | ||
1074 | /// </summary> | ||
1075 | ColumnCategoryCamelCase, | ||
1076 | |||
1077 | /// <summary> | ||
1078 | /// Column element's Modularize attribute is camel-case. | ||
1079 | /// </summary> | ||
1080 | ColumnModularizeCamelCase, | ||
1081 | |||
1082 | /// <summary> | ||
1083 | /// Inner text value should move to an attribute. | ||
1084 | /// </summary> | ||
1085 | InnerTextDeprecated, | ||
1086 | |||
1087 | /// <summary> | ||
1088 | /// Explicit auto-GUID unnecessary. | ||
1089 | /// </summary> | ||
1090 | AutoGuidUnnecessary, | ||
1091 | |||
1092 | /// <summary> | ||
1093 | /// Displayed when the XML declaration is present in the source file. | ||
1094 | /// </summary> | ||
1095 | DeclarationPresent, | ||
1096 | } | ||
1097 | } | ||
1098 | } | ||