diff options
Diffstat (limited to 'src/WixToolset.Converters/Wix3Converter.cs')
-rw-r--r-- | src/WixToolset.Converters/Wix3Converter.cs | 652 |
1 files changed, 652 insertions, 0 deletions
diff --git a/src/WixToolset.Converters/Wix3Converter.cs b/src/WixToolset.Converters/Wix3Converter.cs new file mode 100644 index 00000000..c23930b6 --- /dev/null +++ b/src/WixToolset.Converters/Wix3Converter.cs | |||
@@ -0,0 +1,652 @@ | |||
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 Wix3Converter | ||
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 | |||
28 | private static readonly XName DirectoryElementName = WixNamespace + "Directory"; | ||
29 | private static readonly XName FileElementName = WixNamespace + "File"; | ||
30 | private static readonly XName ExePackageElementName = WixNamespace + "ExePackage"; | ||
31 | private static readonly XName MsiPackageElementName = WixNamespace + "MsiPackage"; | ||
32 | private static readonly XName MspPackageElementName = WixNamespace + "MspPackage"; | ||
33 | private static readonly XName MsuPackageElementName = WixNamespace + "MsuPackage"; | ||
34 | private static readonly XName PayloadElementName = WixNamespace + "Payload"; | ||
35 | private static readonly XName CustomActionElementName = WixNamespace + "CustomAction"; | ||
36 | private static readonly XName PropertyElementName = WixNamespace + "Property"; | ||
37 | private static readonly XName WixElementWithoutNamespaceName = XNamespace.None + "Wix"; | ||
38 | |||
39 | private static readonly Dictionary<string, XNamespace> OldToNewNamespaceMapping = new Dictionary<string, XNamespace>() | ||
40 | { | ||
41 | { "http://schemas.microsoft.com/wix/BalExtension", "http://wixtoolset.org/schemas/v4/wxs/bal" }, | ||
42 | { "http://schemas.microsoft.com/wix/ComPlusExtension", "http://wixtoolset.org/schemas/v4/wxs/complus" }, | ||
43 | { "http://schemas.microsoft.com/wix/DependencyExtension", "http://wixtoolset.org/schemas/v4/wxs/dependency" }, | ||
44 | { "http://schemas.microsoft.com/wix/DifxAppExtension", "http://wixtoolset.org/schemas/v4/wxs/difxapp" }, | ||
45 | { "http://schemas.microsoft.com/wix/FirewallExtension", "http://wixtoolset.org/schemas/v4/wxs/firewall" }, | ||
46 | { "http://schemas.microsoft.com/wix/GamingExtension", "http://wixtoolset.org/schemas/v4/wxs/gaming" }, | ||
47 | { "http://schemas.microsoft.com/wix/IIsExtension", "http://wixtoolset.org/schemas/v4/wxs/iis" }, | ||
48 | { "http://schemas.microsoft.com/wix/MsmqExtension", "http://wixtoolset.org/schemas/v4/wxs/msmq" }, | ||
49 | { "http://schemas.microsoft.com/wix/NetFxExtension", "http://wixtoolset.org/schemas/v4/wxs/netfx" }, | ||
50 | { "http://schemas.microsoft.com/wix/PSExtension", "http://wixtoolset.org/schemas/v4/wxs/powershell" }, | ||
51 | { "http://schemas.microsoft.com/wix/SqlExtension", "http://wixtoolset.org/schemas/v4/wxs/sql" }, | ||
52 | { "http://schemas.microsoft.com/wix/TagExtension", "http://wixtoolset.org/schemas/v4/wxs/tag" }, | ||
53 | { "http://schemas.microsoft.com/wix/UtilExtension", "http://wixtoolset.org/schemas/v4/wxs/util" }, | ||
54 | { "http://schemas.microsoft.com/wix/VSExtension", "http://wixtoolset.org/schemas/v4/wxs/vs" }, | ||
55 | { "http://wixtoolset.org/schemas/thmutil/2010", "http://wixtoolset.org/schemas/v4/thmutil" }, | ||
56 | { "http://schemas.microsoft.com/wix/2009/Lux", "http://wixtoolset.org/schemas/v4/lux" }, | ||
57 | { "http://schemas.microsoft.com/wix/2006/wi", "http://wixtoolset.org/schemas/v4/wxs" }, | ||
58 | { "http://schemas.microsoft.com/wix/2006/localization", "http://wixtoolset.org/schemas/v4/wxl" }, | ||
59 | { "http://schemas.microsoft.com/wix/2006/libraries", "http://wixtoolset.org/schemas/v4/wixlib" }, | ||
60 | { "http://schemas.microsoft.com/wix/2006/objects", "http://wixtoolset.org/schemas/v4/wixobj" }, | ||
61 | { "http://schemas.microsoft.com/wix/2006/outputs", "http://wixtoolset.org/schemas/v4/wixout" }, | ||
62 | { "http://schemas.microsoft.com/wix/2007/pdbs", "http://wixtoolset.org/schemas/v4/wixpdb" }, | ||
63 | { "http://schemas.microsoft.com/wix/2003/04/actions", "http://wixtoolset.org/schemas/v4/wi/actions" }, | ||
64 | { "http://schemas.microsoft.com/wix/2006/tables", "http://wixtoolset.org/schemas/v4/wi/tables" }, | ||
65 | { "http://schemas.microsoft.com/wix/2006/WixUnit", "http://wixtoolset.org/schemas/v4/wixunit" }, | ||
66 | }; | ||
67 | |||
68 | private readonly Dictionary<XName, Action<XElement>> ConvertElementMapping; | ||
69 | |||
70 | /// <summary> | ||
71 | /// Instantiate a new Converter class. | ||
72 | /// </summary> | ||
73 | /// <param name="indentationAmount">Indentation value to use when validating leading whitespace.</param> | ||
74 | /// <param name="errorsAsWarnings">Test errors to display as warnings.</param> | ||
75 | /// <param name="ignoreErrors">Test errors to ignore.</param> | ||
76 | public Wix3Converter(IMessaging messaging, int indentationAmount, IEnumerable<string> errorsAsWarnings = null, IEnumerable<string> ignoreErrors = null) | ||
77 | { | ||
78 | this.ConvertElementMapping = new Dictionary<XName, Action<XElement>> | ||
79 | { | ||
80 | { Wix3Converter.DirectoryElementName, this.ConvertDirectoryElement }, | ||
81 | { Wix3Converter.FileElementName, this.ConvertFileElement }, | ||
82 | { Wix3Converter.ExePackageElementName, this.ConvertSuppressSignatureValidation }, | ||
83 | { Wix3Converter.MsiPackageElementName, this.ConvertSuppressSignatureValidation }, | ||
84 | { Wix3Converter.MspPackageElementName, this.ConvertSuppressSignatureValidation }, | ||
85 | { Wix3Converter.MsuPackageElementName, this.ConvertSuppressSignatureValidation }, | ||
86 | { Wix3Converter.PayloadElementName, this.ConvertSuppressSignatureValidation }, | ||
87 | { Wix3Converter.CustomActionElementName, this.ConvertCustomActionElement }, | ||
88 | { Wix3Converter.PropertyElementName, this.ConvertPropertyElement }, | ||
89 | { Wix3Converter.WixElementWithoutNamespaceName, this.ConvertWixElementWithoutNamespace }, | ||
90 | }; | ||
91 | |||
92 | this.Messaging = messaging; | ||
93 | |||
94 | this.IndentationAmount = indentationAmount; | ||
95 | |||
96 | this.ErrorsAsWarnings = new HashSet<ConverterTestType>(this.YieldConverterTypes(errorsAsWarnings)); | ||
97 | |||
98 | this.IgnoreErrors = new HashSet<ConverterTestType>(this.YieldConverterTypes(ignoreErrors)); | ||
99 | } | ||
100 | |||
101 | private int Errors { get; set; } | ||
102 | |||
103 | private HashSet<ConverterTestType> ErrorsAsWarnings { get; set; } | ||
104 | |||
105 | private HashSet<ConverterTestType> IgnoreErrors { get; set; } | ||
106 | |||
107 | private IMessaging Messaging { get; } | ||
108 | |||
109 | private int IndentationAmount { get; set; } | ||
110 | |||
111 | private string SourceFile { get; set; } | ||
112 | |||
113 | /// <summary> | ||
114 | /// Convert a file. | ||
115 | /// </summary> | ||
116 | /// <param name="sourceFile">The file to convert.</param> | ||
117 | /// <param name="saveConvertedFile">Option to save the converted errors that are found.</param> | ||
118 | /// <returns>The number of errors found.</returns> | ||
119 | public int ConvertFile(string sourceFile, bool saveConvertedFile) | ||
120 | { | ||
121 | XDocument document; | ||
122 | |||
123 | // Set the instance info. | ||
124 | this.Errors = 0; | ||
125 | this.SourceFile = sourceFile; | ||
126 | |||
127 | try | ||
128 | { | ||
129 | document = XDocument.Load(this.SourceFile, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo); | ||
130 | } | ||
131 | catch (XmlException e) | ||
132 | { | ||
133 | this.OnError(ConverterTestType.XmlException, null, "The xml is invalid. Detail: '{0}'", e.Message); | ||
134 | |||
135 | return this.Errors; | ||
136 | } | ||
137 | |||
138 | this.ConvertDocument(document); | ||
139 | |||
140 | // Fix errors if requested and necessary. | ||
141 | if (saveConvertedFile && 0 < this.Errors) | ||
142 | { | ||
143 | try | ||
144 | { | ||
145 | using (var writer = File.CreateText(this.SourceFile)) | ||
146 | { | ||
147 | document.Save(writer, SaveOptions.DisableFormatting | SaveOptions.OmitDuplicateNamespaces); | ||
148 | } | ||
149 | } | ||
150 | catch (UnauthorizedAccessException) | ||
151 | { | ||
152 | this.OnError(ConverterTestType.UnauthorizedAccessException, null, "Could not write to file."); | ||
153 | } | ||
154 | } | ||
155 | |||
156 | return this.Errors; | ||
157 | } | ||
158 | |||
159 | /// <summary> | ||
160 | /// Convert a document. | ||
161 | /// </summary> | ||
162 | /// <param name="document">The document to convert.</param> | ||
163 | /// <returns>The number of errors found.</returns> | ||
164 | public int ConvertDocument(XDocument document) | ||
165 | { | ||
166 | var declaration = document.Declaration; | ||
167 | |||
168 | // Convert the declaration. | ||
169 | if (null != declaration) | ||
170 | { | ||
171 | if (!String.Equals("utf-8", declaration.Encoding, StringComparison.OrdinalIgnoreCase)) | ||
172 | { | ||
173 | if (this.OnError(ConverterTestType.DeclarationEncodingWrong, document.Root, "The XML declaration encoding is not properly set to 'utf-8'.")) | ||
174 | { | ||
175 | declaration.Encoding = "utf-8"; | ||
176 | } | ||
177 | } | ||
178 | } | ||
179 | else // missing declaration | ||
180 | { | ||
181 | if (this.OnError(ConverterTestType.DeclarationMissing, null, "This file is missing an XML declaration on the first line.")) | ||
182 | { | ||
183 | document.Declaration = new XDeclaration("1.0", "utf-8", null); | ||
184 | document.Root.AddBeforeSelf(new XText(XDocumentNewLine.ToString())); | ||
185 | } | ||
186 | } | ||
187 | |||
188 | // Start converting the nodes at the top. | ||
189 | this.ConvertNodes(document.Nodes(), 0); | ||
190 | |||
191 | return this.Errors; | ||
192 | } | ||
193 | |||
194 | private void ConvertNodes(IEnumerable<XNode> nodes, int level) | ||
195 | { | ||
196 | // Note we operate on a copy of the node list since we may | ||
197 | // remove some whitespace nodes during this processing. | ||
198 | foreach (var node in nodes.ToList()) | ||
199 | { | ||
200 | if (node is XText text) | ||
201 | { | ||
202 | if (!String.IsNullOrWhiteSpace(text.Value)) | ||
203 | { | ||
204 | text.Value = text.Value.Trim(); | ||
205 | } | ||
206 | else if (node.NextNode is XCData cdata) | ||
207 | { | ||
208 | this.EnsurePrecedingWhitespaceRemoved(text, node, ConverterTestType.WhitespacePrecedingNodeWrong); | ||
209 | } | ||
210 | else if (node.NextNode is XElement element) | ||
211 | { | ||
212 | this.EnsurePrecedingWhitespaceCorrect(text, node, level, ConverterTestType.WhitespacePrecedingNodeWrong); | ||
213 | } | ||
214 | else if (node.NextNode is null) // this is the space before the close element | ||
215 | { | ||
216 | if (node.PreviousNode is null || node.PreviousNode is XCData) | ||
217 | { | ||
218 | this.EnsurePrecedingWhitespaceRemoved(text, node.Parent, ConverterTestType.WhitespacePrecedingEndElementWrong); | ||
219 | } | ||
220 | else if (level == 0) // root element's close tag | ||
221 | { | ||
222 | this.EnsurePrecedingWhitespaceCorrect(text, node, 0, ConverterTestType.WhitespacePrecedingEndElementWrong); | ||
223 | } | ||
224 | else | ||
225 | { | ||
226 | this.EnsurePrecedingWhitespaceCorrect(text, node, level - 1, ConverterTestType.WhitespacePrecedingEndElementWrong); | ||
227 | } | ||
228 | } | ||
229 | } | ||
230 | else if (node is XElement element) | ||
231 | { | ||
232 | this.ConvertElement(element); | ||
233 | |||
234 | this.ConvertNodes(element.Nodes(), level + 1); | ||
235 | } | ||
236 | } | ||
237 | } | ||
238 | |||
239 | private void EnsurePrecedingWhitespaceCorrect(XText whitespace, XNode node, int level, ConverterTestType testType) | ||
240 | { | ||
241 | if (!Wix3Converter.LeadingWhitespaceValid(this.IndentationAmount, level, whitespace.Value)) | ||
242 | { | ||
243 | var message = testType == ConverterTestType.WhitespacePrecedingEndElementWrong ? "The whitespace preceding this end element is incorrect." : "The whitespace preceding this node is incorrect."; | ||
244 | |||
245 | if (this.OnError(testType, node, message)) | ||
246 | { | ||
247 | Wix3Converter.FixupWhitespace(this.IndentationAmount, level, whitespace); | ||
248 | } | ||
249 | } | ||
250 | } | ||
251 | |||
252 | private void EnsurePrecedingWhitespaceRemoved(XText whitespace, XNode node, ConverterTestType testType) | ||
253 | { | ||
254 | if (!String.IsNullOrEmpty(whitespace.Value)) | ||
255 | { | ||
256 | var message = testType == ConverterTestType.WhitespacePrecedingEndElementWrong ? "The whitespace preceding this end element is incorrect." : "The whitespace preceding this node is incorrect."; | ||
257 | |||
258 | if (this.OnError(testType, node, message)) | ||
259 | { | ||
260 | whitespace.Remove(); | ||
261 | } | ||
262 | } | ||
263 | } | ||
264 | |||
265 | private void ConvertElement(XElement element) | ||
266 | { | ||
267 | // Gather any deprecated namespaces, then update this element tree based on those deprecations. | ||
268 | var deprecatedToUpdatedNamespaces = new Dictionary<XNamespace, XNamespace>(); | ||
269 | |||
270 | foreach (var declaration in element.Attributes().Where(a => a.IsNamespaceDeclaration)) | ||
271 | { | ||
272 | if (Wix3Converter.OldToNewNamespaceMapping.TryGetValue(declaration.Value, out var ns)) | ||
273 | { | ||
274 | if (this.OnError(ConverterTestType.XmlnsValueWrong, declaration, "The namespace '{0}' is out of date. It must be '{1}'.", declaration.Value, ns.NamespaceName)) | ||
275 | { | ||
276 | deprecatedToUpdatedNamespaces.Add(declaration.Value, ns); | ||
277 | } | ||
278 | } | ||
279 | } | ||
280 | |||
281 | if (deprecatedToUpdatedNamespaces.Any()) | ||
282 | { | ||
283 | Wix3Converter.UpdateElementsWithDeprecatedNamespaces(element.DescendantsAndSelf(), deprecatedToUpdatedNamespaces); | ||
284 | } | ||
285 | |||
286 | // Apply any specialized conversion actions. | ||
287 | if (this.ConvertElementMapping.TryGetValue(element.Name, out var convert)) | ||
288 | { | ||
289 | convert(element); | ||
290 | } | ||
291 | } | ||
292 | |||
293 | private void ConvertDirectoryElement(XElement element) | ||
294 | { | ||
295 | if (null == element.Attribute("Name")) | ||
296 | { | ||
297 | var attribute = element.Attribute("ShortName"); | ||
298 | if (null != attribute) | ||
299 | { | ||
300 | var shortName = attribute.Value; | ||
301 | if (this.OnError(ConverterTestType.AssignDirectoryNameFromShortName, element, "The directory ShortName attribute is being renamed to Name since Name wasn't specified for value '{0}'", shortName)) | ||
302 | { | ||
303 | element.Add(new XAttribute("Name", shortName)); | ||
304 | attribute.Remove(); | ||
305 | } | ||
306 | } | ||
307 | } | ||
308 | } | ||
309 | |||
310 | private void ConvertFileElement(XElement element) | ||
311 | { | ||
312 | if (null == element.Attribute("Id")) | ||
313 | { | ||
314 | var attribute = element.Attribute("Name"); | ||
315 | |||
316 | if (null == attribute) | ||
317 | { | ||
318 | attribute = element.Attribute("Source"); | ||
319 | } | ||
320 | |||
321 | if (null != attribute) | ||
322 | { | ||
323 | var name = Path.GetFileName(attribute.Value); | ||
324 | |||
325 | if (this.OnError(ConverterTestType.AssignAnonymousFileId, element, "The file id is being updated to '{0}' to ensure it remains the same as the default", name)) | ||
326 | { | ||
327 | IEnumerable<XAttribute> attributes = element.Attributes().ToList(); | ||
328 | element.RemoveAttributes(); | ||
329 | element.Add(new XAttribute("Id", GetIdentifierFromName(name))); | ||
330 | element.Add(attributes); | ||
331 | } | ||
332 | } | ||
333 | } | ||
334 | } | ||
335 | |||
336 | private void ConvertSuppressSignatureValidation(XElement element) | ||
337 | { | ||
338 | var suppressSignatureValidation = element.Attribute("SuppressSignatureValidation"); | ||
339 | |||
340 | if (null != suppressSignatureValidation) | ||
341 | { | ||
342 | if (this.OnError(ConverterTestType.SuppressSignatureValidationDeprecated, element, "The chain package element contains deprecated '{0}' attribute. Use the 'EnableSignatureValidation' attribute instead.", suppressSignatureValidation)) | ||
343 | { | ||
344 | if ("no" == suppressSignatureValidation.Value) | ||
345 | { | ||
346 | element.Add(new XAttribute("EnableSignatureValidation", "yes")); | ||
347 | } | ||
348 | } | ||
349 | |||
350 | suppressSignatureValidation.Remove(); | ||
351 | } | ||
352 | } | ||
353 | |||
354 | private void ConvertCustomActionElement(XElement xCustomAction) | ||
355 | { | ||
356 | var xBinaryKey = xCustomAction.Attribute("BinaryKey"); | ||
357 | |||
358 | if (xBinaryKey?.Value == "WixCA") | ||
359 | { | ||
360 | if (this.OnError(ConverterTestType.WixCABinaryIdRenamed, xCustomAction, "The WixCA custom action DLL Binary table id has been renamed. Use the id 'UtilCA' instead.")) | ||
361 | { | ||
362 | xBinaryKey.Value = "UtilCA"; | ||
363 | } | ||
364 | } | ||
365 | |||
366 | var xDllEntry = xCustomAction.Attribute("DllEntry"); | ||
367 | |||
368 | if (xDllEntry?.Value == "CAQuietExec" || xDllEntry?.Value == "CAQuietExec64") | ||
369 | { | ||
370 | if (this.OnError(ConverterTestType.QuietExecCustomActionsRenamed, xCustomAction, "The CAQuietExec and CAQuietExec64 custom action ids have been renamed. Use the ids 'WixQuietExec' and 'WixQuietExec64' instead.")) | ||
371 | { | ||
372 | xDllEntry.Value = xDllEntry.Value.Replace("CAQuietExec", "WixQuietExec"); | ||
373 | } | ||
374 | } | ||
375 | |||
376 | var xProperty = xCustomAction.Attribute("Property"); | ||
377 | |||
378 | if (xProperty?.Value == "QtExecCmdLine" || xProperty?.Value == "QtExec64CmdLine") | ||
379 | { | ||
380 | if (this.OnError(ConverterTestType.QuietExecCustomActionsRenamed, xCustomAction, "The QtExecCmdLine and QtExec64CmdLine property ids have been renamed. Use the ids 'WixQuietExecCmdLine' and 'WixQuietExec64CmdLine' instead.")) | ||
381 | { | ||
382 | xProperty.Value = xProperty.Value.Replace("QtExec", "WixQuietExec"); | ||
383 | } | ||
384 | } | ||
385 | } | ||
386 | |||
387 | private void ConvertPropertyElement(XElement xProperty) | ||
388 | { | ||
389 | var xId = xProperty.Attribute("Id"); | ||
390 | |||
391 | if (xId.Value == "QtExecCmdTimeout") | ||
392 | { | ||
393 | this.OnError(ConverterTestType.QtExecCmdTimeoutAmbiguous, xProperty, "QtExecCmdTimeout was previously used for both CAQuietExec and CAQuietExec64. For WixQuietExec, use WixQuietExecCmdTimeout. For WixQuietExec64, use WixQuietExec64CmdTimeout."); | ||
394 | } | ||
395 | } | ||
396 | |||
397 | /// <summary> | ||
398 | /// Converts a Wix element. | ||
399 | /// </summary> | ||
400 | /// <param name="element">The Wix element to convert.</param> | ||
401 | /// <returns>The converted element.</returns> | ||
402 | private void ConvertWixElementWithoutNamespace(XElement element) | ||
403 | { | ||
404 | if (this.OnError(ConverterTestType.XmlnsMissing, element, "The xmlns attribute is missing. It must be present with a value of '{0}'.", WixNamespace.NamespaceName)) | ||
405 | { | ||
406 | element.Name = WixNamespace.GetName(element.Name.LocalName); | ||
407 | |||
408 | element.Add(new XAttribute("xmlns", WixNamespace.NamespaceName)); // set the default namespace. | ||
409 | |||
410 | foreach (var elementWithoutNamespace in element.Elements().Where(e => XNamespace.None == e.Name.Namespace)) | ||
411 | { | ||
412 | elementWithoutNamespace.Name = WixNamespace.GetName(elementWithoutNamespace.Name.LocalName); | ||
413 | } | ||
414 | } | ||
415 | } | ||
416 | |||
417 | private IEnumerable<ConverterTestType> YieldConverterTypes(IEnumerable<string> types) | ||
418 | { | ||
419 | if (null != types) | ||
420 | { | ||
421 | foreach (var type in types) | ||
422 | { | ||
423 | |||
424 | if (Enum.TryParse<ConverterTestType>(type, true, out var itt)) | ||
425 | { | ||
426 | yield return itt; | ||
427 | } | ||
428 | else // not a known ConverterTestType | ||
429 | { | ||
430 | this.OnError(ConverterTestType.ConverterTestTypeUnknown, null, "Unknown error type: '{0}'.", type); | ||
431 | } | ||
432 | } | ||
433 | } | ||
434 | } | ||
435 | |||
436 | private static void UpdateElementsWithDeprecatedNamespaces(IEnumerable<XElement> elements, Dictionary<XNamespace, XNamespace> deprecatedToUpdatedNamespaces) | ||
437 | { | ||
438 | foreach (var element in elements) | ||
439 | { | ||
440 | |||
441 | if (deprecatedToUpdatedNamespaces.TryGetValue(element.Name.Namespace, out var ns)) | ||
442 | { | ||
443 | element.Name = ns.GetName(element.Name.LocalName); | ||
444 | } | ||
445 | |||
446 | // Remove all the attributes and add them back to with their namespace updated (as necessary). | ||
447 | IEnumerable<XAttribute> attributes = element.Attributes().ToList(); | ||
448 | element.RemoveAttributes(); | ||
449 | |||
450 | foreach (var attribute in attributes) | ||
451 | { | ||
452 | var convertedAttribute = attribute; | ||
453 | |||
454 | if (attribute.IsNamespaceDeclaration) | ||
455 | { | ||
456 | if (deprecatedToUpdatedNamespaces.TryGetValue(attribute.Value, out ns)) | ||
457 | { | ||
458 | convertedAttribute = ("xmlns" == attribute.Name.LocalName) ? new XAttribute(attribute.Name.LocalName, ns.NamespaceName) : new XAttribute(XNamespace.Xmlns + attribute.Name.LocalName, ns.NamespaceName); | ||
459 | } | ||
460 | } | ||
461 | else if (deprecatedToUpdatedNamespaces.TryGetValue(attribute.Name.Namespace, out ns)) | ||
462 | { | ||
463 | convertedAttribute = new XAttribute(ns.GetName(attribute.Name.LocalName), attribute.Value); | ||
464 | } | ||
465 | |||
466 | element.Add(convertedAttribute); | ||
467 | } | ||
468 | } | ||
469 | } | ||
470 | |||
471 | /// <summary> | ||
472 | /// Determine if the whitespace preceding a node is appropriate for its depth level. | ||
473 | /// </summary> | ||
474 | /// <param name="indentationAmount">Indentation value to use when validating leading whitespace.</param> | ||
475 | /// <param name="level">The depth level that should match this whitespace.</param> | ||
476 | /// <param name="whitespace">The whitespace to validate.</param> | ||
477 | /// <returns>true if the whitespace is legal; false otherwise.</returns> | ||
478 | private static bool LeadingWhitespaceValid(int indentationAmount, int level, string whitespace) | ||
479 | { | ||
480 | // Strip off leading newlines; there can be an arbitrary number of these. | ||
481 | whitespace = whitespace.TrimStart(XDocumentNewLine); | ||
482 | |||
483 | var indentation = new string(' ', level * indentationAmount); | ||
484 | |||
485 | return whitespace == indentation; | ||
486 | } | ||
487 | |||
488 | /// <summary> | ||
489 | /// Fix the whitespace in a whitespace node. | ||
490 | /// </summary> | ||
491 | /// <param name="indentationAmount">Indentation value to use when validating leading whitespace.</param> | ||
492 | /// <param name="level">The depth level of the desired whitespace.</param> | ||
493 | /// <param name="whitespace">The whitespace node to fix.</param> | ||
494 | private static void FixupWhitespace(int indentationAmount, int level, XText whitespace) | ||
495 | { | ||
496 | var value = new StringBuilder(whitespace.Value.Length); | ||
497 | |||
498 | // Keep any previous preceeding new lines. | ||
499 | var newlines = whitespace.Value.TakeWhile(c => c == XDocumentNewLine).Count(); | ||
500 | |||
501 | // Ensure there is always at least one new line before the indentation. | ||
502 | value.Append(XDocumentNewLine, newlines == 0 ? 1 : newlines); | ||
503 | |||
504 | whitespace.Value = value.Append(' ', level * indentationAmount).ToString(); | ||
505 | } | ||
506 | |||
507 | /// <summary> | ||
508 | /// Output an error message to the console. | ||
509 | /// </summary> | ||
510 | /// <param name="converterTestType">The type of converter test.</param> | ||
511 | /// <param name="node">The node that caused the error.</param> | ||
512 | /// <param name="message">Detailed error message.</param> | ||
513 | /// <param name="args">Additional formatted string arguments.</param> | ||
514 | /// <returns>Returns true indicating that action should be taken on this error, and false if it should be ignored.</returns> | ||
515 | private bool OnError(ConverterTestType converterTestType, XObject node, string message, params object[] args) | ||
516 | { | ||
517 | if (this.IgnoreErrors.Contains(converterTestType)) // ignore the error | ||
518 | { | ||
519 | return false; | ||
520 | } | ||
521 | |||
522 | // Increase the error count. | ||
523 | this.Errors++; | ||
524 | |||
525 | var sourceLine = (null == node) ? new SourceLineNumber(this.SourceFile ?? "wixcop.exe") : new SourceLineNumber(this.SourceFile, ((IXmlLineInfo)node).LineNumber); | ||
526 | var warning = this.ErrorsAsWarnings.Contains(converterTestType); | ||
527 | var display = String.Format(CultureInfo.CurrentCulture, message, args); | ||
528 | |||
529 | var msg = new Message(sourceLine, warning ? MessageLevel.Warning : MessageLevel.Error, (int)converterTestType, "{0} ({1})", display, converterTestType.ToString()); | ||
530 | |||
531 | this.Messaging.Write(msg); | ||
532 | |||
533 | return true; | ||
534 | } | ||
535 | |||
536 | /// <summary> | ||
537 | /// Return an identifier based on passed file/directory name | ||
538 | /// </summary> | ||
539 | /// <param name="name">File/directory name to generate identifer from</param> | ||
540 | /// <returns>A version of the name that is a legal identifier.</returns> | ||
541 | /// <remarks>This is duplicated from WiX's Common class.</remarks> | ||
542 | private static string GetIdentifierFromName(string name) | ||
543 | { | ||
544 | string result = IllegalIdentifierCharacters.Replace(name, "_"); // replace illegal characters with "_". | ||
545 | |||
546 | // MSI identifiers must begin with an alphabetic character or an | ||
547 | // underscore. Prefix all other values with an underscore. | ||
548 | if (AddPrefix.IsMatch(name)) | ||
549 | { | ||
550 | result = String.Concat("_", result); | ||
551 | } | ||
552 | |||
553 | return result; | ||
554 | } | ||
555 | |||
556 | /// <summary> | ||
557 | /// Converter test types. These are used to condition error messages down to warnings. | ||
558 | /// </summary> | ||
559 | private enum ConverterTestType | ||
560 | { | ||
561 | /// <summary> | ||
562 | /// Internal-only: displayed when a string cannot be converted to an ConverterTestType. | ||
563 | /// </summary> | ||
564 | ConverterTestTypeUnknown, | ||
565 | |||
566 | /// <summary> | ||
567 | /// Displayed when an XML loading exception has occurred. | ||
568 | /// </summary> | ||
569 | XmlException, | ||
570 | |||
571 | /// <summary> | ||
572 | /// Displayed when a file cannot be accessed; typically when trying to save back a fixed file. | ||
573 | /// </summary> | ||
574 | UnauthorizedAccessException, | ||
575 | |||
576 | /// <summary> | ||
577 | /// Displayed when the encoding attribute in the XML declaration is not 'UTF-8'. | ||
578 | /// </summary> | ||
579 | DeclarationEncodingWrong, | ||
580 | |||
581 | /// <summary> | ||
582 | /// Displayed when the XML declaration is missing from the source file. | ||
583 | /// </summary> | ||
584 | DeclarationMissing, | ||
585 | |||
586 | /// <summary> | ||
587 | /// Displayed when the whitespace preceding a CDATA node is wrong. | ||
588 | /// </summary> | ||
589 | WhitespacePrecedingCDATAWrong, | ||
590 | |||
591 | /// <summary> | ||
592 | /// Displayed when the whitespace preceding a node is wrong. | ||
593 | /// </summary> | ||
594 | WhitespacePrecedingNodeWrong, | ||
595 | |||
596 | /// <summary> | ||
597 | /// Displayed when an element is not empty as it should be. | ||
598 | /// </summary> | ||
599 | NotEmptyElement, | ||
600 | |||
601 | /// <summary> | ||
602 | /// Displayed when the whitespace following a CDATA node is wrong. | ||
603 | /// </summary> | ||
604 | WhitespaceFollowingCDATAWrong, | ||
605 | |||
606 | /// <summary> | ||
607 | /// Displayed when the whitespace preceding an end element is wrong. | ||
608 | /// </summary> | ||
609 | WhitespacePrecedingEndElementWrong, | ||
610 | |||
611 | /// <summary> | ||
612 | /// Displayed when the xmlns attribute is missing from the document element. | ||
613 | /// </summary> | ||
614 | XmlnsMissing, | ||
615 | |||
616 | /// <summary> | ||
617 | /// Displayed when the xmlns attribute on the document element is wrong. | ||
618 | /// </summary> | ||
619 | XmlnsValueWrong, | ||
620 | |||
621 | /// <summary> | ||
622 | /// Assign an identifier to a File element when on Id attribute is specified. | ||
623 | /// </summary> | ||
624 | AssignAnonymousFileId, | ||
625 | |||
626 | /// <summary> | ||
627 | /// SuppressSignatureValidation attribute is deprecated and replaced with EnableSignatureValidation. | ||
628 | /// </summary> | ||
629 | SuppressSignatureValidationDeprecated, | ||
630 | |||
631 | /// <summary> | ||
632 | /// WixCA Binary/@Id has been renamed to UtilCA. | ||
633 | /// </summary> | ||
634 | WixCABinaryIdRenamed, | ||
635 | |||
636 | /// <summary> | ||
637 | /// QtExec custom actions have been renamed. | ||
638 | /// </summary> | ||
639 | QuietExecCustomActionsRenamed, | ||
640 | |||
641 | /// <summary> | ||
642 | /// QtExecCmdTimeout was previously used for both CAQuietExec and CAQuietExec64. For WixQuietExec, use WixQuietExecCmdTimeout. For WixQuietExec64, use WixQuietExec64CmdTimeout. | ||
643 | /// </summary> | ||
644 | QtExecCmdTimeoutAmbiguous, | ||
645 | |||
646 | /// <summary> | ||
647 | /// Directory/@ShortName may only be specified with Directory/@Name. | ||
648 | /// </summary> | ||
649 | AssignDirectoryNameFromShortName, | ||
650 | } | ||
651 | } | ||
652 | } | ||