// 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. var transformMediaTable = mainTransform.Transform.Tables["Media"]; if (null != transformMediaTable && 0 < transformMediaTable.Rows.Count) { 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 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(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) { 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 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 = (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, 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 (FileRow mainFileRow in mainFileTable.Rows) { // 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(mainFileRow.SourceLineNumbers); pairedFileRow.Operation = RowOperation.Modify; mainFileRow.CopyTo(pairedFileRow); // Override authored media for patch bind. mainFileRow.DiskId = mediaSymbol.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; } } } // 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; } } }