From 6ff680e386b1543ad1a58d1b1d465ce8aa20bc7d Mon Sep 17 00:00:00 2001 From: Rob Mensching Date: Fri, 24 Jan 2020 15:27:20 -0800 Subject: Start on new patch infrastructure --- .../Bind/GenerateTransformCommand.cs | 588 +++++++++++++++++++++ 1 file changed, 588 insertions(+) create mode 100644 src/WixToolset.Core.WindowsInstaller/Bind/GenerateTransformCommand.cs (limited to 'src/WixToolset.Core.WindowsInstaller/Bind/GenerateTransformCommand.cs') diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/GenerateTransformCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/GenerateTransformCommand.cs new file mode 100644 index 00000000..8a7dd702 --- /dev/null +++ b/src/WixToolset.Core.WindowsInstaller/Bind/GenerateTransformCommand.cs @@ -0,0 +1,588 @@ +// 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.Generic; + using System.Globalization; + using WixToolset.Core.WindowsInstaller.Msi; + using WixToolset.Data; + using WixToolset.Data.Tuples; + using WixToolset.Data.WindowsInstaller; + using WixToolset.Data.WindowsInstaller.Rows; + using WixToolset.Extensibility; + using WixToolset.Extensibility.Services; + + /// + /// Creates a transform by diffing two outputs. + /// + public sealed class GenerateTransformCommand + { + private const char sectionDelimiter = '/'; + private readonly IMessaging messaging; + private SummaryInformationStreams transformSummaryInfo; + + /// + /// Instantiates a new Differ class. + /// + public GenerateTransformCommand(IMessaging messaging, WindowsInstallerData targetOutput, WindowsInstallerData updatedOutput, bool showPedanticMessages) + { + this.messaging = messaging; + this.TargetOutput = targetOutput; + this.UpdatedOutput = updatedOutput; + this.ShowPedanticMessages = showPedanticMessages; + } + + private WindowsInstallerData TargetOutput { get; } + + private WindowsInstallerData UpdatedOutput { get; } + + private TransformFlags ValidationFlags { get; } + + /// + /// Gets or sets the option to show pedantic messages. + /// + /// The option to show pedantic messages. + private bool ShowPedanticMessages { get; } + + /// + /// Gets or sets the option to suppress keeping special rows. + /// + /// The option to suppress keeping special rows. + private bool SuppressKeepingSpecialRows { get; } + + /// + /// 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. + private bool PreserveUnchangedRows { get; } + + public WindowsInstallerData Transform { get; private set; } + + /// + /// Creates a transform by diffing two outputs. + /// + /// The target output. + /// The updated output. + /// + /// The transform. + public WindowsInstallerData Execute() + { + var targetOutput = this.TargetOutput; + var updatedOutput = this.UpdatedOutput; + var validationFlags = this.ValidationFlags; + + var transform = new WindowsInstallerData(null) + { + Type = OutputType.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 (var targetTable in targetOutput.Tables) + { + var updatedTable = updatedOutput.Tables[targetTable.Name]; + var operation = TableOperation.None; + + var rows = this.CompareTables(targetOutput, targetTable, updatedTable, out operation); + + if (TableOperation.Drop == operation) + { + var droppedTable = transform.EnsureTable(targetTable.Definition); + droppedTable.Operation = TableOperation.Drop; + } + else if (TableOperation.None == operation) + { + var modified = transform.EnsureTable(updatedTable.Definition); + foreach (var row in rows) + { + modified.Rows.Add(row); + } + } + } + + // added tables + foreach (var updatedTable in updatedOutput.Tables) + { + if (null == targetOutput.Tables[updatedTable.Name]) + { + var addedTable = transform.EnsureTable(updatedTable.Definition); + addedTable.Operation = TableOperation.Add; + + foreach (var updatedRow in updatedTable.Rows) + { + updatedRow.Operation = RowOperation.Add; + updatedRow.SectionId = sectionDelimiter + updatedRow.SectionId; + addedTable.Rows.Add(updatedRow); + } + } + } + + // set summary information properties + if (!this.SuppressKeepingSpecialRows) + { + var summaryInfoTable = transform.Tables["_SummaryInformation"]; + this.UpdateTransformSummaryInformationTable(summaryInfoTable, validationFlags); + } + + this.Transform = transform; + return this.Transform; + } + + /// + /// Add a row to the using the primary key. + /// + /// The indexed rows. + /// The row to index. + private void AddIndexedRow(Dictionary index, Row row) + { + var primaryKey = row.GetPrimaryKey(); + + if (null != primaryKey) + { + if (index.TryGetValue(primaryKey, out var collisionRow)) + { + // Overriding WixActionRows have a primary key defined and take precedence in the index. + if (row is WixActionRow actionRow) + { + // If the current row is not overridable, see if the indexed row is. + if (!actionRow.Overridable) + { + if (collisionRow is WixActionRow indexedRow && indexedRow.Overridable) + { + // The indexed key is overridable and should be replaced. + index[primaryKey] = actionRow; + } + } + + // If we got this far, the row does not need to be indexed. + return; + } + + if (this.ShowPedanticMessages) + { + this.messaging.Write(ErrorMessages.DuplicatePrimaryKey(row.SourceLineNumbers, primaryKey, row.Table.Name)); + } + } + else + { + index.Add(primaryKey, row); + } + } + 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 bool CompareRows(Table targetTable, Row targetRow, Row updatedRow, out Row comparedRow) + { + comparedRow = null; + + var keepRow = false; + + if (null == targetRow ^ null == updatedRow) + { + if (null == targetRow) + { + updatedRow.Operation = RowOperation.Add; + comparedRow = updatedRow; + } + else if (null == updatedRow) + { + targetRow.Operation = RowOperation.Delete; + 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; + } + } + else + { + if (this.PreserveUnchangedRows) + { + keepRow = true; + } + + for (var i = 0; i < updatedRow.Fields.Length; i++) + { + var columnDefinition = updatedRow.Fields[i].Column; + + if (columnDefinition.Unreal) + { + } + else if (!columnDefinition.PrimaryKey) + { + var 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 = (targetRow.FieldAsInteger(i) != updatedRow.FieldAsInteger(i)); + } + } + else if (ColumnType.Preserved == columnDefinition.Type) + { + updatedRow.Fields[i].PreviousData = targetRow.FieldAsString(i); + + // keep rows containing preserved fields so the historical data is available to the binder + keepRow = !this.SuppressKeepingSpecialRows; + } + else if (ColumnType.Object == columnDefinition.Type) + { + var targetObjectField = (ObjectField)targetRow.Fields[i]; + var 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 = targetObjectField.UnresolvedData; + + // keep rows containing object fields so the files can be compared in the binder + keepRow = !this.SuppressKeepingSpecialRows; + } + else + { + modified = (targetRow.FieldAsString(i) != updatedRow.FieldAsString(i)); + } + + if (modified) + { + if (null != updatedRow.Fields[i].PreviousData) + { + updatedRow.Fields[i].PreviousData = targetRow.FieldAsString(i); + } + + updatedRow.Fields[i].Modified = true; + updatedRow.Operation = RowOperation.Modify; + keepRow = true; + } + } + } + + if (keepRow) + { + comparedRow = updatedRow; + comparedRow.SectionId = targetRow.SectionId + sectionDelimiter + updatedRow.SectionId; + } + } + } + + return keepRow; + } + + private List CompareTables(WindowsInstallerData targetOutput, Table targetTable, Table updatedTable, out TableOperation operation) + { + var 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 + { + var updatedPrimaryKeys = new Dictionary(); + var targetPrimaryKeys = new Dictionary(); + + // 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 (var targetPrimaryKeyEntry in targetPrimaryKeys) + { + var targetPrimaryKey = targetPrimaryKeyEntry.Key; + var targetRow = targetPrimaryKeyEntry.Value; + updatedPrimaryKeys.TryGetValue(targetPrimaryKey, out var updatedRow); + + var keepRow = this.CompareRows(targetTable, targetRow, updatedRow, out var compared); + + if (keepRow) + { + rows.Add(compared); + } + } + + // find the inserted rows + foreach (var updatedPrimaryKeyEntry in updatedPrimaryKeys) + { + var updatedPrimaryKey = updatedPrimaryKeyEntry.Key; + + if (!targetPrimaryKeys.ContainsKey(updatedPrimaryKey)) + { + var updatedRow = updatedPrimaryKeyEntry.Value; + + updatedRow.Operation = RowOperation.Add; + updatedRow.SectionId = sectionDelimiter + updatedRow.SectionId; + rows.Add(updatedRow); + } + } + } + } + + return rows; + } + + private void IndexPrimaryKeys(Table targetTable, Dictionary targetPrimaryKeys, Table updatedTable, Dictionary updatedPrimaryKeys) + { + // index the target rows + foreach (var row in targetTable.Rows) + { + this.AddIndexedRow(targetPrimaryKeys, row); + + if ("Property" == targetTable.Name) + { + var id = row.FieldAsString(0); + + if ("ProductCode" == id) + { + this.transformSummaryInfo.TargetProductCode = row.FieldAsString(1); + + if ("*" == this.transformSummaryInfo.TargetProductCode) + { + this.messaging.Write(ErrorMessages.ProductCodeInvalidForTransform(row.SourceLineNumbers)); + } + } + else if ("ProductVersion" == id) + { + this.transformSummaryInfo.TargetProductVersion = row.FieldAsString(1); + } + else if ("UpgradeCode" == id) + { + this.transformSummaryInfo.TargetUpgradeCode = row.FieldAsString(1); + } + } + else if ("_SummaryInformation" == targetTable.Name) + { + var id = row.FieldAsInteger(0); + + if (1 == id) // PID_CODEPAGE + { + this.transformSummaryInfo.TargetSummaryInfoCodepage = row.FieldAsString(1); + } + else if (7 == id) // PID_TEMPLATE + { + this.transformSummaryInfo.TargetPlatformAndLanguage = row.FieldAsString(1); + } + else if (14 == id) // PID_PAGECOUNT + { + this.transformSummaryInfo.TargetMinimumVersion = row.FieldAsString(1); + } + } + } + + // index the updated rows + foreach (var row in updatedTable.Rows) + { + this.AddIndexedRow(updatedPrimaryKeys, row); + + if ("Property" == updatedTable.Name) + { + var id = row.FieldAsString(0); + + if ("ProductCode" == id) + { + this.transformSummaryInfo.UpdatedProductCode = row.FieldAsString(1); + + if ("*" == this.transformSummaryInfo.UpdatedProductCode) + { + this.messaging.Write(ErrorMessages.ProductCodeInvalidForTransform(row.SourceLineNumbers)); + } + } + else if ("ProductVersion" == id) + { + this.transformSummaryInfo.UpdatedProductVersion = row.FieldAsString(1); + } + } + else if ("_SummaryInformation" == updatedTable.Name) + { + var id = row.FieldAsInteger(0); + + if (1 == id) // PID_CODEPAGE + { + this.transformSummaryInfo.UpdatedSummaryInfoCodepage = row.FieldAsString(1); + } + else if (7 == id) // PID_TEMPLATE + { + this.transformSummaryInfo.UpdatedPlatformAndLanguage = row.FieldAsString(1); + } + else if (14 == id) // PID_PAGECOUNT + { + this.transformSummaryInfo.UpdatedMinimumVersion = row.FieldAsString(1); + } + } + } + } + + private void UpdateTransformSummaryInformationTable(Table summaryInfoTable, TransformFlags validationFlags) + { + // calculate the minimum version of MSI required to process the transform + var minimumVersion = 100; + + if (Int32.TryParse(this.transformSummaryInfo.TargetMinimumVersion, out var targetMin) && Int32.TryParse(this.transformSummaryInfo.UpdatedMinimumVersion, out var updatedMin)) + { + minimumVersion = Math.Max(targetMin, updatedMin); + } + + var summaryRows = new Dictionary(summaryInfoTable.Rows.Count); + + foreach (var row in summaryInfoTable.Rows) + { + var id = row.FieldAsInteger(0); + + summaryRows[id] = row; + + if ((int)SummaryInformation.Transform.CodePage == id) + { + row.Fields[1].Data = this.transformSummaryInfo.UpdatedSummaryInfoCodepage; + row.Fields[1].PreviousData = this.transformSummaryInfo.TargetSummaryInfoCodepage; + } + else if ((int)SummaryInformation.Transform.TargetPlatformAndLanguage == id) + { + row[1] = this.transformSummaryInfo.TargetPlatformAndLanguage; + } + else if ((int)SummaryInformation.Transform.UpdatedPlatformAndLanguage == id) + { + row[1] = this.transformSummaryInfo.UpdatedPlatformAndLanguage; + } + else if ((int)SummaryInformation.Transform.ProductCodes == id) + { + 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 == id) + { + row[1] = minimumVersion.ToString(CultureInfo.InvariantCulture); + } + else if ((int)SummaryInformation.Transform.Security == id) + { + row[1] = "4"; + } + } + + if (!summaryRows.ContainsKey((int)SummaryInformation.Transform.TargetPlatformAndLanguage)) + { + var summaryRow = summaryInfoTable.CreateRow(null); + summaryRow[0] = (int)SummaryInformation.Transform.TargetPlatformAndLanguage; + summaryRow[1] = this.transformSummaryInfo.TargetPlatformAndLanguage; + } + + if (!summaryRows.ContainsKey((int)SummaryInformation.Transform.UpdatedPlatformAndLanguage)) + { + var summaryRow = summaryInfoTable.CreateRow(null); + summaryRow[0] = (int)SummaryInformation.Transform.UpdatedPlatformAndLanguage; + summaryRow[1] = this.transformSummaryInfo.UpdatedPlatformAndLanguage; + } + + if (!summaryRows.ContainsKey((int)SummaryInformation.Transform.ValidationFlags)) + { + var summaryRow = summaryInfoTable.CreateRow(null); + summaryRow[0] = (int)SummaryInformation.Transform.ValidationFlags; + summaryRow[1] = ((int)validationFlags).ToString(CultureInfo.InvariantCulture); + } + + if (!summaryRows.ContainsKey((int)SummaryInformation.Transform.InstallerRequirement)) + { + var summaryRow = summaryInfoTable.CreateRow(null); + summaryRow[0] = (int)SummaryInformation.Transform.InstallerRequirement; + summaryRow[1] = minimumVersion.ToString(CultureInfo.InvariantCulture); + } + + if (!summaryRows.ContainsKey((int)SummaryInformation.Transform.Security)) + { + var 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