// 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.Native.Msi;
using WixToolset.Data;
using WixToolset.Data.Symbols;
using WixToolset.Data.WindowsInstaller;
using WixToolset.Extensibility.Services;
///
/// Creates a transform by diffing two outputs.
///
internal 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 preserveUnchangedRows, bool showPedanticMessages)
{
this.messaging = messaging;
this.TargetOutput = targetOutput;
this.UpdatedOutput = updatedOutput;
this.PreserveUnchangedRows = preserveUnchangedRows;
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.
///
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 modifiedTable = transform.EnsureTable(updatedTable.Definition);
foreach (var row in rows)
{
modifiedTable.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))
{
#if TODO_PATCH // This case doesn't seem like it can happen any longer.
// 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;
}
#endif
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.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; }
}
}
}