From d3d3649a68cb1fa589fdd987a6690dbd5d671f0d Mon Sep 17 00:00:00 2001 From: Rob Mensching Date: Sun, 17 Sep 2017 15:35:20 -0700 Subject: Initial code commit --- src/WixToolset.Core/Bind/BindTransformCommand.cs | 473 +++++++++++++++++++++++ 1 file changed, 473 insertions(+) create mode 100644 src/WixToolset.Core/Bind/BindTransformCommand.cs (limited to 'src/WixToolset.Core/Bind/BindTransformCommand.cs') diff --git a/src/WixToolset.Core/Bind/BindTransformCommand.cs b/src/WixToolset.Core/Bind/BindTransformCommand.cs new file mode 100644 index 00000000..e909f191 --- /dev/null +++ b/src/WixToolset.Core/Bind/BindTransformCommand.cs @@ -0,0 +1,473 @@ +// 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.Bind +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using WixToolset.Data; + using WixToolset.Extensibility; + using WixToolset.Msi; + using WixToolset.Core.Native; + + internal class BindTransformCommand : ICommand + { + public IEnumerable Extensions { private get; set; } + + public IEnumerable FileManagers { private get; set; } + + public TableDefinitionCollection TableDefinitions { private get; set; } + + public string TempFilesLocation { private get; set; } + + public Output Transform { private get; set; } + + public string OutputPath { private get; set; } + + public void Execute() + { + int transformFlags = 0; + + Output targetOutput = new Output(null); + Output updatedOutput = new Output(null); + + // TODO: handle added columns + + // to generate a localized transform, both the target and updated + // databases need to have the same code page. the only reason to + // set different code pages is to support localized primary key + // columns, but that would only support deleting rows. if this + // becomes necessary, define a PreviousCodepage property on the + // Output class and persist this throughout transform generation. + targetOutput.Codepage = this.Transform.Codepage; + updatedOutput.Codepage = this.Transform.Codepage; + + // remove certain Property rows which will be populated from summary information values + string targetUpgradeCode = null; + string updatedUpgradeCode = null; + + Table propertyTable = this.Transform.Tables["Property"]; + if (null != propertyTable) + { + for (int i = propertyTable.Rows.Count - 1; i >= 0; i--) + { + Row row = propertyTable.Rows[i]; + + if ("ProductCode" == (string)row[0] || "ProductLanguage" == (string)row[0] || "ProductVersion" == (string)row[0] || "UpgradeCode" == (string)row[0]) + { + propertyTable.Rows.RemoveAt(i); + + if ("UpgradeCode" == (string)row[0]) + { + updatedUpgradeCode = (string)row[1]; + } + } + } + } + + Table targetSummaryInfo = targetOutput.EnsureTable(this.TableDefinitions["_SummaryInformation"]); + Table updatedSummaryInfo = updatedOutput.EnsureTable(this.TableDefinitions["_SummaryInformation"]); + Table targetPropertyTable = targetOutput.EnsureTable(this.TableDefinitions["Property"]); + Table updatedPropertyTable = updatedOutput.EnsureTable(this.TableDefinitions["Property"]); + + // process special summary information values + foreach (Row row in this.Transform.Tables["_SummaryInformation"].Rows) + { + if ((int)SummaryInformation.Transform.CodePage == (int)row[0]) + { + // convert from a web name if provided + string codePage = (string)row.Fields[1].Data; + if (null == codePage) + { + codePage = "0"; + } + else + { + codePage = Common.GetValidCodePage(codePage).ToString(CultureInfo.InvariantCulture); + } + + string previousCodePage = (string)row.Fields[1].PreviousData; + if (null == previousCodePage) + { + previousCodePage = "0"; + } + else + { + previousCodePage = Common.GetValidCodePage(previousCodePage).ToString(CultureInfo.InvariantCulture); + } + + Row targetCodePageRow = targetSummaryInfo.CreateRow(null); + targetCodePageRow[0] = 1; // PID_CODEPAGE + targetCodePageRow[1] = previousCodePage; + + Row updatedCodePageRow = updatedSummaryInfo.CreateRow(null); + updatedCodePageRow[0] = 1; // PID_CODEPAGE + updatedCodePageRow[1] = codePage; + } + else if ((int)SummaryInformation.Transform.TargetPlatformAndLanguage == (int)row[0] || + (int)SummaryInformation.Transform.UpdatedPlatformAndLanguage == (int)row[0]) + { + // the target language + string[] propertyData = ((string)row[1]).Split(';'); + string lang = 2 == propertyData.Length ? propertyData[1] : "0"; + + Table tempSummaryInfo = (int)SummaryInformation.Transform.TargetPlatformAndLanguage == (int)row[0] ? targetSummaryInfo : updatedSummaryInfo; + Table tempPropertyTable = (int)SummaryInformation.Transform.TargetPlatformAndLanguage == (int)row[0] ? targetPropertyTable : updatedPropertyTable; + + Row productLanguageRow = tempPropertyTable.CreateRow(null); + productLanguageRow[0] = "ProductLanguage"; + productLanguageRow[1] = lang; + + // set the platform;language on the MSI to be generated + Row templateRow = tempSummaryInfo.CreateRow(null); + templateRow[0] = 7; // PID_TEMPLATE + templateRow[1] = (string)row[1]; + } + else if ((int)SummaryInformation.Transform.ProductCodes == (int)row[0]) + { + string[] propertyData = ((string)row[1]).Split(';'); + + Row targetProductCodeRow = targetPropertyTable.CreateRow(null); + targetProductCodeRow[0] = "ProductCode"; + targetProductCodeRow[1] = propertyData[0].Substring(0, 38); + + Row targetProductVersionRow = targetPropertyTable.CreateRow(null); + targetProductVersionRow[0] = "ProductVersion"; + targetProductVersionRow[1] = propertyData[0].Substring(38); + + Row updatedProductCodeRow = updatedPropertyTable.CreateRow(null); + updatedProductCodeRow[0] = "ProductCode"; + updatedProductCodeRow[1] = propertyData[1].Substring(0, 38); + + Row updatedProductVersionRow = updatedPropertyTable.CreateRow(null); + updatedProductVersionRow[0] = "ProductVersion"; + updatedProductVersionRow[1] = propertyData[1].Substring(38); + + // UpgradeCode is optional and may not exists in the target + // or upgraded databases, so do not include a null-valued + // UpgradeCode property. + + targetUpgradeCode = propertyData[2]; + if (!String.IsNullOrEmpty(targetUpgradeCode)) + { + Row targetUpgradeCodeRow = targetPropertyTable.CreateRow(null); + targetUpgradeCodeRow[0] = "UpgradeCode"; + targetUpgradeCodeRow[1] = targetUpgradeCode; + + // If the target UpgradeCode is specified, an updated + // UpgradeCode is required. + if (String.IsNullOrEmpty(updatedUpgradeCode)) + { + updatedUpgradeCode = targetUpgradeCode; + } + } + + if (!String.IsNullOrEmpty(updatedUpgradeCode)) + { + Row updatedUpgradeCodeRow = updatedPropertyTable.CreateRow(null); + updatedUpgradeCodeRow[0] = "UpgradeCode"; + updatedUpgradeCodeRow[1] = updatedUpgradeCode; + } + } + else if ((int)SummaryInformation.Transform.ValidationFlags == (int)row[0]) + { + transformFlags = Convert.ToInt32(row[1], CultureInfo.InvariantCulture); + } + else if ((int)SummaryInformation.Transform.Reserved11 == (int)row[0]) + { + // PID_LASTPRINTED should be null for transforms + row.Operation = RowOperation.None; + } + else + { + // add everything else as is + Row targetRow = targetSummaryInfo.CreateRow(null); + targetRow[0] = row[0]; + targetRow[1] = row[1]; + + Row updatedRow = updatedSummaryInfo.CreateRow(null); + updatedRow[0] = row[0]; + updatedRow[1] = row[1]; + } + } + + // Validate that both databases have an UpgradeCode if the + // authoring transform will validate the UpgradeCode; otherwise, + // MsiCreateTransformSummaryinfo() will fail with 1620. + if (((int)TransformFlags.ValidateUpgradeCode & transformFlags) != 0 && + (String.IsNullOrEmpty(targetUpgradeCode) || String.IsNullOrEmpty(updatedUpgradeCode))) + { + Messaging.Instance.OnMessage(WixErrors.BothUpgradeCodesRequired()); + } + + string emptyFile = null; + + foreach (Table table in this.Transform.Tables) + { + // Ignore unreal tables when building transforms except the _Stream table. + // These tables are ignored when generating the database so there is no reason + // to process them here. + if (table.Definition.Unreal && "_Streams" != table.Name) + { + continue; + } + + // process table operations + switch (table.Operation) + { + case TableOperation.Add: + updatedOutput.EnsureTable(table.Definition); + break; + case TableOperation.Drop: + targetOutput.EnsureTable(table.Definition); + continue; + default: + targetOutput.EnsureTable(table.Definition); + updatedOutput.EnsureTable(table.Definition); + break; + } + + // process row operations + foreach (Row row in table.Rows) + { + switch (row.Operation) + { + case RowOperation.Add: + Table updatedTable = updatedOutput.EnsureTable(table.Definition); + updatedTable.Rows.Add(row); + continue; + case RowOperation.Delete: + Table targetTable = targetOutput.EnsureTable(table.Definition); + targetTable.Rows.Add(row); + + // fill-in non-primary key values + foreach (Field field in row.Fields) + { + if (!field.Column.PrimaryKey) + { + if (ColumnType.Number == field.Column.Type && !field.Column.IsLocalizable) + { + field.Data = field.Column.MinValue; + } + else if (ColumnType.Object == field.Column.Type) + { + if (null == emptyFile) + { + emptyFile = Path.Combine(this.TempFilesLocation, "empty"); + } + + field.Data = emptyFile; + } + else + { + field.Data = "0"; + } + } + } + continue; + } + + // Assure that the file table's sequence is populated + if ("File" == table.Name) + { + foreach (Row fileRow in table.Rows) + { + if (null == fileRow[7]) + { + if (RowOperation.Add == fileRow.Operation) + { + Messaging.Instance.OnMessage(WixErrors.InvalidAddedFileRowWithoutSequence(fileRow.SourceLineNumbers, (string)fileRow[0])); + break; + } + + // Set to 1 to prevent invalid IDT file from being generated + fileRow[7] = 1; + } + } + } + + // process modified and unmodified rows + bool modifiedRow = false; + Row targetRow = new Row(null, table.Definition); + Row updatedRow = row; + for (int i = 0; i < row.Fields.Length; i++) + { + Field updatedField = row.Fields[i]; + + if (updatedField.Modified) + { + // set a different value in the target row to ensure this value will be modified during transform generation + if (ColumnType.Number == updatedField.Column.Type && !updatedField.Column.IsLocalizable) + { + if (null == updatedField.Data || 1 != (int)updatedField.Data) + { + targetRow[i] = 1; + } + else + { + targetRow[i] = 2; + } + } + else if (ColumnType.Object == updatedField.Column.Type) + { + if (null == emptyFile) + { + emptyFile = Path.Combine(this.TempFilesLocation, "empty"); + } + + targetRow[i] = emptyFile; + } + else + { + if ("0" != (string)updatedField.Data) + { + targetRow[i] = "0"; + } + else + { + targetRow[i] = "1"; + } + } + + modifiedRow = true; + } + else if (ColumnType.Object == updatedField.Column.Type) + { + ObjectField objectField = (ObjectField)updatedField; + + // create an empty file for comparing against + if (null == objectField.PreviousData) + { + if (null == emptyFile) + { + emptyFile = Path.Combine(this.TempFilesLocation, "empty"); + } + + targetRow[i] = emptyFile; + modifiedRow = true; + } + else if (!this.CompareFiles(objectField.PreviousData, (string)objectField.Data)) + { + targetRow[i] = objectField.PreviousData; + modifiedRow = true; + } + } + else // unmodified + { + if (null != updatedField.Data) + { + targetRow[i] = updatedField.Data; + } + } + } + + // modified rows and certain special rows go in the target and updated msi databases + if (modifiedRow || + ("Property" == table.Name && + ("ProductCode" == (string)row[0] || + "ProductLanguage" == (string)row[0] || + "ProductVersion" == (string)row[0] || + "UpgradeCode" == (string)row[0]))) + { + Table targetTable = targetOutput.EnsureTable(table.Definition); + targetTable.Rows.Add(targetRow); + + Table updatedTable = updatedOutput.EnsureTable(table.Definition); + updatedTable.Rows.Add(updatedRow); + } + } + } + + foreach (BinderExtension extension in this.Extensions) + { + extension.Finish(this.Transform); + } + + // Any errors encountered up to this point can cause errors during generation. + if (Messaging.Instance.EncounteredError) + { + return; + } + + string transformFileName = Path.GetFileNameWithoutExtension(this.OutputPath); + string targetDatabaseFile = Path.Combine(this.TempFilesLocation, String.Concat(transformFileName, "_target.msi")); + string updatedDatabaseFile = Path.Combine(this.TempFilesLocation, String.Concat(transformFileName, "_updated.msi")); + + try + { + if (!String.IsNullOrEmpty(emptyFile)) + { + using (FileStream fileStream = File.Create(emptyFile)) + { + } + } + + this.GenerateDatabase(targetOutput, targetDatabaseFile, false); + this.GenerateDatabase(updatedOutput, updatedDatabaseFile, true); + + // make sure the directory exists + Directory.CreateDirectory(Path.GetDirectoryName(this.OutputPath)); + + // create the transform file + using (Database targetDatabase = new Database(targetDatabaseFile, OpenDatabase.ReadOnly)) + { + using (Database updatedDatabase = new Database(updatedDatabaseFile, OpenDatabase.ReadOnly)) + { + if (updatedDatabase.GenerateTransform(targetDatabase, this.OutputPath)) + { + updatedDatabase.CreateTransformSummaryInfo(targetDatabase, this.OutputPath, (TransformErrorConditions)(transformFlags & 0xFFFF), (TransformValidations)((transformFlags >> 16) & 0xFFFF)); + } + else + { + Messaging.Instance.OnMessage(WixErrors.NoDifferencesInTransform(this.Transform.SourceLineNumbers)); + } + } + } + } + finally + { + if (!String.IsNullOrEmpty(emptyFile)) + { + File.Delete(emptyFile); + } + } + } + + private bool CompareFiles(string targetFile, string updatedFile) + { + bool? compared = null; + foreach (IBinderFileManager fileManager in this.FileManagers) + { + compared = fileManager.CompareFiles(targetFile, updatedFile); + if (compared.HasValue) + { + break; + } + } + + if (!compared.HasValue) + { + throw new InvalidOperationException(); // TODO: something needs to be said here that none of the binder file managers returned a result. + } + + return compared.Value; + } + + private void GenerateDatabase(Output output, string outputPath, bool keepAddedColumns) + { + GenerateDatabaseCommand command = new GenerateDatabaseCommand(); + command.Codepage = output.Codepage; + command.Extensions = this.Extensions; + command.FileManagers = this.FileManagers; + command.KeepAddedColumns = keepAddedColumns; + command.Output = output; + command.OutputPath = outputPath; + command.TableDefinitions = this.TableDefinitions; + command.TempFilesLocation = this.TempFilesLocation; + command.SuppressAddingValidationRows = true; + command.UseSubDirectory = true; + command.Execute(); + } + } +} -- cgit v1.2.3-55-g6feb