From 6ff680e386b1543ad1a58d1b1d465ce8aa20bc7d Mon Sep 17 00:00:00 2001 From: Rob Mensching Date: Fri, 24 Jan 2020 15:27:20 -0800 Subject: Start on new patch infrastructure --- .../Bind/AttachPatchTransformsCommand.cs | 1322 ++++++++++++++++++++ 1 file changed, 1322 insertions(+) create mode 100644 src/WixToolset.Core.WindowsInstaller/Bind/AttachPatchTransformsCommand.cs (limited to 'src/WixToolset.Core.WindowsInstaller/Bind/AttachPatchTransformsCommand.cs') diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/AttachPatchTransformsCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/AttachPatchTransformsCommand.cs new file mode 100644 index 00000000..aa5ca20a --- /dev/null +++ b/src/WixToolset.Core.WindowsInstaller/Bind/AttachPatchTransformsCommand.cs @@ -0,0 +1,1322 @@ +// 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.WindowsInstaller; + using WixToolset.Core.WindowsInstaller.Msi; + using WixToolset.Data; + using WixToolset.Data.Tuples; + 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, Intermediate intermediate, IEnumerable transforms) + { + this.tableDefinitions = new TableDefinitionCollection(WindowsInstallerStandardInternal.GetTableDefinitions()); + this.Messaging = messaging; + this.Intermediate = intermediate; + this.Transforms = transforms; + } + + private IMessaging Messaging { 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 tuples = this.Intermediate.Sections.SelectMany(s => s.Tuples).ToList(); + + // Get the patch id from the WixPatchId tuple. + var patchIdTuple = tuples.OfType().FirstOrDefault(); + + if (String.IsNullOrEmpty(patchIdTuple.Id?.Id)) + { + this.Messaging.Write(ErrorMessages.ExpectedPatchIdInWixMsp()); + return subStorages; + } + + if (String.IsNullOrEmpty(patchIdTuple.ClientPatchId)) + { + this.Messaging.Write(ErrorMessages.ExpectedClientPatchIdInWixMsp()); + return subStorages; + } + + // enumerate patch.Media to map diskId to Media row + var patchMediaByDiskId = tuples.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, tuples, patchIdTuple, section.Codepage); + + // enumerate transforms + var productCodes = new SortedSet(); + var transformNames = new List(); + var validTransform = new List>(); + + var baselineTuplesById = tuples.OfType().ToDictionary(t => t.Id.Id); + + foreach (var mainTransform in this.Transforms) + { + var baselineTuple = baselineTuplesById[mainTransform.Baseline]; + + var patchRefTuples = tuples.OfType().ToList(); + if (patchRefTuples.Count > 0) + { + if (!this.ReduceTransform(mainTransform.Transform, patchRefTuples)) + { + // 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 mediaTuple = patchMediaByDiskId[baselineTuple.DiskId]; + + // Ensure that files are sequenced after the last file in any transform. + var transformMediaTable = mainTransform.Transform.Tables["Media"]; + if (null != transformMediaTable && 0 < transformMediaTable.Rows.Count) + { + foreach (MediaRow transformMediaRow in transformMediaTable.Rows) + { + if (mediaTuple.LastSequence < transformMediaRow.LastSequence) + { + // The Binder will pre-increment the sequence. + mediaTuple.LastSequence = transformMediaRow.LastSequence; + } + } + } + + // Use the Media/@DiskId if greater than the last sequence for backward compatibility. + if (mediaTuple.LastSequence < mediaTuple.DiskId) + { + mediaTuple.LastSequence = mediaTuple.DiskId; + } + + // Ignore media table in the transform. + mainTransform.Transform.Tables.Remove("Media"); + mainTransform.Transform.Tables.Remove("WixMedia"); + mainTransform.Transform.Tables.Remove("MsiDigitalSignature"); + + var pairedTransform = this.BuildPairedTransform(summaryInfo, patchMetadata, patchIdTuple, mainTransform.Transform, mediaTuple, baselineTuple, out var productCode); + + productCode = productCode.ToUpperInvariant(); + productCodes.Add(productCode); + validTransform.Add(Tuple.Create(productCode, mainTransform.Transform)); + + // attach these transforms to the patch object + // TODO: is this an acceptable way to auto-generate transform stream names? + var transformName = mainTransform.Baseline + "." + validTransform.Count.ToString(CultureInfo.InvariantCulture); + subStorages.Add(new SubStorage(transformName, mainTransform.Transform)); + subStorages.Add(new SubStorage("#" + transformName, pairedTransform)); + + transformNames.Add(":" + transformName); + transformNames.Add(":#" + transformName); + } + + 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(tuples, productCodes); + + // Semicolon delimited list of the product codes that can accept the patch. + summaryInfo.Add(SumaryInformationType.PatchProductCodes, new SummaryInformationTuple(patchIdTuple.SourceLineNumbers) + { + PropertyId = SumaryInformationType.PatchProductCodes, + Value = String.Join(";", productCodes) + }); + + // Semicolon delimited list of transform substorage names in the order they are applied. + summaryInfo.Add(SumaryInformationType.TransformNames, new SummaryInformationTuple(patchIdTuple.SourceLineNumbers) + { + PropertyId = SumaryInformationType.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.Tuples.Add(readSummaryInfo); + } + + this.SubStorages = subStorages; + + return subStorages; + } + + private Dictionary ExtractPatchSummaryInfo() + { + var result = new Dictionary(); + + foreach (var section in this.Intermediate.Sections) + { + for (var i = section.Tuples.Count - 1; i >= 0; i--) + { + if (section.Tuples[i] is SummaryInformationTuple patchSummaryInfo) + { + // Remove all summary information from the tuples and remember those that + // are not calculated or reserved. + section.Tuples.RemoveAt(i); + + if (patchSummaryInfo.PropertyId != SumaryInformationType.PatchProductCodes && + patchSummaryInfo.PropertyId != SumaryInformationType.PatchCode && + patchSummaryInfo.PropertyId != SumaryInformationType.PatchInstallerRequirement && + patchSummaryInfo.PropertyId != SumaryInformationType.Reserved11 && + patchSummaryInfo.PropertyId != SumaryInformationType.Reserved14 && + patchSummaryInfo.PropertyId != SumaryInformationType.Reserved16) + { + result.Add(patchSummaryInfo.PropertyId, patchSummaryInfo); + } + } + } + } + + return result; + } + + private Dictionary PopulateSummaryInformation(Dictionary summaryInfo, List tuples, WixPatchIdTuple patchIdTuple, int codepage) + { + // PID_CODEPAGE + if (!summaryInfo.ContainsKey(SumaryInformationType.Codepage)) + { + // Set the code page by default to the same code page for the + // string pool in the database. + AddSummaryInformation(SumaryInformationType.Codepage, codepage.ToString(CultureInfo.InvariantCulture), patchIdTuple.SourceLineNumbers); + } + + // GUID patch code for the patch. + AddSummaryInformation(SumaryInformationType.PatchCode, patchIdTuple.Id.Id, patchIdTuple.SourceLineNumbers); + + // Indicates the minimum Windows Installer version that is required to install the patch. + AddSummaryInformation(SumaryInformationType.PatchInstallerRequirement, ((int)SummaryInformation.InstallerRequirement.Version31).ToString(CultureInfo.InvariantCulture), patchIdTuple.SourceLineNumbers); + + if (!summaryInfo.ContainsKey(SumaryInformationType.Security)) + { + AddSummaryInformation(SumaryInformationType.Security, "4", patchIdTuple.SourceLineNumbers); // Read-only enforced; + } + + // Use authored comments or default to display name. + MsiPatchMetadataTuple commentsTuple = null; + + var metadataTuples = tuples.OfType().Where(t => String.IsNullOrEmpty(t.Company)).ToDictionary(t => t.Property); + + if (!summaryInfo.ContainsKey(SumaryInformationType.Title) && + metadataTuples.TryGetValue("DisplayName", out var displayName)) + { + AddSummaryInformation(SumaryInformationType.Title, displayName.Value, displayName.SourceLineNumbers); + + // Default comments to use display name as-is. + commentsTuple = displayName; + } + + // TODO: This code below seems unnecessary given the codepage is set at the top of this method. + //if (!summaryInfo.ContainsKey(SumaryInformationType.Codepage) && + // metadataValues.TryGetValue("CodePage", out var codepage)) + //{ + // AddSummaryInformation(SumaryInformationType.Codepage, codepage); + //} + + if (!summaryInfo.ContainsKey(SumaryInformationType.PatchPackageName) && + metadataTuples.TryGetValue("Description", out var description)) + { + AddSummaryInformation(SumaryInformationType.PatchPackageName, description.Value, description.SourceLineNumbers); + } + + if (!summaryInfo.ContainsKey(SumaryInformationType.Author) && + metadataTuples.TryGetValue("ManufacturerName", out var manufacturer)) + { + AddSummaryInformation(SumaryInformationType.Author, manufacturer.Value, manufacturer.SourceLineNumbers); + } + + // Special metadata marshalled through the build. + //var wixMetadataValues = tuples.OfType().ToDictionary(t => t.Id.Id, t => t.Value); + + //if (wixMetadataValues.TryGetValue("Comments", out var wixComments)) + if (metadataTuples.TryGetValue("Comments", out var wixComments)) + { + commentsTuple = wixComments; + } + + // Write the package comments to summary info. + if (!summaryInfo.ContainsKey(SumaryInformationType.Comments) && + commentsTuple != null) + { + AddSummaryInformation(SumaryInformationType.Comments, commentsTuple.Value, commentsTuple.SourceLineNumbers); + } + + return metadataTuples; + + void AddSummaryInformation(SumaryInformationType type, string value, SourceLineNumber sourceLineNumber) + { + summaryInfo.Add(type, new SummaryInformationTuple(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) + { + if (row.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 patchRefTuples) + { + // 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 patchRefTuple in patchRefTuples) + { + var tableName = patchRefTuple.Table; + var key = patchRefTuple.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); + + 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 (var row in fileTable.Rows) + { + if (RowOperation.None == row.Operation) + { + continue; + } + + var fileId = row.FieldAsString(0); + var componentId = row.FieldAsString(1); + + // 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 = Common.GetName(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 = Common.GetName(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 = (string)propertyRow[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, WixPatchIdTuple patchIdTuple, WindowsInstallerData mainTransform, MediaTuple mediaTuple, WixPatchBaselineTuple baselineTuple, 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)baselineTuple.ValidationFlags).ToString(CultureInfo.InvariantCulture); + + if (!mainSummaryRows.ContainsKey((int)SumaryInformationType.TransformValidationFlags)) + { + var mainSummaryRow = mainSummaryTable.CreateRow(baselineTuple.SourceLineNumbers); + mainSummaryRow[0] = (int)SumaryInformationType.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 = (SumaryInformationType)mainSummaryRow.FieldAsInteger(0); + var value = mainSummaryRow.FieldAsString(1); + switch (type) + { + case SumaryInformationType.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 SumaryInformationType.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) + { +#if TODO_PATCHING + // We require file source information. + var mainWixFileTable = mainTransform.Tables["WixFile"]; + if (null == mainWixFileTable) + { + this.Messaging.Write(ErrorMessages.AdminImageRequired(productCode)); + return null; + } + + var mainFileRows = new RowDictionary(mainFileTable); + + var pairedFileTable = pairedTransform.EnsureTable(mainFileTable.Definition); + { + var mainFileRow = mainFileRows[mainWixFileRow.File]; + + // set File.Sequence to non null to satisfy transform bind + mainFileRow.Sequence = 1; + + // delete's don't need rows in the paired transform + if (mainFileRow.Operation == RowOperation.Delete) + { + continue; + } + + var pairedFileRow = (FileRow)pairedFileTable.CreateRow(null); + pairedFileRow.Operation = RowOperation.Modify; + for (var i = 0; i < mainFileRow.Fields.Length; i++) + { + pairedFileRow[i] = mainFileRow[i]; + } + + // override authored media for patch bind + mainWixFileRow.DiskId = mediaTuple.DiskId; + + // suppress any change to File.Sequence to avoid bloat + mainFileRow.Fields[7].Modified = false; + + // force File row 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; + } + } +#endif + } + + // Add Media row to pairedTransform + var pairedMediaTable = pairedTransform.EnsureTable(this.tableDefinitions["Media"]); + var pairedMediaRow = pairedMediaTable.CreateRow(mediaTuple.SourceLineNumbers); + pairedMediaRow.Operation = RowOperation.Add; + pairedMediaRow[0] = mediaTuple.DiskId; + pairedMediaRow[1] = mediaTuple.LastSequence ?? 0; + pairedMediaRow[2] = mediaTuple.DiskPrompt; + pairedMediaRow[3] = mediaTuple.Cabinet; + pairedMediaRow[4] = mediaTuple.VolumeLabel; + pairedMediaRow[5] = mediaTuple.Source; + + // Add PatchPackage for this Media + var pairedPackageTable = pairedTransform.EnsureTable(this.tableDefinitions["PatchPackage"]); + pairedPackageTable.Operation = TableOperation.Add; + var pairedPackageRow = pairedPackageTable.CreateRow(mediaTuple.SourceLineNumbers); + pairedPackageRow.Operation = RowOperation.Add; + pairedPackageRow[0] = patchIdTuple.Id.Id; + pairedPackageRow[1] = mediaTuple.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 allowRemovalTuple); + + var pairedPropertyRow = pairedPropertyTable.CreateRow(allowRemovalTuple?.SourceLineNumbers); + pairedPropertyRow.Operation = RowOperation.Add; + pairedPropertyRow[0] = String.Concat(patchIdTuple.ClientPatchId, ".AllowRemoval"); + pairedPropertyRow[1] = allowRemovalTuple?.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(patchIdTuple.SourceLineNumbers); + pairedPropertyRow.Operation = RowOperation.Add; + pairedPropertyRow[0] = String.Concat(patchIdTuple.ClientPatchId, ".PatchCode"); + pairedPropertyRow[1] = patchIdTuple.Id.Id; + + // Add PATCHNEWPACKAGECODE to apply to admin layouts. + pairedPropertyRow = pairedPropertyTable.CreateRow(patchIdTuple.SourceLineNumbers); + pairedPropertyRow.Operation = RowOperation.Add; + pairedPropertyRow[0] = "PATCHNEWPACKAGECODE"; + pairedPropertyRow[1] = patchIdTuple.Id.Id; + + // Add PATCHNEWSUMMARYCOMMENTS and PATCHNEWSUMMARYSUBJECT to apply to admin layouts. + if (summaryInfo.TryGetValue(SumaryInformationType.Subject, out var subjectTuple)) + { + pairedPropertyRow = pairedPropertyTable.CreateRow(subjectTuple.SourceLineNumbers); + pairedPropertyRow.Operation = RowOperation.Add; + pairedPropertyRow[0] = "PATCHNEWSUMMARYSUBJECT"; + pairedPropertyRow[1] = subjectTuple.Value; + } + + if (summaryInfo.TryGetValue(SumaryInformationType.Comments, out var commentsTuple)) + { + pairedPropertyRow = pairedPropertyTable.CreateRow(commentsTuple.SourceLineNumbers); + pairedPropertyRow.Operation = RowOperation.Add; + pairedPropertyRow[0] = "PATCHNEWSUMMARYCOMMENTS"; + pairedPropertyRow[1] = commentsTuple.Value; + } + + return pairedTransform; + } + + private static SortedSet FinalizePatchProductCodes(List tuples, SortedSet productCodes) + { + var patchTargetTuples = tuples.OfType().ToList(); + + if (patchTargetTuples.Count > 0) + { + var targets = new SortedSet(); + var replace = true; + foreach (var wixPatchTargetRow in patchTargetTuples) + { + 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; + } + } +} -- cgit v1.2.3-55-g6feb