From db22d99a4d603caab18fd42cb40881431c353912 Mon Sep 17 00:00:00 2001 From: Sean Hall Date: Tue, 15 Mar 2022 18:09:12 -0500 Subject: Enhance bundle backend validation. --- src/api/wix/WixToolset.Data/ErrorMessages.cs | 6 + src/api/wix/WixToolset.Data/WarningMessages.cs | 6 + .../Data/BundleConditionPhase.cs | 35 +++ .../Services/IBackendHelper.cs | 11 - .../Services/IBundleValidator.cs | 56 ++++ .../Services/IBurnBackendHelper.cs | 2 +- .../Services/IParseHelper.cs | 13 +- src/burn/test/BurnUnitTest/VariableTest.cpp | 1 + .../test/WixToolsetTest.Bal/BalExtensionFixture.cs | 22 +- .../TestData/Overridable/WrongCaseBundle.wxl | 4 + .../TestData/Overridable/WrongCaseBundle.wxs | 2 + src/ext/Bal/wixext/BalBurnBackendExtension.cs | 23 +- .../WixToolset.Core.Burn/Bind/BindBundleCommand.cs | 15 +- .../PerformBundleBackendValidationCommand.cs | 156 ++++++++++ .../Bundles/ProcessPayloadsCommand.cs | 4 +- .../ExtensibilityServices/BurnBackendHelper.cs | 121 ++++++-- .../WindowsInstallerBackendHelper.cs | 97 ++++-- src/wix/WixToolset.Core/Common.cs | 21 -- src/wix/WixToolset.Core/CompilerCore.cs | 173 +---------- .../ExtensibilityServices/BackendHelper.cs | 5 - .../ExtensibilityServices/BundleValidator.cs | 326 +++++++++++++++++++++ .../ExtensibilityServices/ParseHelper.cs | 35 ++- .../WixToolset.Core/WixToolsetServiceProvider.cs | 1 + .../BadInputFixture.cs | 43 +++ .../BundleWithInvalidLocValues.wxl | 8 + .../BundleWithInvalidLocValues.wxs | 18 ++ 26 files changed, 926 insertions(+), 278 deletions(-) create mode 100644 src/api/wix/WixToolset.Extensibility/Data/BundleConditionPhase.cs create mode 100644 src/api/wix/WixToolset.Extensibility/Services/IBundleValidator.cs create mode 100644 src/ext/Bal/test/WixToolsetTest.Bal/TestData/Overridable/WrongCaseBundle.wxl create mode 100644 src/wix/WixToolset.Core.Burn/Bundles/PerformBundleBackendValidationCommand.cs create mode 100644 src/wix/WixToolset.Core/ExtensibilityServices/BundleValidator.cs create mode 100644 src/wix/test/WixToolsetTest.CoreIntegration/TestData/BundleWithInvalid/BundleWithInvalidLocValues.wxl create mode 100644 src/wix/test/WixToolsetTest.CoreIntegration/TestData/BundleWithInvalid/BundleWithInvalidLocValues.wxs (limited to 'src') diff --git a/src/api/wix/WixToolset.Data/ErrorMessages.cs b/src/api/wix/WixToolset.Data/ErrorMessages.cs index 80738f5e..a833452d 100644 --- a/src/api/wix/WixToolset.Data/ErrorMessages.cs +++ b/src/api/wix/WixToolset.Data/ErrorMessages.cs @@ -1184,6 +1184,11 @@ namespace WixToolset.Data return Message(null, Ids.InvalidCommandLineFileName, "Invalid file name specified on the command line: '{0}'. Error message: '{1}'", fileName, error); } + public static Message InvalidBundleCondition(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string condition) + { + return Message(sourceLineNumbers, Ids.InvalidBundleCondition, "The {0}/@{1} attribute's value '{2}' is not a valid bundle condition.", elementName, attributeName, condition); + } + public static Message InvalidDateTimeFormat(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string value) { return Message(sourceLineNumbers, Ids.InvalidDateTimeFormat, "The {0}/@{1} attribute's value '{2}' is not a valid date/time value. A date/time value should follow the format YYYY-MM-DDTHH:mm:ss.", elementName, attributeName, value); @@ -2686,6 +2691,7 @@ namespace WixToolset.Data MultiplePackagePayloads3 = 406, MissingPackagePayload = 407, ExpectedAttributeWithoutOtherAttributes = 408, + InvalidBundleCondition = 409, } } } diff --git a/src/api/wix/WixToolset.Data/WarningMessages.cs b/src/api/wix/WixToolset.Data/WarningMessages.cs index b749bcf4..f555fd93 100644 --- a/src/api/wix/WixToolset.Data/WarningMessages.cs +++ b/src/api/wix/WixToolset.Data/WarningMessages.cs @@ -593,6 +593,11 @@ namespace WixToolset.Data return Message(null, Ids.UnableToResetAcls, "Unable to reset acls on destination files. Exception detail: {0}", error); } + public static Message UnavailableBundleConditionVariable(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string variable, string illegalValueList) + { + return Message(sourceLineNumbers, Ids.UnavailableBundleConditionVariable, "{0}/@{1} contains the built-in Variable '{2}', which is not available when it is evaluated. (Unavailable Variables are: {3}.). Rewrite the condition to avoid Variables that are never valid during its evaluation.", elementName, attributeName, variable, illegalValueList); + } + public static Message UnclearShortcut(SourceLineNumber sourceLineNumbers, string shortcutId, string fileId, string componentId) { return Message(sourceLineNumbers, Ids.UnclearShortcut, "Because it is an advertised shortcut, the target of shortcut '{0}' will be the keypath of component '{2}' rather than parent file '{1}'. To eliminate this warning, you can (1) make the Shortcut element a child of the File element that is the keypath of component '{2}', (2) make file '{1}' the keypath of component '{2}', or (3) remove the @Advertise attribute so the shortcut is a non-advertised shortcut.", shortcutId, fileId, componentId); @@ -804,6 +809,7 @@ namespace WixToolset.Data DetectConditionRecommended = 1153, CollidingModularizationTypes = 1156, InvalidEnvironmentVariable = 1157, + UnavailableBundleConditionVariable = 1159, } } } diff --git a/src/api/wix/WixToolset.Extensibility/Data/BundleConditionPhase.cs b/src/api/wix/WixToolset.Extensibility/Data/BundleConditionPhase.cs new file mode 100644 index 00000000..6d876bcc --- /dev/null +++ b/src/api/wix/WixToolset.Extensibility/Data/BundleConditionPhase.cs @@ -0,0 +1,35 @@ +// 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.Extensibility.Data +{ + /// + /// The Burn execution phase during which a Condition will be evaluated. + /// + public enum BundleConditionPhase + { + /// + /// Condition is evaluated by the engine before loading the BootstrapperApplication (Bundle/@Condition). + /// + Startup, + + /// + /// Condition is evaluated during Detect (ExePackage/@DetectCondition). + /// + Detect, + + /// + /// Condition is evaluated during Plan (ExePackage/@InstallCondition). + /// + Plan, + + /// + /// Condition is evaluated during Apply (MsiProperty/@Condition). + /// + Execute, + + /// + /// Condition is evaluated after Apply. + /// + Shutdown, + } +} diff --git a/src/api/wix/WixToolset.Extensibility/Services/IBackendHelper.cs b/src/api/wix/WixToolset.Extensibility/Services/IBackendHelper.cs index 29b8f8e6..23ad44f5 100644 --- a/src/api/wix/WixToolset.Extensibility/Services/IBackendHelper.cs +++ b/src/api/wix/WixToolset.Extensibility/Services/IBackendHelper.cs @@ -73,17 +73,6 @@ namespace WixToolset.Extensibility.Services /// The generated identifier. string GenerateIdentifier(string prefix, params string[] args); - /// - /// Validates path is relative and canonicalizes it. - /// For example, "a\..\c\.\d.exe" => "c\d.exe". - /// - /// - /// - /// - /// - /// The original value if not relative, otherwise the canonicalized relative path. - string GetCanonicalRelativePath(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string relativePath); - /// /// Gets a valid code page from the given web name or integer value. /// diff --git a/src/api/wix/WixToolset.Extensibility/Services/IBundleValidator.cs b/src/api/wix/WixToolset.Extensibility/Services/IBundleValidator.cs new file mode 100644 index 00000000..fc88a443 --- /dev/null +++ b/src/api/wix/WixToolset.Extensibility/Services/IBundleValidator.cs @@ -0,0 +1,56 @@ +// 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.Extensibility.Services +{ + using WixToolset.Data; + using WixToolset.Extensibility.Data; + + /// + /// Interface provided to help with bundle validation. + /// + public interface IBundleValidator + { + + /// + /// Validates path is relative and canonicalizes it. + /// For example, "a\..\c\.\d.exe" => "c\d.exe". + /// + /// + /// + /// + /// + /// The original value if not relative, otherwise the canonicalized relative path. + string GetCanonicalRelativePath(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string relativePath); + + /// + /// Validates an MsiProperty name value and displays an error for an illegal value. + /// + /// + /// + /// + /// + /// Whether the name is valid. + bool ValidateBundleMsiPropertyName(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string propertyName); + + /// + /// Validates a Bundle variable name and displays an error for an illegal value. + /// + /// + /// + /// + /// + /// Whether the name is valid. + bool ValidateBundleVariableName(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string variableName); + + /// + /// Validates a bundle condition and displays an error for an illegal value. + /// + /// + /// + /// + /// + /// + /// Whether the condition is valid. + bool ValidateBundleCondition(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string condition, BundleConditionPhase phase); + } +} diff --git a/src/api/wix/WixToolset.Extensibility/Services/IBurnBackendHelper.cs b/src/api/wix/WixToolset.Extensibility/Services/IBurnBackendHelper.cs index ef5fcc65..1b6a2828 100644 --- a/src/api/wix/WixToolset.Extensibility/Services/IBurnBackendHelper.cs +++ b/src/api/wix/WixToolset.Extensibility/Services/IBurnBackendHelper.cs @@ -7,7 +7,7 @@ namespace WixToolset.Extensibility.Services /// /// Interface provided to help Burn backend extensions. /// - public interface IBurnBackendHelper : IBackendHelper + public interface IBurnBackendHelper : IBackendHelper, IBundleValidator { /// /// Adds the given XML to the BootstrapperApplicationData manifest. diff --git a/src/api/wix/WixToolset.Extensibility/Services/IParseHelper.cs b/src/api/wix/WixToolset.Extensibility/Services/IParseHelper.cs index fbe5aae4..de2c6f9f 100644 --- a/src/api/wix/WixToolset.Extensibility/Services/IParseHelper.cs +++ b/src/api/wix/WixToolset.Extensibility/Services/IParseHelper.cs @@ -13,7 +13,7 @@ namespace WixToolset.Extensibility.Services /// /// Interface provided to help compiler extensions parse. /// - public interface IParseHelper + public interface IParseHelper : IBundleValidator { /// /// Creates a version 3 name-based UUID. @@ -322,17 +322,6 @@ namespace WixToolset.Extensibility.Services /// The attribute's YesNoType value. YesNoDefaultType GetAttributeYesNoDefaultValue(SourceLineNumber sourceLineNumbers, XAttribute attribute); - /// - /// Validates path is relative and canonicalizes it. - /// For example, "a\..\c\.\d.exe" => "c\d.exe". - /// - /// - /// - /// - /// - /// The original value if not relative, otherwise the canonicalized relative path. - string GetCanonicalRelativePath(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string relativePath); - /// /// Gets a source line number for an element. /// diff --git a/src/burn/test/BurnUnitTest/VariableTest.cpp b/src/burn/test/BurnUnitTest/VariableTest.cpp index 53105e69..d0361c0e 100644 --- a/src/burn/test/BurnUnitTest/VariableTest.cpp +++ b/src/burn/test/BurnUnitTest/VariableTest.cpp @@ -381,6 +381,7 @@ namespace Bootstrapper Assert::True(EvaluateConditionHelper(&variables, L"(PROP3 = \"NOT\" OR PROP1 = \"VAL1\") AND PROP2 = \"VAL2\"")); Assert::True(EvaluateConditionHelper(&variables, L"PROP3 = \"NOT\" OR (PROP1 = \"VAL1\" AND PROP2 = \"VAL2\")")); + Assert::True(EvaluateFailureConditionHelper(&variables, L"")); Assert::True(EvaluateFailureConditionHelper(&variables, L"=")); Assert::True(EvaluateFailureConditionHelper(&variables, L"(PROP1")); Assert::True(EvaluateFailureConditionHelper(&variables, L"(PROP1 = \"")); diff --git a/src/ext/Bal/test/WixToolsetTest.Bal/BalExtensionFixture.cs b/src/ext/Bal/test/WixToolsetTest.Bal/BalExtensionFixture.cs index ef4ee49a..9aea8c1d 100644 --- a/src/ext/Bal/test/WixToolsetTest.Bal/BalExtensionFixture.cs +++ b/src/ext/Bal/test/WixToolsetTest.Bal/BalExtensionFixture.cs @@ -2,6 +2,7 @@ namespace WixToolsetTest.Bal { + using System; using System.IO; using System.Linq; using System.Xml; @@ -138,20 +139,33 @@ namespace WixToolsetTest.Bal { var baseFolder = fs.GetFolder(); var bundleFile = Path.Combine(baseFolder, "bin", "test.exe"); - var bundleSourceFolder = TestData.Get(@"TestData\Overridable"); + var bundleSourceFolder = TestData.Get(@"TestData"); var intermediateFolder = Path.Combine(baseFolder, "obj"); var baFolderPath = Path.Combine(baseFolder, "ba"); var extractFolderPath = Path.Combine(baseFolder, "extract"); - var compileResult = WixRunner.Execute(new[] + var result = WixRunner.Execute(new[] { "build", - Path.Combine(bundleSourceFolder, "WrongCaseBundle.wxs"), + Path.Combine(bundleSourceFolder, "Overridable", "WrongCaseBundle.wxs"), + "-loc", Path.Combine(bundleSourceFolder, "Overridable", "WrongCaseBundle.wxl"), + "-bindpath", Path.Combine(bundleSourceFolder, "WixStdBa", "Data"), "-ext", TestData.Get(@"WixToolset.Bal.wixext.dll"), "-intermediateFolder", intermediateFolder, "-o", bundleFile, }); - Assert.Equal((int)BalErrors.Ids.NonUpperCaseOverridableVariable, compileResult.ExitCode); + + Assert.InRange(result.ExitCode, 2, Int32.MaxValue); + + var messages = result.Messages.Select(m => m.ToString()).ToList(); + messages.Sort(); + + WixAssert.CompareLineByLine(new[] + { + "bal:Condition/@Condition contains the built-in Variable 'WixBundleAction', which is not available when it is evaluated. (Unavailable Variables are: 'WixBundleAction'.). Rewrite the condition to avoid Variables that are never valid during its evaluation.", + "Overridable variable 'Test1' must be 'TEST1' with Bundle/@CommandLineVariables value 'upperCase'.", + "The *Package/@bal:DisplayInternalUICondition attribute's value '=' is not a valid bundle condition.", + }, messages.ToArray()); } } } diff --git a/src/ext/Bal/test/WixToolsetTest.Bal/TestData/Overridable/WrongCaseBundle.wxl b/src/ext/Bal/test/WixToolsetTest.Bal/TestData/Overridable/WrongCaseBundle.wxl new file mode 100644 index 00000000..223a7874 --- /dev/null +++ b/src/ext/Bal/test/WixToolsetTest.Bal/TestData/Overridable/WrongCaseBundle.wxl @@ -0,0 +1,4 @@ + + WixBundleAction = 4 + = + diff --git a/src/ext/Bal/test/WixToolsetTest.Bal/TestData/Overridable/WrongCaseBundle.wxs b/src/ext/Bal/test/WixToolsetTest.Bal/TestData/Overridable/WrongCaseBundle.wxs index 91380c69..547af644 100644 --- a/src/ext/Bal/test/WixToolsetTest.Bal/TestData/Overridable/WrongCaseBundle.wxs +++ b/src/ext/Bal/test/WixToolsetTest.Bal/TestData/Overridable/WrongCaseBundle.wxs @@ -8,6 +8,8 @@ + + diff --git a/src/ext/Bal/wixext/BalBurnBackendExtension.cs b/src/ext/Bal/wixext/BalBurnBackendExtension.cs index 854b8b35..3b19ae78 100644 --- a/src/ext/Bal/wixext/BalBurnBackendExtension.cs +++ b/src/ext/Bal/wixext/BalBurnBackendExtension.cs @@ -10,6 +10,7 @@ namespace WixToolset.Bal using WixToolset.Data.Burn; using WixToolset.Data.Symbols; using WixToolset.Extensibility; + using WixToolset.Extensibility.Data; public class BalBurnBackendExtension : BaseBurnBackendBinderExtension { @@ -31,6 +32,8 @@ namespace WixToolset.Bal { base.SymbolsFinalized(section); + this.VerifyBalConditions(section); + this.VerifyBalPackageInfos(section); this.VerifyOverridableVariables(section); var baSymbol = section.Symbols.OfType().SingleOrDefault(); @@ -100,7 +103,7 @@ namespace WixToolset.Bal { foreach (var payloadPropertiesSymbol in payloadPropertiesSymbols) { - if (string.Equals(payloadPropertiesSymbol.Name, "bafunctions.dll", StringComparison.OrdinalIgnoreCase) && + if (String.Equals(payloadPropertiesSymbol.Name, "bafunctions.dll", StringComparison.OrdinalIgnoreCase) && BurnConstants.BurnUXContainerName == payloadPropertiesSymbol.ContainerRef) { this.Messaging.Write(BalWarnings.UnmarkedBAFunctionsDLL(payloadPropertiesSymbol.SourceLineNumbers)); @@ -120,6 +123,24 @@ namespace WixToolset.Bal } } + private void VerifyBalConditions(IntermediateSection section) + { + var balConditionSymbols = section.Symbols.OfType().ToList(); + foreach (var balConditionSymbol in balConditionSymbols) + { + this.BackendHelper.ValidateBundleCondition(balConditionSymbol.SourceLineNumbers, "bal:Condition", "Condition", balConditionSymbol.Condition, BundleConditionPhase.Detect); + } + } + + private void VerifyBalPackageInfos(IntermediateSection section) + { + var balPackageInfoSymbols = section.Symbols.OfType().ToList(); + foreach (var balPackageInfoSymbol in balPackageInfoSymbols) + { + this.BackendHelper.ValidateBundleCondition(balPackageInfoSymbol.SourceLineNumbers, "*Package", "bal:DisplayInternalUICondition", balPackageInfoSymbol.DisplayInternalUICondition, BundleConditionPhase.Plan); + } + } + private void VerifyOverridableVariables(IntermediateSection section) { var bundleSymbol = section.Symbols.OfType().Single(); diff --git a/src/wix/WixToolset.Core.Burn/Bind/BindBundleCommand.cs b/src/wix/WixToolset.Core.Burn/Bind/BindBundleCommand.cs index 16e63492..a73992da 100644 --- a/src/wix/WixToolset.Core.Burn/Bind/BindBundleCommand.cs +++ b/src/wix/WixToolset.Core.Burn/Bind/BindBundleCommand.cs @@ -150,7 +150,7 @@ namespace WixToolset.Core.Burn // Process the explicitly authored payloads. ISet processedPayloads; { - var command = new ProcessPayloadsCommand(this.BackendHelper, this.PayloadHarvester, payloadSymbols.Values, bundleSymbol.DefaultPackagingType, layoutDirectory); + var command = new ProcessPayloadsCommand(this.InternalBurnBackendHelper, this.PayloadHarvester, payloadSymbols.Values, bundleSymbol.DefaultPackagingType, layoutDirectory); command.Execute(); fileTransfers.AddRange(command.FileTransfers); @@ -228,7 +228,7 @@ namespace WixToolset.Core.Burn { var toProcess = payloadSymbols.Values.Where(r => !processedPayloads.Contains(r.Id.Id)).ToList(); - var command = new ProcessPayloadsCommand(this.BackendHelper, this.PayloadHarvester, toProcess, bundleSymbol.DefaultPackagingType, layoutDirectory); + var command = new ProcessPayloadsCommand(this.InternalBurnBackendHelper, this.PayloadHarvester, toProcess, bundleSymbol.DefaultPackagingType, layoutDirectory); command.Execute(); fileTransfers.AddRange(command.FileTransfers); @@ -381,6 +381,17 @@ namespace WixToolset.Core.Burn return; } + // Now that extensions can't change anything else, verify everything is still valid. + { + var command = new PerformBundleBackendValidationCommand(this.Messaging, this.InternalBurnBackendHelper, section, facades); + command.Execute(); + } + + if (this.Messaging.EncounteredError) + { + return; + } + // Generate data for all manifests. { var command = new GenerateManifestDataFromIRCommand(this.Messaging, section, this.BackendExtensions, this.InternalBurnBackendHelper, extensionSearchSymbolsById); diff --git a/src/wix/WixToolset.Core.Burn/Bundles/PerformBundleBackendValidationCommand.cs b/src/wix/WixToolset.Core.Burn/Bundles/PerformBundleBackendValidationCommand.cs new file mode 100644 index 00000000..ee18ff2c --- /dev/null +++ b/src/wix/WixToolset.Core.Burn/Bundles/PerformBundleBackendValidationCommand.cs @@ -0,0 +1,156 @@ +// 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.Burn.Bundles +{ + using System; + using System.Collections.Generic; + using WixToolset.Data; + using WixToolset.Data.Symbols; + using WixToolset.Extensibility.Data; + using WixToolset.Extensibility.Services; + + internal class PerformBundleBackendValidationCommand + { + public PerformBundleBackendValidationCommand(IMessaging messaging, IBurnBackendHelper burnBackendHelper, IntermediateSection section, IDictionary packageFacadesById) + { + this.Messaging = messaging; + this.BackendHelper = burnBackendHelper; + this.Section = section; + this.PackageFacadesById = packageFacadesById; + } + + public IMessaging Messaging { get; } + + public IBurnBackendHelper BackendHelper { get; } + + public IntermediateSection Section { get; } + + public IDictionary PackageFacadesById { get; } + + public void Execute() + { + foreach (var symbol in this.Section.Symbols) + { + if (symbol is WixBundleSymbol wixBundleSymbol) + { + this.ValidateBundle(wixBundleSymbol); + } + else if (symbol is WixBundleMsiPropertySymbol wixBundleMsiPropertySymbol) + { + this.ValidateMsiProperty(wixBundleMsiPropertySymbol); + } + else if (symbol is WixBundleVariableSymbol wixBundleVariableSymbol) + { + this.ValidateVariable(wixBundleVariableSymbol); + } + else if (symbol is WixBundlePackageCommandLineSymbol wixBundlePackageCommandLineSymbol) + { + this.ValidatePackageCommandLine(wixBundlePackageCommandLineSymbol); + } + else if (symbol is WixSearchSymbol wixSearchSymbol) + { + this.ValidateSearch(wixSearchSymbol); + } + } + + foreach (var packageFacade in this.PackageFacadesById.Values) + { + if (packageFacade.SpecificPackageSymbol is WixBundleExePackageSymbol wixBundleExePackageSymbol) + { + this.ValidateExePackage(wixBundleExePackageSymbol, packageFacade.PackageSymbol); + } + else if (packageFacade.SpecificPackageSymbol is WixBundleMsiPackageSymbol wixBundleMsiPackageSymbol) + { + this.ValidateMsiPackage(wixBundleMsiPackageSymbol, packageFacade.PackageSymbol); + } + else if (packageFacade.SpecificPackageSymbol is WixBundleMspPackageSymbol wixBundleMspPackageSymbol) + { + this.ValidateMspPackage(wixBundleMspPackageSymbol, packageFacade.PackageSymbol); + } + else if (packageFacade.SpecificPackageSymbol is WixBundleMsuPackageSymbol wixBundleMsuPackageSymbol) + { + this.ValidateMsuPackage(wixBundleMsuPackageSymbol, packageFacade.PackageSymbol); + } + } + } + + private void ValidateBundle(WixBundleSymbol symbol) + { + if (symbol.Condition != null) + { + this.BackendHelper.ValidateBundleCondition(symbol.SourceLineNumbers, "Bundle", "Condition", symbol.Condition, BundleConditionPhase.Startup); + } + } + + private void ValidateChainPackage(WixBundlePackageSymbol symbol, string elementName) + { + if (!String.IsNullOrEmpty(symbol.InstallCondition)) + { + this.BackendHelper.ValidateBundleCondition(symbol.SourceLineNumbers, elementName, "InstallCondition", symbol.InstallCondition, BundleConditionPhase.Plan); + } + } + + private void ValidateExePackage(WixBundleExePackageSymbol symbol, WixBundlePackageSymbol packageSymbol) + { + this.ValidateChainPackage(packageSymbol, "ExePackage"); + + if (!packageSymbol.Permanent) + { + this.BackendHelper.ValidateBundleCondition(symbol.SourceLineNumbers, "ExePackage", "DetectCondition", symbol.DetectCondition, BundleConditionPhase.Detect); + } + } + + private void ValidateMsiPackage(WixBundleMsiPackageSymbol symbol, WixBundlePackageSymbol packageSymbol) + { + this.ValidateChainPackage(packageSymbol, "MsiPackage"); + } + + private void ValidateMsiProperty(WixBundleMsiPropertySymbol symbol) + { + this.BackendHelper.ValidateBundleMsiPropertyName(symbol.SourceLineNumbers, "MsiProperty", "Name", symbol.Name); + + if (symbol.Condition != null) + { + this.BackendHelper.ValidateBundleCondition(symbol.SourceLineNumbers, "MsiProperty", "Condition", symbol.Condition, BundleConditionPhase.Execute); + } + } + + private void ValidateMspPackage(WixBundleMspPackageSymbol symbol, WixBundlePackageSymbol packageSymbol) + { + this.ValidateChainPackage(packageSymbol, "MspPackage"); + } + + private void ValidateMsuPackage(WixBundleMsuPackageSymbol symbol, WixBundlePackageSymbol packageSymbol) + { + this.ValidateChainPackage(packageSymbol, "MsuPackage"); + + if (!packageSymbol.Permanent) + { + this.BackendHelper.ValidateBundleCondition(symbol.SourceLineNumbers, "MsuPackage", "DetectCondition", symbol.DetectCondition, BundleConditionPhase.Detect); + } + } + + private void ValidatePackageCommandLine(WixBundlePackageCommandLineSymbol symbol) + { + if (symbol.Condition != null) + { + this.BackendHelper.ValidateBundleCondition(symbol.SourceLineNumbers, "CommandLine", "Condition", symbol.Condition, BundleConditionPhase.Execute); + } + } + + private void ValidateSearch(WixSearchSymbol symbol) + { + this.BackendHelper.ValidateBundleVariableName(symbol.SourceLineNumbers, "*Search", "Variable", symbol.Variable); + + if (symbol.Condition != null) + { + this.BackendHelper.ValidateBundleCondition(symbol.SourceLineNumbers, "*Search", "Condition", symbol.Condition, BundleConditionPhase.Detect); + } + } + + private void ValidateVariable(WixBundleVariableSymbol symbol) + { + this.BackendHelper.ValidateBundleVariableName(symbol.SourceLineNumbers, "Variable", "Name", symbol.Id.Id); + } + } +} diff --git a/src/wix/WixToolset.Core.Burn/Bundles/ProcessPayloadsCommand.cs b/src/wix/WixToolset.Core.Burn/Bundles/ProcessPayloadsCommand.cs index 3bd1f938..c3285884 100644 --- a/src/wix/WixToolset.Core.Burn/Bundles/ProcessPayloadsCommand.cs +++ b/src/wix/WixToolset.Core.Burn/Bundles/ProcessPayloadsCommand.cs @@ -15,7 +15,7 @@ namespace WixToolset.Core.Burn.Bundles internal class ProcessPayloadsCommand { - public ProcessPayloadsCommand(IBackendHelper backendHelper, IPayloadHarvester payloadHarvester, IEnumerable payloads, PackagingType defaultPackaging, string layoutDirectory) + public ProcessPayloadsCommand(IBurnBackendHelper backendHelper, IPayloadHarvester payloadHarvester, IEnumerable payloads, PackagingType defaultPackaging, string layoutDirectory) { this.BackendHelper = backendHelper; this.PayloadHarvester = payloadHarvester; @@ -28,7 +28,7 @@ namespace WixToolset.Core.Burn.Bundles public IEnumerable TrackedFiles { get; private set; } - private IBackendHelper BackendHelper { get; } + private IBurnBackendHelper BackendHelper { get; } private IPayloadHarvester PayloadHarvester { get; } diff --git a/src/wix/WixToolset.Core.Burn/ExtensibilityServices/BurnBackendHelper.cs b/src/wix/WixToolset.Core.Burn/ExtensibilityServices/BurnBackendHelper.cs index e4d2b0c9..d77606fb 100644 --- a/src/wix/WixToolset.Core.Burn/ExtensibilityServices/BurnBackendHelper.cs +++ b/src/wix/WixToolset.Core.Burn/ExtensibilityServices/BurnBackendHelper.cs @@ -20,6 +20,7 @@ namespace WixToolset.Core.Burn.ExtensibilityServices public static readonly XmlWriterSettings WriterSettings = new XmlWriterSettings { ConformanceLevel = ConformanceLevel.Fragment }; private readonly IBackendHelper backendHelper; + private readonly IBundleValidator bundleValidator; private ManifestData BootstrapperApplicationManifestData { get; } = new ManifestData(); @@ -28,49 +29,105 @@ namespace WixToolset.Core.Burn.ExtensibilityServices public BurnBackendHelper(IServiceProvider serviceProvider) { this.backendHelper = serviceProvider.GetService(); + this.bundleValidator = serviceProvider.GetService(); } #region IBackendHelper interfaces - public IFileFacade CreateFileFacade(FileSymbol file, AssemblySymbol assembly) => this.backendHelper.CreateFileFacade(file, assembly); - - public IFileFacade CreateFileFacade(FileRow fileRow) => this.backendHelper.CreateFileFacade(fileRow); + public IFileFacade CreateFileFacade(FileSymbol file, AssemblySymbol assembly) + { + return this.backendHelper.CreateFileFacade(file, assembly); + } - public IFileFacade CreateFileFacadeFromMergeModule(FileSymbol fileSymbol) => this.backendHelper.CreateFileFacadeFromMergeModule(fileSymbol); + public IFileFacade CreateFileFacade(FileRow fileRow) + { + return this.backendHelper.CreateFileFacade(fileRow); + } - public IFileTransfer CreateFileTransfer(string source, string destination, bool move, SourceLineNumber sourceLineNumbers = null) => this.backendHelper.CreateFileTransfer(source, destination, move, sourceLineNumbers); + public IFileFacade CreateFileFacadeFromMergeModule(FileSymbol fileSymbol) + { + return this.backendHelper.CreateFileFacadeFromMergeModule(fileSymbol); + } - public string CreateGuid() => this.backendHelper.CreateGuid(); + public IFileTransfer CreateFileTransfer(string source, string destination, bool move, SourceLineNumber sourceLineNumbers = null) + { + return this.backendHelper.CreateFileTransfer(source, destination, move, sourceLineNumbers); + } - public string CreateGuid(Guid namespaceGuid, string value) => this.backendHelper.CreateGuid(namespaceGuid, value); + public string CreateGuid() + { + return this.backendHelper.CreateGuid(); + } - public IResolvedDirectory CreateResolvedDirectory(string directoryParent, string name) => this.backendHelper.CreateResolvedDirectory(directoryParent, name); + public string CreateGuid(Guid namespaceGuid, string value) + { + return this.backendHelper.CreateGuid(namespaceGuid, value); + } - public IReadOnlyList ExtractEmbeddedFiles(IEnumerable embeddedFiles) => this.backendHelper.ExtractEmbeddedFiles(embeddedFiles); + public IResolvedDirectory CreateResolvedDirectory(string directoryParent, string name) + { + return this.backendHelper.CreateResolvedDirectory(directoryParent, name); + } - public string GenerateIdentifier(string prefix, params string[] args) => this.backendHelper.GenerateIdentifier(prefix, args); + public IReadOnlyList ExtractEmbeddedFiles(IEnumerable embeddedFiles) + { + return this.backendHelper.ExtractEmbeddedFiles(embeddedFiles); + } - public string GetCanonicalRelativePath(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string relativePath) => this.backendHelper.GetCanonicalRelativePath(sourceLineNumbers, elementName, attributeName, relativePath); + public string GenerateIdentifier(string prefix, params string[] args) + { + return this.backendHelper.GenerateIdentifier(prefix, args); + } - public int GetValidCodePage(string value, bool allowNoChange, bool onlyAnsi = false, SourceLineNumber sourceLineNumbers = null) => this.backendHelper.GetValidCodePage(value, allowNoChange, onlyAnsi, sourceLineNumbers); + public int GetValidCodePage(string value, bool allowNoChange, bool onlyAnsi = false, SourceLineNumber sourceLineNumbers = null) + { + return this.backendHelper.GetValidCodePage(value, allowNoChange, onlyAnsi, sourceLineNumbers); + } - public string GetMsiFileName(string value, bool source, bool longName) => this.backendHelper.GetMsiFileName(value, source, longName); + public string GetMsiFileName(string value, bool source, bool longName) + { + return this.backendHelper.GetMsiFileName(value, source, longName); + } - public bool IsValidBinderVariable(string variable) => this.backendHelper.IsValidBinderVariable(variable); + public bool IsValidBinderVariable(string variable) + { + return this.backendHelper.IsValidBinderVariable(variable); + } - public bool IsValidFourPartVersion(string version) => this.backendHelper.IsValidFourPartVersion(version); + public bool IsValidFourPartVersion(string version) + { + return this.backendHelper.IsValidFourPartVersion(version); + } - public bool IsValidIdentifier(string id) => this.backendHelper.IsValidIdentifier(id); + public bool IsValidIdentifier(string id) + { + return this.backendHelper.IsValidIdentifier(id); + } - public bool IsValidLongFilename(string filename, bool allowWildcards, bool allowRelative) => this.backendHelper.IsValidLongFilename(filename, allowWildcards, allowRelative); + public bool IsValidLongFilename(string filename, bool allowWildcards, bool allowRelative) + { + return this.backendHelper.IsValidLongFilename(filename, allowWildcards, allowRelative); + } - public bool IsValidShortFilename(string filename, bool allowWildcards) => this.backendHelper.IsValidShortFilename(filename, allowWildcards); + public bool IsValidShortFilename(string filename, bool allowWildcards) + { + return this.backendHelper.IsValidShortFilename(filename, allowWildcards); + } - public void ResolveDelayedFields(IEnumerable delayedFields, Dictionary variableCache) => this.backendHelper.ResolveDelayedFields(delayedFields, variableCache); + public void ResolveDelayedFields(IEnumerable delayedFields, Dictionary variableCache) + { + this.backendHelper.ResolveDelayedFields(delayedFields, variableCache); + } - public string[] SplitMsiFileName(string value) => this.backendHelper.SplitMsiFileName(value); + public string[] SplitMsiFileName(string value) + { + return this.backendHelper.SplitMsiFileName(value); + } - public ITrackedFile TrackFile(string path, TrackedFileType type, SourceLineNumber sourceLineNumbers = null) => this.backendHelper.TrackFile(path, type, sourceLineNumbers); + public ITrackedFile TrackFile(string path, TrackedFileType type, SourceLineNumber sourceLineNumbers = null) + { + return this.backendHelper.TrackFile(path, type, sourceLineNumbers); + } #endregion @@ -100,6 +157,28 @@ namespace WixToolset.Core.Burn.ExtensibilityServices #endregion + #region IBundleValidator + public string GetCanonicalRelativePath(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string relativePath) + { + return this.bundleValidator.GetCanonicalRelativePath(sourceLineNumbers, elementName, attributeName, relativePath); + } + + public bool ValidateBundleMsiPropertyName(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string propertyName) + { + return this.bundleValidator.ValidateBundleMsiPropertyName(sourceLineNumbers, elementName, attributeName, propertyName); + } + + public bool ValidateBundleVariableName(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string variableName) + { + return this.bundleValidator.ValidateBundleVariableName(sourceLineNumbers, elementName, attributeName, variableName); + } + + public bool ValidateBundleCondition(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string condition, BundleConditionPhase phase) + { + return this.bundleValidator.ValidateBundleCondition(sourceLineNumbers, elementName, attributeName, condition, phase); + } + #endregion + #region IInternalBurnBackendHelper interfaces public void WriteBootstrapperApplicationData(XmlWriter writer) diff --git a/src/wix/WixToolset.Core.WindowsInstaller/ExtensibilityServices/WindowsInstallerBackendHelper.cs b/src/wix/WixToolset.Core.WindowsInstaller/ExtensibilityServices/WindowsInstallerBackendHelper.cs index 8305b5e6..b3b4421a 100644 --- a/src/wix/WixToolset.Core.WindowsInstaller/ExtensibilityServices/WindowsInstallerBackendHelper.cs +++ b/src/wix/WixToolset.Core.WindowsInstaller/ExtensibilityServices/WindowsInstallerBackendHelper.cs @@ -23,45 +23,100 @@ namespace WixToolset.Core.WindowsInstaller.ExtensibilityServices #region IBackendHelper interfaces - public IFileFacade CreateFileFacade(FileSymbol file, AssemblySymbol assembly) => this.backendHelper.CreateFileFacade(file, assembly); - - public IFileFacade CreateFileFacade(FileRow fileRow) => this.backendHelper.CreateFileFacade(fileRow); + public IFileFacade CreateFileFacade(FileSymbol file, AssemblySymbol assembly) + { + return this.backendHelper.CreateFileFacade(file, assembly); + } - public IFileFacade CreateFileFacadeFromMergeModule(FileSymbol fileSymbol) => this.backendHelper.CreateFileFacadeFromMergeModule(fileSymbol); + public IFileFacade CreateFileFacade(FileRow fileRow) + { + return this.backendHelper.CreateFileFacade(fileRow); + } - public IFileTransfer CreateFileTransfer(string source, string destination, bool move, SourceLineNumber sourceLineNumbers = null) => this.backendHelper.CreateFileTransfer(source, destination, move, sourceLineNumbers); + public IFileFacade CreateFileFacadeFromMergeModule(FileSymbol fileSymbol) + { + return this.backendHelper.CreateFileFacadeFromMergeModule(fileSymbol); + } - public string CreateGuid() => this.backendHelper.CreateGuid(); + public IFileTransfer CreateFileTransfer(string source, string destination, bool move, SourceLineNumber sourceLineNumbers = null) + { + return this.backendHelper.CreateFileTransfer(source, destination, move, sourceLineNumbers); + } - public string CreateGuid(Guid namespaceGuid, string value) => this.backendHelper.CreateGuid(namespaceGuid, value); + public string CreateGuid() + { + return this.backendHelper.CreateGuid(); + } - public IResolvedDirectory CreateResolvedDirectory(string directoryParent, string name) => this.backendHelper.CreateResolvedDirectory(directoryParent, name); + public string CreateGuid(Guid namespaceGuid, string value) + { + return this.backendHelper.CreateGuid(namespaceGuid, value); + } - public IReadOnlyList ExtractEmbeddedFiles(IEnumerable embeddedFiles) => this.backendHelper.ExtractEmbeddedFiles(embeddedFiles); + public IResolvedDirectory CreateResolvedDirectory(string directoryParent, string name) + { + return this.backendHelper.CreateResolvedDirectory(directoryParent, name); + } - public string GenerateIdentifier(string prefix, params string[] args) => this.backendHelper.GenerateIdentifier(prefix, args); + public IReadOnlyList ExtractEmbeddedFiles(IEnumerable embeddedFiles) + { + return this.backendHelper.ExtractEmbeddedFiles(embeddedFiles); + } - public string GetCanonicalRelativePath(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string relativePath) => this.backendHelper.GetCanonicalRelativePath(sourceLineNumbers, elementName, attributeName, relativePath); + public string GenerateIdentifier(string prefix, params string[] args) + { + return this.backendHelper.GenerateIdentifier(prefix, args); + } - public int GetValidCodePage(string value, bool allowNoChange, bool onlyAnsi = false, SourceLineNumber sourceLineNumbers = null) => this.backendHelper.GetValidCodePage(value, allowNoChange, onlyAnsi, sourceLineNumbers); + public int GetValidCodePage(string value, bool allowNoChange, bool onlyAnsi = false, SourceLineNumber sourceLineNumbers = null) + { + return this.backendHelper.GetValidCodePage(value, allowNoChange, onlyAnsi, sourceLineNumbers); + } - public string GetMsiFileName(string value, bool source, bool longName) => this.backendHelper.GetMsiFileName(value, source, longName); + public string GetMsiFileName(string value, bool source, bool longName) + { + return this.backendHelper.GetMsiFileName(value, source, longName); + } - public bool IsValidBinderVariable(string variable) => this.backendHelper.IsValidBinderVariable(variable); + public bool IsValidBinderVariable(string variable) + { + return this.backendHelper.IsValidBinderVariable(variable); + } - public bool IsValidFourPartVersion(string version) => this.backendHelper.IsValidFourPartVersion(version); + public bool IsValidFourPartVersion(string version) + { + return this.backendHelper.IsValidFourPartVersion(version); + } - public bool IsValidIdentifier(string id) => this.backendHelper.IsValidIdentifier(id); + public bool IsValidIdentifier(string id) + { + return this.backendHelper.IsValidIdentifier(id); + } - public bool IsValidLongFilename(string filename, bool allowWildcards, bool allowRelative) => this.backendHelper.IsValidLongFilename(filename, allowWildcards, allowRelative); + public bool IsValidLongFilename(string filename, bool allowWildcards, bool allowRelative) + { + return this.backendHelper.IsValidLongFilename(filename, allowWildcards, allowRelative); + } - public bool IsValidShortFilename(string filename, bool allowWildcards) => this.backendHelper.IsValidShortFilename(filename, allowWildcards); + public bool IsValidShortFilename(string filename, bool allowWildcards) + { + return this.backendHelper.IsValidShortFilename(filename, allowWildcards); + } - public void ResolveDelayedFields(IEnumerable delayedFields, Dictionary variableCache) => this.backendHelper.ResolveDelayedFields(delayedFields, variableCache); + public void ResolveDelayedFields(IEnumerable delayedFields, Dictionary variableCache) + { + this.backendHelper.ResolveDelayedFields(delayedFields, variableCache); + } - public string[] SplitMsiFileName(string value) => this.backendHelper.SplitMsiFileName(value); + public string[] SplitMsiFileName(string value) + { + return this.backendHelper.SplitMsiFileName(value); + } - public ITrackedFile TrackFile(string path, TrackedFileType type, SourceLineNumber sourceLineNumbers = null) => this.backendHelper.TrackFile(path, type, sourceLineNumbers); + public ITrackedFile TrackFile(string path, TrackedFileType type, SourceLineNumber sourceLineNumbers = null) + { + return this.backendHelper.TrackFile(path, type, sourceLineNumbers); + } #endregion diff --git a/src/wix/WixToolset.Core/Common.cs b/src/wix/WixToolset.Core/Common.cs index 848f009a..8e341c52 100644 --- a/src/wix/WixToolset.Core/Common.cs +++ b/src/wix/WixToolset.Core/Common.cs @@ -27,27 +27,6 @@ namespace WixToolset.Core internal static readonly char[] IllegalRelativeLongFilenameCharacters = new[] { '?', '*', '|', '>', '<', ':', '\"' }; // like illegal, but we allow '\' and '/' internal static readonly char[] IllegalWildcardLongFilenameCharacters = new[] { '\\', '/', '|', '>', '<', ':', '\"' }; // like illegal: but we allow '*' and '?' - public static string GetCanonicalRelativePath(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string relativePath, IMessaging messageHandler) - { - const string root = @"C:\"; - if (!Path.IsPathRooted(relativePath)) - { - var normalizedPath = Path.GetFullPath(root + relativePath); - if (normalizedPath.StartsWith(root)) - { - var canonicalizedPath = normalizedPath.Substring(root.Length); - if (canonicalizedPath != relativePath) - { - messageHandler.Write(WarningMessages.PathCanonicalized(sourceLineNumbers, elementName, attributeName, relativePath, canonicalizedPath)); - } - return canonicalizedPath; - } - } - - messageHandler.Write(ErrorMessages.PayloadMustBeRelativeToCache(sourceLineNumbers, elementName, attributeName, relativePath)); - return relativePath; - } - /// /// Gets a valid code page from the given web name or integer value. /// diff --git a/src/wix/WixToolset.Core/CompilerCore.cs b/src/wix/WixToolset.Core/CompilerCore.cs index dc44f1b6..7effa0b9 100644 --- a/src/wix/WixToolset.Core/CompilerCore.cs +++ b/src/wix/WixToolset.Core/CompilerCore.cs @@ -17,24 +17,6 @@ namespace WixToolset.Core using WixToolset.Extensibility.Data; using WixToolset.Extensibility.Services; - internal enum ValueListKind - { - /// - /// A list of values with nothing before the final value. - /// - None, - - /// - /// A list of values with 'and' before the final value. - /// - And, - - /// - /// A list of values with 'or' before the final value. - /// - Or - } - /// /// Core class for the compiler. /// @@ -43,80 +25,6 @@ namespace WixToolset.Core internal static readonly XNamespace W3SchemaPrefix = "http://www.w3.org/"; internal static readonly XNamespace WixNamespace = "http://wixtoolset.org/schemas/v4/wxs"; - // Built-in variables (from burn\engine\variable.cpp, "vrgBuiltInVariables", around line 113) - private static readonly List BuiltinBundleVariables = new List( - new string[] { - "AdminToolsFolder", - "AppDataFolder", - "CommonAppDataFolder", - "CommonFiles64Folder", - "CommonFilesFolder", - "CompatibilityMode", - "Date", - "DesktopFolder", - "FavoritesFolder", - "FontsFolder", - "InstallerName", - "InstallerVersion", - "LocalAppDataFolder", - "LogonUser", - "MyPicturesFolder", - "NativeMachine", - "NTProductType", - "NTSuiteBackOffice", - "NTSuiteDataCenter", - "NTSuiteEnterprise", - "NTSuitePersonal", - "NTSuiteSmallBusiness", - "NTSuiteSmallBusinessRestricted", - "NTSuiteWebServer", - "PersonalFolder", - "Privileged", - "ProgramFiles64Folder", - "ProgramFiles6432Folder", - "ProgramFilesFolder", - "ProgramMenuFolder", - "RebootPending", - "SendToFolder", - "ServicePackLevel", - "StartMenuFolder", - "StartupFolder", - "System64Folder", - "SystemFolder", - "TempFolder", - "TemplateFolder", - "TerminalServer", - "UserLanguageID", - "UserUILanguageID", - "VersionMsi", - "VersionNT", - "VersionNT64", - "WindowsFolder", - "WindowsVolume", - "WixBundleAction", - "WixBundleCommandLineAction", - "WixBundleForcedRestartPackage", - "WixBundleElevated", - "WixBundleInstalled", - "WixBundleProviderKey", - "WixBundleTag", - "WixBundleVersion", - }); - - private static readonly List DisallowedMsiProperties = new List( - new string[] { - "ACTION", - "ADDLOCAL", - "ADDSOURCE", - "ADDDEFAULT", - "ADVERTISE", - "ALLUSERS", - "REBOOT", - "REINSTALL", - "REINSTALLMODE", - "REMOVE" - }); - private readonly Dictionary extensions; private readonly IParseHelper parseHelper; private readonly Intermediate intermediate; @@ -857,11 +765,7 @@ namespace WixToolset.Core if (!String.IsNullOrEmpty(value)) { - if (CompilerCore.BuiltinBundleVariables.Contains(value)) - { - string illegalValues = CompilerCore.CreateValueList(ValueListKind.Or, CompilerCore.BuiltinBundleVariables); - this.Write(ErrorMessages.IllegalAttributeValueWithIllegalList(sourceLineNumbers, attribute.Parent.Name.LocalName, attribute.Name.LocalName, value, illegalValues)); - } + this.parseHelper.ValidateBundleVariableName(sourceLineNumbers, attribute.Parent.Name.LocalName, attribute.Name.LocalName, value); } return value; @@ -879,11 +783,7 @@ namespace WixToolset.Core if (0 < value.Length) { - if (CompilerCore.DisallowedMsiProperties.Contains(value)) - { - string illegalValues = CompilerCore.CreateValueList(ValueListKind.Or, CompilerCore.DisallowedMsiProperties); - this.Write(ErrorMessages.DisallowedMsiProperty(sourceLineNumbers, value, illegalValues)); - } + this.parseHelper.ValidateBundleMsiPropertyName(sourceLineNumbers, attribute.Parent.Name.LocalName, attribute.Name.LocalName, value); } return value; @@ -1095,74 +995,5 @@ namespace WixToolset.Core { return this.parseHelper.ScheduleActionSymbol(this.ActiveSection, sourceLineNumbers, access, sequence, actionName, condition, beforeAction, afterAction, overridable); } - - private static string CreateValueList(ValueListKind kind, IEnumerable values) - { - // Ideally, we could denote the list kind (and the list itself) directly in the - // message XML, and detect and expand in the MessageHandler.GenerateMessageString() - // method. Doing so would make vararg-style messages much easier, but impacts - // every single message we format. For now, callers just have to know when a - // message takes a list of values in a single string argument, the caller will - // have to do the expansion themselves. (And, unfortunately, hard-code the knowledge - // that the list is an 'and' or 'or' list.) - - // For a localizable solution, we need to be able to get the list format string - // from resources. We aren't currently localized right now, so the values are - // just hard-coded. - const string valueFormat = "'{0}'"; - const string valueSeparator = ", "; - string terminalTerm = String.Empty; - - switch (kind) - { - case ValueListKind.None: - terminalTerm = ""; - break; - case ValueListKind.And: - terminalTerm = "and "; - break; - case ValueListKind.Or: - terminalTerm = "or "; - break; - } - - StringBuilder list = new StringBuilder(); - - // This weird construction helps us determine when we're adding the last value - // to the list. Instead of adding them as we encounter them, we cache the current - // value and append the *previous* one. - string previousValue = null; - bool haveValues = false; - foreach (string value in values) - { - if (null != previousValue) - { - if (haveValues) - { - list.Append(valueSeparator); - } - list.AppendFormat(valueFormat, previousValue); - haveValues = true; - } - - previousValue = value; - } - - // If we have no previous value, that means that the list contained no values, and - // something has gone very wrong. - Debug.Assert(null != previousValue); - if (null != previousValue) - { - if (haveValues) - { - list.Append(valueSeparator); - list.Append(terminalTerm); - } - list.AppendFormat(valueFormat, previousValue); - haveValues = true; - } - - return list.ToString(); - } } } diff --git a/src/wix/WixToolset.Core/ExtensibilityServices/BackendHelper.cs b/src/wix/WixToolset.Core/ExtensibilityServices/BackendHelper.cs index 9d657e1a..3348ad0b 100644 --- a/src/wix/WixToolset.Core/ExtensibilityServices/BackendHelper.cs +++ b/src/wix/WixToolset.Core/ExtensibilityServices/BackendHelper.cs @@ -64,11 +64,6 @@ namespace WixToolset.Core.ExtensibilityServices return Common.GenerateIdentifier(prefix, args); } - public string GetCanonicalRelativePath(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string relativePath) - { - return Common.GetCanonicalRelativePath(sourceLineNumbers, elementName, attributeName, relativePath, this.Messaging); - } - public int GetValidCodePage(string value, bool allowNoChange = false, bool onlyAnsi = false, SourceLineNumber sourceLineNumbers = null) { return Common.GetValidCodePage(value, allowNoChange, onlyAnsi, sourceLineNumbers); diff --git a/src/wix/WixToolset.Core/ExtensibilityServices/BundleValidator.cs b/src/wix/WixToolset.Core/ExtensibilityServices/BundleValidator.cs new file mode 100644 index 00000000..0149fe94 --- /dev/null +++ b/src/wix/WixToolset.Core/ExtensibilityServices/BundleValidator.cs @@ -0,0 +1,326 @@ +// 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.ExtensibilityServices +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.IO; + using System.Text; + using WixToolset.Data; + using WixToolset.Extensibility.Data; + using WixToolset.Extensibility.Services; + + internal class BundleValidator : IBundleValidator + { + public BundleValidator(IServiceProvider serviceProvider) + { + this.Messaging = serviceProvider.GetService(); + } + + protected IMessaging Messaging { get; } + + private enum ValueListKind + { + /// + /// A list of values with nothing before the final value. + /// + None, + + /// + /// A list of values with 'and' before the final value. + /// + And, + + /// + /// A list of values with 'or' before the final value. + /// + Or, + } + + // Built-in variables (from burn\engine\variable.cpp, "vrgBuiltInVariables", around line 207) + private static readonly List BuiltinBundleVariables = new List( + new string[] { + "AdminToolsFolder", + "AppDataFolder", + "CommonAppDataFolder", + "CommonFiles64Folder", + "CommonFilesFolder", + "CompatibilityMode", + "Date", + "DesktopFolder", + "FavoritesFolder", + "FontsFolder", + "InstallerName", + "InstallerVersion", + "LocalAppDataFolder", + "LogonUser", + "MyPicturesFolder", + "NativeMachine", + "NTProductType", + "NTSuiteBackOffice", + "NTSuiteDataCenter", + "NTSuiteEnterprise", + "NTSuitePersonal", + "NTSuiteSmallBusiness", + "NTSuiteSmallBusinessRestricted", + "NTSuiteWebServer", + "PersonalFolder", + "Privileged", + "ProgramFiles64Folder", + "ProgramFiles6432Folder", + "ProgramFilesFolder", + "ProgramMenuFolder", + "RebootPending", + "SendToFolder", + "ServicePackLevel", + "StartMenuFolder", + "StartupFolder", + "System64Folder", + "SystemFolder", + "TempFolder", + "TemplateFolder", + "TerminalServer", + "UserLanguageID", + "UserUILanguageID", + "VersionMsi", + "VersionNT", + "VersionNT64", + "WindowsFolder", + "WindowsVolume", + "WixBundleAction", + "WixBundleCommandLineAction", + "WixBundleForcedRestartPackage", + "WixBundleElevated", + "WixBundleInstalled", + "WixBundleProviderKey", + "WixBundleTag", + "WixBundleVersion", + }); + + private static readonly List DisallowedMsiProperties = new List( + new string[] { + "ACTION", + "ADDLOCAL", + "ADDSOURCE", + "ADDDEFAULT", + "ADVERTISE", + "ALLUSERS", + "REBOOT", + "REINSTALL", + "REINSTALLMODE", + "REMOVE" + }); + + private static readonly List UnavailableStartupVariables = new List( + new string[] { + "RebootPending", + "WixBundleAction", + "WixBundleInstalled", + }); + + private static readonly List UnavailableDetectVariables = new List( + new string[] { + "WixBundleAction", + }); + + public string GetCanonicalRelativePath(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string relativePath) + { + const string root = @"C:\"; + if (!Path.IsPathRooted(relativePath)) + { + var normalizedPath = Path.GetFullPath(root + relativePath); + if (normalizedPath.StartsWith(root)) + { + var canonicalizedPath = normalizedPath.Substring(root.Length); + if (canonicalizedPath != relativePath) + { + this.Messaging.Write(WarningMessages.PathCanonicalized(sourceLineNumbers, elementName, attributeName, relativePath, canonicalizedPath)); + } + return canonicalizedPath; + } + } + + this.Messaging.Write(ErrorMessages.PayloadMustBeRelativeToCache(sourceLineNumbers, elementName, attributeName, relativePath)); + return relativePath; + } + + public bool ValidateBundleVariableName(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string variableName) + { + if (String.IsNullOrEmpty(variableName)) + { + this.Messaging.Write(ErrorMessages.IllegalEmptyAttributeValue(sourceLineNumbers, elementName, attributeName)); + + return false; + } + else if (BuiltinBundleVariables.Contains(variableName)) + { + var illegalValues = CreateValueList(ValueListKind.Or, BuiltinBundleVariables); + this.Messaging.Write(ErrorMessages.IllegalAttributeValueWithIllegalList(sourceLineNumbers, elementName, attributeName, variableName, illegalValues)); + + return false; + } + else + { + return true; + } + } + + public bool ValidateBundleMsiPropertyName(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string propertyName) + { + if (String.IsNullOrEmpty(propertyName)) + { + this.Messaging.Write(ErrorMessages.IllegalEmptyAttributeValue(sourceLineNumbers, elementName, attributeName)); + + return false; + } + else if (DisallowedMsiProperties.Contains(propertyName)) + { + var illegalValues = CreateValueList(ValueListKind.Or, DisallowedMsiProperties); + this.Messaging.Write(ErrorMessages.DisallowedMsiProperty(sourceLineNumbers, propertyName, illegalValues)); + + return false; + } + else + { + return true; + } + } + + public bool ValidateBundleCondition(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string condition, BundleConditionPhase phase) + { + if (!this.TryParseCondition(sourceLineNumbers, elementName, attributeName, condition)) + { + return false; + } + + + // TODO: These lists are incomplete. + List unavailableVariables = null; + switch (phase) + { + case BundleConditionPhase.Startup: + unavailableVariables = UnavailableStartupVariables; + break; + case BundleConditionPhase.Detect: + unavailableVariables = UnavailableDetectVariables; + break; + } + + if (unavailableVariables != null) + { + return this.ValidateBundleConditionUnavailableVariables(sourceLineNumbers, elementName, attributeName, condition, unavailableVariables); + } + else + { + return true; + } + } + + private bool ValidateBundleConditionUnavailableVariables(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string condition, List unavailableVariables) + { + foreach (var variableName in unavailableVariables) + { + //TODO: use the results of parsing to validate that the restricted variables are actually used as variables + if (condition.Contains(variableName)) + { + var illegalValues = CreateValueList(ValueListKind.Or, unavailableVariables); + this.Messaging.Write(WarningMessages.UnavailableBundleConditionVariable(sourceLineNumbers, elementName, attributeName, variableName, illegalValues)); + + return false; + } + } + + return true; + } + + private bool TryParseCondition(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string condition) + { + if (String.IsNullOrEmpty(condition)) + { + this.Messaging.Write(ErrorMessages.IllegalEmptyAttributeValue(sourceLineNumbers, elementName, attributeName)); + + return false; + } + //TODO: Actually parse the condition to definitively tell which Variables are referenced. + else if (condition.Trim() == "=") + { + this.Messaging.Write(ErrorMessages.InvalidBundleCondition(sourceLineNumbers, elementName, attributeName, condition)); + return false; + } + else + { + return true; + } + } + + private static string CreateValueList(ValueListKind kind, IEnumerable values) + { + // Ideally, we could denote the list kind (and the list itself) directly in the + // message XML, and detect and expand in the MessageHandler.GenerateMessageString() + // method. Doing so would make vararg-style messages much easier, but impacts + // every single message we format. For now, callers just have to know when a + // message takes a list of values in a single string argument, the caller will + // have to do the expansion themselves. (And, unfortunately, hard-code the knowledge + // that the list is an 'and' or 'or' list.) + + // For a localizable solution, we need to be able to get the list format string + // from resources. We aren't currently localized right now, so the values are + // just hard-coded. + const string valueFormat = "'{0}'"; + const string valueSeparator = ", "; + var terminalTerm = String.Empty; + + switch (kind) + { + case ValueListKind.None: + terminalTerm = ""; + break; + case ValueListKind.And: + terminalTerm = "and "; + break; + case ValueListKind.Or: + terminalTerm = "or "; + break; + } + + var list = new StringBuilder(); + + // This weird construction helps us determine when we're adding the last value + // to the list. Instead of adding them as we encounter them, we cache the current + // value and append the *previous* one. + string previousValue = null; + var haveValues = false; + foreach (var value in values) + { + if (null != previousValue) + { + if (haveValues) + { + list.Append(valueSeparator); + } + list.AppendFormat(valueFormat, previousValue); + haveValues = true; + } + + previousValue = value; + } + + // If we have no previous value, that means that the list contained no values, and + // something has gone very wrong. + Debug.Assert(null != previousValue); + if (null != previousValue) + { + if (haveValues) + { + list.Append(valueSeparator); + list.Append(terminalTerm); + } + list.AppendFormat(valueFormat, previousValue); + //haveValues = true; + } + + return list.ToString(); + } + } +} diff --git a/src/wix/WixToolset.Core/ExtensibilityServices/ParseHelper.cs b/src/wix/WixToolset.Core/ExtensibilityServices/ParseHelper.cs index 1a678b0d..fa4f50ba 100644 --- a/src/wix/WixToolset.Core/ExtensibilityServices/ParseHelper.cs +++ b/src/wix/WixToolset.Core/ExtensibilityServices/ParseHelper.cs @@ -22,11 +22,14 @@ namespace WixToolset.Core.ExtensibilityServices { this.ServiceProvider = serviceProvider; + this.BundleValidator = serviceProvider.GetService(); this.Messaging = serviceProvider.GetService(); } private IServiceProvider ServiceProvider { get; } + private IBundleValidator BundleValidator { get; } + private IMessaging Messaging { get; } private ISymbolDefinitionCreator Creator { get; set; } @@ -239,11 +242,14 @@ namespace WixToolset.Core.ExtensibilityServices public void CreateWixSearchSymbol(IntermediateSection section, SourceLineNumber sourceLineNumbers, string elementName, Identifier id, string variable, string condition, string after, string bundleExtensionId) { - // TODO: verify variable is not a standard bundle variable if (variable == null) { this.Messaging.Write(ErrorMessages.ExpectedAttribute(sourceLineNumbers, elementName, "Variable")); } + else + { + this.BundleValidator.ValidateBundleVariableName(sourceLineNumbers, elementName, "Variable", variable); + } section.AddSymbol(new WixSearchSymbol(sourceLineNumbers, id) { @@ -623,11 +629,6 @@ namespace WixToolset.Core.ExtensibilityServices } } - public string GetCanonicalRelativePath(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string relativePath) - { - return Common.GetCanonicalRelativePath(sourceLineNumbers, elementName, attributeName, relativePath, this.Messaging); - } - public SourceLineNumber GetSourceLineNumbers(XElement element) { return Preprocessor.GetSourceLineNumbers(element); @@ -858,5 +859,27 @@ namespace WixToolset.Core.ExtensibilityServices return extension != null; } + + #region IBundleValidator + public string GetCanonicalRelativePath(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string relativePath) + { + return this.BundleValidator.GetCanonicalRelativePath(sourceLineNumbers, elementName, attributeName, relativePath); + } + + public bool ValidateBundleMsiPropertyName(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string propertyName) + { + return this.BundleValidator.ValidateBundleMsiPropertyName(sourceLineNumbers, elementName, attributeName, propertyName); + } + + public bool ValidateBundleVariableName(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string variableName) + { + return this.BundleValidator.ValidateBundleVariableName(sourceLineNumbers, elementName, attributeName, variableName); + } + + public bool ValidateBundleCondition(SourceLineNumber sourceLineNumbers, string elementName, string attributeName, string condition, BundleConditionPhase phase) + { + return this.BundleValidator.ValidateBundleCondition(sourceLineNumbers, elementName, attributeName, condition, phase); + } + #endregion } } diff --git a/src/wix/WixToolset.Core/WixToolsetServiceProvider.cs b/src/wix/WixToolset.Core/WixToolsetServiceProvider.cs index a74ba6b3..9fbf6717 100644 --- a/src/wix/WixToolset.Core/WixToolsetServiceProvider.cs +++ b/src/wix/WixToolset.Core/WixToolsetServiceProvider.cs @@ -20,6 +20,7 @@ namespace WixToolset.Core // Singletons. this.AddService((provider, singletons) => AddSingleton(singletons, new ExtensionManager(provider))); this.AddService((provider, singletons) => AddSingleton(singletons, new Messaging())); + this.AddService((provider, singletons) => AddSingleton(singletons, new BundleValidator(provider))); this.AddService((provider, singletons) => AddSingleton(singletons, new SymbolDefinitionCreator(provider))); this.AddService((provider, singletons) => AddSingleton(singletons, new ParseHelper(provider))); this.AddService((provider, singletons) => AddSingleton(singletons, new PreprocessHelper(provider))); diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/BadInputFixture.cs b/src/wix/test/WixToolsetTest.CoreIntegration/BadInputFixture.cs index f91c0ab0..e5d6ecf1 100644 --- a/src/wix/test/WixToolsetTest.CoreIntegration/BadInputFixture.cs +++ b/src/wix/test/WixToolsetTest.CoreIntegration/BadInputFixture.cs @@ -4,6 +4,7 @@ namespace WixToolsetTest.CoreIntegration { using System; using System.IO; + using System.Linq; using WixBuildTools.TestSupport; using WixToolset.Core.TestPackage; using Xunit; @@ -129,5 +130,47 @@ namespace WixToolsetTest.CoreIntegration Assert.Equal(193, result.ExitCode); } } + + [Fact] + public void GuardsAgainstVariousBundleValuesFromLoc() + { + var folder = TestData.Get(@"TestData"); + + using (var fs = new DisposableFileSystem()) + { + var baseFolder = fs.GetFolder(); + var intermediateFolder = Path.Combine(baseFolder, "obj"); + + var result = WixRunner.Execute(new[] + { + "build", + Path.Combine(folder, "BundleWithInvalid", "BundleWithInvalidLocValues.wxs"), + "-loc", Path.Combine(folder, "BundleWithInvalid", "BundleWithInvalidLocValues.wxl"), + "-bindpath", Path.Combine(folder, ".Data"), + "-bindpath", Path.Combine(folder, "DecompileSingleFileCompressed"), + "-bindpath", Path.Combine(folder, "SimpleBundle", "data"), + "-intermediateFolder", intermediateFolder, + "-o", Path.Combine(baseFolder, @"bin\test.exe") + }); + + Assert.InRange(result.ExitCode, 2, Int32.MaxValue); + + var messages = result.Messages.Select(m => m.ToString()).ToList(); + messages.Sort(); + + WixAssert.CompareLineByLine(new[] + { + "*Search/@Condition contains the built-in Variable 'WixBundleAction', which is not available when it is evaluated. (Unavailable Variables are: 'WixBundleAction'.). Rewrite the condition to avoid Variables that are never valid during its evaluation.", + "Bundle/@Condition contains the built-in Variable 'WixBundleInstalled', which is not available when it is evaluated. (Unavailable Variables are: 'RebootPending', 'WixBundleAction', or 'WixBundleInstalled'.). Rewrite the condition to avoid Variables that are never valid during its evaluation.", + "ExePackage/@DetectCondition contains the built-in Variable 'WixBundleAction', which is not available when it is evaluated. (Unavailable Variables are: 'WixBundleAction'.). Rewrite the condition to avoid Variables that are never valid during its evaluation.", + "The *Search/@Variable attribute's value, 'WixBundleInstalled', is one of the illegal options: 'AdminToolsFolder', 'AppDataFolder', 'CommonAppDataFolder', 'CommonFiles64Folder', 'CommonFilesFolder', 'CompatibilityMode', 'Date', 'DesktopFolder', 'FavoritesFolder', 'FontsFolder', 'InstallerName', 'InstallerVersion', 'LocalAppDataFolder', 'LogonUser', 'MyPicturesFolder', 'NativeMachine', 'NTProductType', 'NTSuiteBackOffice', 'NTSuiteDataCenter', 'NTSuiteEnterprise', 'NTSuitePersonal', 'NTSuiteSmallBusiness', 'NTSuiteSmallBusinessRestricted', 'NTSuiteWebServer', 'PersonalFolder', 'Privileged', 'ProgramFiles64Folder', 'ProgramFiles6432Folder', 'ProgramFilesFolder', 'ProgramMenuFolder', 'RebootPending', 'SendToFolder', 'ServicePackLevel', 'StartMenuFolder', 'StartupFolder', 'System64Folder', 'SystemFolder', 'TempFolder', 'TemplateFolder', 'TerminalServer', 'UserLanguageID', 'UserUILanguageID', 'VersionMsi', 'VersionNT', 'VersionNT64', 'WindowsFolder', 'WindowsVolume', 'WixBundleAction', 'WixBundleCommandLineAction', 'WixBundleForcedRestartPackage', 'WixBundleElevated', 'WixBundleInstalled', 'WixBundleProviderKey', 'WixBundleTag', or 'WixBundleVersion'.", + "The CommandLine/@Condition attribute's value '=' is not a valid bundle condition.", + "The MsiPackage/@InstallCondition attribute's value '=' is not a valid bundle condition.", + "The MsiProperty/@Condition attribute's value '=' is not a valid bundle condition.", + //"The Variable/@Name attribute's value, 'WixBundleInstalled', is one of the illegal options: 'AdminToolsFolder', 'AppDataFolder', 'CommonAppDataFolder', 'CommonFiles64Folder', 'CommonFilesFolder', 'CompatibilityMode', 'Date', 'DesktopFolder', 'FavoritesFolder', 'FontsFolder', 'InstallerName', 'InstallerVersion', 'LocalAppDataFolder', 'LogonUser', 'MyPicturesFolder', 'NativeMachine', 'NTProductType', 'NTSuiteBackOffice', 'NTSuiteDataCenter', 'NTSuiteEnterprise', 'NTSuitePersonal', 'NTSuiteSmallBusiness', 'NTSuiteSmallBusinessRestricted', 'NTSuiteWebServer', 'PersonalFolder', 'Privileged', 'ProgramFiles64Folder', 'ProgramFiles6432Folder', 'ProgramFilesFolder', 'ProgramMenuFolder', 'RebootPending', 'SendToFolder', 'ServicePackLevel', 'StartMenuFolder', 'StartupFolder', 'System64Folder', 'SystemFolder', 'TempFolder', 'TemplateFolder', 'TerminalServer', 'UserLanguageID', 'UserUILanguageID', 'VersionMsi', 'VersionNT', 'VersionNT64', 'WindowsFolder', 'WindowsVolume', 'WixBundleAction', 'WixBundleCommandLineAction', 'WixBundleForcedRestartPackage', 'WixBundleElevated', 'WixBundleInstalled', 'WixBundleProviderKey', 'WixBundleTag', or 'WixBundleVersion'.", + "The 'REINSTALLMODE' MsiProperty is controlled by the bootstrapper and cannot be authored. (Illegal properties are: 'ACTION', 'ADDLOCAL', 'ADDSOURCE', 'ADDDEFAULT', 'ADVERTISE', 'ALLUSERS', 'REBOOT', 'REINSTALL', 'REINSTALLMODE', or 'REMOVE'.) Remove the MsiProperty element.", + }, messages.ToArray()); + } + } } } diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/BundleWithInvalid/BundleWithInvalidLocValues.wxl b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/BundleWithInvalid/BundleWithInvalidLocValues.wxl new file mode 100644 index 00000000..0b5fac56 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/BundleWithInvalid/BundleWithInvalidLocValues.wxl @@ -0,0 +1,8 @@ + + REINSTALLMODE + WixBundleInstalled + WixBundleAction = 4 + = + WixBundleInstalled <> 1 + = + diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/BundleWithInvalid/BundleWithInvalidLocValues.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/BundleWithInvalid/BundleWithInvalidLocValues.wxs new file mode 100644 index 00000000..504f6e48 --- /dev/null +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/BundleWithInvalid/BundleWithInvalidLocValues.wxs @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + -- cgit v1.2.3-55-g6feb