// 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 { using System; using System.Collections; using System.Collections.Generic; using System.Globalization; using WixToolset.Data; using WixToolset.Data.WindowsInstaller; using WixToolset.Data.WindowsInstaller.Rows; using WixToolset.Extensibility; using WixToolset.Extensibility.Services; using WixToolset.Msi; /// /// Creates a transform by diffing two outputs. /// public sealed class Differ { private List inspectorExtensions; private bool showPedanticMessages; private bool suppressKeepingSpecialRows; private bool preserveUnchangedRows; private const char sectionDelimiter = '/'; private readonly IMessaging messaging; private SummaryInformationStreams transformSummaryInfo; /// /// Instantiates a new Differ class. /// public Differ(IMessaging messaging) { this.inspectorExtensions = new List(); this.messaging = messaging; } /// /// Gets or sets the option to show pedantic messages. /// /// The option to show pedantic messages. public bool ShowPedanticMessages { get { return this.showPedanticMessages; } set { this.showPedanticMessages = value; } } /// /// Gets or sets the option to suppress keeping special rows. /// /// The option to suppress keeping special rows. public bool SuppressKeepingSpecialRows { get { return this.suppressKeepingSpecialRows; } set { this.suppressKeepingSpecialRows = value; } } /// /// Gets or sets the flag to determine if all rows, even unchanged ones will be persisted in the output. /// /// The option to keep all rows including unchanged rows. public bool PreserveUnchangedRows { get { return this.preserveUnchangedRows; } set { this.preserveUnchangedRows = value; } } /// /// Adds an extension. /// /// The extension to add. public void AddExtension(IInspectorExtension extension) { this.inspectorExtensions.Add(extension); } /// /// Creates a transform by diffing two outputs. /// /// The target output. /// The updated output. /// The transform. public Output Diff(Output targetOutput, Output updatedOutput) { return Diff(targetOutput, updatedOutput, 0); } /// /// Creates a transform by diffing two outputs. /// /// The target output. /// The updated output. /// /// The transform. public Output Diff(Output targetOutput, Output updatedOutput, TransformFlags validationFlags) { Output transform = new Output(null); transform.Type = OutputType.Transform; transform.Codepage = updatedOutput.Codepage; this.transformSummaryInfo = new SummaryInformationStreams(); // compare the codepages if (targetOutput.Codepage != updatedOutput.Codepage && 0 == (TransformFlags.ErrorChangeCodePage & validationFlags)) { this.messaging.Write(ErrorMessages.OutputCodepageMismatch(targetOutput.SourceLineNumbers, targetOutput.Codepage, updatedOutput.Codepage)); if (null != updatedOutput.SourceLineNumbers) { this.messaging.Write(ErrorMessages.OutputCodepageMismatch2(updatedOutput.SourceLineNumbers)); } } // compare the output types if (targetOutput.Type != updatedOutput.Type) { throw new WixException(ErrorMessages.OutputTypeMismatch(targetOutput.SourceLineNumbers, targetOutput.Type.ToString(), updatedOutput.Type.ToString())); } // compare the contents of the tables foreach (Table targetTable in targetOutput.Tables) { Table updatedTable = updatedOutput.Tables[targetTable.Name]; TableOperation operation = TableOperation.None; List rows = this.CompareTables(targetOutput, targetTable, updatedTable, out operation); if (TableOperation.Drop == operation) { Table droppedTable = transform.EnsureTable(targetTable.Definition); droppedTable.Operation = TableOperation.Drop; } else if (TableOperation.None == operation) { Table modified = transform.EnsureTable(updatedTable.Definition); rows.ForEach(r => modified.Rows.Add(r)); } } // added tables foreach (Table updatedTable in updatedOutput.Tables) { if (null == targetOutput.Tables[updatedTable.Name]) { Table addedTable = transform.EnsureTable(updatedTable.Definition); addedTable.Operation = TableOperation.Add; foreach (Row updatedRow in updatedTable.Rows) { updatedRow.Operation = RowOperation.Add; updatedRow.SectionId = sectionDelimiter + updatedRow.SectionId; addedTable.Rows.Add(updatedRow); } } } // set summary information properties if (!this.suppressKeepingSpecialRows) { Table summaryInfoTable = transform.Tables["_SummaryInformation"]; this.UpdateTransformSummaryInformationTable(summaryInfoTable, validationFlags); } return transform; } /// /// Add a row to the using the primary key. /// /// The indexed rows. /// The row to index. private void AddIndexedRow(IDictionary index, Row row) { string primaryKey = row.GetPrimaryKey('/'); if (null != primaryKey) { // Overriding WixActionRows have a primary key defined and take precedence in the index. if (row is WixActionRow) { WixActionRow currentRow = (WixActionRow)row; if (index.Contains(primaryKey)) { // If the current row is not overridable, see if the indexed row is. if (!currentRow.Overridable) { WixActionRow indexedRow = index[primaryKey] as WixActionRow; if (null != indexedRow && indexedRow.Overridable) { // The indexed key is overridable and should be replaced // (not removed and re-added which results in two Array.Copy // operations for SortedList, or may be re-hashing in other // implementations of IDictionary). index[primaryKey] = currentRow; } } // If we got this far, the row does not need to be indexed. return; } } // Nothing else should be added more than once. if (!index.Contains(primaryKey)) { index.Add(primaryKey, row); } else if (this.showPedanticMessages) { this.messaging.Write(ErrorMessages.DuplicatePrimaryKey(row.SourceLineNumbers, primaryKey, row.Table.Name)); } } else // use the string representation of the row as its primary key (it may not be unique) { // this is provided for compatibility with unreal tables with no primary key // all real tables must specify at least one column as the primary key primaryKey = row.ToString(); index[primaryKey] = row; } } private Row CompareRows(Table targetTable, Row targetRow, Row updatedRow, out RowOperation operation, out bool keepRow) { Row comparedRow = null; keepRow = false; operation = RowOperation.None; if (null == targetRow ^ null == updatedRow) { if (null == targetRow) { operation = updatedRow.Operation = RowOperation.Add; comparedRow = updatedRow; } else if (null == updatedRow) { operation = targetRow.Operation = RowOperation.Delete; targetRow.SectionId = targetRow.SectionId + sectionDelimiter; comparedRow = targetRow; keepRow = true; } } else // possibly modified { updatedRow.Operation = RowOperation.None; if (!this.suppressKeepingSpecialRows && "_SummaryInformation" == targetTable.Name) { // ignore rows that shouldn't be in a transform if (Enum.IsDefined(typeof(SummaryInformation.Transform), (int)updatedRow[0])) { updatedRow.SectionId = targetRow.SectionId + sectionDelimiter + updatedRow.SectionId; comparedRow = updatedRow; keepRow = true; operation = RowOperation.Modify; } } else { if (this.preserveUnchangedRows) { keepRow = true; } for (int i = 0; i < updatedRow.Fields.Length; i++) { ColumnDefinition columnDefinition = updatedRow.Fields[i].Column; if (!columnDefinition.PrimaryKey) { bool modified = false; if (i >= targetRow.Fields.Length) { columnDefinition.Added = true; modified = true; } else if (ColumnType.Number == columnDefinition.Type && !columnDefinition.IsLocalizable) { if (null == targetRow[i] ^ null == updatedRow[i]) { modified = true; } else if (null != targetRow[i] && null != updatedRow[i]) { modified = ((int)targetRow[i] != (int)updatedRow[i]); } } else if (ColumnType.Preserved == columnDefinition.Type) { updatedRow.Fields[i].PreviousData = (string)targetRow.Fields[i].Data; // keep rows containing preserved fields so the historical data is available to the binder keepRow = !this.suppressKeepingSpecialRows; } else if (ColumnType.Object == columnDefinition.Type) { ObjectField targetObjectField = (ObjectField)targetRow.Fields[i]; ObjectField updatedObjectField = (ObjectField)updatedRow.Fields[i]; updatedObjectField.PreviousEmbeddedFileIndex = targetObjectField.EmbeddedFileIndex; updatedObjectField.PreviousBaseUri = targetObjectField.BaseUri; // always keep a copy of the previous data even if they are identical // This makes diff.wixmst clean and easier to control patch logic updatedObjectField.PreviousData = (string)targetObjectField.Data; // always remember the unresolved data for target build updatedObjectField.UnresolvedPreviousData = (string)targetObjectField.UnresolvedData; // keep rows containing object fields so the files can be compared in the binder keepRow = !this.suppressKeepingSpecialRows; } else { modified = ((string)targetRow[i] != (string)updatedRow[i]); } if (modified) { if (null != updatedRow.Fields[i].PreviousData) { updatedRow.Fields[i].PreviousData = targetRow.Fields[i].Data.ToString(); } updatedRow.Fields[i].Modified = true; operation = updatedRow.Operation = RowOperation.Modify; keepRow = true; } } } if (keepRow) { comparedRow = updatedRow; comparedRow.SectionId = targetRow.SectionId + sectionDelimiter + updatedRow.SectionId; } } } return comparedRow; } private List CompareTables(Output targetOutput, Table targetTable, Table updatedTable, out TableOperation operation) { List rows = new List(); operation = TableOperation.None; // dropped tables if (null == updatedTable ^ null == targetTable) { if (null == targetTable) { operation = TableOperation.Add; rows.AddRange(updatedTable.Rows); } else if (null == updatedTable) { operation = TableOperation.Drop; } } else // possibly modified tables { SortedList updatedPrimaryKeys = new SortedList(); SortedList targetPrimaryKeys = new SortedList(); // compare the table definitions if (0 != targetTable.Definition.CompareTo(updatedTable.Definition)) { // continue to the next table; may be more mismatches this.messaging.Write(ErrorMessages.DatabaseSchemaMismatch(targetOutput.SourceLineNumbers, targetTable.Name)); } else { this.IndexPrimaryKeys(targetTable, targetPrimaryKeys, updatedTable, updatedPrimaryKeys); // diff the target and updated rows foreach (DictionaryEntry targetPrimaryKeyEntry in targetPrimaryKeys) { string targetPrimaryKey = (string)targetPrimaryKeyEntry.Key; bool keepRow = false; RowOperation rowOperation = RowOperation.None; Row compared = this.CompareRows(targetTable, targetPrimaryKeyEntry.Value as Row, updatedPrimaryKeys[targetPrimaryKey] as Row, out rowOperation, out keepRow); if (keepRow) { rows.Add(compared); } } // find the inserted rows foreach (DictionaryEntry updatedPrimaryKeyEntry in updatedPrimaryKeys) { string updatedPrimaryKey = (string)updatedPrimaryKeyEntry.Key; if (!targetPrimaryKeys.Contains(updatedPrimaryKey)) { Row updatedRow = (Row)updatedPrimaryKeyEntry.Value; updatedRow.Operation = RowOperation.Add; updatedRow.SectionId = sectionDelimiter + updatedRow.SectionId; rows.Add(updatedRow); } } } } return rows; } private void IndexPrimaryKeys(Table targetTable, SortedList targetPrimaryKeys, Table updatedTable, SortedList updatedPrimaryKeys) { // index the target rows foreach (Row row in targetTable.Rows) { this.AddIndexedRow(targetPrimaryKeys, row); if ("Property" == targetTable.Name) { if ("ProductCode" == (string)row[0]) { this.transformSummaryInfo.TargetProductCode = (string)row[1]; if ("*" == this.transformSummaryInfo.TargetProductCode) { this.messaging.Write(ErrorMessages.ProductCodeInvalidForTransform(row.SourceLineNumbers)); } } else if ("ProductVersion" == (string)row[0]) { this.transformSummaryInfo.TargetProductVersion = (string)row[1]; } else if ("UpgradeCode" == (string)row[0]) { this.transformSummaryInfo.TargetUpgradeCode = (string)row[1]; } } else if ("_SummaryInformation" == targetTable.Name) { if (1 == (int)row[0]) // PID_CODEPAGE { this.transformSummaryInfo.TargetSummaryInfoCodepage = (string)row[1]; } else if (7 == (int)row[0]) // PID_TEMPLATE { this.transformSummaryInfo.TargetPlatformAndLanguage = (string)row[1]; } else if (14 == (int)row[0]) // PID_PAGECOUNT { this.transformSummaryInfo.TargetMinimumVersion = (string)row[1]; } } } // index the updated rows foreach (Row row in updatedTable.Rows) { this.AddIndexedRow(updatedPrimaryKeys, row); if ("Property" == updatedTable.Name) { if ("ProductCode" == (string)row[0]) { this.transformSummaryInfo.UpdatedProductCode = (string)row[1]; if ("*" == this.transformSummaryInfo.UpdatedProductCode) { this.messaging.Write(ErrorMessages.ProductCodeInvalidForTransform(row.SourceLineNumbers)); } } else if ("ProductVersion" == (string)row[0]) { this.transformSummaryInfo.UpdatedProductVersion = (string)row[1]; } } else if ("_SummaryInformation" == updatedTable.Name) { if (1 == (int)row[0]) // PID_CODEPAGE { this.transformSummaryInfo.UpdatedSummaryInfoCodepage = (string)row[1]; } else if (7 == (int)row[0]) // PID_TEMPLATE { this.transformSummaryInfo.UpdatedPlatformAndLanguage = (string)row[1]; } else if (14 == (int)row[0]) // PID_PAGECOUNT { this.transformSummaryInfo.UpdatedMinimumVersion = (string)row[1]; } } } } private void UpdateTransformSummaryInformationTable(Table summaryInfoTable, TransformFlags validationFlags) { // calculate the minimum version of MSI required to process the transform int targetMin; int updatedMin; int minimumVersion = 100; if (Int32.TryParse(this.transformSummaryInfo.TargetMinimumVersion, out targetMin) && Int32.TryParse(this.transformSummaryInfo.UpdatedMinimumVersion, out updatedMin)) { minimumVersion = Math.Max(targetMin, updatedMin); } Hashtable summaryRows = new Hashtable(summaryInfoTable.Rows.Count); foreach (Row row in summaryInfoTable.Rows) { summaryRows[row[0]] = row; if ((int)SummaryInformation.Transform.CodePage == (int)row[0]) { row.Fields[1].Data = this.transformSummaryInfo.UpdatedSummaryInfoCodepage; row.Fields[1].PreviousData = this.transformSummaryInfo.TargetSummaryInfoCodepage; } else if ((int)SummaryInformation.Transform.TargetPlatformAndLanguage == (int)row[0]) { row[1] = this.transformSummaryInfo.TargetPlatformAndLanguage; } else if ((int)SummaryInformation.Transform.UpdatedPlatformAndLanguage == (int)row[0]) { row[1] = this.transformSummaryInfo.UpdatedPlatformAndLanguage; } else if ((int)SummaryInformation.Transform.ProductCodes == (int)row[0]) { row[1] = String.Concat(this.transformSummaryInfo.TargetProductCode, this.transformSummaryInfo.TargetProductVersion, ';', this.transformSummaryInfo.UpdatedProductCode, this.transformSummaryInfo.UpdatedProductVersion, ';', this.transformSummaryInfo.TargetUpgradeCode); } else if ((int)SummaryInformation.Transform.InstallerRequirement == (int)row[0]) { row[1] = minimumVersion.ToString(CultureInfo.InvariantCulture); } else if ((int)SummaryInformation.Transform.Security == (int)row[0]) { row[1] = "4"; } } if (!summaryRows.Contains((int)SummaryInformation.Transform.TargetPlatformAndLanguage)) { Row summaryRow = summaryInfoTable.CreateRow(null); summaryRow[0] = (int)SummaryInformation.Transform.TargetPlatformAndLanguage; summaryRow[1] = this.transformSummaryInfo.TargetPlatformAndLanguage; } if (!summaryRows.Contains((int)SummaryInformation.Transform.UpdatedPlatformAndLanguage)) { Row summaryRow = summaryInfoTable.CreateRow(null); summaryRow[0] = (int)SummaryInformation.Transform.UpdatedPlatformAndLanguage; summaryRow[1] = this.transformSummaryInfo.UpdatedPlatformAndLanguage; } if (!summaryRows.Contains((int)SummaryInformation.Transform.ValidationFlags)) { Row summaryRow = summaryInfoTable.CreateRow(null); summaryRow[0] = (int)SummaryInformation.Transform.ValidationFlags; summaryRow[1] = ((int)validationFlags).ToString(CultureInfo.InvariantCulture); } if (!summaryRows.Contains((int)SummaryInformation.Transform.InstallerRequirement)) { Row summaryRow = summaryInfoTable.CreateRow(null); summaryRow[0] = (int)SummaryInformation.Transform.InstallerRequirement; summaryRow[1] = minimumVersion.ToString(CultureInfo.InvariantCulture); } if (!summaryRows.Contains((int)SummaryInformation.Transform.Security)) { Row summaryRow = summaryInfoTable.CreateRow(null); summaryRow[0] = (int)SummaryInformation.Transform.Security; summaryRow[1] = "4"; } } private class SummaryInformationStreams { public string TargetSummaryInfoCodepage { get; set; } public string TargetPlatformAndLanguage { get; set; } public string TargetProductCode { get; set; } public string TargetProductVersion { get; set; } public string TargetUpgradeCode { get; set; } public string TargetMinimumVersion { get; set; } public string UpdatedSummaryInfoCodepage { get; set; } public string UpdatedPlatformAndLanguage { get; set; } public string UpdatedProductCode { get; set; } public string UpdatedProductVersion { get; set; } public string UpdatedMinimumVersion { get; set; } } } }