// 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.Bind { using System; using System.Collections.Generic; using System.Globalization; using System.IO; using WixToolset.Core.Native; using WixToolset.Data; using WixToolset.Data.WindowsInstaller; using WixToolset.Extensibility; using WixToolset.Extensibility.Services; using WixToolset.Msi; internal class BindTransformCommand { public IEnumerable Extensions { private get; set; } public TableDefinitionCollection TableDefinitions { private get; set; } public string TempFilesLocation { private get; set; } public Output Transform { private get; set; } public IMessaging Messaging { 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))) { this.Messaging.Write(ErrorMessages.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) { this.Messaging.Write(ErrorMessages.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.PostBind(this.Context); //} // Any errors encountered up to this point can cause errors during generation. if (this.Messaging.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 { this.Messaging.Write(ErrorMessages.NoDifferencesInTransform(this.Transform.SourceLineNumbers)); } } } } finally { if (!String.IsNullOrEmpty(emptyFile)) { File.Delete(emptyFile); } } } private bool CompareFiles(string targetFile, string updatedFile) { bool? compared = null; foreach (var extension in this.Extensions) { compared = extension.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) { var command = new GenerateDatabaseCommand(); command.Codepage = output.Codepage; command.Extensions = this.Extensions; 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(); } } }