From 28a1a91cd2c20486276ee37dacce623ed8a75d6e Mon Sep 17 00:00:00 2001 From: Rob Mensching Date: Tue, 27 Sep 2022 01:44:33 -0700 Subject: Refactor patch filtering on the path towards making it work properly --- .../Bind/AttachPatchTransformsCommand.cs | 1312 -------------------- .../Bind/CreatePatchSubStoragesCommand.cs | 791 ++++++++++++ .../Bind/CreatePatchTransformsCommand.cs | 17 +- .../Bind/GenerateSectionIdsCommand.cs | 225 ++++ .../Bind/ReduceTransformCommand.cs | 549 ++++++++ .../WixToolset.Core.WindowsInstaller/MspBackend.cs | 8 +- .../Unbind/UnbindDatabaseCommand.cs | 220 +--- src/wix/WixToolset.Core/Compiler.cs | 6 +- .../WixToolsetTest.CoreIntegration/PatchFixture.cs | 29 + .../TestData/PatchFamilyFilter/Patch.wxs | 4 +- 10 files changed, 1630 insertions(+), 1531 deletions(-) delete mode 100644 src/wix/WixToolset.Core.WindowsInstaller/Bind/AttachPatchTransformsCommand.cs create mode 100644 src/wix/WixToolset.Core.WindowsInstaller/Bind/CreatePatchSubStoragesCommand.cs create mode 100644 src/wix/WixToolset.Core.WindowsInstaller/Bind/GenerateSectionIdsCommand.cs create mode 100644 src/wix/WixToolset.Core.WindowsInstaller/Bind/ReduceTransformCommand.cs diff --git a/src/wix/WixToolset.Core.WindowsInstaller/Bind/AttachPatchTransformsCommand.cs b/src/wix/WixToolset.Core.WindowsInstaller/Bind/AttachPatchTransformsCommand.cs deleted file mode 100644 index 6d37fdc2..00000000 --- a/src/wix/WixToolset.Core.WindowsInstaller/Bind/AttachPatchTransformsCommand.cs +++ /dev/null @@ -1,1312 +0,0 @@ -// 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.WindowsInstaller.Bind -{ - using System; - using System.Collections.Generic; - using System.Globalization; - using System.Linq; - using System.Text.RegularExpressions; - using WixToolset.Core.Native.Msi; - using WixToolset.Data; - using WixToolset.Data.Symbols; - using WixToolset.Data.WindowsInstaller; - using WixToolset.Data.WindowsInstaller.Rows; - using WixToolset.Extensibility.Services; - - /// - /// Include transforms in a patch. - /// - internal class AttachPatchTransformsCommand - { - private static readonly string[] PatchUninstallBreakingTables = new[] - { - "AppId", - "BindImage", - "Class", - "Complus", - "CreateFolder", - "DuplicateFile", - "Environment", - "Extension", - "Font", - "IniFile", - "IsolatedComponent", - "LockPermissions", - "MIME", - "MoveFile", - "MsiLockPermissionsEx", - "MsiServiceConfig", - "MsiServiceConfigFailureActions", - "ODBCAttribute", - "ODBCDataSource", - "ODBCDriver", - "ODBCSourceAttribute", - "ODBCTranslator", - "ProgId", - "PublishComponent", - "RemoveIniFile", - "SelfReg", - "ServiceControl", - "ServiceInstall", - "TypeLib", - "Verb", - }; - - private readonly TableDefinitionCollection tableDefinitions; - - public AttachPatchTransformsCommand(IMessaging messaging, IBackendHelper backendHelper, Intermediate intermediate, IEnumerable transforms) - { - this.tableDefinitions = new TableDefinitionCollection(WindowsInstallerTableDefinitions.All); - this.Messaging = messaging; - this.BackendHelper = backendHelper; - this.Intermediate = intermediate; - this.Transforms = transforms; - } - - private IMessaging Messaging { get; } - - private IBackendHelper BackendHelper { get; } - - private Intermediate Intermediate { get; } - - private IEnumerable Transforms { get; } - - public IEnumerable SubStorages { get; private set; } - - public IEnumerable Execute() - { - var subStorages = new List(); - - if (this.Transforms == null || !this.Transforms.Any()) - { - this.Messaging.Write(ErrorMessages.PatchWithoutTransforms()); - return subStorages; - } - - var summaryInfo = this.ExtractPatchSummaryInfo(); - - var section = this.Intermediate.Sections.First(); - - var symbols = this.Intermediate.Sections.SelectMany(s => s.Symbols).ToList(); - - // Get the patch id from the WixPatchId symbol. - var patchSymbol = symbols.OfType().FirstOrDefault(); - - if (String.IsNullOrEmpty(patchSymbol.Id?.Id)) - { - this.Messaging.Write(ErrorMessages.ExpectedPatchIdInWixMsp()); - return subStorages; - } - - if (String.IsNullOrEmpty(patchSymbol.ClientPatchId)) - { - this.Messaging.Write(ErrorMessages.ExpectedClientPatchIdInWixMsp()); - return subStorages; - } - - // enumerate patch.Media to map diskId to Media row - var patchMediaByDiskId = symbols.OfType().ToDictionary(t => t.DiskId); - - if (patchMediaByDiskId.Count == 0) - { - this.Messaging.Write(ErrorMessages.ExpectedMediaRowsInWixMsp()); - return subStorages; - } - - // populate MSP summary information - var patchMetadata = this.PopulateSummaryInformation(summaryInfo, symbols, patchSymbol); - - // enumerate transforms - var productCodes = new SortedSet(); - var transformNames = new List(); - var validTransform = new List>(); - - var baselineSymbolsById = symbols.OfType().ToDictionary(t => t.Id.Id); - - foreach (var mainTransform in this.Transforms) - { - var baselineSymbol = baselineSymbolsById[mainTransform.Baseline]; - - var patchRefSymbols = symbols.OfType().ToList(); - if (patchRefSymbols.Count > 0) - { - if (!this.ReduceTransform(mainTransform.Transform, patchRefSymbols)) - { - // transform has none of the content authored into this patch - continue; - } - } - - // Validate the transform doesn't break any patch specific rules. - this.Validate(mainTransform); - - // ensure consistent File.Sequence within each Media - var mediaSymbol = patchMediaByDiskId[baselineSymbol.DiskId]; - - // Ensure that files are sequenced after the last file in any transform. - if (mainTransform.Transform.Tables.TryGetTable("Media", out var transformMediaTable)) - { - foreach (MediaRow transformMediaRow in transformMediaTable.Rows) - { - if (!mediaSymbol.LastSequence.HasValue || mediaSymbol.LastSequence < transformMediaRow.LastSequence) - { - // The Binder will pre-increment the sequence. - mediaSymbol.LastSequence = transformMediaRow.LastSequence; - } - } - } - - // Use the Media/@DiskId if greater than the last sequence for backward compatibility. - if (!mediaSymbol.LastSequence.HasValue || mediaSymbol.LastSequence < mediaSymbol.DiskId) - { - mediaSymbol.LastSequence = mediaSymbol.DiskId; - } - - // Ignore media table in the transform. - mainTransform.Transform.Tables.Remove("Media"); - mainTransform.Transform.Tables.Remove("MsiDigitalSignature"); - - var pairedTransform = this.BuildPairedTransform(summaryInfo, patchMetadata, patchSymbol, mainTransform.Transform, mediaSymbol, baselineSymbol, out var productCode); - - productCode = productCode.ToUpperInvariant(); - productCodes.Add(productCode); - validTransform.Add(Tuple.Create(productCode, mainTransform.Transform)); - - // Attach the main and paired transforms to the patch object. - var baseTransformName = mainTransform.Baseline; - var countSuffix = "." + validTransform.Count.ToString(CultureInfo.InvariantCulture); - - if (PatchConstants.PairedPatchTransformPrefix.Length + baseTransformName.Length + countSuffix.Length > PatchConstants.MaxPatchTransformName) - { - var trimmedTransformName = baseTransformName.Substring(0, PatchConstants.MaxPatchTransformName - PatchConstants.PairedPatchTransformPrefix.Length - countSuffix.Length); - - this.Messaging.Write(WindowsInstallerBackendWarnings.LongPatchBaselineIdTrimmed(baselineSymbol.SourceLineNumbers, baseTransformName, trimmedTransformName)); - - baseTransformName = trimmedTransformName; - } - - var transformName = baseTransformName + countSuffix; - subStorages.Add(new SubStorage(transformName, mainTransform.Transform)); - transformNames.Add(":" + transformName); - - var pairedTransformName = PatchConstants.PairedPatchTransformPrefix + transformName; - subStorages.Add(new SubStorage(pairedTransformName, pairedTransform)); - transformNames.Add(":" + pairedTransformName); - } - - if (validTransform.Count == 0) - { - this.Messaging.Write(ErrorMessages.PatchWithoutValidTransforms()); - return subStorages; - } - - // Validate that a patch authored as removable is actually removable - if (patchMetadata.TryGetValue("AllowRemoval", out var allowRemoval) && allowRemoval.Value == "1") - { - var uninstallable = true; - - foreach (var entry in validTransform) - { - uninstallable &= this.CheckUninstallableTransform(entry.Item1, entry.Item2); - } - - if (!uninstallable) - { - this.Messaging.Write(ErrorMessages.PatchNotRemovable()); - return subStorages; - } - } - - // Finish filling tables with transform-dependent data. - productCodes = FinalizePatchProductCodes(symbols, productCodes); - - // Semicolon delimited list of the product codes that can accept the patch. - summaryInfo.Add(SummaryInformationType.PatchProductCodes, new SummaryInformationSymbol(patchSymbol.SourceLineNumbers) - { - PropertyId = SummaryInformationType.PatchProductCodes, - Value = String.Join(";", productCodes) - }); - - // Semicolon delimited list of transform substorage names in the order they are applied. - summaryInfo.Add(SummaryInformationType.TransformNames, new SummaryInformationSymbol(patchSymbol.SourceLineNumbers) - { - PropertyId = SummaryInformationType.TransformNames, - Value = String.Join(";", transformNames) - }); - - // Put the summary information that was extracted back in now that it is updated. - foreach (var readSummaryInfo in summaryInfo.Values.OrderBy(s => s.PropertyId)) - { - section.AddSymbol(readSummaryInfo); - } - - this.SubStorages = subStorages; - - return subStorages; - } - - private Dictionary ExtractPatchSummaryInfo() - { - var result = new Dictionary(); - - foreach (var section in this.Intermediate.Sections) - { - // Remove all summary information from the symbols and remember those that - // are not calculated or reserved. - foreach (var patchSummaryInfo in section.Symbols.OfType().ToList()) - { - section.RemoveSymbol(patchSummaryInfo); - - if (patchSummaryInfo.PropertyId != SummaryInformationType.PatchProductCodes && - patchSummaryInfo.PropertyId != SummaryInformationType.PatchCode && - patchSummaryInfo.PropertyId != SummaryInformationType.PatchInstallerRequirement && - patchSummaryInfo.PropertyId != SummaryInformationType.Reserved11 && - patchSummaryInfo.PropertyId != SummaryInformationType.Reserved14 && - patchSummaryInfo.PropertyId != SummaryInformationType.Reserved16) - { - result.Add(patchSummaryInfo.PropertyId, patchSummaryInfo); - } - } - } - - return result; - } - - private Dictionary PopulateSummaryInformation(Dictionary summaryInfo, List symbols, WixPatchSymbol patchSymbol) - { - // PID_CODEPAGE - if (!summaryInfo.ContainsKey(SummaryInformationType.Codepage)) - { - // Set the code page by default to the same code page for the - // string pool in the database. - AddSummaryInformation(SummaryInformationType.Codepage, patchSymbol.Codepage?.ToString(CultureInfo.InvariantCulture) ?? "0", patchSymbol.SourceLineNumbers); - } - - // GUID patch code for the patch. - AddSummaryInformation(SummaryInformationType.PatchCode, patchSymbol.Id.Id, patchSymbol.SourceLineNumbers); - - // Indicates the minimum Windows Installer version that is required to install the patch. - AddSummaryInformation(SummaryInformationType.PatchInstallerRequirement, ((int)SummaryInformation.InstallerRequirement.Version31).ToString(CultureInfo.InvariantCulture), patchSymbol.SourceLineNumbers); - - if (!summaryInfo.ContainsKey(SummaryInformationType.Security)) - { - AddSummaryInformation(SummaryInformationType.Security, "4", patchSymbol.SourceLineNumbers); // Read-only enforced; - } - - // Use authored comments or default to display name. - MsiPatchMetadataSymbol commentsSymbol = null; - - var metadataSymbols = symbols.OfType().Where(t => String.IsNullOrEmpty(t.Company)).ToDictionary(t => t.Property); - - if (!summaryInfo.ContainsKey(SummaryInformationType.Title) && - metadataSymbols.TryGetValue("DisplayName", out var displayName)) - { - AddSummaryInformation(SummaryInformationType.Title, displayName.Value, displayName.SourceLineNumbers); - - // Default comments to use display name as-is. - commentsSymbol = displayName; - } - - // TODO: This code below seems unnecessary given the codepage is set at the top of this method. - //if (!summaryInfo.ContainsKey(SummaryInformationType.Codepage) && - // metadataValues.TryGetValue("CodePage", out var codepage)) - //{ - // AddSummaryInformation(SummaryInformationType.Codepage, codepage); - //} - - if (!summaryInfo.ContainsKey(SummaryInformationType.PatchPackageName) && - metadataSymbols.TryGetValue("Description", out var description)) - { - AddSummaryInformation(SummaryInformationType.PatchPackageName, description.Value, description.SourceLineNumbers); - } - - if (!summaryInfo.ContainsKey(SummaryInformationType.Author) && - metadataSymbols.TryGetValue("ManufacturerName", out var manufacturer)) - { - AddSummaryInformation(SummaryInformationType.Author, manufacturer.Value, manufacturer.SourceLineNumbers); - } - - // Special metadata marshalled through the build. - //var wixMetadataValues = symbols.OfType().ToDictionary(t => t.Id.Id, t => t.Value); - - //if (wixMetadataValues.TryGetValue("Comments", out var wixComments)) - if (metadataSymbols.TryGetValue("Comments", out var wixComments)) - { - commentsSymbol = wixComments; - } - - // Write the package comments to summary info. - if (!summaryInfo.ContainsKey(SummaryInformationType.Comments) && - commentsSymbol != null) - { - AddSummaryInformation(SummaryInformationType.Comments, commentsSymbol.Value, commentsSymbol.SourceLineNumbers); - } - - return metadataSymbols; - - void AddSummaryInformation(SummaryInformationType type, string value, SourceLineNumber sourceLineNumber) - { - summaryInfo.Add(type, new SummaryInformationSymbol(sourceLineNumber) - { - PropertyId = type, - Value = value - }); - } - } - - /// - /// Ensure transform is uninstallable. - /// - /// Product code in transform. - /// Transform generated by torch. - /// True if the transform is uninstallable - private bool CheckUninstallableTransform(string productCode, WindowsInstallerData transform) - { - var success = true; - - foreach (var tableName in PatchUninstallBreakingTables) - { - if (transform.TryGetTable(tableName, out var table)) - { - foreach (var row in table.Rows.Where(r => r.Operation == RowOperation.Add)) - { - success = false; - - var primaryKey = row.GetPrimaryKey('/') ?? String.Empty; - - this.Messaging.Write(ErrorMessages.NewRowAddedInTable(row.SourceLineNumbers, productCode, table.Name, primaryKey)); - } - } - } - - return success; - } - - /// - /// Reduce the transform according to the patch references. - /// - /// transform generated by torch. - /// Table contains patch family filter. - /// true if the transform is not empty - private bool ReduceTransform(WindowsInstallerData transform, IEnumerable patchRefSymbols) - { - // identify sections to keep - var oldSections = new Dictionary(); - var newSections = new Dictionary(); - var tableKeyRows = new Dictionary>(); - var sequenceList = new List(); - var componentFeatureAddsIndex = new Dictionary>(); - var customActionTable = new Dictionary(); - var directoryTableAdds = new Dictionary(); - var featureTableAdds = new Dictionary(); - var keptComponents = new Dictionary(); - var keptDirectories = new Dictionary(); - var keptFeatures = new Dictionary(); - var keptLockPermissions = new HashSet(); - var keptMsiLockPermissionExs = new HashSet(); - - var componentCreateFolderIndex = new Dictionary>(); - var directoryLockPermissionsIndex = new Dictionary>(); - var directoryMsiLockPermissionsExIndex = new Dictionary>(); - - foreach (var patchRefSymbol in patchRefSymbols) - { - var tableName = patchRefSymbol.Table; - var key = patchRefSymbol.PrimaryKeys; - - // Short circuit filtering if all changes should be included. - if ("*" == tableName && "*" == key) - { - RemoveProductCodeFromTransform(transform); - return true; - } - - if (!transform.Tables.TryGetTable(tableName, out var table)) - { - // Table not found. - continue; - } - - // Index the table. - if (!tableKeyRows.TryGetValue(tableName, out var keyRows)) - { - keyRows = new Dictionary(); - tableKeyRows.Add(tableName, keyRows); - - foreach (var newRow in table.Rows) - { - var primaryKey = newRow.GetPrimaryKey(); - keyRows.Add(primaryKey, newRow); - } - } - - if (!keyRows.TryGetValue(key, out var row)) - { - // Row not found. - continue; - } - - // Differ.sectionDelimiter - var sections = row.SectionId.Split('/'); - oldSections[sections[0]] = row; - newSections[sections[1]] = row; - } - - // throw away sections not referenced - var keptRows = 0; - Table directoryTable = null; - Table featureTable = null; - Table lockPermissionsTable = null; - Table msiLockPermissionsTable = null; - - foreach (var table in transform.Tables) - { - if ("_SummaryInformation" == table.Name) - { - continue; - } - - if (table.Name == "AdminExecuteSequence" - || table.Name == "AdminUISequence" - || table.Name == "AdvtExecuteSequence" - || table.Name == "InstallUISequence" - || table.Name == "InstallExecuteSequence") - { - sequenceList.Add(table); - continue; - } - - for (var i = 0; i < table.Rows.Count; i++) - { - var row = table.Rows[i]; - - if (table.Name == "CreateFolder") - { - var createFolderComponentId = row.FieldAsString(1); - - if (!componentCreateFolderIndex.TryGetValue(createFolderComponentId, out var directoryList)) - { - directoryList = new List(); - componentCreateFolderIndex.Add(createFolderComponentId, directoryList); - } - - directoryList.Add(row.FieldAsString(0)); - } - - if (table.Name == "CustomAction") - { - customActionTable.Add(row.FieldAsString(0), row); - } - - if (table.Name == "Directory") - { - directoryTable = table; - if (RowOperation.Add == row.Operation) - { - directoryTableAdds.Add(row.FieldAsString(0), row); - } - } - - if (table.Name == "Feature") - { - featureTable = table; - if (RowOperation.Add == row.Operation) - { - featureTableAdds.Add(row.FieldAsString(0), row); - } - } - - if (table.Name == "FeatureComponents") - { - if (RowOperation.Add == row.Operation) - { - var featureId = row.FieldAsString(0); - var componentId = row.FieldAsString(1); - - if (!componentFeatureAddsIndex.TryGetValue(componentId, out var featureList)) - { - featureList = new List(); - componentFeatureAddsIndex.Add(componentId, featureList); - } - - featureList.Add(featureId); - } - } - - if (table.Name == "LockPermissions") - { - lockPermissionsTable = table; - if ("CreateFolder" == row.FieldAsString(1)) - { - var directoryId = row.FieldAsString(0); - - if (!directoryLockPermissionsIndex.TryGetValue(directoryId, out var rowList)) - { - rowList = new List(); - directoryLockPermissionsIndex.Add(directoryId, rowList); - } - - rowList.Add(row); - } - } - - if (table.Name == "MsiLockPermissionsEx") - { - msiLockPermissionsTable = table; - if ("CreateFolder" == row.FieldAsString(1)) - { - var directoryId = row.FieldAsString(0); - - if (!directoryMsiLockPermissionsExIndex.TryGetValue(directoryId, out var rowList)) - { - rowList = new List(); - directoryMsiLockPermissionsExIndex.Add(directoryId, rowList); - } - - rowList.Add(row); - } - } - - if (null == row.SectionId) - { - table.Rows.RemoveAt(i); - i--; - } - else - { - var sections = row.SectionId.Split('/'); - // ignore the row without section id. - if (0 == sections[0].Length && 0 == sections[1].Length) - { - table.Rows.RemoveAt(i); - i--; - } - else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections)) - { - if ("Component" == table.Name) - { - keptComponents.Add(row.FieldAsString(0), row); - } - - if ("Directory" == table.Name) - { - keptDirectories.Add(row.FieldAsString(0), row); - } - - if ("Feature" == table.Name) - { - keptFeatures.Add(row.FieldAsString(0), row); - } - - keptRows++; - } - else - { - table.Rows.RemoveAt(i); - i--; - } - } - } - } - - keptRows += ReduceTransformSequenceTable(sequenceList, oldSections, newSections, customActionTable); - - if (null != directoryTable) - { - foreach (var componentRow in keptComponents.Values) - { - var componentId = componentRow.FieldAsString(0); - - if (RowOperation.Add == componentRow.Operation) - { - // Make sure each added component has its required directory and feature heirarchy. - var directoryId = componentRow.FieldAsString(2); - while (null != directoryId && directoryTableAdds.TryGetValue(directoryId, out var directoryRow)) - { - if (!keptDirectories.ContainsKey(directoryId)) - { - directoryTable.Rows.Add(directoryRow); - keptDirectories.Add(directoryId, directoryRow); - keptRows++; - } - - directoryId = directoryRow.FieldAsString(1); - } - - if (componentFeatureAddsIndex.TryGetValue(componentId, out var componentFeatureIds)) - { - foreach (var featureId in componentFeatureIds) - { - var currentFeatureId = featureId; - while (null != currentFeatureId && featureTableAdds.TryGetValue(currentFeatureId, out var featureRow)) - { - if (!keptFeatures.ContainsKey(currentFeatureId)) - { - featureTable.Rows.Add(featureRow); - keptFeatures.Add(currentFeatureId, featureRow); - keptRows++; - } - - currentFeatureId = featureRow.FieldAsString(1); - } - } - } - } - - // Hook in changes LockPermissions and MsiLockPermissions for folders for each component that has been kept. - foreach (var keptComponentId in keptComponents.Keys) - { - if (componentCreateFolderIndex.TryGetValue(keptComponentId, out var directoryList)) - { - foreach (var directoryId in directoryList) - { - if (directoryLockPermissionsIndex.TryGetValue(directoryId, out var lockPermissionsRowList)) - { - foreach (var lockPermissionsRow in lockPermissionsRowList) - { - var key = lockPermissionsRow.GetPrimaryKey('/'); - if (keptLockPermissions.Add(key)) - { - lockPermissionsTable.Rows.Add(lockPermissionsRow); - keptRows++; - } - } - } - - if (directoryMsiLockPermissionsExIndex.TryGetValue(directoryId, out var msiLockPermissionsExRowList)) - { - foreach (var msiLockPermissionsExRow in msiLockPermissionsExRowList) - { - var key = msiLockPermissionsExRow.GetPrimaryKey('/'); - if (keptMsiLockPermissionExs.Add(key)) - { - msiLockPermissionsTable.Rows.Add(msiLockPermissionsExRow); - keptRows++; - } - } - } - } - } - } - } - } - - keptRows += ReduceTransformSequenceTable(sequenceList, oldSections, newSections, customActionTable); - - // Delete tables that are empty. - var tablesToDelete = transform.Tables.Where(t => t.Rows.Count == 0).Select(t => t.Name).ToList(); - - foreach (var tableName in tablesToDelete) - { - transform.Tables.Remove(tableName); - } - - return keptRows > 0; - } - - private void Validate(PatchTransform patchTransform) - { - var transformPath = patchTransform.Baseline; // TODO: this is used in error messages, how best to set it? - var transform = patchTransform.Transform; - - // Changing the ProdocutCode in a patch transform is not recommended. - if (transform.TryGetTable("Property", out var propertyTable)) - { - foreach (var row in propertyTable.Rows) - { - // Only interested in modified rows; fast check. - if (RowOperation.Modify == row.Operation && - "ProductCode".Equals(row.FieldAsString(0), StringComparison.Ordinal)) - { - this.Messaging.Write(WarningMessages.MajorUpgradePatchNotRecommended()); - } - } - } - - // If there is nothing in the component table we can return early because the remaining checks are component based. - if (!transform.TryGetTable("Component", out var componentTable)) - { - return; - } - - // Index Feature table row operations - var featureOps = new Dictionary(); - if (transform.TryGetTable("Feature", out var featureTable)) - { - foreach (var row in featureTable.Rows) - { - featureOps[row.FieldAsString(0)] = row.Operation; - } - } - - // Index Component table and check for keypath modifications - var componentKeyPath = new Dictionary(); - var deletedComponent = new Dictionary(); - foreach (var row in componentTable.Rows) - { - var id = row.FieldAsString(0); - var keypath = row.FieldAsString(5) ?? String.Empty; - - componentKeyPath.Add(id, keypath); - - if (RowOperation.Delete == row.Operation) - { - deletedComponent.Add(id, row); - } - else if (RowOperation.Modify == row.Operation) - { - if (row.Fields[1].Modified) - { - // Changing the guid of a component is equal to deleting the old one and adding a new one. - deletedComponent.Add(id, row); - } - - // If the keypath is modified its an error - if (row.Fields[5].Modified) - { - this.Messaging.Write(ErrorMessages.InvalidKeypathChange(row.SourceLineNumbers, id, transformPath)); - } - } - } - - // Verify changes in the file table - if (transform.TryGetTable("File", out var fileTable)) - { - var componentWithChangedKeyPath = new Dictionary(); - foreach (FileRow row in fileTable.Rows) - { - if (RowOperation.None == row.Operation) - { - continue; - } - - var fileId = row.File; - var componentId = row.Component; - - // If this file is the keypath of a component - if (componentKeyPath.TryGetValue(componentId, out var keyPath) && keyPath.Equals(fileId, StringComparison.Ordinal)) - { - if (row.Fields[2].Modified) - { - // You can't change the filename of a file that is the keypath of a component. - this.Messaging.Write(ErrorMessages.InvalidKeypathChange(row.SourceLineNumbers, componentId, transformPath)); - } - - if (!componentWithChangedKeyPath.ContainsKey(componentId)) - { - componentWithChangedKeyPath.Add(componentId, fileId); - } - } - - if (RowOperation.Delete == row.Operation) - { - // If the file is removed from a component that is not deleted. - if (!deletedComponent.ContainsKey(componentId)) - { - var foundRemoveFileEntry = false; - var filename = this.BackendHelper.GetMsiFileName(row.FieldAsString(2), false, true); - - if (transform.TryGetTable("RemoveFile", out var removeFileTable)) - { - foreach (var removeFileRow in removeFileTable.Rows) - { - if (RowOperation.Delete == removeFileRow.Operation) - { - continue; - } - - if (componentId == removeFileRow.FieldAsString(1)) - { - // Check if there is a RemoveFile entry for this file - if (null != removeFileRow[2]) - { - var removeFileName = this.BackendHelper.GetMsiFileName(removeFileRow.FieldAsString(2), false, true); - - // Convert the MSI format for a wildcard string to Regex format. - removeFileName = removeFileName.Replace('.', '|').Replace('?', '.').Replace("*", ".*").Replace("|", "\\."); - - var regex = new Regex(removeFileName, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); - if (regex.IsMatch(filename)) - { - foundRemoveFileEntry = true; - break; - } - } - } - } - } - - if (!foundRemoveFileEntry) - { - this.Messaging.Write(WarningMessages.InvalidRemoveFile(row.SourceLineNumbers, fileId, componentId)); - } - } - } - } - } - - var featureComponentsTable = transform.Tables["FeatureComponents"]; - - if (0 < deletedComponent.Count) - { - // Index FeatureComponents table. - var featureComponents = new Dictionary>(); - - if (null != featureComponentsTable) - { - foreach (var row in featureComponentsTable.Rows) - { - var componentId = row.FieldAsString(1); - - if (!featureComponents.TryGetValue(componentId, out var features)) - { - features = new List(); - featureComponents.Add(componentId, features); - } - - features.Add(row.FieldAsString(0)); - } - } - - // Check to make sure if a component was deleted, the feature was too. - foreach (var entry in deletedComponent) - { - if (featureComponents.TryGetValue(entry.Key, out var features)) - { - foreach (var featureId in features) - { - if (!featureOps.TryGetValue(featureId, out var op) || op != RowOperation.Delete) - { - // The feature was not deleted. - this.Messaging.Write(ErrorMessages.InvalidRemoveComponent(((Row)entry.Value).SourceLineNumbers, entry.Key.ToString(), featureId, transformPath)); - } - } - } - } - } - - // Warn if new components are added to existing features - if (null != featureComponentsTable) - { - foreach (var row in featureComponentsTable.Rows) - { - if (RowOperation.Add == row.Operation) - { - // Check if the feature is in the Feature table - var feature_ = row.FieldAsString(0); - var component_ = row.FieldAsString(1); - - // Features may not be present if not referenced - if (!featureOps.ContainsKey(feature_) || RowOperation.Add != (RowOperation)featureOps[feature_]) - { - this.Messaging.Write(WarningMessages.NewComponentAddedToExistingFeature(row.SourceLineNumbers, component_, feature_, transformPath)); - } - } - } - } - } - - /// - /// Remove the ProductCode property from the transform. - /// - /// The transform. - /// - /// Changing the ProductCode is not supported in a patch. - /// - private static void RemoveProductCodeFromTransform(WindowsInstallerData transform) - { - if (transform.Tables.TryGetTable("Property", out var propertyTable)) - { - for (var i = 0; i < propertyTable.Rows.Count; ++i) - { - var propertyRow = propertyTable.Rows[i]; - var property = propertyRow.FieldAsString(0); - - if ("ProductCode" == property) - { - propertyTable.Rows.RemoveAt(i); - break; - } - } - } - } - - /// - /// Check if the section is in a PatchFamily. - /// - /// Section id in target wixout - /// Section id in upgrade wixout - /// Dictionary contains section id should be kept in the baseline wixout. - /// Dictionary contains section id should be kept in the upgrade wixout. - /// true if section in patch family - private static bool IsInPatchFamily(string oldSection, string newSection, Dictionary oldSections, Dictionary newSections) - { - var result = false; - - if ((String.IsNullOrEmpty(oldSection) && newSections.ContainsKey(newSection)) || (String.IsNullOrEmpty(newSection) && oldSections.ContainsKey(oldSection))) - { - result = true; - } - else if (!String.IsNullOrEmpty(oldSection) && !String.IsNullOrEmpty(newSection) && (oldSections.ContainsKey(oldSection) || newSections.ContainsKey(newSection))) - { - result = true; - } - - return result; - } - - /// - /// Reduce the transform sequence tables. - /// - /// ArrayList of tables to be reduced - /// Hashtable contains section id should be kept in the baseline wixout. - /// Hashtable contains section id should be kept in the target wixout. - /// Hashtable contains all the rows in the CustomAction table. - /// Number of rows left - private static int ReduceTransformSequenceTable(List
sequenceList, Dictionary oldSections, Dictionary newSections, Dictionary customAction) - { - var keptRows = 0; - - foreach (var currentTable in sequenceList) - { - for (var i = 0; i < currentTable.Rows.Count; i++) - { - var row = currentTable.Rows[i]; - var actionName = row.Fields[0].Data.ToString(); - var sections = row.SectionId.Split('/'); - var isSectionIdEmpty = (sections[0].Length == 0 && sections[1].Length == 0); - - if (row.Operation == RowOperation.None) - { - // Ignore the rows without section id. - if (isSectionIdEmpty) - { - currentTable.Rows.RemoveAt(i); - i--; - } - else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections)) - { - keptRows++; - } - else - { - currentTable.Rows.RemoveAt(i); - i--; - } - } - else if (row.Operation == RowOperation.Modify) - { - var sequenceChanged = row.Fields[2].Modified; - var conditionChanged = row.Fields[1].Modified; - - if (sequenceChanged && !conditionChanged) - { - keptRows++; - } - else if (!sequenceChanged && conditionChanged) - { - if (isSectionIdEmpty) - { - currentTable.Rows.RemoveAt(i); - i--; - } - else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections)) - { - keptRows++; - } - else - { - currentTable.Rows.RemoveAt(i); - i--; - } - } - else if (sequenceChanged && conditionChanged) - { - if (isSectionIdEmpty) - { - row.Fields[1].Modified = false; - keptRows++; - } - else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections)) - { - keptRows++; - } - else - { - row.Fields[1].Modified = false; - keptRows++; - } - } - } - else if (row.Operation == RowOperation.Delete) - { - if (isSectionIdEmpty) - { - // it is a stardard action which is added by wix, we should keep this action. - row.Operation = RowOperation.None; - keptRows++; - } - else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections)) - { - keptRows++; - } - else - { - if (customAction.ContainsKey(actionName)) - { - currentTable.Rows.RemoveAt(i); - i--; - } - else - { - // it is a stardard action, we should keep this action. - row.Operation = RowOperation.None; - keptRows++; - } - } - } - else if (row.Operation == RowOperation.Add) - { - if (isSectionIdEmpty) - { - keptRows++; - } - else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections)) - { - keptRows++; - } - else - { - if (customAction.ContainsKey(actionName)) - { - currentTable.Rows.RemoveAt(i); - i--; - } - else - { - keptRows++; - } - } - } - } - } - - return keptRows; - } - - /// - /// Create the #transform for the given main transform. - /// - private WindowsInstallerData BuildPairedTransform(Dictionary summaryInfo, Dictionary patchMetadata, WixPatchSymbol patchIdSymbol, WindowsInstallerData mainTransform, MediaSymbol mediaSymbol, WixPatchBaselineSymbol baselineSymbol, out string productCode) - { - productCode = null; - - var pairedTransform = new WindowsInstallerData(null) - { - Type = OutputType.Transform, - Codepage = mainTransform.Codepage - }; - - // lookup productVersion property to correct summaryInformation - var newProductVersion = mainTransform.Tables["Property"]?.Rows.FirstOrDefault(r => r.FieldAsString(0) == "ProductVersion")?.FieldAsString(1); - - var mainSummaryTable = mainTransform.Tables["_SummaryInformation"]; - var mainSummaryRows = mainSummaryTable.Rows.ToDictionary(r => r.FieldAsInteger(0)); - - var baselineValidationFlags = ((int)baselineSymbol.ValidationFlags).ToString(CultureInfo.InvariantCulture); - - if (!mainSummaryRows.ContainsKey((int)SummaryInformationType.TransformValidationFlags)) - { - var mainSummaryRow = mainSummaryTable.CreateRow(baselineSymbol.SourceLineNumbers); - mainSummaryRow[0] = (int)SummaryInformationType.TransformValidationFlags; - mainSummaryRow[1] = baselineValidationFlags; - } - - // copy summary information from core transform - var pairedSummaryTable = pairedTransform.EnsureTable(this.tableDefinitions["_SummaryInformation"]); - - foreach (var mainSummaryRow in mainSummaryTable.Rows) - { - var type = (SummaryInformationType)mainSummaryRow.FieldAsInteger(0); - var value = mainSummaryRow.FieldAsString(1); - switch (type) - { - case SummaryInformationType.TransformProductCodes: - var propertyData = value.Split(';'); - var oldProductVersion = propertyData[0].Substring(38); - var upgradeCode = propertyData[2]; - productCode = propertyData[0].Substring(0, 38); - - if (newProductVersion == null) - { - newProductVersion = oldProductVersion; - } - - // Force mainTranform to 'old;new;upgrade' and pairedTransform to 'new;new;upgrade' - mainSummaryRow[1] = String.Concat(productCode, oldProductVersion, ';', productCode, newProductVersion, ';', upgradeCode); - value = String.Concat(productCode, newProductVersion, ';', productCode, newProductVersion, ';', upgradeCode); - break; - case SummaryInformationType.TransformValidationFlags: // use validation flags authored into the patch XML. - value = baselineValidationFlags; - mainSummaryRow[1] = value; - break; - } - - var pairedSummaryRow = pairedSummaryTable.CreateRow(mainSummaryRow.SourceLineNumbers); - pairedSummaryRow[0] = mainSummaryRow[0]; - pairedSummaryRow[1] = value; - } - - if (productCode == null) - { - this.Messaging.Write(ErrorMessages.CouldNotDetermineProductCodeFromTransformSummaryInfo()); - return null; - } - - // Copy File table - if (mainTransform.Tables.TryGetTable("File", out var mainFileTable) && 0 < mainFileTable.Rows.Count) - { - var pairedFileTable = pairedTransform.EnsureTable(mainFileTable.Definition); - - foreach (var mainFileRow in mainFileTable.Rows.Cast()) - { - // Set File.Sequence to non null to satisfy transform bind and suppress any - // change to File.Sequence to avoid bloat. - mainFileRow.Sequence = 1; - mainFileRow.Fields[7].Modified = false; - - // Override authored media to the media provided in the patch. - mainFileRow.DiskId = mediaSymbol.DiskId; - - // Delete's don't need rows in the paired transform. - if (mainFileRow.Operation == RowOperation.Delete) - { - continue; - } - - var pairedFileRow = (FileRow)pairedFileTable.CreateRow(mainFileRow.SourceLineNumbers); - pairedFileRow.Operation = RowOperation.Modify; - mainFileRow.CopyTo(pairedFileRow); - - // Force modified File rows to appear in the transform. - switch (mainFileRow.Operation) - { - case RowOperation.Modify: - case RowOperation.Add: - pairedFileRow.Attributes |= WindowsInstallerConstants.MsidbFileAttributesPatchAdded; - pairedFileRow.Fields[6].Modified = true; - pairedFileRow.Operation = mainFileRow.Operation; - break; - default: - pairedFileRow.Fields[6].Modified = false; - break; - } - } - } - - // Add Media row to pairedTransform - var pairedMediaTable = pairedTransform.EnsureTable(this.tableDefinitions["Media"]); - var pairedMediaRow = (MediaRow)pairedMediaTable.CreateRow(mediaSymbol.SourceLineNumbers); - pairedMediaRow.Operation = RowOperation.Add; - pairedMediaRow.DiskId = mediaSymbol.DiskId; - pairedMediaRow.LastSequence = mediaSymbol.LastSequence ?? 0; - pairedMediaRow.DiskPrompt = mediaSymbol.DiskPrompt; - pairedMediaRow.Cabinet = mediaSymbol.Cabinet; - pairedMediaRow.VolumeLabel = mediaSymbol.VolumeLabel; - pairedMediaRow.Source = mediaSymbol.Source; - - // Add PatchPackage for this Media - var pairedPackageTable = pairedTransform.EnsureTable(this.tableDefinitions["PatchPackage"]); - pairedPackageTable.Operation = TableOperation.Add; - var pairedPackageRow = pairedPackageTable.CreateRow(mediaSymbol.SourceLineNumbers); - pairedPackageRow.Operation = RowOperation.Add; - pairedPackageRow[0] = patchIdSymbol.Id.Id; - pairedPackageRow[1] = mediaSymbol.DiskId; - - // Add the property to the patch transform's Property table. - var pairedPropertyTable = pairedTransform.EnsureTable(this.tableDefinitions["Property"]); - pairedPropertyTable.Operation = TableOperation.Add; - - // Add property to both identify client patches and whether those patches are removable or not - patchMetadata.TryGetValue("AllowRemoval", out var allowRemovalSymbol); - - var pairedPropertyRow = pairedPropertyTable.CreateRow(allowRemovalSymbol?.SourceLineNumbers); - pairedPropertyRow.Operation = RowOperation.Add; - pairedPropertyRow[0] = String.Concat(patchIdSymbol.ClientPatchId, ".AllowRemoval"); - pairedPropertyRow[1] = allowRemovalSymbol?.Value ?? "0"; - - // Add this patch code GUID to the patch transform to identify - // which patches are installed, including in multi-patch - // installations. - pairedPropertyRow = pairedPropertyTable.CreateRow(patchIdSymbol.SourceLineNumbers); - pairedPropertyRow.Operation = RowOperation.Add; - pairedPropertyRow[0] = String.Concat(patchIdSymbol.ClientPatchId, ".PatchCode"); - pairedPropertyRow[1] = patchIdSymbol.Id.Id; - - // Add PATCHNEWPACKAGECODE to apply to admin layouts. - pairedPropertyRow = pairedPropertyTable.CreateRow(patchIdSymbol.SourceLineNumbers); - pairedPropertyRow.Operation = RowOperation.Add; - pairedPropertyRow[0] = "PATCHNEWPACKAGECODE"; - pairedPropertyRow[1] = patchIdSymbol.Id.Id; - - // Add PATCHNEWSUMMARYCOMMENTS and PATCHNEWSUMMARYSUBJECT to apply to admin layouts. - if (summaryInfo.TryGetValue(SummaryInformationType.Subject, out var subjectSymbol)) - { - pairedPropertyRow = pairedPropertyTable.CreateRow(subjectSymbol.SourceLineNumbers); - pairedPropertyRow.Operation = RowOperation.Add; - pairedPropertyRow[0] = "PATCHNEWSUMMARYSUBJECT"; - pairedPropertyRow[1] = subjectSymbol.Value; - } - - if (summaryInfo.TryGetValue(SummaryInformationType.Comments, out var commentsSymbol)) - { - pairedPropertyRow = pairedPropertyTable.CreateRow(commentsSymbol.SourceLineNumbers); - pairedPropertyRow.Operation = RowOperation.Add; - pairedPropertyRow[0] = "PATCHNEWSUMMARYCOMMENTS"; - pairedPropertyRow[1] = commentsSymbol.Value; - } - - return pairedTransform; - } - - private static SortedSet FinalizePatchProductCodes(List symbols, SortedSet productCodes) - { - var patchTargetSymbols = symbols.OfType().ToList(); - - if (patchTargetSymbols.Any()) - { - var targets = new SortedSet(); - var replace = true; - foreach (var wixPatchTargetRow in patchTargetSymbols) - { - var target = wixPatchTargetRow.ProductCode.ToUpperInvariant(); - if (target == "*") - { - replace = false; - } - else - { - targets.Add(target); - } - } - - // Replace the target ProductCodes with the authored list. - if (replace) - { - productCodes = targets; - } - else - { - // Copy the authored target ProductCodes into the list. - foreach (var target in targets) - { - productCodes.Add(target); - } - } - } - - return productCodes; - } - } -} diff --git a/src/wix/WixToolset.Core.WindowsInstaller/Bind/CreatePatchSubStoragesCommand.cs b/src/wix/WixToolset.Core.WindowsInstaller/Bind/CreatePatchSubStoragesCommand.cs new file mode 100644 index 00000000..db121137 --- /dev/null +++ b/src/wix/WixToolset.Core.WindowsInstaller/Bind/CreatePatchSubStoragesCommand.cs @@ -0,0 +1,791 @@ +// 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.WindowsInstaller.Bind +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Text.RegularExpressions; + using WixToolset.Core.Native.Msi; + using WixToolset.Data; + using WixToolset.Data.Symbols; + using WixToolset.Data.WindowsInstaller; + using WixToolset.Data.WindowsInstaller.Rows; + using WixToolset.Extensibility.Services; + + /// + /// Include transforms in a patch. + /// + internal class CreatePatchSubStoragesCommand + { + private static readonly string[] PatchUninstallBreakingTables = new[] + { + "AppId", + "BindImage", + "Class", + "Complus", + "CreateFolder", + "DuplicateFile", + "Environment", + "Extension", + "Font", + "IniFile", + "IsolatedComponent", + "LockPermissions", + "MIME", + "MoveFile", + "MsiLockPermissionsEx", + "MsiServiceConfig", + "MsiServiceConfigFailureActions", + "ODBCAttribute", + "ODBCDataSource", + "ODBCDriver", + "ODBCSourceAttribute", + "ODBCTranslator", + "ProgId", + "PublishComponent", + "RemoveIniFile", + "SelfReg", + "ServiceControl", + "ServiceInstall", + "TypeLib", + "Verb", + }; + + private readonly TableDefinitionCollection tableDefinitions; + + public CreatePatchSubStoragesCommand(IMessaging messaging, IBackendHelper backendHelper, Intermediate intermediate, IEnumerable transforms) + { + this.tableDefinitions = new TableDefinitionCollection(WindowsInstallerTableDefinitions.All); + this.Messaging = messaging; + this.BackendHelper = backendHelper; + this.Intermediate = intermediate; + this.Transforms = transforms; + } + + private IMessaging Messaging { get; } + + private IBackendHelper BackendHelper { get; } + + private Intermediate Intermediate { get; } + + private IEnumerable Transforms { get; } + + public IEnumerable SubStorages { get; private set; } + + public IEnumerable Execute() + { + var subStorages = new List(); + + if (this.Transforms == null || !this.Transforms.Any()) + { + this.Messaging.Write(ErrorMessages.PatchWithoutTransforms()); + return subStorages; + } + + var summaryInfo = this.ExtractPatchSummaryInfo(); + + var section = this.Intermediate.Sections.First(); + + var symbols = this.Intermediate.Sections.SelectMany(s => s.Symbols).ToList(); + + // Get the patch id from the WixPatchId symbol. + var patchSymbol = symbols.OfType().FirstOrDefault(); + + if (String.IsNullOrEmpty(patchSymbol.Id?.Id)) + { + this.Messaging.Write(ErrorMessages.ExpectedPatchIdInWixMsp()); + return subStorages; + } + + if (String.IsNullOrEmpty(patchSymbol.ClientPatchId)) + { + this.Messaging.Write(ErrorMessages.ExpectedClientPatchIdInWixMsp()); + return subStorages; + } + + // enumerate patch.Media to map diskId to Media row + var patchMediaByDiskId = symbols.OfType().ToDictionary(t => t.DiskId); + + if (patchMediaByDiskId.Count == 0) + { + this.Messaging.Write(ErrorMessages.ExpectedMediaRowsInWixMsp()); + return subStorages; + } + + // populate MSP summary information + var patchMetadata = this.PopulateSummaryInformation(summaryInfo, symbols, patchSymbol); + + // enumerate transforms + var productCodes = new SortedSet(); + var transformNames = new List(); + var validTransform = new List>(); + + var baselineSymbolsById = symbols.OfType().ToDictionary(t => t.Id.Id); + + foreach (var mainTransform in this.Transforms) + { + // Validate the transform doesn't break any patch specific rules. + this.Validate(mainTransform); + + // ensure consistent File.Sequence within each Media + var baselineSymbol = baselineSymbolsById[mainTransform.Baseline]; + var mediaSymbol = patchMediaByDiskId[baselineSymbol.DiskId]; + + // Ensure that files are sequenced after the last file in any transform. + if (mainTransform.Transform.Tables.TryGetTable("Media", out var transformMediaTable)) + { + foreach (MediaRow transformMediaRow in transformMediaTable.Rows) + { + if (!mediaSymbol.LastSequence.HasValue || mediaSymbol.LastSequence < transformMediaRow.LastSequence) + { + // The Binder will pre-increment the sequence. + mediaSymbol.LastSequence = transformMediaRow.LastSequence; + } + } + } + + // Use the Media/@DiskId if greater than the last sequence for backward compatibility. + if (!mediaSymbol.LastSequence.HasValue || mediaSymbol.LastSequence < mediaSymbol.DiskId) + { + mediaSymbol.LastSequence = mediaSymbol.DiskId; + } + + // Ignore media table in the transform. + mainTransform.Transform.Tables.Remove("Media"); + mainTransform.Transform.Tables.Remove("MsiDigitalSignature"); + + var pairedTransform = this.BuildPairedTransform(summaryInfo, patchMetadata, patchSymbol, mainTransform.Transform, mediaSymbol, baselineSymbol, out var productCode); + + productCode = productCode.ToUpperInvariant(); + productCodes.Add(productCode); + validTransform.Add(Tuple.Create(productCode, mainTransform.Transform)); + + // Attach the main and paired transforms to the patch object. + var baseTransformName = mainTransform.Baseline; + var countSuffix = "." + validTransform.Count.ToString(CultureInfo.InvariantCulture); + + if (PatchConstants.PairedPatchTransformPrefix.Length + baseTransformName.Length + countSuffix.Length > PatchConstants.MaxPatchTransformName) + { + var trimmedTransformName = baseTransformName.Substring(0, PatchConstants.MaxPatchTransformName - PatchConstants.PairedPatchTransformPrefix.Length - countSuffix.Length); + + this.Messaging.Write(WindowsInstallerBackendWarnings.LongPatchBaselineIdTrimmed(baselineSymbol.SourceLineNumbers, baseTransformName, trimmedTransformName)); + + baseTransformName = trimmedTransformName; + } + + var transformName = baseTransformName + countSuffix; + subStorages.Add(new SubStorage(transformName, mainTransform.Transform)); + transformNames.Add(":" + transformName); + + var pairedTransformName = PatchConstants.PairedPatchTransformPrefix + transformName; + subStorages.Add(new SubStorage(pairedTransformName, pairedTransform)); + transformNames.Add(":" + pairedTransformName); + } + + if (validTransform.Count == 0) + { + this.Messaging.Write(ErrorMessages.PatchWithoutValidTransforms()); + return subStorages; + } + + // Validate that a patch authored as removable is actually removable + if (patchMetadata.TryGetValue("AllowRemoval", out var allowRemoval) && allowRemoval.Value == "1") + { + var uninstallable = true; + + foreach (var entry in validTransform) + { + uninstallable &= this.CheckUninstallableTransform(entry.Item1, entry.Item2); + } + + if (!uninstallable) + { + this.Messaging.Write(ErrorMessages.PatchNotRemovable()); + return subStorages; + } + } + + // Finish filling tables with transform-dependent data. + productCodes = FinalizePatchProductCodes(symbols, productCodes); + + // Semicolon delimited list of the product codes that can accept the patch. + summaryInfo.Add(SummaryInformationType.PatchProductCodes, new SummaryInformationSymbol(patchSymbol.SourceLineNumbers) + { + PropertyId = SummaryInformationType.PatchProductCodes, + Value = String.Join(";", productCodes) + }); + + // Semicolon delimited list of transform substorage names in the order they are applied. + summaryInfo.Add(SummaryInformationType.TransformNames, new SummaryInformationSymbol(patchSymbol.SourceLineNumbers) + { + PropertyId = SummaryInformationType.TransformNames, + Value = String.Join(";", transformNames) + }); + + // Put the summary information that was extracted back in now that it is updated. + foreach (var readSummaryInfo in summaryInfo.Values.OrderBy(s => s.PropertyId)) + { + section.AddSymbol(readSummaryInfo); + } + + this.SubStorages = subStorages; + + return subStorages; + } + + private Dictionary ExtractPatchSummaryInfo() + { + var result = new Dictionary(); + + foreach (var section in this.Intermediate.Sections) + { + // Remove all summary information from the symbols and remember those that + // are not calculated or reserved. + foreach (var patchSummaryInfo in section.Symbols.OfType().ToList()) + { + section.RemoveSymbol(patchSummaryInfo); + + if (patchSummaryInfo.PropertyId != SummaryInformationType.PatchProductCodes && + patchSummaryInfo.PropertyId != SummaryInformationType.PatchCode && + patchSummaryInfo.PropertyId != SummaryInformationType.PatchInstallerRequirement && + patchSummaryInfo.PropertyId != SummaryInformationType.Reserved11 && + patchSummaryInfo.PropertyId != SummaryInformationType.Reserved14 && + patchSummaryInfo.PropertyId != SummaryInformationType.Reserved16) + { + result.Add(patchSummaryInfo.PropertyId, patchSummaryInfo); + } + } + } + + return result; + } + + private Dictionary PopulateSummaryInformation(Dictionary summaryInfo, List symbols, WixPatchSymbol patchSymbol) + { + // PID_CODEPAGE + if (!summaryInfo.ContainsKey(SummaryInformationType.Codepage)) + { + // Set the code page by default to the same code page for the + // string pool in the database. + AddSummaryInformation(SummaryInformationType.Codepage, patchSymbol.Codepage?.ToString(CultureInfo.InvariantCulture) ?? "0", patchSymbol.SourceLineNumbers); + } + + // GUID patch code for the patch. + AddSummaryInformation(SummaryInformationType.PatchCode, patchSymbol.Id.Id, patchSymbol.SourceLineNumbers); + + // Indicates the minimum Windows Installer version that is required to install the patch. + AddSummaryInformation(SummaryInformationType.PatchInstallerRequirement, ((int)SummaryInformation.InstallerRequirement.Version31).ToString(CultureInfo.InvariantCulture), patchSymbol.SourceLineNumbers); + + if (!summaryInfo.ContainsKey(SummaryInformationType.Security)) + { + AddSummaryInformation(SummaryInformationType.Security, "4", patchSymbol.SourceLineNumbers); // Read-only enforced; + } + + // Use authored comments or default to display name. + MsiPatchMetadataSymbol commentsSymbol = null; + + var metadataSymbols = symbols.OfType().Where(t => String.IsNullOrEmpty(t.Company)).ToDictionary(t => t.Property); + + if (!summaryInfo.ContainsKey(SummaryInformationType.Title) && + metadataSymbols.TryGetValue("DisplayName", out var displayName)) + { + AddSummaryInformation(SummaryInformationType.Title, displayName.Value, displayName.SourceLineNumbers); + + // Default comments to use display name as-is. + commentsSymbol = displayName; + } + + // TODO: This code below seems unnecessary given the codepage is set at the top of this method. + //if (!summaryInfo.ContainsKey(SummaryInformationType.Codepage) && + // metadataValues.TryGetValue("CodePage", out var codepage)) + //{ + // AddSummaryInformation(SummaryInformationType.Codepage, codepage); + //} + + if (!summaryInfo.ContainsKey(SummaryInformationType.PatchPackageName) && + metadataSymbols.TryGetValue("Description", out var description)) + { + AddSummaryInformation(SummaryInformationType.PatchPackageName, description.Value, description.SourceLineNumbers); + } + + if (!summaryInfo.ContainsKey(SummaryInformationType.Author) && + metadataSymbols.TryGetValue("ManufacturerName", out var manufacturer)) + { + AddSummaryInformation(SummaryInformationType.Author, manufacturer.Value, manufacturer.SourceLineNumbers); + } + + // Special metadata marshalled through the build. + //var wixMetadataValues = symbols.OfType().ToDictionary(t => t.Id.Id, t => t.Value); + + //if (wixMetadataValues.TryGetValue("Comments", out var wixComments)) + if (metadataSymbols.TryGetValue("Comments", out var wixComments)) + { + commentsSymbol = wixComments; + } + + // Write the package comments to summary info. + if (!summaryInfo.ContainsKey(SummaryInformationType.Comments) && + commentsSymbol != null) + { + AddSummaryInformation(SummaryInformationType.Comments, commentsSymbol.Value, commentsSymbol.SourceLineNumbers); + } + + return metadataSymbols; + + void AddSummaryInformation(SummaryInformationType type, string value, SourceLineNumber sourceLineNumber) + { + summaryInfo.Add(type, new SummaryInformationSymbol(sourceLineNumber) + { + PropertyId = type, + Value = value + }); + } + } + + /// + /// Ensure transform is uninstallable. + /// + /// Product code in transform. + /// Transform generated by torch. + /// True if the transform is uninstallable + private bool CheckUninstallableTransform(string productCode, WindowsInstallerData transform) + { + var success = true; + + foreach (var tableName in PatchUninstallBreakingTables) + { + if (transform.TryGetTable(tableName, out var table)) + { + foreach (var row in table.Rows.Where(r => r.Operation == RowOperation.Add)) + { + success = false; + + var primaryKey = row.GetPrimaryKey('/') ?? String.Empty; + + this.Messaging.Write(ErrorMessages.NewRowAddedInTable(row.SourceLineNumbers, productCode, table.Name, primaryKey)); + } + } + } + + return success; + } + + private void Validate(PatchTransform patchTransform) + { + var transformPath = patchTransform.Baseline; + var transform = patchTransform.Transform; + + // Changing the ProdocutCode in a patch transform is not recommended. + if (transform.TryGetTable("Property", out var propertyTable)) + { + foreach (var row in propertyTable.Rows) + { + // Only interested in modified rows; fast check. + if (RowOperation.Modify == row.Operation && + "ProductCode".Equals(row.FieldAsString(0), StringComparison.Ordinal)) + { + this.Messaging.Write(WarningMessages.MajorUpgradePatchNotRecommended()); + } + } + } + + // If there is nothing in the component table we can return early because the remaining checks are component based. + if (!transform.TryGetTable("Component", out var componentTable)) + { + return; + } + + // Index Feature table row operations + var featureOps = new Dictionary(); + if (transform.TryGetTable("Feature", out var featureTable)) + { + foreach (var row in featureTable.Rows) + { + featureOps[row.FieldAsString(0)] = row.Operation; + } + } + + // Index Component table and check for keypath modifications + var componentKeyPath = new Dictionary(); + var deletedComponent = new Dictionary(); + foreach (var row in componentTable.Rows) + { + var id = row.FieldAsString(0); + var keypath = row.FieldAsString(5) ?? String.Empty; + + componentKeyPath.Add(id, keypath); + + if (RowOperation.Delete == row.Operation) + { + deletedComponent.Add(id, row); + } + else if (RowOperation.Modify == row.Operation) + { + if (row.Fields[1].Modified) + { + // Changing the guid of a component is equal to deleting the old one and adding a new one. + deletedComponent.Add(id, row); + } + + // If the keypath is modified its an error + if (row.Fields[5].Modified) + { + this.Messaging.Write(ErrorMessages.InvalidKeypathChange(row.SourceLineNumbers, id, transformPath)); + } + } + } + + // Verify changes in the file table + if (transform.TryGetTable("File", out var fileTable)) + { + var componentWithChangedKeyPath = new Dictionary(); + foreach (FileRow row in fileTable.Rows) + { + if (RowOperation.None == row.Operation) + { + continue; + } + + var fileId = row.File; + var componentId = row.Component; + + // If this file is the keypath of a component + if (componentKeyPath.TryGetValue(componentId, out var keyPath) && keyPath.Equals(fileId, StringComparison.Ordinal)) + { + if (row.Fields[2].Modified) + { + // You can't change the filename of a file that is the keypath of a component. + this.Messaging.Write(ErrorMessages.InvalidKeypathChange(row.SourceLineNumbers, componentId, transformPath)); + } + + if (!componentWithChangedKeyPath.ContainsKey(componentId)) + { + componentWithChangedKeyPath.Add(componentId, fileId); + } + } + + if (RowOperation.Delete == row.Operation) + { + // If the file is removed from a component that is not deleted. + if (!deletedComponent.ContainsKey(componentId)) + { + var foundRemoveFileEntry = false; + var filename = this.BackendHelper.GetMsiFileName(row.FieldAsString(2), false, true); + + if (transform.TryGetTable("RemoveFile", out var removeFileTable)) + { + foreach (var removeFileRow in removeFileTable.Rows) + { + if (RowOperation.Delete == removeFileRow.Operation) + { + continue; + } + + if (componentId == removeFileRow.FieldAsString(1)) + { + // Check if there is a RemoveFile entry for this file + if (null != removeFileRow[2]) + { + var removeFileName = this.BackendHelper.GetMsiFileName(removeFileRow.FieldAsString(2), false, true); + + // Convert the MSI format for a wildcard string to Regex format. + removeFileName = removeFileName.Replace('.', '|').Replace('?', '.').Replace("*", ".*").Replace("|", "\\."); + + var regex = new Regex(removeFileName, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + if (regex.IsMatch(filename)) + { + foundRemoveFileEntry = true; + break; + } + } + } + } + } + + if (!foundRemoveFileEntry) + { + this.Messaging.Write(WarningMessages.InvalidRemoveFile(row.SourceLineNumbers, fileId, componentId)); + } + } + } + } + } + + var featureComponentsTable = transform.Tables["FeatureComponents"]; + + if (0 < deletedComponent.Count) + { + // Index FeatureComponents table. + var featureComponents = new Dictionary>(); + + if (null != featureComponentsTable) + { + foreach (var row in featureComponentsTable.Rows) + { + var componentId = row.FieldAsString(1); + + if (!featureComponents.TryGetValue(componentId, out var features)) + { + features = new List(); + featureComponents.Add(componentId, features); + } + + features.Add(row.FieldAsString(0)); + } + } + + // Check to make sure if a component was deleted, the feature was too. + foreach (var entry in deletedComponent) + { + if (featureComponents.TryGetValue(entry.Key, out var features)) + { + foreach (var featureId in features) + { + if (!featureOps.TryGetValue(featureId, out var op) || op != RowOperation.Delete) + { + // The feature was not deleted. + this.Messaging.Write(ErrorMessages.InvalidRemoveComponent(((Row)entry.Value).SourceLineNumbers, entry.Key.ToString(), featureId, transformPath)); + } + } + } + } + } + + // Warn if new components are added to existing features + if (null != featureComponentsTable) + { + foreach (var row in featureComponentsTable.Rows) + { + if (RowOperation.Add == row.Operation) + { + // Check if the feature is in the Feature table + var feature_ = row.FieldAsString(0); + var component_ = row.FieldAsString(1); + + // Features may not be present if not referenced + if (!featureOps.ContainsKey(feature_) || RowOperation.Add != (RowOperation)featureOps[feature_]) + { + this.Messaging.Write(WarningMessages.NewComponentAddedToExistingFeature(row.SourceLineNumbers, component_, feature_, transformPath)); + } + } + } + } + } + + /// + /// Create the #transform for the given main transform. + /// + private WindowsInstallerData BuildPairedTransform(Dictionary summaryInfo, Dictionary patchMetadata, WixPatchSymbol patchIdSymbol, WindowsInstallerData mainTransform, MediaSymbol mediaSymbol, WixPatchBaselineSymbol baselineSymbol, out string productCode) + { + productCode = null; + + var pairedTransform = new WindowsInstallerData(null) + { + Type = OutputType.Transform, + Codepage = mainTransform.Codepage + }; + + // lookup productVersion property to correct summaryInformation + var newProductVersion = mainTransform.Tables["Property"]?.Rows.FirstOrDefault(r => r.FieldAsString(0) == "ProductVersion")?.FieldAsString(1); + + var mainSummaryTable = mainTransform.Tables["_SummaryInformation"]; + var mainSummaryRows = mainSummaryTable.Rows.ToDictionary(r => r.FieldAsInteger(0)); + + var baselineValidationFlags = ((int)baselineSymbol.ValidationFlags).ToString(CultureInfo.InvariantCulture); + + if (!mainSummaryRows.ContainsKey((int)SummaryInformationType.TransformValidationFlags)) + { + var mainSummaryRow = mainSummaryTable.CreateRow(baselineSymbol.SourceLineNumbers); + mainSummaryRow[0] = (int)SummaryInformationType.TransformValidationFlags; + mainSummaryRow[1] = baselineValidationFlags; + } + + // copy summary information from core transform + var pairedSummaryTable = pairedTransform.EnsureTable(this.tableDefinitions["_SummaryInformation"]); + + foreach (var mainSummaryRow in mainSummaryTable.Rows) + { + var type = (SummaryInformationType)mainSummaryRow.FieldAsInteger(0); + var value = mainSummaryRow.FieldAsString(1); + switch (type) + { + case SummaryInformationType.TransformProductCodes: + var propertyData = value.Split(';'); + var oldProductVersion = propertyData[0].Substring(38); + var upgradeCode = propertyData[2]; + productCode = propertyData[0].Substring(0, 38); + + if (newProductVersion == null) + { + newProductVersion = oldProductVersion; + } + + // Force mainTranform to 'old;new;upgrade' and pairedTransform to 'new;new;upgrade' + mainSummaryRow[1] = String.Concat(productCode, oldProductVersion, ';', productCode, newProductVersion, ';', upgradeCode); + value = String.Concat(productCode, newProductVersion, ';', productCode, newProductVersion, ';', upgradeCode); + break; + case SummaryInformationType.TransformValidationFlags: // use validation flags authored into the patch XML. + value = baselineValidationFlags; + mainSummaryRow[1] = value; + break; + } + + var pairedSummaryRow = pairedSummaryTable.CreateRow(mainSummaryRow.SourceLineNumbers); + pairedSummaryRow[0] = mainSummaryRow[0]; + pairedSummaryRow[1] = value; + } + + if (productCode == null) + { + this.Messaging.Write(ErrorMessages.CouldNotDetermineProductCodeFromTransformSummaryInfo()); + return null; + } + + // Copy File table + if (mainTransform.Tables.TryGetTable("File", out var mainFileTable) && 0 < mainFileTable.Rows.Count) + { + var pairedFileTable = pairedTransform.EnsureTable(mainFileTable.Definition); + + foreach (var mainFileRow in mainFileTable.Rows.Cast()) + { + // Set File.Sequence to non null to satisfy transform bind and suppress any + // change to File.Sequence to avoid bloat. + mainFileRow.Sequence = 1; + mainFileRow.Fields[7].Modified = false; + + // Override authored media to the media provided in the patch. + mainFileRow.DiskId = mediaSymbol.DiskId; + + // Delete's don't need rows in the paired transform. + if (mainFileRow.Operation == RowOperation.Delete) + { + continue; + } + + var pairedFileRow = (FileRow)pairedFileTable.CreateRow(mainFileRow.SourceLineNumbers); + pairedFileRow.Operation = RowOperation.Modify; + mainFileRow.CopyTo(pairedFileRow); + + // Force modified File rows to appear in the transform. + switch (mainFileRow.Operation) + { + case RowOperation.Modify: + case RowOperation.Add: + pairedFileRow.Attributes |= WindowsInstallerConstants.MsidbFileAttributesPatchAdded; + pairedFileRow.Fields[6].Modified = true; + pairedFileRow.Operation = mainFileRow.Operation; + break; + default: + pairedFileRow.Fields[6].Modified = false; + break; + } + } + } + + // Add Media row to pairedTransform + var pairedMediaTable = pairedTransform.EnsureTable(this.tableDefinitions["Media"]); + var pairedMediaRow = (MediaRow)pairedMediaTable.CreateRow(mediaSymbol.SourceLineNumbers); + pairedMediaRow.Operation = RowOperation.Add; + pairedMediaRow.DiskId = mediaSymbol.DiskId; + pairedMediaRow.LastSequence = mediaSymbol.LastSequence ?? 0; + pairedMediaRow.DiskPrompt = mediaSymbol.DiskPrompt; + pairedMediaRow.Cabinet = mediaSymbol.Cabinet; + pairedMediaRow.VolumeLabel = mediaSymbol.VolumeLabel; + pairedMediaRow.Source = mediaSymbol.Source; + + // Add PatchPackage for this Media + var pairedPackageTable = pairedTransform.EnsureTable(this.tableDefinitions["PatchPackage"]); + pairedPackageTable.Operation = TableOperation.Add; + var pairedPackageRow = pairedPackageTable.CreateRow(mediaSymbol.SourceLineNumbers); + pairedPackageRow.Operation = RowOperation.Add; + pairedPackageRow[0] = patchIdSymbol.Id.Id; + pairedPackageRow[1] = mediaSymbol.DiskId; + + // Add the property to the patch transform's Property table. + var pairedPropertyTable = pairedTransform.EnsureTable(this.tableDefinitions["Property"]); + pairedPropertyTable.Operation = TableOperation.Add; + + // Add property to both identify client patches and whether those patches are removable or not + patchMetadata.TryGetValue("AllowRemoval", out var allowRemovalSymbol); + + var pairedPropertyRow = pairedPropertyTable.CreateRow(allowRemovalSymbol?.SourceLineNumbers); + pairedPropertyRow.Operation = RowOperation.Add; + pairedPropertyRow[0] = String.Concat(patchIdSymbol.ClientPatchId, ".AllowRemoval"); + pairedPropertyRow[1] = allowRemovalSymbol?.Value ?? "0"; + + // Add this patch code GUID to the patch transform to identify + // which patches are installed, including in multi-patch + // installations. + pairedPropertyRow = pairedPropertyTable.CreateRow(patchIdSymbol.SourceLineNumbers); + pairedPropertyRow.Operation = RowOperation.Add; + pairedPropertyRow[0] = String.Concat(patchIdSymbol.ClientPatchId, ".PatchCode"); + pairedPropertyRow[1] = patchIdSymbol.Id.Id; + + // Add PATCHNEWPACKAGECODE to apply to admin layouts. + pairedPropertyRow = pairedPropertyTable.CreateRow(patchIdSymbol.SourceLineNumbers); + pairedPropertyRow.Operation = RowOperation.Add; + pairedPropertyRow[0] = "PATCHNEWPACKAGECODE"; + pairedPropertyRow[1] = patchIdSymbol.Id.Id; + + // Add PATCHNEWSUMMARYCOMMENTS and PATCHNEWSUMMARYSUBJECT to apply to admin layouts. + if (summaryInfo.TryGetValue(SummaryInformationType.Subject, out var subjectSymbol)) + { + pairedPropertyRow = pairedPropertyTable.CreateRow(subjectSymbol.SourceLineNumbers); + pairedPropertyRow.Operation = RowOperation.Add; + pairedPropertyRow[0] = "PATCHNEWSUMMARYSUBJECT"; + pairedPropertyRow[1] = subjectSymbol.Value; + } + + if (summaryInfo.TryGetValue(SummaryInformationType.Comments, out var commentsSymbol)) + { + pairedPropertyRow = pairedPropertyTable.CreateRow(commentsSymbol.SourceLineNumbers); + pairedPropertyRow.Operation = RowOperation.Add; + pairedPropertyRow[0] = "PATCHNEWSUMMARYCOMMENTS"; + pairedPropertyRow[1] = commentsSymbol.Value; + } + + return pairedTransform; + } + + private static SortedSet FinalizePatchProductCodes(List symbols, SortedSet productCodes) + { + var patchTargetSymbols = symbols.OfType().ToList(); + + if (patchTargetSymbols.Any()) + { + var targets = new SortedSet(); + var replace = true; + foreach (var wixPatchTargetRow in patchTargetSymbols) + { + var target = wixPatchTargetRow.ProductCode.ToUpperInvariant(); + if (target == "*") + { + replace = false; + } + else + { + targets.Add(target); + } + } + + // Replace the target ProductCodes with the authored list. + if (replace) + { + productCodes = targets; + } + else + { + // Copy the authored target ProductCodes into the list. + foreach (var target in targets) + { + productCodes.Add(target); + } + } + } + + return productCodes; + } + } +} diff --git a/src/wix/WixToolset.Core.WindowsInstaller/Bind/CreatePatchTransformsCommand.cs b/src/wix/WixToolset.Core.WindowsInstaller/Bind/CreatePatchTransformsCommand.cs index 6d5bff69..17583e96 100644 --- a/src/wix/WixToolset.Core.WindowsInstaller/Bind/CreatePatchTransformsCommand.cs +++ b/src/wix/WixToolset.Core.WindowsInstaller/Bind/CreatePatchTransformsCommand.cs @@ -50,13 +50,26 @@ namespace WixToolset.Core.WindowsInstaller.Bind { var patchTransforms = new List(); - var symbols = this.Intermediate.Sections.SelectMany(s => s.Symbols).OfType(); + var symbols = this.Intermediate.Sections.SelectMany(s => s.Symbols); - foreach (var symbol in symbols) + var patchBaselineSymbols = symbols.OfType(); + + var patchRefSymbols = symbols.OfType().ToList(); + + foreach (var symbol in patchBaselineSymbols) { var targetData = this.GetWindowsInstallerData(symbol.BaselineFile.Path, BindStage.Target); var updatedData = this.GetWindowsInstallerData(symbol.UpdateFile.Path, BindStage.Updated); + if (patchRefSymbols.Count > 0) + { + var targetCommand = new GenerateSectionIdsCommand(targetData); + targetCommand.Execute(); + + var updatedCommand = new GenerateSectionIdsCommand(updatedData); + updatedCommand.Execute(); + } + var command = new GenerateTransformCommand(this.Messaging, targetData, updatedData, preserveUnchangedRows: true, showPedanticMessages: false); var transform = command.Execute(); diff --git a/src/wix/WixToolset.Core.WindowsInstaller/Bind/GenerateSectionIdsCommand.cs b/src/wix/WixToolset.Core.WindowsInstaller/Bind/GenerateSectionIdsCommand.cs new file mode 100644 index 00000000..c7bebbed --- /dev/null +++ b/src/wix/WixToolset.Core.WindowsInstaller/Bind/GenerateSectionIdsCommand.cs @@ -0,0 +1,225 @@ +// 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.WindowsInstaller.Bind +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using WixToolset.Data.WindowsInstaller; + + /// + /// Creates section ids on rows which form logical groupings of resources. + /// + internal class GenerateSectionIdsCommand + { + private int sectionCount; + + public GenerateSectionIdsCommand(WindowsInstallerData data) + { + this.Data = data; + } + + private WindowsInstallerData Data { get; } + + public void Execute() + { + var output = this.Data; + + this.sectionCount = 0; + + // First assign and index section ids for the tables that are in their own sections. + this.AssignSectionIdsToTable(output.Tables["Binary"], 0); + var componentSectionIdIndex = this.AssignSectionIdsToTable(output.Tables["Component"], 0); + var customActionSectionIdIndex = this.AssignSectionIdsToTable(output.Tables["CustomAction"], 0); + this.AssignSectionIdsToTable(output.Tables["Directory"], 0); + var featureSectionIdIndex = this.AssignSectionIdsToTable(output.Tables["Feature"], 0); + this.AssignSectionIdsToTable(output.Tables["Icon"], 0); + var digitalCertificateSectionIdIndex = this.AssignSectionIdsToTable(output.Tables["MsiDigitalCertificate"], 0); + this.AssignSectionIdsToTable(output.Tables["Property"], 0); + + // Now handle all the tables that rely on the first set of indexes but also produce their own indexes. Order matters here. + var fileSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["File"], componentSectionIdIndex, 1, 0); + var appIdSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["Class"], componentSectionIdIndex, 2, 5); + var odbcDataSourceSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["ODBCDataSource"], componentSectionIdIndex, 1, 0); + var odbcDriverSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["ODBCDriver"], componentSectionIdIndex, 1, 0); + var registrySectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["Registry"], componentSectionIdIndex, 5, 0); + var serviceInstallSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["ServiceInstall"], componentSectionIdIndex, 11, 0); + + // Now handle all the tables which only rely on previous indexes and order does not matter. + foreach (var table in output.Tables) + { + switch (table.Name) + { + case "MsiFileHash": + ConnectTableToSection(table, fileSectionIdIndex, 0); + break; + case "MsiAssembly": + case "MsiAssemblyName": + ConnectTableToSection(table, componentSectionIdIndex, 0); + break; + case "MsiPackageCertificate": + case "MsiPatchCertificate": + ConnectTableToSection(table, digitalCertificateSectionIdIndex, 1); + break; + case "CreateFolder": + case "FeatureComponents": + case "MoveFile": + case "ReserveCost": + case "ODBCTranslator": + ConnectTableToSection(table, componentSectionIdIndex, 1); + break; + case "TypeLib": + ConnectTableToSection(table, componentSectionIdIndex, 2); + break; + case "Shortcut": + case "Environment": + ConnectTableToSection(table, componentSectionIdIndex, 3); + break; + case "RemoveRegistry": + ConnectTableToSection(table, componentSectionIdIndex, 4); + break; + case "ServiceControl": + ConnectTableToSection(table, componentSectionIdIndex, 5); + break; + case "IniFile": + case "RemoveIniFile": + ConnectTableToSection(table, componentSectionIdIndex, 7); + break; + case "AppId": + ConnectTableToSection(table, appIdSectionIdIndex, 0); + break; + case "Condition": + ConnectTableToSection(table, featureSectionIdIndex, 0); + break; + case "ODBCSourceAttribute": + ConnectTableToSection(table, odbcDataSourceSectionIdIndex, 0); + break; + case "ODBCAttribute": + ConnectTableToSection(table, odbcDriverSectionIdIndex, 0); + break; + case "AdminExecuteSequence": + case "AdminUISequence": + case "AdvtExecuteSequence": + case "AdvtUISequence": + case "InstallExecuteSequence": + case "InstallUISequence": + ConnectTableToSection(table, customActionSectionIdIndex, 0); + break; + case "LockPermissions": + case "MsiLockPermissions": + foreach (var row in table.Rows) + { + var lockObject = row.FieldAsString(0); + var tableName = row.FieldAsString(1); + switch (tableName) + { + case "File": + row.SectionId = fileSectionIdIndex[lockObject]; + break; + case "Registry": + row.SectionId = registrySectionIdIndex[lockObject]; + break; + case "ServiceInstall": + row.SectionId = serviceInstallSectionIdIndex[lockObject]; + break; + } + } + break; + } + } + + // Now pass the output to each unbinder extension to allow them to analyze the output and determine their proper section ids. + //foreach (IUnbinderExtension extension in this.unbinderExtensions) + //{ + // extension.GenerateSectionIds(output); + //} + } + + /// + /// Creates new section ids on all the rows in a table. + /// + /// The table to add sections to. + /// The index of the column which is used by other tables to reference this table. + /// A dictionary containing the tables key for each row paired with its assigned section id. + private Dictionary AssignSectionIdsToTable(Table table, int rowPrimaryKeyIndex) + { + var primaryKeyToSectionId = new Dictionary(); + + if (null != table) + { + foreach (var row in table.Rows) + { + row.SectionId = this.GetNewSectionId(); + + primaryKeyToSectionId.Add(row.FieldAsString(rowPrimaryKeyIndex), row.SectionId); + } + } + + return primaryKeyToSectionId; + } + + /// + /// Connects a table's rows to an already sectioned table. + /// + /// The table containing rows that need to be connected to sections. + /// A hashtable containing keys to map table to its section. + /// The index of the column which is used as the foreign key in to the sectionIdIndex. + private static void ConnectTableToSection(Table table, Dictionary sectionIdIndex, int rowIndex) + { + if (null != table) + { + foreach (var row in table.Rows) + { + if (sectionIdIndex.TryGetValue(row.FieldAsString(rowIndex), out var sectionId)) + { + row.SectionId = sectionId; + } + } + } + } + + /// + /// Connects a table's rows to an already sectioned table and produces an index for other tables to connect to it. + /// + /// The table containing rows that need to be connected to sections. + /// A dictionary containing keys to map table to its section. + /// The index of the column which is used as the foreign key in to the sectionIdIndex. + /// The index of the column which is used by other tables to reference this table. + /// A dictionary containing the tables key for each row paired with its assigned section id. + private static Dictionary ConnectTableToSectionAndIndex(Table table, Dictionary sectionIdIndex, int rowIndex, int rowPrimaryKeyIndex) + { + var newPrimaryKeyToSectionId = new Dictionary(); + + if (null != table) + { + foreach (var row in table.Rows) + { + var foreignKey = row.FieldAsString(rowIndex); + + if (!sectionIdIndex.TryGetValue(foreignKey, out var sectionId)) + { + continue; + } + + row.SectionId = sectionId; + + var primaryKey = row.FieldAsString(rowPrimaryKeyIndex); + + if (!String.IsNullOrEmpty(primaryKey) && sectionIdIndex.ContainsKey(primaryKey)) + { + newPrimaryKeyToSectionId.Add(primaryKey, row.SectionId); + } + } + } + + return newPrimaryKeyToSectionId; + } + + private string GetNewSectionId() + { + this.sectionCount++; + + return "wix.section." + this.sectionCount.ToString(CultureInfo.InvariantCulture); + } + } +} diff --git a/src/wix/WixToolset.Core.WindowsInstaller/Bind/ReduceTransformCommand.cs b/src/wix/WixToolset.Core.WindowsInstaller/Bind/ReduceTransformCommand.cs new file mode 100644 index 00000000..4966a0b4 --- /dev/null +++ b/src/wix/WixToolset.Core.WindowsInstaller/Bind/ReduceTransformCommand.cs @@ -0,0 +1,549 @@ +// 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.WindowsInstaller.Bind +{ + using System; + using System.Collections.Generic; + using System.Linq; + using WixToolset.Data; + using WixToolset.Data.Symbols; + using WixToolset.Data.WindowsInstaller; + + internal class ReduceTransformCommand + { + private const char SectionDelimiter = '/'; + + public ReduceTransformCommand(Intermediate intermediate, IEnumerable patchTransforms) + { + this.Intermediate = intermediate; + this.PatchTransforms = patchTransforms; + } + + private Intermediate Intermediate { get; } + + private IEnumerable PatchTransforms { get; } + + public void Execute() + { + var symbols = this.Intermediate.Sections.SelectMany(s => s.Symbols).ToList(); + + var patchRefSymbols = symbols.OfType().ToList(); + + if (patchRefSymbols.Count > 0) + { + foreach (var patchTransform in this.PatchTransforms) + { + if (!this.ReduceTransform(patchTransform.Transform, patchRefSymbols)) + { + // transform has none of the content authored into this patch + continue; + } + } + } + } + + /// + /// Reduce the transform according to the patch references. + /// + /// transform generated by torch. + /// Table contains patch family filter. + /// true if the transform is not empty + private bool ReduceTransform(WindowsInstallerData transform, IEnumerable patchRefSymbols) + { + // identify sections to keep + var oldSections = new Dictionary(); + var newSections = new Dictionary(); + var tableKeyRows = new Dictionary>(); + var sequenceList = new List
(); + var componentFeatureAddsIndex = new Dictionary>(); + var customActionTable = new Dictionary(); + var directoryTableAdds = new Dictionary(); + var featureTableAdds = new Dictionary(); + var keptComponents = new Dictionary(); + var keptDirectories = new Dictionary(); + var keptFeatures = new Dictionary(); + var keptLockPermissions = new HashSet(); + var keptMsiLockPermissionExs = new HashSet(); + + var componentCreateFolderIndex = new Dictionary>(); + var directoryLockPermissionsIndex = new Dictionary>(); + var directoryMsiLockPermissionsExIndex = new Dictionary>(); + + foreach (var patchRefSymbol in patchRefSymbols) + { + var tableName = patchRefSymbol.Table; + var key = patchRefSymbol.PrimaryKeys; + + // Short circuit filtering if all changes should be included. + if ("*" == tableName && "*" == key) + { + RemoveProductCodeFromTransform(transform); + return true; + } + + if (!transform.Tables.TryGetTable(tableName, out var table)) + { + // Table not found. + continue; + } + + // Index the table. + if (!tableKeyRows.TryGetValue(tableName, out var keyRows)) + { + keyRows = table.Rows.ToDictionary(r => r.GetPrimaryKey()); + tableKeyRows.Add(tableName, keyRows); + } + + if (!keyRows.TryGetValue(key, out var row)) + { + // Row not found. + continue; + } + + // Differ.sectionDelimiter + var sections = row.SectionId.Split(SectionDelimiter); + oldSections[sections[0]] = row; + newSections[sections[1]] = row; + } + + // throw away sections not referenced + var keptRows = 0; + Table directoryTable = null; + Table featureTable = null; + Table lockPermissionsTable = null; + Table msiLockPermissionsTable = null; + + foreach (var table in transform.Tables) + { + if ("_SummaryInformation" == table.Name) + { + continue; + } + + if (table.Name == "AdminExecuteSequence" + || table.Name == "AdminUISequence" + || table.Name == "AdvtExecuteSequence" + || table.Name == "InstallUISequence" + || table.Name == "InstallExecuteSequence") + { + sequenceList.Add(table); + continue; + } + + for (var i = 0; i < table.Rows.Count; i++) + { + var row = table.Rows[i]; + + if (table.Name == "CreateFolder") + { + var createFolderComponentId = row.FieldAsString(1); + + if (!componentCreateFolderIndex.TryGetValue(createFolderComponentId, out var directoryList)) + { + directoryList = new List(); + componentCreateFolderIndex.Add(createFolderComponentId, directoryList); + } + + directoryList.Add(row.FieldAsString(0)); + } + + if (table.Name == "CustomAction") + { + customActionTable.Add(row.FieldAsString(0), row); + } + + if (table.Name == "Directory") + { + directoryTable = table; + if (RowOperation.Add == row.Operation) + { + directoryTableAdds.Add(row.FieldAsString(0), row); + } + } + + if (table.Name == "Feature") + { + featureTable = table; + if (RowOperation.Add == row.Operation) + { + featureTableAdds.Add(row.FieldAsString(0), row); + } + } + + if (table.Name == "FeatureComponents") + { + if (RowOperation.Add == row.Operation) + { + var featureId = row.FieldAsString(0); + var componentId = row.FieldAsString(1); + + if (!componentFeatureAddsIndex.TryGetValue(componentId, out var featureList)) + { + featureList = new List(); + componentFeatureAddsIndex.Add(componentId, featureList); + } + + featureList.Add(featureId); + } + } + + if (table.Name == "LockPermissions") + { + lockPermissionsTable = table; + if ("CreateFolder" == row.FieldAsString(1)) + { + var directoryId = row.FieldAsString(0); + + if (!directoryLockPermissionsIndex.TryGetValue(directoryId, out var rowList)) + { + rowList = new List(); + directoryLockPermissionsIndex.Add(directoryId, rowList); + } + + rowList.Add(row); + } + } + + if (table.Name == "MsiLockPermissionsEx") + { + msiLockPermissionsTable = table; + if ("CreateFolder" == row.FieldAsString(1)) + { + var directoryId = row.FieldAsString(0); + + if (!directoryMsiLockPermissionsExIndex.TryGetValue(directoryId, out var rowList)) + { + rowList = new List(); + directoryMsiLockPermissionsExIndex.Add(directoryId, rowList); + } + + rowList.Add(row); + } + } + + if (null == row.SectionId) + { + table.Rows.RemoveAt(i); + i--; + } + else + { + var sections = row.SectionId.Split(SectionDelimiter); + // ignore the row without section id. + if (0 == sections[0].Length && 0 == sections[1].Length) + { + table.Rows.RemoveAt(i); + i--; + } + else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections)) + { + if ("Component" == table.Name) + { + keptComponents.Add(row.FieldAsString(0), row); + } + + if ("Directory" == table.Name) + { + keptDirectories.Add(row.FieldAsString(0), row); + } + + if ("Feature" == table.Name) + { + keptFeatures.Add(row.FieldAsString(0), row); + } + + keptRows++; + } + else + { + table.Rows.RemoveAt(i); + i--; + } + } + } + } + + keptRows += ReduceTransformSequenceTable(sequenceList, oldSections, newSections, customActionTable); + + if (null != directoryTable) + { + foreach (var componentRow in keptComponents.Values) + { + var componentId = componentRow.FieldAsString(0); + + if (RowOperation.Add == componentRow.Operation) + { + // Make sure each added component has its required directory and feature heirarchy. + var directoryId = componentRow.FieldAsString(2); + while (null != directoryId && directoryTableAdds.TryGetValue(directoryId, out var directoryRow)) + { + if (!keptDirectories.ContainsKey(directoryId)) + { + directoryTable.Rows.Add(directoryRow); + keptDirectories.Add(directoryId, directoryRow); + keptRows++; + } + + directoryId = directoryRow.FieldAsString(1); + } + + if (componentFeatureAddsIndex.TryGetValue(componentId, out var componentFeatureIds)) + { + foreach (var featureId in componentFeatureIds) + { + var currentFeatureId = featureId; + while (null != currentFeatureId && featureTableAdds.TryGetValue(currentFeatureId, out var featureRow)) + { + if (!keptFeatures.ContainsKey(currentFeatureId)) + { + featureTable.Rows.Add(featureRow); + keptFeatures.Add(currentFeatureId, featureRow); + keptRows++; + } + + currentFeatureId = featureRow.FieldAsString(1); + } + } + } + } + + // Hook in changes LockPermissions and MsiLockPermissions for folders for each component that has been kept. + foreach (var keptComponentId in keptComponents.Keys) + { + if (componentCreateFolderIndex.TryGetValue(keptComponentId, out var directoryList)) + { + foreach (var directoryId in directoryList) + { + if (directoryLockPermissionsIndex.TryGetValue(directoryId, out var lockPermissionsRowList)) + { + foreach (var lockPermissionsRow in lockPermissionsRowList) + { + var key = lockPermissionsRow.GetPrimaryKey('/'); + if (keptLockPermissions.Add(key)) + { + lockPermissionsTable.Rows.Add(lockPermissionsRow); + keptRows++; + } + } + } + + if (directoryMsiLockPermissionsExIndex.TryGetValue(directoryId, out var msiLockPermissionsExRowList)) + { + foreach (var msiLockPermissionsExRow in msiLockPermissionsExRowList) + { + var key = msiLockPermissionsExRow.GetPrimaryKey('/'); + if (keptMsiLockPermissionExs.Add(key)) + { + msiLockPermissionsTable.Rows.Add(msiLockPermissionsExRow); + keptRows++; + } + } + } + } + } + } + } + } + + keptRows += ReduceTransformSequenceTable(sequenceList, oldSections, newSections, customActionTable); + + // Delete tables that are empty. + var tablesToDelete = transform.Tables.Where(t => t.Rows.Count == 0).Select(t => t.Name).ToList(); + + foreach (var tableName in tablesToDelete) + { + transform.Tables.Remove(tableName); + } + + return keptRows > 0; + } + + /// + /// Check if the section is in a PatchFamily. + /// + /// Section id in target wixout + /// Section id in upgrade wixout + /// Dictionary contains section id should be kept in the baseline wixout. + /// Dictionary contains section id should be kept in the upgrade wixout. + /// true if section in patch family + private static bool IsInPatchFamily(string oldSection, string newSection, Dictionary oldSections, Dictionary newSections) + { + var result = false; + + if ((String.IsNullOrEmpty(oldSection) && newSections.ContainsKey(newSection)) || (String.IsNullOrEmpty(newSection) && oldSections.ContainsKey(oldSection))) + { + result = true; + } + else if (!String.IsNullOrEmpty(oldSection) && !String.IsNullOrEmpty(newSection) && (oldSections.ContainsKey(oldSection) || newSections.ContainsKey(newSection))) + { + result = true; + } + + return result; + } + + /// + /// Remove the ProductCode property from the transform. + /// + /// The transform. + /// + /// Changing the ProductCode is not supported in a patch. + /// + private static void RemoveProductCodeFromTransform(WindowsInstallerData transform) + { + if (transform.Tables.TryGetTable("Property", out var propertyTable)) + { + for (var i = 0; i < propertyTable.Rows.Count; ++i) + { + var propertyRow = propertyTable.Rows[i]; + var property = propertyRow.FieldAsString(0); + + if ("ProductCode" == property) + { + propertyTable.Rows.RemoveAt(i); + break; + } + } + } + } + + /// + /// Reduce the transform sequence tables. + /// + /// ArrayList of tables to be reduced + /// Hashtable contains section id should be kept in the baseline wixout. + /// Hashtable contains section id should be kept in the target wixout. + /// Hashtable contains all the rows in the CustomAction table. + /// Number of rows left + private static int ReduceTransformSequenceTable(List
sequenceList, Dictionary oldSections, Dictionary newSections, Dictionary customAction) + { + var keptRows = 0; + + foreach (var currentTable in sequenceList) + { + for (var i = 0; i < currentTable.Rows.Count; i++) + { + var row = currentTable.Rows[i]; + var actionName = row.Fields[0].Data.ToString(); + var sections = row.SectionId.Split(SectionDelimiter); + var isSectionIdEmpty = (sections[0].Length == 0 && sections[1].Length == 0); + + if (row.Operation == RowOperation.None) + { + // Ignore the rows without section id. + if (isSectionIdEmpty) + { + currentTable.Rows.RemoveAt(i); + i--; + } + else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections)) + { + keptRows++; + } + else + { + currentTable.Rows.RemoveAt(i); + i--; + } + } + else if (row.Operation == RowOperation.Modify) + { + var sequenceChanged = row.Fields[2].Modified; + var conditionChanged = row.Fields[1].Modified; + + if (sequenceChanged && !conditionChanged) + { + keptRows++; + } + else if (!sequenceChanged && conditionChanged) + { + if (isSectionIdEmpty) + { + currentTable.Rows.RemoveAt(i); + i--; + } + else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections)) + { + keptRows++; + } + else + { + currentTable.Rows.RemoveAt(i); + i--; + } + } + else if (sequenceChanged && conditionChanged) + { + if (isSectionIdEmpty) + { + row.Fields[1].Modified = false; + keptRows++; + } + else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections)) + { + keptRows++; + } + else + { + row.Fields[1].Modified = false; + keptRows++; + } + } + } + else if (row.Operation == RowOperation.Delete) + { + if (isSectionIdEmpty) + { + // it is a stardard action which is added by wix, we should keep this action. + row.Operation = RowOperation.None; + keptRows++; + } + else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections)) + { + keptRows++; + } + else + { + if (customAction.ContainsKey(actionName)) + { + currentTable.Rows.RemoveAt(i); + i--; + } + else + { + // it is a stardard action, we should keep this action. + row.Operation = RowOperation.None; + keptRows++; + } + } + } + else if (row.Operation == RowOperation.Add) + { + if (isSectionIdEmpty) + { + keptRows++; + } + else if (IsInPatchFamily(sections[0], sections[1], oldSections, newSections)) + { + keptRows++; + } + else + { + if (customAction.ContainsKey(actionName)) + { + currentTable.Rows.RemoveAt(i); + i--; + } + else + { + keptRows++; + } + } + } + } + } + + return keptRows; + } + } +} diff --git a/src/wix/WixToolset.Core.WindowsInstaller/MspBackend.cs b/src/wix/WixToolset.Core.WindowsInstaller/MspBackend.cs index bccdd3d4..d1d7c19b 100644 --- a/src/wix/WixToolset.Core.WindowsInstaller/MspBackend.cs +++ b/src/wix/WixToolset.Core.WindowsInstaller/MspBackend.cs @@ -39,10 +39,16 @@ namespace WixToolset.Core.WindowsInstaller patchTransforms = command.Execute(); } + // Reduce transforms. + { + var command = new ReduceTransformCommand(context.IntermediateRepresentation, patchTransforms); + command.Execute(); + } + // Enhance the intermediate by attaching the created patch transforms. IEnumerable subStorages; { - var command = new AttachPatchTransformsCommand(messaging, backendHelper, context.IntermediateRepresentation, patchTransforms); + var command = new CreatePatchSubStoragesCommand(messaging, backendHelper, context.IntermediateRepresentation, patchTransforms); subStorages = command.Execute(); } diff --git a/src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindDatabaseCommand.cs b/src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindDatabaseCommand.cs index 7bbbbd76..5eeb67c8 100644 --- a/src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindDatabaseCommand.cs +++ b/src/wix/WixToolset.Core.WindowsInstaller/Unbind/UnbindDatabaseCommand.cs @@ -21,8 +21,6 @@ namespace WixToolset.Core.WindowsInstaller.Unbind { private static readonly Regex Modularization = new Regex(@"\.[0-9A-Fa-f]{8}_[0-9A-Fa-f]{4}_[0-9A-Fa-f]{4}_[0-9A-Fa-f]{4}_[0-9A-Fa-f]{12}"); - private int sectionCount; - public UnbindDatabaseCommand(IMessaging messaging, IBackendHelper backendHelper, IPathResolver pathResolver, string databasePath, OutputType outputType, string exportBasePath, string extractFilesFolder, string intermediateFolder, bool enableDemodularization, bool skipSummaryInfo) { this.Messaging = messaging; @@ -65,6 +63,8 @@ namespace WixToolset.Core.WindowsInstaller.Unbind public bool AdminImage { get; private set; } + public WindowsInstallerData Data { get; private set; } + public IEnumerable ExportedFiles { get; private set; } public WindowsInstallerData Execute() @@ -72,7 +72,7 @@ namespace WixToolset.Core.WindowsInstaller.Unbind var adminImage = false; var exportedFiles = new List(); - var output = new WindowsInstallerData(new SourceLineNumber(this.DatabasePath)) + var data = new WindowsInstallerData(new SourceLineNumber(this.DatabasePath)) { Type = this.OutputType }; @@ -85,15 +85,13 @@ namespace WixToolset.Core.WindowsInstaller.Unbind Directory.CreateDirectory(this.IntermediateFolder); - output.Codepage = this.GetCodePage(); + data.Codepage = this.GetCodePage(); - var modularizationGuid = this.ProcessTables(output, exportedFiles); + var modularizationGuid = this.ProcessTables(data, exportedFiles); - var summaryInfo = this.ProcessSummaryInfo(output, modularizationGuid); + var summaryInfo = this.ProcessSummaryInfo(data, modularizationGuid); - this.UpdateUnrealFileColumns(this.DatabasePath, output, summaryInfo, exportedFiles); - - this.GenerateSectionIds(output); + this.UpdateUnrealFileColumns(this.DatabasePath, data, summaryInfo, exportedFiles); } } catch (Win32Exception e) @@ -107,9 +105,10 @@ namespace WixToolset.Core.WindowsInstaller.Unbind } this.AdminImage = adminImage; + this.Data = data; this.ExportedFiles = exportedFiles; - return output; + return data; } private int GetCodePage() @@ -657,207 +656,6 @@ namespace WixToolset.Core.WindowsInstaller.Unbind } } - /// - /// Creates section ids on rows which form logical groupings of resources. - /// - /// The Output that represents the msi database. - private void GenerateSectionIds(WindowsInstallerData output) - { - // First assign and index section ids for the tables that are in their own sections. - this.AssignSectionIdsToTable(output.Tables["Binary"], 0); - var componentSectionIdIndex = this.AssignSectionIdsToTable(output.Tables["Component"], 0); - var customActionSectionIdIndex = this.AssignSectionIdsToTable(output.Tables["CustomAction"], 0); - this.AssignSectionIdsToTable(output.Tables["Directory"], 0); - var featureSectionIdIndex = this.AssignSectionIdsToTable(output.Tables["Feature"], 0); - this.AssignSectionIdsToTable(output.Tables["Icon"], 0); - var digitalCertificateSectionIdIndex = this.AssignSectionIdsToTable(output.Tables["MsiDigitalCertificate"], 0); - this.AssignSectionIdsToTable(output.Tables["Property"], 0); - - // Now handle all the tables that rely on the first set of indexes but also produce their own indexes. Order matters here. - var fileSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["File"], componentSectionIdIndex, 1, 0); - var appIdSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["Class"], componentSectionIdIndex, 2, 5); - var odbcDataSourceSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["ODBCDataSource"], componentSectionIdIndex, 1, 0); - var odbcDriverSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["ODBCDriver"], componentSectionIdIndex, 1, 0); - var registrySectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["Registry"], componentSectionIdIndex, 5, 0); - var serviceInstallSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["ServiceInstall"], componentSectionIdIndex, 11, 0); - - // Now handle all the tables which only rely on previous indexes and order does not matter. - foreach (var table in output.Tables) - { - switch (table.Name) - { - case "MsiFileHash": - ConnectTableToSection(table, fileSectionIdIndex, 0); - break; - case "MsiAssembly": - case "MsiAssemblyName": - ConnectTableToSection(table, componentSectionIdIndex, 0); - break; - case "MsiPackageCertificate": - case "MsiPatchCertificate": - ConnectTableToSection(table, digitalCertificateSectionIdIndex, 1); - break; - case "CreateFolder": - case "FeatureComponents": - case "MoveFile": - case "ReserveCost": - case "ODBCTranslator": - ConnectTableToSection(table, componentSectionIdIndex, 1); - break; - case "TypeLib": - ConnectTableToSection(table, componentSectionIdIndex, 2); - break; - case "Shortcut": - case "Environment": - ConnectTableToSection(table, componentSectionIdIndex, 3); - break; - case "RemoveRegistry": - ConnectTableToSection(table, componentSectionIdIndex, 4); - break; - case "ServiceControl": - ConnectTableToSection(table, componentSectionIdIndex, 5); - break; - case "IniFile": - case "RemoveIniFile": - ConnectTableToSection(table, componentSectionIdIndex, 7); - break; - case "AppId": - ConnectTableToSection(table, appIdSectionIdIndex, 0); - break; - case "Condition": - ConnectTableToSection(table, featureSectionIdIndex, 0); - break; - case "ODBCSourceAttribute": - ConnectTableToSection(table, odbcDataSourceSectionIdIndex, 0); - break; - case "ODBCAttribute": - ConnectTableToSection(table, odbcDriverSectionIdIndex, 0); - break; - case "AdminExecuteSequence": - case "AdminUISequence": - case "AdvtExecuteSequence": - case "AdvtUISequence": - case "InstallExecuteSequence": - case "InstallUISequence": - ConnectTableToSection(table, customActionSectionIdIndex, 0); - break; - case "LockPermissions": - case "MsiLockPermissions": - foreach (var row in table.Rows) - { - var lockObject = (string)row[0]; - var tableName = (string)row[1]; - switch (tableName) - { - case "File": - row.SectionId = (string)fileSectionIdIndex[lockObject]; - break; - case "Registry": - row.SectionId = (string)registrySectionIdIndex[lockObject]; - break; - case "ServiceInstall": - row.SectionId = (string)serviceInstallSectionIdIndex[lockObject]; - break; - } - } - break; - } - } - - // Now pass the output to each unbinder extension to allow them to analyze the output and determine their proper section ids. - //foreach (IUnbinderExtension extension in this.unbinderExtensions) - //{ - // extension.GenerateSectionIds(output); - //} - } - - /// - /// Creates new section ids on all the rows in a table. - /// - /// The table to add sections to. - /// The index of the column which is used by other tables to reference this table. - /// A dictionary containing the tables key for each row paired with its assigned section id. - private Dictionary AssignSectionIdsToTable(Table table, int rowPrimaryKeyIndex) - { - var primaryKeyToSectionId = new Dictionary(); - - if (null != table) - { - foreach (var row in table.Rows) - { - row.SectionId = this.GetNewSectionId(); - - primaryKeyToSectionId.Add(row.FieldAsString(rowPrimaryKeyIndex), row.SectionId); - } - } - - return primaryKeyToSectionId; - } - - /// - /// Connects a table's rows to an already sectioned table. - /// - /// The table containing rows that need to be connected to sections. - /// A hashtable containing keys to map table to its section. - /// The index of the column which is used as the foreign key in to the sectionIdIndex. - private static void ConnectTableToSection(Table table, Dictionary sectionIdIndex, int rowIndex) - { - if (null != table) - { - foreach (var row in table.Rows) - { - if (sectionIdIndex.TryGetValue(row.FieldAsString(rowIndex), out var sectionId)) - { - row.SectionId = sectionId; - } - } - } - } - - /// - /// Connects a table's rows to an already sectioned table and produces an index for other tables to connect to it. - /// - /// The table containing rows that need to be connected to sections. - /// A dictionary containing keys to map table to its section. - /// The index of the column which is used as the foreign key in to the sectionIdIndex. - /// The index of the column which is used by other tables to reference this table. - /// A dictionary containing the tables key for each row paired with its assigned section id. - private static Dictionary ConnectTableToSectionAndIndex(Table table, Dictionary sectionIdIndex, int rowIndex, int rowPrimaryKeyIndex) - { - var newPrimaryKeyToSectionId = new Dictionary(); - - if (null != table) - { - foreach (var row in table.Rows) - { - var foreignKey = row.FieldAsString(rowIndex); - - if (!sectionIdIndex.TryGetValue(foreignKey, out var sectionId)) - { - continue; - } - - row.SectionId = sectionId; - - var primaryKey = row.FieldAsString(rowPrimaryKeyIndex); - - if (!String.IsNullOrEmpty(primaryKey) && sectionIdIndex.ContainsKey(primaryKey)) - { - newPrimaryKeyToSectionId.Add(primaryKey, row.SectionId); - } - } - } - - return newPrimaryKeyToSectionId; - } - - private string GetNewSectionId() - { - this.sectionCount++; - - return "wix.section." + this.sectionCount.ToString(CultureInfo.InvariantCulture); - } - private class SummaryInformationBits { public bool AdminImage { get; set; } diff --git a/src/wix/WixToolset.Core/Compiler.cs b/src/wix/WixToolset.Core/Compiler.cs index 8f1ae7fb..e100c5be 100644 --- a/src/wix/WixToolset.Core/Compiler.cs +++ b/src/wix/WixToolset.Core/Compiler.cs @@ -5793,13 +5793,13 @@ namespace WixToolset.Core this.ParseCertificatesElement(child); break; case "PatchFamily": - this.ParsePatchFamilyElement(child, ComplexReferenceParentType.Unknown, id.Id); + this.ParsePatchFamilyElement(child, ComplexReferenceParentType.Unknown, id?.Id); break; case "PatchFamilyGroup": - this.ParsePatchFamilyGroupElement(child, ComplexReferenceParentType.Unknown, id.Id); + this.ParsePatchFamilyGroupElement(child, ComplexReferenceParentType.Unknown, id?.Id); break; case "PatchFamilyGroupRef": - this.ParsePatchFamilyGroupRefElement(child, ComplexReferenceParentType.Unknown, id.Id); + this.ParsePatchFamilyGroupRefElement(child, ComplexReferenceParentType.Unknown, id?.Id); break; case "PayloadGroup": this.ParsePayloadGroupElement(child, ComplexReferenceParentType.Unknown, null); diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/PatchFixture.cs b/src/wix/test/WixToolsetTest.CoreIntegration/PatchFixture.cs index 945c346b..b0d49c05 100644 --- a/src/wix/test/WixToolsetTest.CoreIntegration/PatchFixture.cs +++ b/src/wix/test/WixToolsetTest.CoreIntegration/PatchFixture.cs @@ -394,6 +394,35 @@ namespace WixToolsetTest.CoreIntegration } } + [Fact] + public void CanBuildPatchWithFiltering() + { + var sourceFolder = TestData.Get(@"TestData", "PatchFamilyFilter"); + + using (var fs = new DisposableFileSystem()) + { + var baseFolder = fs.GetFolder(); + var tempFolderPatch = Path.Combine(baseFolder, "patch"); + + var patchPath = BuildMsp("Patch1.msp", sourceFolder, tempFolderPatch, "1.0.1", bindpaths: new[] { Path.GetDirectoryName(this.templateBaselinePdb), Path.GetDirectoryName(this.templateUpdatePdb) }); + + var doc = GetExtractPatchXml(patchPath); + WixAssert.StringEqual("{11111111-2222-3333-4444-555555555555}", doc.Root.Element(TargetProductCodeName).Value); + + var names = Query.GetSubStorageNames(patchPath); + WixAssert.CompareLineByLine(new[] { "#RTM.1", "RTM.1" }, names); + + var cab = Path.Combine(baseFolder, "foo.cab"); + Query.ExtractStream(patchPath, "foo.cab", cab); + + var files = Query.GetCabinetFiles(cab); + var file = files.Single(); + WixAssert.StringEqual("a.txt", file.Name); + var contents = file.OpenText().ReadToEnd(); + WixAssert.StringEqual("This is A v1.0.1 from the '.update-data' folder in 'PatchTemplatePackage'.\r\n\r\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod.\r\n", contents); + } + } + private static string BuildMsi(string outputName, string sourceFolder, string baseFolder, string defineV, string defineA, string defineB, IEnumerable bindpaths = null) { var extensionPath = Path.GetFullPath(new Uri(typeof(ExampleExtensionFactory).Assembly.CodeBase).LocalPath); diff --git a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/PatchFamilyFilter/Patch.wxs b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/PatchFamilyFilter/Patch.wxs index d39170c0..f48fd1ef 100644 --- a/src/wix/test/WixToolsetTest.CoreIntegration/TestData/PatchFamilyFilter/Patch.wxs +++ b/src/wix/test/WixToolsetTest.CoreIntegration/TestData/PatchFamilyFilter/Patch.wxs @@ -1,8 +1,8 @@ - + - + -- cgit v1.2.3-55-g6feb