// 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.ComponentModel;
using System.IO;
using System.Linq;
using System.Text;
using WixToolset.Core.WindowsInstaller.Msi;
using WixToolset.Data;
using WixToolset.Data.WindowsInstaller;
using WixToolset.Extensibility.Data;
using WixToolset.Extensibility.Services;
internal class GenerateDatabaseCommand
{
public GenerateDatabaseCommand(IMessaging messaging, IBackendHelper backendHelper, FileSystemManager fileSystemManager, WindowsInstallerData data, string outputPath, TableDefinitionCollection tableDefinitions, string intermediateFolder, int codepage, bool keepAddedColumns, bool suppressAddingValidationRows, bool useSubdirectory)
{
this.Messaging = messaging;
this.BackendHelper = backendHelper;
this.FileSystemManager = fileSystemManager;
this.Data = data;
this.OutputPath = outputPath;
this.TableDefinitions = tableDefinitions;
this.IntermediateFolder = intermediateFolder;
this.Codepage = codepage;
this.KeepAddedColumns = keepAddedColumns;
this.SuppressAddingValidationRows = suppressAddingValidationRows;
this.UseSubDirectory = useSubdirectory;
}
private int Codepage { get; }
private IBackendHelper BackendHelper { get; }
private FileSystemManager FileSystemManager { get; }
///
/// Whether to keep columns added in a transform.
///
private bool KeepAddedColumns { get; }
private IMessaging Messaging { get; }
private WindowsInstallerData Data { get; }
private string OutputPath { get; }
private TableDefinitionCollection TableDefinitions { get; }
private string IntermediateFolder { get; }
public List GeneratedTemporaryFiles { get; } = new List();
///
/// Whether to use a subdirectory based on the file name for intermediate files.
///
private bool SuppressAddingValidationRows { get; }
private bool UseSubDirectory { get; }
public void Execute()
{
// Add the _Validation rows.
if (!this.SuppressAddingValidationRows)
{
this.AddValidationRows();
}
var baseDirectory = this.IntermediateFolder;
if (this.UseSubDirectory)
{
var filename = Path.GetFileNameWithoutExtension(this.OutputPath);
baseDirectory = Path.Combine(baseDirectory, filename);
}
var idtFolder = Path.Combine(baseDirectory, "_idts");
var type = OpenDatabase.CreateDirect;
if (OutputType.Patch == this.Data.Type)
{
type |= OpenDatabase.OpenPatchFile;
}
// Localize the codepage if a value was specified directly.
if (-1 != this.Codepage)
{
this.Data.Codepage = this.Codepage;
}
try
{
#if DEBUG
Console.WriteLine("Opening database at: {0}", this.OutputPath);
#endif
Directory.CreateDirectory(Path.GetDirectoryName(this.OutputPath));
Directory.CreateDirectory(idtFolder);
using (var db = new Database(this.OutputPath, type))
{
// If we're not using the default codepage, import a new one into our
// database before we add any tables (or the tables would be added
// with the wrong codepage).
if (0 != this.Data.Codepage)
{
this.SetDatabaseCodepage(db, this.Data.Codepage, idtFolder);
}
this.ImportTables(db, idtFolder);
// Insert substorages (usually transforms inside a patch or instance transforms in a package).
this.ImportSubStorages(db);
// We're good, commit the changes to the new database.
db.Commit();
}
}
catch (IOException e)
{
// TODO: this error message doesn't seem specific enough
throw new WixException(ErrorMessages.FileNotFound(new SourceLineNumber(this.OutputPath), this.OutputPath), e);
}
}
private void AddValidationRows()
{
var validationTable = this.Data.EnsureTable(this.TableDefinitions["_Validation"]);
// Add the validation rows for real tables and columns.
foreach (var table in this.Data.Tables.Where(t => !t.Definition.Unreal))
{
foreach (var columnDef in table.Definition.Columns.Where(c => !c.Unreal))
{
var row = validationTable.CreateRow(null);
row[0] = table.Name;
row[1] = columnDef.Name;
if (columnDef.Nullable)
{
row[2] = "Y";
}
else
{
row[2] = "N";
}
if (columnDef.MinValue.HasValue)
{
row[3] = columnDef.MinValue.Value;
}
if (columnDef.MaxValue.HasValue)
{
row[4] = columnDef.MaxValue.Value;
}
row[5] = columnDef.KeyTable;
if (columnDef.KeyColumn.HasValue)
{
row[6] = columnDef.KeyColumn.Value;
}
if (ColumnCategory.Unknown != columnDef.Category)
{
row[7] = columnDef.Category.ToString();
}
row[8] = columnDef.Possibilities;
row[9] = columnDef.Description;
}
}
}
private void ImportTables(Database db, string idtDirectory)
{
foreach (var table in this.Data.Tables)
{
var importTable = table;
var hasBinaryColumn = false;
// Skip all unreal tables other than _Streams.
if (table.Definition.Unreal && "_Streams" != table.Name)
{
continue;
}
// Do not put the _Validation table in patches, it is not needed.
if (OutputType.Patch == this.Data.Type && "_Validation" == table.Name)
{
continue;
}
// The only way to import binary data is to copy it to a local subdirectory first.
// To avoid this extra copying and perf hit, import an empty table with the same
// definition and later import the binary data from source using records.
foreach (var columnDefinition in table.Definition.Columns)
{
if (ColumnType.Object == columnDefinition.Type)
{
importTable = new Table(table.Definition);
hasBinaryColumn = true;
break;
}
}
// Create the table via IDT import.
if ("_Streams" != importTable.Name)
{
try
{
var command = new CreateIdtFileCommand(this.Messaging, importTable, this.Data.Codepage, idtDirectory, this.KeepAddedColumns);
command.Execute();
var buildOutput = this.BackendHelper.TrackFile(command.IdtPath, TrackedFileType.Temporary);
this.GeneratedTemporaryFiles.Add(buildOutput);
db.Import(command.IdtPath);
}
catch (WixInvalidIdtException)
{
// If ValidateRows finds anything it doesn't like, it throws
importTable.ValidateRows();
// Otherwise we rethrow the InvalidIdt
throw;
}
}
// insert the rows via SQL query if this table contains object fields
if (hasBinaryColumn)
{
var query = new StringBuilder("SELECT ");
// Build the query for the view.
var firstColumn = true;
foreach (var columnDefinition in table.Definition.Columns)
{
if (columnDefinition.Unreal)
{
continue;
}
if (!firstColumn)
{
query.Append(",");
}
query.AppendFormat(" `{0}`", columnDefinition.Name);
firstColumn = false;
}
query.AppendFormat(" FROM `{0}`", table.Name);
using (var tableView = db.OpenExecuteView(query.ToString()))
{
// Import each row containing a stream
foreach (var row in table.Rows)
{
using (var record = new Record(table.Definition.Columns.Length))
{
// Stream names are created by concatenating the name of the table with the values
// of the primary key (delimited by periods).
var streamName = new StringBuilder();
// the _Streams table doesn't prepend the table name (or a period)
if ("_Streams" != table.Name)
{
streamName.Append(table.Name);
}
var needStream = false;
for (var i = 0; i < table.Definition.Columns.Length; i++)
{
var columnDefinition = table.Definition.Columns[i];
if (columnDefinition.Unreal)
{
continue;
}
switch (columnDefinition.Type)
{
case ColumnType.Localized:
case ColumnType.Preserved:
case ColumnType.String:
var str = row.FieldAsString(i);
if (columnDefinition.PrimaryKey)
{
if (0 < streamName.Length)
{
streamName.Append(".");
}
streamName.Append(str);
}
record.SetString(i + 1, str);
break;
case ColumnType.Number:
record.SetInteger(i + 1, row.FieldAsInteger(i));
break;
case ColumnType.Object:
var path = row.FieldAsString(i);
if (null != path)
{
needStream = true;
try
{
record.SetStream(i + 1, path);
}
catch (Win32Exception e)
{
if (0xA1 == e.NativeErrorCode) // ERROR_BAD_PATHNAME
{
throw new WixException(ErrorMessages.FileNotFound(row.SourceLineNumbers, path));
}
else
{
throw new WixException(ErrorMessages.Win32Exception(e.NativeErrorCode, e.Message));
}
}
}
break;
}
}
// check for a stream name that is more than 62 characters long (the maximum allowed length)
if (needStream && MsiInterop.MsiMaxStreamNameLength < streamName.Length)
{
this.Messaging.Write(ErrorMessages.StreamNameTooLong(row.SourceLineNumbers, table.Name, streamName.ToString(), streamName.Length));
}
else // add the row to the database
{
tableView.Modify(ModifyView.Assign, record);
}
}
}
}
// Remove rows from the _Streams table for wixpdbs.
if ("_Streams" == table.Name)
{
table.Rows.Clear();
}
}
}
}
private void ImportSubStorages(Database db)
{
if (0 < this.Data.SubStorages.Count)
{
using (var storagesView = new View(db, "SELECT `Name`, `Data` FROM `_Storages`"))
{
foreach (var subStorage in this.Data.SubStorages)
{
var transformFile = Path.Combine(this.IntermediateFolder, String.Concat(subStorage.Name, ".mst"));
// Bind the transform.
var command = new BindTransformCommand(this.Messaging, this.BackendHelper, this.FileSystemManager, this.IntermediateFolder, subStorage.Data, transformFile, this.TableDefinitions);
command.Execute();
if (this.Messaging.EncounteredError)
{
continue;
}
// Add the storage to the database.
using (var record = new Record(2))
{
record.SetString(1, subStorage.Name);
record.SetStream(2, transformFile);
storagesView.Modify(ModifyView.Assign, record);
}
}
}
}
}
private void SetDatabaseCodepage(Database db, int codepage, string idtFolder)
{
// Write out the _ForceCodepage IDT file.
var idtPath = Path.Combine(idtFolder, "_ForceCodepage.idt");
using (var idtFile = new StreamWriter(idtPath, false, Encoding.ASCII))
{
idtFile.WriteLine(); // dummy column name record
idtFile.WriteLine(); // dummy column definition record
idtFile.Write(codepage);
idtFile.WriteLine("\t_ForceCodepage");
}
var trackId = this.BackendHelper.TrackFile(idtPath, TrackedFileType.Temporary);
this.GeneratedTemporaryFiles.Add(trackId);
// Try to import the table into the MSI.
try
{
db.Import(idtPath);
}
catch (WixInvalidIdtException)
{
// The IDT should be valid, so an invalid code page was given.
throw new WixException(ErrorMessages.IllegalCodepage(codepage));
}
}
}
}