// 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. namespace WixToolset.Core { using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Xml; using System.Xml.Linq; using WixToolset.Data; using WixToolset.Extensibility.Services; /// /// WiX source code converter. /// public class Converter { private const string XDocumentNewLine = "\n"; // XDocument normlizes "\r\n" to just "\n". private static readonly XNamespace WixNamespace = "http://wixtoolset.org/schemas/v4/wxs"; private static readonly XName FileElementName = WixNamespace + "File"; private static readonly XName ExePackageElementName = WixNamespace + "ExePackage"; private static readonly XName MsiPackageElementName = WixNamespace + "MsiPackage"; private static readonly XName MspPackageElementName = WixNamespace + "MspPackage"; private static readonly XName MsuPackageElementName = WixNamespace + "MsuPackage"; private static readonly XName PayloadElementName = WixNamespace + "Payload"; private static readonly XName WixElementWithoutNamespaceName = XNamespace.None + "Wix"; private static readonly Dictionary OldToNewNamespaceMapping = new Dictionary() { { "http://schemas.microsoft.com/wix/BalExtension", "http://wixtoolset.org/schemas/v4/wxs/bal" }, { "http://schemas.microsoft.com/wix/ComPlusExtension", "http://wixtoolset.org/schemas/v4/wxs/complus" }, { "http://schemas.microsoft.com/wix/DependencyExtension", "http://wixtoolset.org/schemas/v4/wxs/dependency" }, { "http://schemas.microsoft.com/wix/DifxAppExtension", "http://wixtoolset.org/schemas/v4/wxs/difxapp" }, { "http://schemas.microsoft.com/wix/FirewallExtension", "http://wixtoolset.org/schemas/v4/wxs/firewall" }, { "http://schemas.microsoft.com/wix/GamingExtension", "http://wixtoolset.org/schemas/v4/wxs/gaming" }, { "http://schemas.microsoft.com/wix/IIsExtension", "http://wixtoolset.org/schemas/v4/wxs/iis" }, { "http://schemas.microsoft.com/wix/MsmqExtension", "http://wixtoolset.org/schemas/v4/wxs/msmq" }, { "http://schemas.microsoft.com/wix/NetFxExtension", "http://wixtoolset.org/schemas/v4/wxs/netfx" }, { "http://schemas.microsoft.com/wix/PSExtension", "http://wixtoolset.org/schemas/v4/wxs/powershell" }, { "http://schemas.microsoft.com/wix/SqlExtension", "http://wixtoolset.org/schemas/v4/wxs/sql" }, { "http://schemas.microsoft.com/wix/TagExtension", "http://wixtoolset.org/schemas/v4/wxs/tag" }, { "http://schemas.microsoft.com/wix/UtilExtension", "http://wixtoolset.org/schemas/v4/wxs/util" }, { "http://schemas.microsoft.com/wix/VSExtension", "http://wixtoolset.org/schemas/v4/wxs/vs" }, { "http://wixtoolset.org/schemas/thmutil/2010", "http://wixtoolset.org/schemas/v4/thmutil" }, { "http://schemas.microsoft.com/wix/2009/Lux", "http://wixtoolset.org/schemas/v4/lux" }, { "http://schemas.microsoft.com/wix/2006/wi", "http://wixtoolset.org/schemas/v4/wxs" }, { "http://schemas.microsoft.com/wix/2006/localization", "http://wixtoolset.org/schemas/v4/wxl" }, { "http://schemas.microsoft.com/wix/2006/libraries", "http://wixtoolset.org/schemas/v4/wixlib" }, { "http://schemas.microsoft.com/wix/2006/objects", "http://wixtoolset.org/schemas/v4/wixobj" }, { "http://schemas.microsoft.com/wix/2006/outputs", "http://wixtoolset.org/schemas/v4/wixout" }, { "http://schemas.microsoft.com/wix/2007/pdbs", "http://wixtoolset.org/schemas/v4/wixpdb" }, { "http://schemas.microsoft.com/wix/2003/04/actions", "http://wixtoolset.org/schemas/v4/wi/actions" }, { "http://schemas.microsoft.com/wix/2006/tables", "http://wixtoolset.org/schemas/v4/wi/tables" }, { "http://schemas.microsoft.com/wix/2006/WixUnit", "http://wixtoolset.org/schemas/v4/wixunit" }, }; private Dictionary> ConvertElementMapping; /// /// Instantiate a new Converter class. /// /// Indentation value to use when validating leading whitespace. /// Test errors to display as warnings. /// Test errors to ignore. public Converter(IMessaging messaging, int indentationAmount, IEnumerable errorsAsWarnings = null, IEnumerable ignoreErrors = null) { this.ConvertElementMapping = new Dictionary>() { { FileElementName, this.ConvertFileElement }, { ExePackageElementName, this.ConvertSuppressSignatureValidation }, { MsiPackageElementName, this.ConvertSuppressSignatureValidation }, { MspPackageElementName, this.ConvertSuppressSignatureValidation }, { MsuPackageElementName, this.ConvertSuppressSignatureValidation }, { PayloadElementName, this.ConvertSuppressSignatureValidation }, { WixElementWithoutNamespaceName, this.ConvertWixElementWithoutNamespace }, }; this.Messaging = messaging; this.IndentationAmount = indentationAmount; this.ErrorsAsWarnings = new HashSet(this.YieldConverterTypes(errorsAsWarnings)); this.IgnoreErrors = new HashSet(this.YieldConverterTypes(ignoreErrors)); } private int Errors { get; set; } private HashSet ErrorsAsWarnings { get; set; } private HashSet IgnoreErrors { get; set; } private IMessaging Messaging { get; } private int IndentationAmount { get; set; } private string SourceFile { get; set; } /// /// Convert a file. /// /// The file to convert. /// Option to save the converted errors that are found. /// The number of errors found. public int ConvertFile(string sourceFile, bool saveConvertedFile) { XDocument document; // Set the instance info. this.Errors = 0; this.SourceFile = sourceFile; try { document = XDocument.Load(this.SourceFile, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo); } catch (XmlException e) { this.OnError(ConverterTestType.XmlException, (XObject)null, "The xml is invalid. Detail: '{0}'", e.Message); return this.Errors; } this.ConvertDocument(document); // Fix errors if requested and necessary. if (saveConvertedFile && 0 < this.Errors) { try { using (StreamWriter writer = File.CreateText(this.SourceFile)) { document.Save(writer, SaveOptions.DisableFormatting | SaveOptions.OmitDuplicateNamespaces); } } catch (UnauthorizedAccessException) { this.OnError(ConverterTestType.UnauthorizedAccessException, (XObject)null, "Could not write to file."); } } return this.Errors; } /// /// Convert a document. /// /// The document to convert. /// The number of errors found. public int ConvertDocument(XDocument document) { XDeclaration declaration = document.Declaration; // Convert the declaration. if (null != declaration) { if (!String.Equals("utf-8", declaration.Encoding, StringComparison.OrdinalIgnoreCase)) { if (this.OnError(ConverterTestType.DeclarationEncodingWrong, document.Root, "The XML declaration encoding is not properly set to 'utf-8'.")) { declaration.Encoding = "utf-8"; } } } else // missing declaration { if (this.OnError(ConverterTestType.DeclarationMissing, (XNode)null, "This file is missing an XML declaration on the first line.")) { document.Declaration = new XDeclaration("1.0", "utf-8", null); document.Root.AddBeforeSelf(new XText(XDocumentNewLine)); } } // Start converting the nodes at the top. this.ConvertNode(document.Root, 0); return this.Errors; } /// /// Convert a single xml node. /// /// The node to convert. /// The depth level of the node. /// The converted node. private void ConvertNode(XNode node, int level) { // Convert this node's whitespace. if ((XmlNodeType.Comment == node.NodeType && 0 > ((XComment)node).Value.IndexOf(XDocumentNewLine, StringComparison.Ordinal)) || XmlNodeType.CDATA == node.NodeType || XmlNodeType.Element == node.NodeType || XmlNodeType.ProcessingInstruction == node.NodeType) { this.ConvertWhitespace(node, level); } // Convert this node if it is an element. XElement element = node as XElement; if (null != element) { this.ConvertElement(element); // Convert all children of this element. IEnumerable children = element.Nodes().ToList(); foreach (XNode child in children) { this.ConvertNode(child, level + 1); } } } private void ConvertElement(XElement element) { // Gather any deprecated namespaces, then update this element tree based on those deprecations. Dictionary deprecatedToUpdatedNamespaces = new Dictionary(); foreach (XAttribute declaration in element.Attributes().Where(a => a.IsNamespaceDeclaration)) { XNamespace ns; if (Converter.OldToNewNamespaceMapping.TryGetValue(declaration.Value, out ns)) { if (this.OnError(ConverterTestType.XmlnsValueWrong, declaration, "The namespace '{0}' is out of date. It must be '{1}'.", declaration.Value, ns.NamespaceName)) { deprecatedToUpdatedNamespaces.Add(declaration.Value, ns); } } } if (deprecatedToUpdatedNamespaces.Any()) { Converter.UpdateElementsWithDeprecatedNamespaces(element.DescendantsAndSelf(), deprecatedToUpdatedNamespaces); } // Convert the node in much greater detail. Action convert; if (this.ConvertElementMapping.TryGetValue(element.Name, out convert)) { convert(element); } } private void ConvertFileElement(XElement element) { if (null == element.Attribute("Id")) { XAttribute attribute = element.Attribute("Name"); if (null == attribute) { attribute = element.Attribute("Source"); } if (null != attribute) { string name = Path.GetFileName(attribute.Value); if (this.OnError(ConverterTestType.AssignAnonymousFileId, element, "The file id is being updated to '{0}' to ensure it remains the same as the default", name)) { IEnumerable attributes = element.Attributes().ToList(); element.RemoveAttributes(); element.Add(new XAttribute("Id", Common.GetIdentifierFromName(name))); element.Add(attributes); } } } } private void ConvertSuppressSignatureValidation(XElement element) { XAttribute suppressSignatureValidation = element.Attribute("SuppressSignatureValidation"); if (null != suppressSignatureValidation) { if (this.OnError(ConverterTestType.SuppressSignatureValidationDeprecated, element, "The chain package element contains deprecated '{0}' attribute. Use the 'EnableSignatureValidation' instead.", suppressSignatureValidation)) { if ("no" == suppressSignatureValidation.Value) { element.Add(new XAttribute("EnableSignatureValidation", "yes")); } } suppressSignatureValidation.Remove(); } } /// /// Converts a Wix element. /// /// The Wix element to convert. /// The converted element. private void ConvertWixElementWithoutNamespace(XElement element) { if (this.OnError(ConverterTestType.XmlnsMissing, element, "The xmlns attribute is missing. It must be present with a value of '{0}'.", WixNamespace.NamespaceName)) { element.Name = WixNamespace.GetName(element.Name.LocalName); element.Add(new XAttribute("xmlns", WixNamespace.NamespaceName)); // set the default namespace. foreach (XElement elementWithoutNamespace in element.Elements().Where(e => XNamespace.None == e.Name.Namespace)) { elementWithoutNamespace.Name = WixNamespace.GetName(elementWithoutNamespace.Name.LocalName); } } } /// /// Convert the whitespace adjacent to a node. /// /// The node to convert. /// The depth level of the node. private void ConvertWhitespace(XNode node, int level) { // Fix the whitespace before this node. XText whitespace = node.PreviousNode as XText; if (null != whitespace) { if (XmlNodeType.CDATA == node.NodeType) { if (this.OnError(ConverterTestType.WhitespacePrecedingCDATAWrong, node, "There should be no whitespace preceding a CDATA node.")) { whitespace.Remove(); } } else { if (!Converter.IsLegalWhitespace(this.IndentationAmount, level, whitespace.Value)) { if (this.OnError(ConverterTestType.WhitespacePrecedingNodeWrong, node, "The whitespace preceding this node is incorrect.")) { Converter.FixWhitespace(this.IndentationAmount, level, whitespace); } } } } // Fix the whitespace after CDATA nodes. XCData cdata = node as XCData; if (null != cdata) { whitespace = cdata.NextNode as XText; if (null != whitespace) { if (this.OnError(ConverterTestType.WhitespaceFollowingCDATAWrong, node, "There should be no whitespace following a CDATA node.")) { whitespace.Remove(); } } } else { // Fix the whitespace inside and after this node (except for Error which may contain just whitespace). XElement element = node as XElement; if (null != element && "Error" != element.Name.LocalName) { if (!element.HasElements && !element.IsEmpty && String.IsNullOrEmpty(element.Value.Trim())) { if (this.OnError(ConverterTestType.NotEmptyElement, element, "This should be an empty element since it contains nothing but whitespace.")) { element.RemoveNodes(); } } whitespace = node.NextNode as XText; if (null != whitespace) { if (!Converter.IsLegalWhitespace(this.IndentationAmount, level - 1, whitespace.Value)) { if (this.OnError(ConverterTestType.WhitespacePrecedingEndElementWrong, whitespace, "The whitespace preceding this end element is incorrect.")) { Converter.FixWhitespace(this.IndentationAmount, level - 1, whitespace); } } } } } } private IEnumerable YieldConverterTypes(IEnumerable types) { if (null != types) { foreach (string type in types) { ConverterTestType itt; if (Enum.TryParse(type, true, out itt)) { yield return itt; } else // not a known ConverterTestType { this.OnError(ConverterTestType.ConverterTestTypeUnknown, (XObject)null, "Unknown error type: '{0}'.", type); } } } } private static void UpdateElementsWithDeprecatedNamespaces(IEnumerable elements, Dictionary deprecatedToUpdatedNamespaces) { foreach (XElement element in elements) { XNamespace ns; if (deprecatedToUpdatedNamespaces.TryGetValue(element.Name.Namespace, out ns)) { element.Name = ns.GetName(element.Name.LocalName); } // Remove all the attributes and add them back to with their namespace updated (as necessary). IEnumerable attributes = element.Attributes().ToList(); element.RemoveAttributes(); foreach (XAttribute attribute in attributes) { XAttribute convertedAttribute = attribute; if (attribute.IsNamespaceDeclaration) { if (deprecatedToUpdatedNamespaces.TryGetValue(attribute.Value, out ns)) { convertedAttribute = ("xmlns" == attribute.Name.LocalName) ? new XAttribute(attribute.Name.LocalName, ns.NamespaceName) : new XAttribute(XNamespace.Xmlns + attribute.Name.LocalName, ns.NamespaceName); } } else if (deprecatedToUpdatedNamespaces.TryGetValue(attribute.Name.Namespace, out ns)) { convertedAttribute = new XAttribute(ns.GetName(attribute.Name.LocalName), attribute.Value); } element.Add(convertedAttribute); } } } /// /// Determine if the whitespace preceding a node is appropriate for its depth level. /// /// Indentation value to use when validating leading whitespace. /// The depth level that should match this whitespace. /// The whitespace to validate. /// true if the whitespace is legal; false otherwise. private static bool IsLegalWhitespace(int indentationAmount, int level, string whitespace) { // strip off leading newlines; there can be an arbitrary number of these while (whitespace.StartsWith(XDocumentNewLine, StringComparison.Ordinal)) { whitespace = whitespace.Substring(XDocumentNewLine.Length); } // check the length if (whitespace.Length != level * indentationAmount) { return false; } // check the spaces foreach (char character in whitespace) { if (' ' != character) { return false; } } return true; } /// /// Fix the whitespace in a Whitespace node. /// /// Indentation value to use when validating leading whitespace. /// The depth level of the desired whitespace. /// The whitespace node to fix. private static void FixWhitespace(int indentationAmount, int level, XText whitespace) { int newLineCount = 0; for (int i = 0; i + 1 < whitespace.Value.Length; ++i) { if (XDocumentNewLine == whitespace.Value.Substring(i, 2)) { ++i; // skip an extra character ++newLineCount; } } if (0 == newLineCount) { newLineCount = 1; } // reset the whitespace value whitespace.Value = String.Empty; // add the correct number of newlines for (int i = 0; i < newLineCount; ++i) { whitespace.Value = String.Concat(whitespace.Value, XDocumentNewLine); } // add the correct number of spaces based on configured indentation amount whitespace.Value = String.Concat(whitespace.Value, new string(' ', level * indentationAmount)); } /// /// Output an error message to the console. /// /// The type of converter test. /// The node that caused the error. /// Detailed error message. /// Additional formatted string arguments. /// Returns true indicating that action should be taken on this error, and false if it should be ignored. private bool OnError(ConverterTestType converterTestType, XObject node, string message, params object[] args) { if (this.IgnoreErrors.Contains(converterTestType)) // ignore the error { return false; } // Increase the error count. this.Errors++; SourceLineNumber sourceLine = (null == node) ? new SourceLineNumber(this.SourceFile ?? "wixcop.exe") : new SourceLineNumber(this.SourceFile, ((IXmlLineInfo)node).LineNumber); bool warning = this.ErrorsAsWarnings.Contains(converterTestType); string display = String.Format(CultureInfo.CurrentCulture, message, args); var msg = new Message(sourceLine, warning ? MessageLevel.Warning : MessageLevel.Error, (int)converterTestType, "{0} ({1})", display, converterTestType.ToString()); this.Messaging.Write(msg); return true; } /// /// Converter test types. These are used to condition error messages down to warnings. /// private enum ConverterTestType { /// /// Internal-only: displayed when a string cannot be converted to an ConverterTestType. /// ConverterTestTypeUnknown, /// /// Displayed when an XML loading exception has occurred. /// XmlException, /// /// Displayed when a file cannot be accessed; typically when trying to save back a fixed file. /// UnauthorizedAccessException, /// /// Displayed when the encoding attribute in the XML declaration is not 'UTF-8'. /// DeclarationEncodingWrong, /// /// Displayed when the XML declaration is missing from the source file. /// DeclarationMissing, /// /// Displayed when the whitespace preceding a CDATA node is wrong. /// WhitespacePrecedingCDATAWrong, /// /// Displayed when the whitespace preceding a node is wrong. /// WhitespacePrecedingNodeWrong, /// /// Displayed when an element is not empty as it should be. /// NotEmptyElement, /// /// Displayed when the whitespace following a CDATA node is wrong. /// WhitespaceFollowingCDATAWrong, /// /// Displayed when the whitespace preceding an end element is wrong. /// WhitespacePrecedingEndElementWrong, /// /// Displayed when the xmlns attribute is missing from the document element. /// XmlnsMissing, /// /// Displayed when the xmlns attribute on the document element is wrong. /// XmlnsValueWrong, /// /// Assign an identifier to a File element when on Id attribute is specified. /// AssignAnonymousFileId, /// /// SuppressSignatureValidation attribute is deprecated and replaced with EnableSignatureValidation. /// SuppressSignatureValidationDeprecated, } } }