// 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.Data { using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using WixToolset.Core.WindowsInstaller; using WixToolset.Data.WindowsInstaller; using WixToolset.Extensibility; /// /// Contains output tables and logic for building an MSP package. /// public class Patch { private List inspectorExtensions; private Output patch; private TableDefinitionCollection tableDefinitions; public Output PatchOutput { get { return this.patch; } } public Patch() { this.inspectorExtensions = new List(); this.tableDefinitions = new TableDefinitionCollection(WindowsInstallerStandardInternal.GetTableDefinitions()); } /// /// Adds an extension. /// /// The extension to add. public void AddExtension(IInspectorExtension extension) { this.inspectorExtensions.Add(extension); } public void Load(string patchPath) { this.patch = Output.Load(patchPath, false); } /// /// Include transforms in a patch. /// /// List of transforms to attach. [SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters", MessageId = "System.InvalidOperationException.#ctor(System.String)")] public void AttachTransforms(List transforms) { #if REVISIT_FOR_PATCHING // Track if at least one transform gets attached. bool attachedTransform = false; if (transforms == null || transforms.Count == 0) { throw new WixException(WixErrors.PatchWithoutTransforms()); } // Get the patch id from the WixPatchId table. string patchId = null; string clientPatchId = null; Table wixPatchIdTable = this.patch.Tables["WixPatchId"]; if (null != wixPatchIdTable && 0 < wixPatchIdTable.Rows.Count) { Row patchIdRow = wixPatchIdTable.Rows[0]; if (null != patchIdRow) { patchId = patchIdRow[0].ToString(); clientPatchId = patchIdRow[1].ToString(); } } if (null == patchId) { throw new WixException(WixErrors.ExpectedPatchIdInWixMsp()); } if (null == clientPatchId) { throw new WixException(WixErrors.ExpectedClientPatchIdInWixMsp()); } // enumerate patch.Media to map diskId to Media row Table patchMediaTable = patch.Tables["Media"]; if (null == patchMediaTable || patchMediaTable.Rows.Count == 0) { throw new WixException(WixErrors.ExpectedMediaRowsInWixMsp()); } Hashtable mediaRows = new Hashtable(patchMediaTable.Rows.Count); foreach (MediaRow row in patchMediaTable.Rows) { int media = row.DiskId; mediaRows[media] = row; } // enumerate patch.WixPatchBaseline to map baseline to diskId Table patchBaselineTable = patch.Tables["WixPatchBaseline"]; int numPatchBaselineRows = (null != patchBaselineTable) ? patchBaselineTable.Rows.Count : 0; Hashtable baselineMedia = new Hashtable(numPatchBaselineRows); if (patchBaselineTable != null) { foreach (Row row in patchBaselineTable.Rows) { string baseline = (string)row[0]; int media = (int)row[1]; int validationFlags = (int)row[2]; if (baselineMedia.Contains(baseline)) { this.OnMessage(WixErrors.SamePatchBaselineId(row.SourceLineNumbers, baseline)); } baselineMedia[baseline] = new int[] { media, validationFlags }; } } // populate MSP summary information Table patchSummaryInfo = patch.EnsureTable(this.tableDefinitions["_SummaryInformation"]); // Remove properties that will be calculated or are reserved. for (int i = patchSummaryInfo.Rows.Count - 1; i >= 0; i--) { Row row = patchSummaryInfo.Rows[i]; switch ((SummaryInformation.Patch)row[0]) { case SummaryInformation.Patch.ProductCodes: case SummaryInformation.Patch.TransformNames: case SummaryInformation.Patch.PatchCode: case SummaryInformation.Patch.InstallerRequirement: case SummaryInformation.Patch.Reserved11: case SummaryInformation.Patch.Reserved14: case SummaryInformation.Patch.Reserved16: patchSummaryInfo.Rows.RemoveAt(i); break; } } // Index remaining summary properties. SummaryInfoRowCollection summaryInfo = new SummaryInfoRowCollection(patchSummaryInfo); // PID_CODEPAGE if (!summaryInfo.Contains((int)SummaryInformation.Patch.CodePage)) { // set the code page by default to the same code page for the // string pool in the database. Row codePage = patchSummaryInfo.CreateRow(null); codePage[0] = (int)SummaryInformation.Patch.CodePage; codePage[1] = this.patch.Codepage.ToString(CultureInfo.InvariantCulture); } // GUID patch code for the patch. Row revisionRow = patchSummaryInfo.CreateRow(null); revisionRow[0] = (int)SummaryInformation.Patch.PatchCode; revisionRow[1] = patchId; // Indicates the minimum Windows Installer version that is required to install the patch. Row wordsRow = patchSummaryInfo.CreateRow(null); wordsRow[0] = (int)SummaryInformation.Patch.InstallerRequirement; wordsRow[1] = ((int)SummaryInformation.InstallerRequirement.Version31).ToString(CultureInfo.InvariantCulture); if (!summaryInfo.Contains((int)SummaryInformation.Patch.Security)) { Row security = patchSummaryInfo.CreateRow(null); security[0] = (int)SummaryInformation.Patch.Security; security[1] = "4"; // Read-only enforced } // use authored comments or default to DisplayName (required) string comments = null; Table msiPatchMetadataTable = patch.Tables["MsiPatchMetadata"]; Hashtable metadataTable = new Hashtable(); if (null != msiPatchMetadataTable) { foreach (Row row in msiPatchMetadataTable.Rows) { metadataTable.Add(row.Fields[1].Data.ToString(), row.Fields[2].Data.ToString()); } if (!summaryInfo.Contains((int)SummaryInformation.Patch.Title) && metadataTable.Contains("DisplayName")) { string displayName = (string)metadataTable["DisplayName"]; Row title = patchSummaryInfo.CreateRow(null); title[0] = (int)SummaryInformation.Patch.Title; title[1] = displayName; // default comments use DisplayName as-is (no loc) comments = displayName; } if (!summaryInfo.Contains((int)SummaryInformation.Patch.CodePage) && metadataTable.Contains("CodePage")) { Row codePage = patchSummaryInfo.CreateRow(null); codePage[0] = (int)SummaryInformation.Patch.CodePage; codePage[1] = metadataTable["CodePage"]; } if (!summaryInfo.Contains((int)SummaryInformation.Patch.PackageName) && metadataTable.Contains("Description")) { Row subject = patchSummaryInfo.CreateRow(null); subject[0] = (int)SummaryInformation.Patch.PackageName; subject[1] = metadataTable["Description"]; } if (!summaryInfo.Contains((int)SummaryInformation.Patch.Manufacturer) && metadataTable.Contains("ManufacturerName")) { Row author = patchSummaryInfo.CreateRow(null); author[0] = (int)SummaryInformation.Patch.Manufacturer; author[1] = metadataTable["ManufacturerName"]; } } // special metadata marshalled through the build Table wixPatchMetadataTable = patch.Tables["WixPatchMetadata"]; Hashtable wixMetadataTable = new Hashtable(); if (null != wixPatchMetadataTable) { foreach (Row row in wixPatchMetadataTable.Rows) { wixMetadataTable.Add(row.Fields[0].Data.ToString(), row.Fields[1].Data.ToString()); } if (wixMetadataTable.Contains("Comments")) { comments = (string)wixMetadataTable["Comments"]; } } // write the package comments to summary info if (!summaryInfo.Contains((int)SummaryInformation.Patch.Comments) && null != comments) { Row commentsRow = patchSummaryInfo.CreateRow(null); commentsRow[0] = (int)SummaryInformation.Patch.Comments; commentsRow[1] = comments; } // enumerate transforms Dictionary productCodes = new Dictionary(); ArrayList transformNames = new ArrayList(); ArrayList validTransform = new ArrayList(); int transformCount = 0; foreach (PatchTransform mainTransform in transforms) { string baseline = null; int media = -1; int validationFlags = 0; if (baselineMedia.Contains(mainTransform.Baseline)) { int[] baselineData = (int[])baselineMedia[mainTransform.Baseline]; int newMedia = baselineData[0]; if (media != -1 && media != newMedia) { throw new InvalidOperationException(String.Format(CultureInfo.CurrentUICulture, WixStrings.EXP_TransformAuthoredIntoMultipleMedia, media, newMedia)); } baseline = mainTransform.Baseline; media = newMedia; validationFlags = baselineData[1]; } if (media == -1) { // transform's baseline not attached to any Media continue; } Table patchRefTable = patch.Tables["WixPatchRef"]; if (patchRefTable != null && patchRefTable.Rows.Count > 0) { if (!Patch.ReduceTransform(mainTransform.Transform, patchRefTable)) { // transform has none of the content authored into this patch continue; } } // Validate the transform doesn't break any patch specific rules. mainTransform.Validate(); // ensure consistent File.Sequence within each Media MediaRow mediaRow = (MediaRow)mediaRows[media]; // Ensure that files are sequenced after the last file in any transform. Table transformMediaTable = mainTransform.Transform.Tables["Media"]; if (null != transformMediaTable && 0 < transformMediaTable.Rows.Count) { foreach (MediaRow transformMediaRow in transformMediaTable.Rows) { if (mediaRow.LastSequence < transformMediaRow.LastSequence) { // The Binder will pre-increment the sequence. mediaRow.LastSequence = transformMediaRow.LastSequence; } } } // Use the Media/@DiskId if greater for backward compatibility. if (mediaRow.LastSequence < mediaRow.DiskId) { mediaRow.LastSequence = mediaRow.DiskId; } // ignore media table from transform. mainTransform.Transform.Tables.Remove("Media"); mainTransform.Transform.Tables.Remove("WixMedia"); mainTransform.Transform.Tables.Remove("MsiDigitalSignature"); string productCode; Output pairedTransform = this.BuildPairedTransform(patchId, clientPatchId, mainTransform.Transform, mediaRow, validationFlags, out productCode); productCodes[productCode] = null; DictionaryEntry entry = new DictionaryEntry(); entry.Key = productCode; entry.Value = mainTransform.Transform; validTransform.Add(entry); // attach these transforms to the patch object // TODO: is this an acceptable way to auto-generate transform stream names? string transformName = baseline + "." + (++transformCount).ToString(CultureInfo.InvariantCulture); patch.SubStorages.Add(new SubStorage(transformName, mainTransform.Transform)); patch.SubStorages.Add(new SubStorage("#" + transformName, pairedTransform)); transformNames.Add(":" + transformName); transformNames.Add(":#" + transformName); attachedTransform = true; } if (!attachedTransform) { throw new WixException(WixErrors.PatchWithoutValidTransforms()); } // Validate that a patch authored as removable is actually removable if (metadataTable.Contains("AllowRemoval")) { if ("1" == metadataTable["AllowRemoval"].ToString()) { ArrayList tables = Patch.GetPatchUninstallBreakingTables(); bool result = true; foreach (DictionaryEntry entry in validTransform) { result &= this.CheckUninstallableTransform(entry.Key.ToString(), (Output)entry.Value, tables); } if (!result) { throw new WixException(WixErrors.PatchNotRemovable()); } } } // Finish filling tables with transform-dependent data. // Semicolon delimited list of the product codes that can accept the patch. Table wixPatchTargetTable = patch.Tables["WixPatchTarget"]; if (null != wixPatchTargetTable) { Dictionary targets = new Dictionary(); bool replace = true; foreach (Row wixPatchTargetRow in wixPatchTargetTable.Rows) { string target = wixPatchTargetRow[0].ToString(); if (0 == String.CompareOrdinal("*", target)) { replace = false; } else { targets[target] = null; } } // Replace the target ProductCodes with the authored list. if (replace) { productCodes = targets; } else { // Copy the authored target ProductCodes into the list. foreach (string target in targets.Keys) { productCodes[target] = null; } } } string[] uniqueProductCodes = new string[productCodes.Keys.Count]; productCodes.Keys.CopyTo(uniqueProductCodes, 0); Row templateRow = patchSummaryInfo.CreateRow(null); templateRow[0] = (int)SummaryInformation.Patch.ProductCodes; templateRow[1] = String.Join(";", uniqueProductCodes); // Semicolon delimited list of transform substorage names in the order they are applied. Row savedbyRow = patchSummaryInfo.CreateRow(null); savedbyRow[0] = (int)SummaryInformation.Patch.TransformNames; savedbyRow[1] = String.Join(";", (string[])transformNames.ToArray(typeof(string))); } /// /// Ensure transform is uninstallable. /// /// Product code in transform. /// Transform generated by torch. /// Tables to be checked /// True if the transform is uninstallable private bool CheckUninstallableTransform(string productCode, Output transform, ArrayList tables) { bool ret = true; foreach (string table in tables) { Table wixTable = transform.Tables[table]; if (null != wixTable) { foreach (Row row in wixTable.Rows) { if (row.Operation == RowOperation.Add) { ret = false; string primaryKey = row.GetPrimaryKey('/'); if (null == primaryKey) { primaryKey = string.Empty; } this.OnMessage(WixErrors.NewRowAddedInTable(row.SourceLineNumbers, productCode, wixTable.Name, primaryKey)); } } } } return ret; } /// /// Tables affect patch uninstall. /// /// list of tables to be checked private static ArrayList GetPatchUninstallBreakingTables() { ArrayList tables = new ArrayList(); tables.Add("AppId"); tables.Add("BindImage"); tables.Add("Class"); tables.Add("Complus"); tables.Add("CreateFolder"); tables.Add("DuplicateFile"); tables.Add("Environment"); tables.Add("Extension"); tables.Add("Font"); tables.Add("IniFile"); tables.Add("IsolatedComponent"); tables.Add("LockPermissions"); tables.Add("MIME"); tables.Add("MoveFile"); tables.Add("MsiLockPermissionsEx"); tables.Add("MsiServiceConfig"); tables.Add("MsiServiceConfigFailureActions"); tables.Add("ODBCAttribute"); tables.Add("ODBCDataSource"); tables.Add("ODBCDriver"); tables.Add("ODBCSourceAttribute"); tables.Add("ODBCTranslator"); tables.Add("ProgId"); tables.Add("PublishComponent"); tables.Add("RemoveIniFile"); tables.Add("SelfReg"); tables.Add("ServiceControl"); tables.Add("ServiceInstall"); tables.Add("TypeLib"); tables.Add("Verb"); return tables; } /// /// Reduce the transform according to the patch references. /// /// transform generated by torch. /// Table contains patch family filter. /// true if the transform is not empty public static bool ReduceTransform(Output transform, Table patchRefTable) { // identify sections to keep Hashtable oldSections = new Hashtable(patchRefTable.Rows.Count); Hashtable newSections = new Hashtable(patchRefTable.Rows.Count); Hashtable tableKeyRows = new Hashtable(); ArrayList sequenceList = new ArrayList(); Hashtable componentFeatureAddsIndex = new Hashtable(); Hashtable customActionTable = new Hashtable(); Hashtable directoryTableAdds = new Hashtable(); Hashtable featureTableAdds = new Hashtable(); Hashtable keptComponents = new Hashtable(); Hashtable keptDirectories = new Hashtable(); Hashtable keptFeatures = new Hashtable(); Hashtable keptLockPermissions = new Hashtable(); Hashtable keptMsiLockPermissionExs = new Hashtable(); Dictionary> componentCreateFolderIndex = new Dictionary>(); Dictionary> directoryLockPermissionsIndex = new Dictionary>(); Dictionary> directoryMsiLockPermissionsExIndex = new Dictionary>(); foreach (Row patchRefRow in patchRefTable.Rows) { string tableName = (string)patchRefRow[0]; string key = (string)patchRefRow[1]; // Short circuit filtering if all changes should be included. if ("*" == tableName && "*" == key) { Patch.RemoveProductCodeFromTransform(transform); return true; } Table table = transform.Tables[tableName]; if (table == null) { // table not found continue; } // index this table if (!tableKeyRows.Contains(tableName)) { Hashtable newKeyRows = new Hashtable(); foreach (Row newRow in table.Rows) { newKeyRows[newRow.GetPrimaryKey('/')] = newRow; } tableKeyRows[tableName] = newKeyRows; } Hashtable keyRows = (Hashtable)tableKeyRows[tableName]; Row row = (Row)keyRows[key]; if (row == null) { // row not found continue; } // Differ.sectionDelimiter string[] sections = row.SectionId.Split('/'); oldSections[sections[0]] = row; newSections[sections[1]] = row; } // throw away sections not referenced int keptRows = 0; Table directoryTable = null; Table featureTable = null; Table lockPermissionsTable = null; Table msiLockPermissionsTable = null; foreach (Table 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 (int i = 0; i < table.Rows.Count; i++) { Row row = table.Rows[i]; if (table.Name == "CreateFolder") { string createFolderComponentId = (string)row[1]; List directoryList; if (!componentCreateFolderIndex.TryGetValue(createFolderComponentId, out directoryList)) { directoryList = new List(); componentCreateFolderIndex.Add(createFolderComponentId, directoryList); } directoryList.Add((string)row[0]); } if (table.Name == "CustomAction") { customActionTable.Add(row[0], row); } if (table.Name == "Directory") { directoryTable = table; if (RowOperation.Add == row.Operation) { directoryTableAdds.Add(row[0], row); } } if (table.Name == "Feature") { featureTable = table; if (RowOperation.Add == row.Operation) { featureTableAdds.Add(row[0], row); } } if (table.Name == "FeatureComponents") { if (RowOperation.Add == row.Operation) { string featureId = (string)row[0]; string componentId = (string)row[1]; if (componentFeatureAddsIndex.ContainsKey(componentId)) { ArrayList featureList = (ArrayList)componentFeatureAddsIndex[componentId]; featureList.Add(featureId); } else { ArrayList featureList = new ArrayList(); componentFeatureAddsIndex.Add(componentId, featureList); featureList.Add(featureId); } } } if (table.Name == "LockPermissions") { lockPermissionsTable = table; if ("CreateFolder" == (string)row[1]) { string directoryId = (string)row[0]; List rowList; if (!directoryLockPermissionsIndex.TryGetValue(directoryId, out rowList)) { rowList = new List(); directoryLockPermissionsIndex.Add(directoryId, rowList); } rowList.Add(row); } } if (table.Name == "MsiLockPermissionsEx") { msiLockPermissionsTable = table; if ("CreateFolder" == (string)row[1]) { string directoryId = (string)row[0]; List rowList; if (!directoryMsiLockPermissionsExIndex.TryGetValue(directoryId, out rowList)) { rowList = new List(); directoryMsiLockPermissionsExIndex.Add(directoryId, rowList); } rowList.Add(row); } } if (null == row.SectionId) { table.Rows.RemoveAt(i); i--; } else { string[] 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((string)row[0], row); } if ("Directory" == table.Name) { keptDirectories.Add(row[0], row); } if ("Feature" == table.Name) { keptFeatures.Add(row[0], row); } keptRows++; } else { table.Rows.RemoveAt(i); i--; } } } } keptRows += ReduceTransformSequenceTable(sequenceList, oldSections, newSections, customActionTable); if (null != directoryTable) { foreach (Row componentRow in keptComponents.Values) { string componentId = (string)componentRow[0]; if (RowOperation.Add == componentRow.Operation) { // make sure each added component has its required directory and feature heirarchy. string directoryId = (string)componentRow[2]; while (null != directoryId && directoryTableAdds.ContainsKey(directoryId)) { Row directoryRow = (Row)directoryTableAdds[directoryId]; if (!keptDirectories.ContainsKey(directoryId)) { directoryTable.Rows.Add(directoryRow); keptDirectories.Add(directoryRow[0], null); keptRows++; } directoryId = (string)directoryRow[1]; } if (componentFeatureAddsIndex.ContainsKey(componentId)) { foreach (string featureId in (ArrayList)componentFeatureAddsIndex[componentId]) { string currentFeatureId = featureId; while (null != currentFeatureId && featureTableAdds.ContainsKey(currentFeatureId)) { Row featureRow = (Row)featureTableAdds[currentFeatureId]; if (!keptFeatures.ContainsKey(currentFeatureId)) { featureTable.Rows.Add(featureRow); keptFeatures.Add(featureRow[0], null); keptRows++; } currentFeatureId = (string)featureRow[1]; } } } } // Hook in changes LockPermissions and MsiLockPermissions for folders for each component that has been kept. foreach (string keptComponentId in keptComponents.Keys) { List directoryList; if (componentCreateFolderIndex.TryGetValue(keptComponentId, out directoryList)) { foreach (string directoryId in directoryList) { List lockPermissionsRowList; if (directoryLockPermissionsIndex.TryGetValue(directoryId, out lockPermissionsRowList)) { foreach (Row lockPermissionsRow in lockPermissionsRowList) { string key = lockPermissionsRow.GetPrimaryKey('/'); if (!keptLockPermissions.ContainsKey(key)) { lockPermissionsTable.Rows.Add(lockPermissionsRow); keptLockPermissions.Add(key, null); keptRows++; } } } List msiLockPermissionsExRowList; if (directoryMsiLockPermissionsExIndex.TryGetValue(directoryId, out msiLockPermissionsExRowList)) { foreach (Row msiLockPermissionsExRow in msiLockPermissionsExRowList) { string key = msiLockPermissionsExRow.GetPrimaryKey('/'); if (!keptMsiLockPermissionExs.ContainsKey(key)) { msiLockPermissionsTable.Rows.Add(msiLockPermissionsExRow); keptMsiLockPermissionExs.Add(key, null); keptRows++; } } } } } } } } keptRows += ReduceTransformSequenceTable(sequenceList, oldSections, newSections, customActionTable); // Delete tables that are empty. ArrayList tablesToDelete = new ArrayList(); foreach (Table table in transform.Tables) { if (0 == table.Rows.Count) { tablesToDelete.Add(table.Name); } } // delete separately to avoid messing up enumeration foreach (string tableName in tablesToDelete) { transform.Tables.Remove(tableName); } return keptRows > 0; } /// /// Remove the ProductCode property from the transform. /// /// The transform. /// /// Changing the ProductCode is not supported in a patch. /// private static void RemoveProductCodeFromTransform(Output transform) { Table propertyTable = transform.Tables["Property"]; if (null != propertyTable) { for (int i = 0; i < propertyTable.Rows.Count; ++i) { Row propertyRow = propertyTable.Rows[i]; string 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 /// Hashtable contains section id should be kept in the baseline wixout. /// Hashtable contains section id should be kept in the upgrade wixout. /// true if section in patch family private static bool IsInPatchFamily(string oldSection, string newSection, Hashtable oldSections, Hashtable newSections) { bool result = false; if ((String.IsNullOrEmpty(oldSection) && newSections.Contains(newSection)) || (String.IsNullOrEmpty(newSection) && oldSections.Contains(oldSection))) { result = true; } else if (!String.IsNullOrEmpty(oldSection) && !String.IsNullOrEmpty(newSection) && (oldSections.Contains(oldSection) || newSections.Contains(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(ArrayList sequenceList, Hashtable oldSections, Hashtable newSections, Hashtable customAction) { int keptRows = 0; foreach (Table currentTable in sequenceList) { for (int i = 0; i < currentTable.Rows.Count; i++) { Row row = currentTable.Rows[i]; string actionName = row.Fields[0].Data.ToString(); string[] sections = row.SectionId.Split('/'); bool 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) { bool sequenceChanged = row.Fields[2].Modified; bool 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. /// /// Patch GUID from patch authoring. /// Easily referenced identity for this patch. /// Transform generated by torch. /// Media authored into patch. /// Transform validation flags for the summary information stream. /// Output string to receive ProductCode. [SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters", MessageId = "System.InvalidOperationException.#ctor(System.String)")] public Output BuildPairedTransform(string patchId, string clientPatchId, Output mainTransform, MediaRow mediaRow, int validationFlags, out string productCode) { productCode = null; Output pairedTransform = new Output(null); pairedTransform.Type = OutputType.Transform; pairedTransform.Codepage = mainTransform.Codepage; // lookup productVersion property to correct summaryInformation string newProductVersion = null; Table mainPropertyTable = mainTransform.Tables["Property"]; if (null != mainPropertyTable) { foreach (Row row in mainPropertyTable.Rows) { if ("ProductVersion" == (string)row[0]) { newProductVersion = (string)row[1]; } } } // TODO: build class for manipulating SummaryInformation table Table mainSummaryTable = mainTransform.Tables["_SummaryInformation"]; // add required properties Hashtable mainSummaryRows = new Hashtable(); foreach (Row mainSummaryRow in mainSummaryTable.Rows) { mainSummaryRows[mainSummaryRow[0]] = mainSummaryRow; } if (!mainSummaryRows.Contains((int)SummaryInformation.Transform.ValidationFlags)) { Row mainSummaryRow = mainSummaryTable.CreateRow(null); mainSummaryRow[0] = (int)SummaryInformation.Transform.ValidationFlags; mainSummaryRow[1] = validationFlags.ToString(CultureInfo.InvariantCulture); } // copy summary information from core transform Table pairedSummaryTable = pairedTransform.EnsureTable(this.tableDefinitions["_SummaryInformation"]); foreach (Row mainSummaryRow in mainSummaryTable.Rows) { string value = (string)mainSummaryRow[1]; switch ((SummaryInformation.Transform)mainSummaryRow[0]) { case SummaryInformation.Transform.ProductCodes: string[] propertyData = value.Split(';'); string oldProductVersion = propertyData[0].Substring(38); string 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 SummaryInformation.Transform.ValidationFlags: // use validation flags authored into the patch XML mainSummaryRow[1] = value = validationFlags.ToString(CultureInfo.InvariantCulture); break; } Row pairedSummaryRow = pairedSummaryTable.CreateRow(null); pairedSummaryRow[0] = mainSummaryRow[0]; pairedSummaryRow[1] = value; } if (productCode == null) { throw new InvalidOperationException(WixStrings.EXP_CouldnotDetermineProductCodeFromTransformSummaryInfo); } // copy File table Table mainFileTable = mainTransform.Tables["File"]; if (null != mainFileTable && 0 < mainFileTable.Rows.Count) { // We require file source information. Table mainWixFileTable = mainTransform.Tables["WixFile"]; if (null == mainWixFileTable) { throw new WixException(WixErrors.AdminImageRequired(productCode)); } RowDictionary mainFileRows = new RowDictionary(mainFileTable); Table pairedFileTable = pairedTransform.EnsureTable(mainFileTable.Definition); foreach (WixFileRow mainWixFileRow in mainWixFileTable.Rows) { FileRow 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; } FileRow pairedFileRow = (FileRow)pairedFileTable.CreateRow(null); pairedFileRow.Operation = RowOperation.Modify; for (int i = 0; i < mainFileRow.Fields.Length; i++) { pairedFileRow[i] = mainFileRow[i]; } // override authored media for patch bind mainWixFileRow.DiskId = mediaRow.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: // set msidbFileAttributesPatchAdded pairedFileRow.Attributes |= MsiInterop.MsidbFileAttributesPatchAdded; pairedFileRow.Fields[6].Modified = true; pairedFileRow.Operation = mainFileRow.Operation; break; default: pairedFileRow.Fields[6].Modified = false; break; } } } // add Media row to pairedTransform Table pairedMediaTable = pairedTransform.EnsureTable(this.tableDefinitions["Media"]); Row pairedMediaRow = pairedMediaTable.CreateRow(null); pairedMediaRow.Operation = RowOperation.Add; for (int i = 0; i < mediaRow.Fields.Length; i++) { pairedMediaRow[i] = mediaRow[i]; } // add PatchPackage for this Media Table pairedPackageTable = pairedTransform.EnsureTable(this.tableDefinitions["PatchPackage"]); pairedPackageTable.Operation = TableOperation.Add; Row pairedPackageRow = pairedPackageTable.CreateRow(null); pairedPackageRow.Operation = RowOperation.Add; pairedPackageRow[0] = patchId; pairedPackageRow[1] = mediaRow.DiskId; // add property to both identify client patches and whether those patches are removable or not int allowRemoval = 0; Table msiPatchMetadataTable = this.patch.Tables["MsiPatchMetadata"]; if (null != msiPatchMetadataTable) { foreach (Row msiPatchMetadataRow in msiPatchMetadataTable.Rows) { // get the value of the standard AllowRemoval property, if present string company = (string)msiPatchMetadataRow[0]; if ((null == company || 0 == company.Length) && "AllowRemoval" == (string)msiPatchMetadataRow[1]) { allowRemoval = Int32.Parse((string)msiPatchMetadataRow[2], CultureInfo.InvariantCulture); } } } // add the property to the patch transform's Property table Table pairedPropertyTable = pairedTransform.EnsureTable(this.tableDefinitions["Property"]); pairedPropertyTable.Operation = TableOperation.Add; Row pairedPropertyRow = pairedPropertyTable.CreateRow(null); pairedPropertyRow.Operation = RowOperation.Add; pairedPropertyRow[0] = string.Concat(clientPatchId, ".AllowRemoval"); pairedPropertyRow[1] = allowRemoval.ToString(CultureInfo.InvariantCulture); // add this patch code GUID to the patch transform to identify // which patches are installed, including in multi-patch // installations. pairedPropertyRow = pairedPropertyTable.CreateRow(null); pairedPropertyRow.Operation = RowOperation.Add; pairedPropertyRow[0] = string.Concat(clientPatchId, ".PatchCode"); pairedPropertyRow[1] = patchId; // add PATCHNEWPACKAGECODE to apply to admin layouts pairedPropertyRow = pairedPropertyTable.CreateRow(null); pairedPropertyRow.Operation = RowOperation.Add; pairedPropertyRow[0] = "PATCHNEWPACKAGECODE"; pairedPropertyRow[1] = patchId; // add PATCHNEWSUMMARYCOMMENTS and PATCHNEWSUMMARYSUBJECT to apply to admin layouts Table _summaryInformationTable = this.patch.Tables["_SummaryInformation"]; if (null != _summaryInformationTable) { foreach (Row row in _summaryInformationTable.Rows) { if (3 == (int)row[0]) // PID_SUBJECT { pairedPropertyRow = pairedPropertyTable.CreateRow(null); pairedPropertyRow.Operation = RowOperation.Add; pairedPropertyRow[0] = "PATCHNEWSUMMARYSUBJECT"; pairedPropertyRow[1] = row[1]; } else if (6 == (int)row[0]) // PID_COMMENTS { pairedPropertyRow = pairedPropertyTable.CreateRow(null); pairedPropertyRow.Operation = RowOperation.Add; pairedPropertyRow[0] = "PATCHNEWSUMMARYCOMMENTS"; pairedPropertyRow[1] = row[1]; } } } return pairedTransform; #endif throw new NotImplementedException(); } } }