// 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,
}
}
}