From 4965ca76323d2ee709bc1790ed1e49ab958445b4 Mon Sep 17 00:00:00 2001 From: Bob Arnson Date: Thu, 5 Mar 2020 19:52:35 -0500 Subject: Handle versioned extension ids. --- src/WixToolset.Converters/Wix3Converter.cs | 546 +++++++++++---------- .../WixToolsetTest.Converters/ConverterFixture.cs | 34 ++ 2 files changed, 311 insertions(+), 269 deletions(-) (limited to 'src') diff --git a/src/WixToolset.Converters/Wix3Converter.cs b/src/WixToolset.Converters/Wix3Converter.cs index b6366a83..1ae65e4f 100644 --- a/src/WixToolset.Converters/Wix3Converter.cs +++ b/src/WixToolset.Converters/Wix3Converter.cs @@ -305,368 +305,376 @@ namespace WixToolset.Converters } } - private void ConvertDirectoryElement(XElement element) - { - if (null == element.Attribute("Name")) + private void ConvertDirectoryElement(XElement element) { - var attribute = element.Attribute("ShortName"); - if (null != attribute) + if (null == element.Attribute("Name")) { - var shortName = attribute.Value; - if (this.OnError(ConverterTestType.AssignDirectoryNameFromShortName, element, "The directory ShortName attribute is being renamed to Name since Name wasn't specified for value '{0}'", shortName)) + var attribute = element.Attribute("ShortName"); + if (null != attribute) { - element.Add(new XAttribute("Name", shortName)); - attribute.Remove(); + var shortName = attribute.Value; + if (this.OnError(ConverterTestType.AssignDirectoryNameFromShortName, element, "The directory ShortName attribute is being renamed to Name since Name wasn't specified for value '{0}'", shortName)) + { + element.Add(new XAttribute("Name", shortName)); + attribute.Remove(); + } } } } - } - private void ConvertFileElement(XElement element) - { - if (null == element.Attribute("Id")) + private void ConvertFileElement(XElement element) { - var attribute = element.Attribute("Name"); - - if (null == attribute) + if (null == element.Attribute("Id")) { - attribute = element.Attribute("Source"); - } + var attribute = element.Attribute("Name"); - if (null != attribute) - { - var name = Path.GetFileName(attribute.Value); + if (null == attribute) + { + attribute = element.Attribute("Source"); + } - if (this.OnError(ConverterTestType.AssignAnonymousFileId, element, "The file id is being updated to '{0}' to ensure it remains the same as the default", name)) + if (null != attribute) { - IEnumerable attributes = element.Attributes().ToList(); - element.RemoveAttributes(); - element.Add(new XAttribute("Id", GetIdentifierFromName(name))); - element.Add(attributes); + var 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", GetIdentifierFromName(name))); + element.Add(attributes); + } } } } - } - - private void ConvertSuppressSignatureValidation(XElement element) - { - var suppressSignatureValidation = element.Attribute("SuppressSignatureValidation"); - if (null != suppressSignatureValidation) + private void ConvertSuppressSignatureValidation(XElement element) { - if (this.OnError(ConverterTestType.SuppressSignatureValidationDeprecated, element, "The chain package element contains deprecated '{0}' attribute. Use the 'EnableSignatureValidation' attribute instead.", suppressSignatureValidation.Name)) + var suppressSignatureValidation = element.Attribute("SuppressSignatureValidation"); + + if (null != suppressSignatureValidation) { - if ("no" == suppressSignatureValidation.Value) + if (this.OnError(ConverterTestType.SuppressSignatureValidationDeprecated, element, "The chain package element contains deprecated '{0}' attribute. Use the 'EnableSignatureValidation' attribute instead.", suppressSignatureValidation.Name)) { - element.Add(new XAttribute("EnableSignatureValidation", "yes")); + if ("no" == suppressSignatureValidation.Value) + { + element.Add(new XAttribute("EnableSignatureValidation", "yes")); + } } - } - suppressSignatureValidation.Remove(); + suppressSignatureValidation.Remove(); + } } - } - - private void ConvertCustomActionElement(XElement xCustomAction) - { - var xBinaryKey = xCustomAction.Attribute("BinaryKey"); - if (xBinaryKey?.Value == "WixCA") + private void ConvertCustomActionElement(XElement xCustomAction) { - if (this.OnError(ConverterTestType.WixCABinaryIdRenamed, xCustomAction, "The WixCA custom action DLL Binary table id has been renamed. Use the id 'UtilCA' instead.")) + var xBinaryKey = xCustomAction.Attribute("BinaryKey"); + + if (xBinaryKey?.Value == "WixCA" || xBinaryKey?.Value == "UtilCA") { - xBinaryKey.Value = "UtilCA"; + if (this.OnError(ConverterTestType.WixCABinaryIdRenamed, xCustomAction, "The WixCA custom action DLL Binary table id has been renamed. Use the id 'Wix4UtilCA_X86' instead.")) + { + xBinaryKey.Value = "Wix4UtilCA_X86"; + } } - } - - var xDllEntry = xCustomAction.Attribute("DllEntry"); - if (xDllEntry?.Value == "CAQuietExec" || xDllEntry?.Value == "CAQuietExec64") - { - if (this.OnError(ConverterTestType.QuietExecCustomActionsRenamed, xCustomAction, "The CAQuietExec and CAQuietExec64 custom action ids have been renamed. Use the ids 'WixQuietExec' and 'WixQuietExec64' instead.")) + if (xBinaryKey?.Value == "WixCA_x64" || xBinaryKey?.Value == "UtilCA_x64") { - xDllEntry.Value = xDllEntry.Value.Replace("CAQuietExec", "WixQuietExec"); + if (this.OnError(ConverterTestType.WixCABinaryIdRenamed, xCustomAction, "The WixCA_x64 custom action DLL Binary table id has been renamed. Use the id 'Wix4UtilCA_X64' instead.")) + { + xBinaryKey.Value = "Wix4UtilCA_X64"; + } } - } - var xProperty = xCustomAction.Attribute("Property"); + var xDllEntry = xCustomAction.Attribute("DllEntry"); - if (xProperty?.Value == "QtExecCmdLine" || xProperty?.Value == "QtExec64CmdLine") - { - if (this.OnError(ConverterTestType.QuietExecCustomActionsRenamed, xCustomAction, "The QtExecCmdLine and QtExec64CmdLine property ids have been renamed. Use the ids 'WixQuietExecCmdLine' and 'WixQuietExec64CmdLine' instead.")) + if (xDllEntry?.Value == "CAQuietExec" || xDllEntry?.Value == "CAQuietExec64") { - xProperty.Value = xProperty.Value.Replace("QtExec", "WixQuietExec"); + if (this.OnError(ConverterTestType.QuietExecCustomActionsRenamed, xCustomAction, "The CAQuietExec and CAQuietExec64 custom action ids have been renamed. Use the ids 'WixQuietExec' and 'WixQuietExec64' instead.")) + { + xDllEntry.Value = xDllEntry.Value.Replace("CAQuietExec", "WixQuietExec"); + } } - } - } - private void ConvertPropertyElement(XElement xProperty) - { - var xId = xProperty.Attribute("Id"); + var xProperty = xCustomAction.Attribute("Property"); - if (xId.Value == "QtExecCmdTimeout") - { - this.OnError(ConverterTestType.QtExecCmdTimeoutAmbiguous, xProperty, "QtExecCmdTimeout was previously used for both CAQuietExec and CAQuietExec64. For WixQuietExec, use WixQuietExecCmdTimeout. For WixQuietExec64, use WixQuietExec64CmdTimeout."); + if (xProperty?.Value == "QtExecCmdLine" || xProperty?.Value == "QtExec64CmdLine") + { + if (this.OnError(ConverterTestType.QuietExecCustomActionsRenamed, xCustomAction, "The QtExecCmdLine and QtExec64CmdLine property ids have been renamed. Use the ids 'WixQuietExecCmdLine' and 'WixQuietExec64CmdLine' instead.")) + { + xProperty.Value = xProperty.Value.Replace("QtExec", "WixQuietExec"); + } + } } - } - /// - /// Converts a Wix element. - /// - /// The Wix element to convert. - /// The converted element. - private void ConvertElementWithoutNamespace(XElement element) - { - if (this.OnError(ConverterTestType.XmlnsMissing, element, "The xmlns attribute is missing. It must be present with a value of '{0}'.", WixNamespace.NamespaceName)) + private void ConvertPropertyElement(XElement xProperty) { - element.Name = WixNamespace.GetName(element.Name.LocalName); - - element.Add(new XAttribute("xmlns", WixNamespace.NamespaceName)); // set the default namespace. + var xId = xProperty.Attribute("Id"); - foreach (var elementWithoutNamespace in element.DescendantsAndSelf().Where(e => XNamespace.None == e.Name.Namespace)) + if (xId.Value == "QtExecCmdTimeout") { - elementWithoutNamespace.Name = WixNamespace.GetName(elementWithoutNamespace.Name.LocalName); + this.OnError(ConverterTestType.QtExecCmdTimeoutAmbiguous, xProperty, "QtExecCmdTimeout was previously used for both CAQuietExec and CAQuietExec64. For WixQuietExec, use WixQuietExecCmdTimeout. For WixQuietExec64, use WixQuietExec64CmdTimeout."); } } - } - private IEnumerable YieldConverterTypes(IEnumerable types) - { - if (null != types) + /// + /// Converts a Wix element. + /// + /// The Wix element to convert. + /// The converted element. + private void ConvertElementWithoutNamespace(XElement element) { - foreach (var type in types) + 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); - if (Enum.TryParse(type, true, out var itt)) - { - yield return itt; - } - else // not a known ConverterTestType + element.Add(new XAttribute("xmlns", WixNamespace.NamespaceName)); // set the default namespace. + + foreach (var elementWithoutNamespace in element.DescendantsAndSelf().Where(e => XNamespace.None == e.Name.Namespace)) { - this.OnError(ConverterTestType.ConverterTestTypeUnknown, null, "Unknown error type: '{0}'.", type); + elementWithoutNamespace.Name = WixNamespace.GetName(elementWithoutNamespace.Name.LocalName); } } } - } - private static void UpdateElementsWithDeprecatedNamespaces(IEnumerable elements, Dictionary deprecatedToUpdatedNamespaces) - { - foreach (var element in elements) + private IEnumerable YieldConverterTypes(IEnumerable types) { - - if (deprecatedToUpdatedNamespaces.TryGetValue(element.Name.Namespace, out var 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 (var attribute in attributes) + if (null != types) { - var convertedAttribute = attribute; - - if (attribute.IsNamespaceDeclaration) + foreach (var type in types) { - if (deprecatedToUpdatedNamespaces.TryGetValue(attribute.Value, out ns)) + + if (Enum.TryParse(type, true, out var itt)) { - convertedAttribute = ("xmlns" == attribute.Name.LocalName) ? new XAttribute(attribute.Name.LocalName, ns.NamespaceName) : new XAttribute(XNamespace.Xmlns + attribute.Name.LocalName, ns.NamespaceName); + yield return itt; + } + else // not a known ConverterTestType + { + this.OnError(ConverterTestType.ConverterTestTypeUnknown, null, "Unknown error type: '{0}'.", type); } } - 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 LeadingWhitespaceValid(int indentationAmount, int level, string whitespace) - { - // Strip off leading newlines; there can be an arbitrary number of these. - whitespace = whitespace.TrimStart(XDocumentNewLine); - - var indentation = new string(' ', level * indentationAmount); - - return whitespace == indentation; - } - - /// - /// 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 FixupWhitespace(int indentationAmount, int level, XText whitespace) - { - var value = new StringBuilder(whitespace.Value.Length); - - // Keep any previous preceeding new lines. - var newlines = whitespace.Value.TakeWhile(c => c == XDocumentNewLine).Count(); - - // Ensure there is always at least one new line before the indentation. - value.Append(XDocumentNewLine, newlines == 0 ? 1 : newlines); - - whitespace.Value = value.Append(' ', level * indentationAmount).ToString(); - } - - /// - /// 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 + private static void UpdateElementsWithDeprecatedNamespaces(IEnumerable elements, Dictionary deprecatedToUpdatedNamespaces) { - return false; - } - - // Increase the error count. - this.Errors++; - - var sourceLine = (null == node) ? new SourceLineNumber(this.SourceFile ?? "wixcop.exe") : new SourceLineNumber(this.SourceFile, ((IXmlLineInfo)node).LineNumber); - var warning = this.ErrorsAsWarnings.Contains(converterTestType); - var display = String.Format(CultureInfo.CurrentCulture, message, args); + foreach (var element in elements) + { - var msg = new Message(sourceLine, warning ? MessageLevel.Warning : MessageLevel.Error, (int)converterTestType, "{0} ({1})", display, converterTestType.ToString()); + if (deprecatedToUpdatedNamespaces.TryGetValue(element.Name.Namespace, out var ns)) + { + element.Name = ns.GetName(element.Name.LocalName); + } - this.Messaging.Write(msg); + // Remove all the attributes and add them back to with their namespace updated (as necessary). + IEnumerable attributes = element.Attributes().ToList(); + element.RemoveAttributes(); - return true; - } + foreach (var attribute in attributes) + { + var convertedAttribute = attribute; - /// - /// Return an identifier based on passed file/directory name - /// - /// File/directory name to generate identifer from - /// A version of the name that is a legal identifier. - /// This is duplicated from WiX's Common class. - private static string GetIdentifierFromName(string name) - { - string result = IllegalIdentifierCharacters.Replace(name, "_"); // replace illegal characters with "_". + 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); + } - // MSI identifiers must begin with an alphabetic character or an - // underscore. Prefix all other values with an underscore. - if (AddPrefix.IsMatch(name)) - { - result = String.Concat("_", result); + element.Add(convertedAttribute); + } + } } - return result; - } - - /// - /// 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. + /// Determine if the whitespace preceding a node is appropriate for its depth level. /// - XmlException, - - /// - /// Displayed when a file cannot be accessed; typically when trying to save back a fixed file. - /// - UnauthorizedAccessException, + /// 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 LeadingWhitespaceValid(int indentationAmount, int level, string whitespace) + { + // Strip off leading newlines; there can be an arbitrary number of these. + whitespace = whitespace.TrimStart(XDocumentNewLine); - /// - /// Displayed when the encoding attribute in the XML declaration is not 'UTF-8'. - /// - DeclarationEncodingWrong, + var indentation = new string(' ', level * indentationAmount); - /// - /// Displayed when the XML declaration is missing from the source file. - /// - DeclarationMissing, + return whitespace == indentation; + } /// - /// Displayed when the whitespace preceding a CDATA node is wrong. + /// Fix the whitespace in a whitespace node. /// - WhitespacePrecedingCDATAWrong, + /// Indentation value to use when validating leading whitespace. + /// The depth level of the desired whitespace. + /// The whitespace node to fix. + private static void FixupWhitespace(int indentationAmount, int level, XText whitespace) + { + var value = new StringBuilder(whitespace.Value.Length); - /// - /// Displayed when the whitespace preceding a node is wrong. - /// - WhitespacePrecedingNodeWrong, + // Keep any previous preceeding new lines. + var newlines = whitespace.Value.TakeWhile(c => c == XDocumentNewLine).Count(); - /// - /// Displayed when an element is not empty as it should be. - /// - NotEmptyElement, + // Ensure there is always at least one new line before the indentation. + value.Append(XDocumentNewLine, newlines == 0 ? 1 : newlines); - /// - /// Displayed when the whitespace following a CDATA node is wrong. - /// - WhitespaceFollowingCDATAWrong, + whitespace.Value = value.Append(' ', level * indentationAmount).ToString(); + } /// - /// Displayed when the whitespace preceding an end element is wrong. + /// Output an error message to the console. /// - WhitespacePrecedingEndElementWrong, + /// 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; + } - /// - /// Displayed when the xmlns attribute is missing from the document element. - /// - XmlnsMissing, + // Increase the error count. + this.Errors++; - /// - /// Displayed when the xmlns attribute on the document element is wrong. - /// - XmlnsValueWrong, + var sourceLine = (null == node) ? new SourceLineNumber(this.SourceFile ?? "wixcop.exe") : new SourceLineNumber(this.SourceFile, ((IXmlLineInfo)node).LineNumber); + var warning = this.ErrorsAsWarnings.Contains(converterTestType); + var display = String.Format(CultureInfo.CurrentCulture, message, args); - /// - /// Assign an identifier to a File element when on Id attribute is specified. - /// - AssignAnonymousFileId, + var msg = new Message(sourceLine, warning ? MessageLevel.Warning : MessageLevel.Error, (int)converterTestType, "{0} ({1})", display, converterTestType.ToString()); - /// - /// SuppressSignatureValidation attribute is deprecated and replaced with EnableSignatureValidation. - /// - SuppressSignatureValidationDeprecated, + this.Messaging.Write(msg); - /// - /// WixCA Binary/@Id has been renamed to UtilCA. - /// - WixCABinaryIdRenamed, + return true; + } /// - /// QtExec custom actions have been renamed. + /// Return an identifier based on passed file/directory name /// - QuietExecCustomActionsRenamed, + /// File/directory name to generate identifer from + /// A version of the name that is a legal identifier. + /// This is duplicated from WiX's Common class. + private static string GetIdentifierFromName(string name) + { + string result = IllegalIdentifierCharacters.Replace(name, "_"); // replace illegal characters with "_". - /// - /// QtExecCmdTimeout was previously used for both CAQuietExec and CAQuietExec64. For WixQuietExec, use WixQuietExecCmdTimeout. For WixQuietExec64, use WixQuietExec64CmdTimeout. - /// - QtExecCmdTimeoutAmbiguous, + // MSI identifiers must begin with an alphabetic character or an + // underscore. Prefix all other values with an underscore. + if (AddPrefix.IsMatch(name)) + { + result = String.Concat("_", result); + } - /// - /// Directory/@ShortName may only be specified with Directory/@Name. - /// - AssignDirectoryNameFromShortName, + return result; + } /// - /// BootstrapperApplicationData attribute is deprecated and replaced with Unreal. + /// Converter test types. These are used to condition error messages down to warnings. /// - BootstrapperApplicationDataDeprecated, + 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, + + /// + /// WixCA Binary/@Id has been renamed to UtilCA. + /// + WixCABinaryIdRenamed, + + /// + /// QtExec custom actions have been renamed. + /// + QuietExecCustomActionsRenamed, + + /// + /// QtExecCmdTimeout was previously used for both CAQuietExec and CAQuietExec64. For WixQuietExec, use WixQuietExecCmdTimeout. For WixQuietExec64, use WixQuietExec64CmdTimeout. + /// + QtExecCmdTimeoutAmbiguous, + + /// + /// Directory/@ShortName may only be specified with Directory/@Name. + /// + AssignDirectoryNameFromShortName, + + /// + /// BootstrapperApplicationData attribute is deprecated and replaced with Unreal. + /// + BootstrapperApplicationDataDeprecated, + } } } -} diff --git a/src/test/WixToolsetTest.Converters/ConverterFixture.cs b/src/test/WixToolsetTest.Converters/ConverterFixture.cs index cb70b35a..c6037787 100644 --- a/src/test/WixToolsetTest.Converters/ConverterFixture.cs +++ b/src/test/WixToolsetTest.Converters/ConverterFixture.cs @@ -590,6 +590,40 @@ namespace WixToolsetTest.Converters Assert.Equal(expected, actual); } + [Fact] + public void CanConvertCustomAction() + { + var parse = String.Join(Environment.NewLine, + "", + "", + " ", + " ", + " ", + " ", + ""); + + var expected = String.Join(Environment.NewLine, + "", + "", + " ", + " ", + " ", + " ", + ""); + + var document = XDocument.Parse(parse, LoadOptions.PreserveWhitespace | LoadOptions.SetLineInfo); + + var messaging = new DummyMessaging(); + var converter = new Wix3Converter(messaging, 2, null, null); + + var errors = converter.ConvertDocument(document); + + var actual = UnformattedDocumentString(document); + + Assert.Equal(6, errors); + Assert.Equal(expected, actual); + } + private static string UnformattedDocumentString(XDocument document) { var sb = new StringBuilder(); -- cgit v1.2.3-55-g6feb