From dbde9e7104b907bbbaea17e21247d8cafc8b3a4c Mon Sep 17 00:00:00 2001 From: Rob Mensching Date: Sat, 14 Oct 2017 16:12:07 -0700 Subject: Massive refactoring to introduce the concept of IBackend --- src/WixToolset.Core.WindowsInstaller/Differ.cs | 611 +++++++++++++++++++++++++ 1 file changed, 611 insertions(+) create mode 100644 src/WixToolset.Core.WindowsInstaller/Differ.cs (limited to 'src/WixToolset.Core.WindowsInstaller/Differ.cs') diff --git a/src/WixToolset.Core.WindowsInstaller/Differ.cs b/src/WixToolset.Core.WindowsInstaller/Differ.cs new file mode 100644 index 00000000..bdd06d32 --- /dev/null +++ b/src/WixToolset.Core.WindowsInstaller/Differ.cs @@ -0,0 +1,611 @@ +// 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 +{ + using System; + using System.Collections; + using System.Collections.Generic; + using System.Globalization; + using WixToolset.Core; + using WixToolset.Data; + using WixToolset.Data.Rows; + using WixToolset.Extensibility; + using WixToolset.Msi; + + /// + /// Creates a transform by diffing two outputs. + /// + public sealed class Differ : IMessageHandler + { + private List inspectorExtensions; + private bool showPedanticMessages; + private bool suppressKeepingSpecialRows; + private bool preserveUnchangedRows; + private const char sectionDelimiter = '/'; + private SummaryInformationStreams transformSummaryInfo; + + /// + /// Instantiates a new Differ class. + /// + public Differ() + { + this.inspectorExtensions = new List(); + } + + /// + /// 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.OnMessage(WixErrors.OutputCodepageMismatch(targetOutput.SourceLineNumbers, targetOutput.Codepage, updatedOutput.Codepage)); + if (null != updatedOutput.SourceLineNumbers) + { + this.OnMessage(WixErrors.OutputCodepageMismatch2(updatedOutput.SourceLineNumbers)); + } + } + + // compare the output types + if (targetOutput.Type != updatedOutput.Type) + { + throw new WixException(WixErrors.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; + } + + /// + /// Sends a message to the message delegate if there is one. + /// + /// Message event arguments. + public void OnMessage(MessageEventArgs e) + { + Messaging.Instance.OnMessage(e); + } + + /// + /// 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.OnMessage(WixWarnings.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.OnMessage(WixErrors.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.OnMessage(WixErrors.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.OnMessage(WixErrors.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; } + } + } +} -- cgit v1.2.3-55-g6feb