// 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
{
using System;
using System.CodeDom.Compiler;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using WixToolset.Bind;
using WixToolset.Bind.Bundles;
using WixToolset.Cab;
using WixToolset.Data;
using WixToolset.Data.Rows;
using WixToolset.Extensibility;
using WixToolset.Msi;
using WixToolset.Core.Native;
using WixToolset.Ole32;
///
/// Unbinder core of the WiX toolset.
///
public sealed class Unbinder : IMessageHandler
{
private string emptyFile;
private bool isAdminImage;
private int sectionCount;
private bool suppressDemodularization;
private bool suppressExtractCabinets;
private TableDefinitionCollection tableDefinitions;
private ArrayList unbinderExtensions;
// private TempFileCollection tempFiles;
///
/// Creates a new unbinder object with a default set of table definitions.
///
public Unbinder()
{
this.tableDefinitions = new TableDefinitionCollection(WindowsInstallerStandard.GetTableDefinitions());
this.unbinderExtensions = new ArrayList();
}
///
/// Gets or sets whether the input msi is an admin image.
///
/// Set to true if the input msi is part of an admin image.
public bool IsAdminImage
{
get { return this.isAdminImage; }
set { this.isAdminImage = value; }
}
///
/// Gets or sets the option to suppress demodularizing values.
///
/// The option to suppress demodularizing values.
public bool SuppressDemodularization
{
get { return this.suppressDemodularization; }
set { this.suppressDemodularization = value; }
}
///
/// Gets or sets the option to suppress extracting cabinets.
///
/// The option to suppress extracting cabinets.
public bool SuppressExtractCabinets
{
get { return this.suppressExtractCabinets; }
set { this.suppressExtractCabinets = value; }
}
///
/// Gets or sets the temporary path for the Binder. If left null, the binder
/// will use %TEMP% environment variable.
///
/// Path to temp files.
public string TempFilesLocation
{
get
{
// return null == this.tempFiles ? String.Empty : this.tempFiles.BasePath;
return Path.GetTempPath();
}
// set
// {
// if (null == value)
// {
// this.tempFiles = new TempFileCollection();
// }
// else
// {
// this.tempFiles = new TempFileCollection(value);
// }
// }
}
///
/// Adds extension data.
///
/// The extension data to add.
public void AddExtensionData(IExtensionData data)
{
if (null != data.TableDefinitions)
{
foreach (TableDefinition tableDefinition in data.TableDefinitions)
{
if (!this.tableDefinitions.Contains(tableDefinition.Name))
{
this.tableDefinitions.Add(tableDefinition);
}
else
{
Messaging.Instance.OnMessage(WixErrors.DuplicateExtensionTable(data.GetType().ToString(), tableDefinition.Name));
}
}
}
}
///
/// Adds an extension.
///
/// The extension to add.
public void AddExtension(IUnbinderExtension extension)
{
this.unbinderExtensions.Add(extension);
}
///
/// Unbind a Windows Installer file.
///
/// The Windows Installer file.
/// The type of output to create.
/// The path where files should be exported.
/// The output representing the database.
public Output Unbind(string file, OutputType outputType, string exportBasePath)
{
if (!File.Exists(file))
{
if (OutputType.Transform == outputType)
{
throw new WixException(WixErrors.FileNotFound(null, file, "Transform"));
}
else
{
throw new WixException(WixErrors.FileNotFound(null, file, "Database"));
}
}
// if we don't have the temporary files object yet, get one
Directory.CreateDirectory(this.TempFilesLocation); // ensure the base path is there
if (OutputType.Patch == outputType)
{
return this.UnbindPatch(file, exportBasePath);
}
else if (OutputType.Transform == outputType)
{
return this.UnbindTransform(file, exportBasePath);
}
else if (OutputType.Bundle == outputType)
{
return this.UnbindBundle(file, exportBasePath);
}
else // other database types
{
return this.UnbindDatabase(file, outputType, exportBasePath);
}
}
///
/// Cleans up the temp files used by the Decompiler.
///
/// True if all files were deleted, false otherwise.
///
/// This should be called after every call to Decompile to ensure there
/// are no conflicts between each decompiled database.
///
public bool DeleteTempFiles()
{
#if REDO_IN_NETCORE
bool deleted = Common.DeleteTempFiles(this.tempFiles.BasePath, this);
if (deleted)
{
this.tempFiles = null; // temp files have been deleted, no need to remember this now
}
return deleted;
#endif
return true;
}
///
/// Sends a message to the message delegate if there is one.
///
/// Message event arguments.
public void OnMessage(MessageEventArgs e)
{
Messaging.Instance.OnMessage(e);
}
///
/// Unbind an MSI database file.
///
/// The database file.
/// The output type.
/// The path where files should be exported.
/// The unbound database.
private Output UnbindDatabase(string databaseFile, OutputType outputType, string exportBasePath)
{
Output output;
try
{
using (Database database = new Database(databaseFile, OpenDatabase.ReadOnly))
{
output = this.UnbindDatabase(databaseFile, database, outputType, exportBasePath, false);
// extract the files from the cabinets
if (null != exportBasePath && !this.suppressExtractCabinets)
{
this.ExtractCabinets(output, database, databaseFile, exportBasePath);
}
}
}
catch (Win32Exception e)
{
if (0x6E == e.NativeErrorCode) // ERROR_OPEN_FAILED
{
throw new WixException(WixErrors.OpenDatabaseFailed(databaseFile));
}
throw;
}
return output;
}
///
/// Unbind an MSI database file.
///
/// The database file.
/// The opened database.
/// The type of output to create.
/// The path where files should be exported.
/// Option to skip unbinding the _SummaryInformation table.
/// The output representing the database.
private Output UnbindDatabase(string databaseFile, Database database, OutputType outputType, string exportBasePath, bool skipSummaryInfo)
{
string modularizationGuid = null;
Output output = new Output(new SourceLineNumber(databaseFile));
View validationView = null;
// set the output type
output.Type = outputType;
// get the codepage
database.Export("_ForceCodepage", this.TempFilesLocation, "_ForceCodepage.idt");
using (StreamReader sr = File.OpenText(Path.Combine(this.TempFilesLocation, "_ForceCodepage.idt")))
{
string line;
while (null != (line = sr.ReadLine()))
{
string[] data = line.Split('\t');
if (2 == data.Length)
{
output.Codepage = Convert.ToInt32(data[0], CultureInfo.InvariantCulture);
}
}
}
// get the summary information table if it exists; it won't if unbinding a transform
if (!skipSummaryInfo)
{
using (SummaryInformation summaryInformation = new SummaryInformation(database))
{
Table table = new Table(null, this.tableDefinitions["_SummaryInformation"]);
for (int i = 1; 19 >= i; i++)
{
string value = summaryInformation.GetProperty(i);
if (0 < value.Length)
{
Row row = table.CreateRow(output.SourceLineNumbers);
row[0] = i;
row[1] = value;
}
}
output.Tables.Add(table);
}
}
try
{
// open a view on the validation table if it exists
if (database.TableExists("_Validation"))
{
validationView = database.OpenView("SELECT * FROM `_Validation` WHERE `Table` = ? AND `Column` = ?");
}
// get the normal tables
using (View tablesView = database.OpenExecuteView("SELECT * FROM _Tables"))
{
while (true)
{
using (Record tableRecord = tablesView.Fetch())
{
if (null == tableRecord)
{
break;
}
string tableName = tableRecord.GetString(1);
using (View tableView = database.OpenExecuteView(String.Format(CultureInfo.InvariantCulture, "SELECT * FROM `{0}`", tableName)))
{
List columns;
using (Record columnNameRecord = tableView.GetColumnInfo(MsiInterop.MSICOLINFONAMES),
columnTypeRecord = tableView.GetColumnInfo(MsiInterop.MSICOLINFOTYPES))
{
// index the primary keys
HashSet tablePrimaryKeys = new HashSet();
using (Record primaryKeysRecord = database.PrimaryKeys(tableName))
{
int primaryKeysFieldCount = primaryKeysRecord.GetFieldCount();
for (int i = 1; i <= primaryKeysFieldCount; i++)
{
tablePrimaryKeys.Add(primaryKeysRecord.GetString(i));
}
}
int columnCount = columnNameRecord.GetFieldCount();
columns = new List(columnCount);
for (int i = 1; i <= columnCount; i++)
{
string columnName = columnNameRecord.GetString(i);
string idtType = columnTypeRecord.GetString(i);
ColumnType columnType;
int length;
bool nullable;
ColumnCategory columnCategory = ColumnCategory.Unknown;
ColumnModularizeType columnModularizeType = ColumnModularizeType.None;
bool primary = tablePrimaryKeys.Contains(columnName);
bool minValueSet = false;
int minValue = -1;
bool maxValueSet = false;
int maxValue = -1;
string keyTable = null;
bool keyColumnSet = false;
int keyColumn = -1;
string category = null;
string set = null;
string description = null;
// get the column type, length, and whether its nullable
switch (Char.ToLower(idtType[0], CultureInfo.InvariantCulture))
{
case 'i':
columnType = ColumnType.Number;
break;
case 'l':
columnType = ColumnType.Localized;
break;
case 's':
columnType = ColumnType.String;
break;
case 'v':
columnType = ColumnType.Object;
break;
default:
// TODO: error
columnType = ColumnType.Unknown;
break;
}
length = Convert.ToInt32(idtType.Substring(1), CultureInfo.InvariantCulture);
nullable = Char.IsUpper(idtType[0]);
// try to get validation information
if (null != validationView)
{
using (Record validationRecord = new Record(2))
{
validationRecord.SetString(1, tableName);
validationRecord.SetString(2, columnName);
validationView.Execute(validationRecord);
}
using (Record validationRecord = validationView.Fetch())
{
if (null != validationRecord)
{
string validationNullable = validationRecord.GetString(3);
minValueSet = !validationRecord.IsNull(4);
minValue = (minValueSet ? validationRecord.GetInteger(4) : -1);
maxValueSet = !validationRecord.IsNull(5);
maxValue = (maxValueSet ? validationRecord.GetInteger(5) : -1);
keyTable = (!validationRecord.IsNull(6) ? validationRecord.GetString(6) : null);
keyColumnSet = !validationRecord.IsNull(7);
keyColumn = (keyColumnSet ? validationRecord.GetInteger(7) : -1);
category = (!validationRecord.IsNull(8) ? validationRecord.GetString(8) : null);
set = (!validationRecord.IsNull(9) ? validationRecord.GetString(9) : null);
description = (!validationRecord.IsNull(10) ? validationRecord.GetString(10) : null);
// check the validation nullable value against the column definition
if (null == validationNullable)
{
// TODO: warn for illegal validation nullable column
}
else if ((nullable && "Y" != validationNullable) || (!nullable && "N" != validationNullable))
{
// TODO: warn for mismatch between column definition and validation nullable
}
// convert category to ColumnCategory
if (null != category)
{
try
{
columnCategory = (ColumnCategory)Enum.Parse(typeof(ColumnCategory), category, true);
}
catch (ArgumentException)
{
columnCategory = ColumnCategory.Unknown;
}
}
}
else
{
// TODO: warn about no validation information
}
}
}
// guess the modularization type
if ("Icon" == keyTable && 1 == keyColumn)
{
columnModularizeType = ColumnModularizeType.Icon;
}
else if ("Condition" == columnName)
{
columnModularizeType = ColumnModularizeType.Condition;
}
else if (ColumnCategory.Formatted == columnCategory || ColumnCategory.FormattedSDDLText == columnCategory)
{
columnModularizeType = ColumnModularizeType.Property;
}
else if (ColumnCategory.Identifier == columnCategory)
{
columnModularizeType = ColumnModularizeType.Column;
}
columns.Add(new ColumnDefinition(columnName, columnType, length, primary, nullable, columnModularizeType, (ColumnType.Localized == columnType), minValueSet, minValue, maxValueSet, maxValue, keyTable, keyColumnSet, keyColumn, columnCategory, set, description, true, true));
}
}
TableDefinition tableDefinition = new TableDefinition(tableName, columns, false, false);
// use our table definitions if core properties are the same; this allows us to take advantage
// of wix concepts like localizable columns which current code assumes
if (this.tableDefinitions.Contains(tableName) && 0 == tableDefinition.CompareTo(this.tableDefinitions[tableName]))
{
tableDefinition = this.tableDefinitions[tableName];
}
Table table = new Table(null, tableDefinition);
while (true)
{
using (Record rowRecord = tableView.Fetch())
{
if (null == rowRecord)
{
break;
}
int recordCount = rowRecord.GetFieldCount();
Row row = table.CreateRow(output.SourceLineNumbers);
for (int i = 0; recordCount > i && row.Fields.Length > i; i++)
{
if (rowRecord.IsNull(i + 1))
{
if (!row.Fields[i].Column.Nullable)
{
// TODO: display an error for a null value in a non-nullable field OR
// display a warning and put an empty string in the value to let the compiler handle it
// (the second option is risky because the later code may make certain assumptions about
// the contents of a row value)
}
}
else
{
switch (row.Fields[i].Column.Type)
{
case ColumnType.Number:
bool success = false;
int intValue = rowRecord.GetInteger(i + 1);
if (row.Fields[i].Column.IsLocalizable)
{
success = row.BestEffortSetField(i, Convert.ToString(intValue, CultureInfo.InvariantCulture));
}
else
{
success = row.BestEffortSetField(i, intValue);
}
if (!success)
{
this.OnMessage(WixWarnings.BadColumnDataIgnored(row.SourceLineNumbers, Convert.ToString(intValue, CultureInfo.InvariantCulture), tableName, row.Fields[i].Column.Name));
}
break;
case ColumnType.Object:
string sourceFile = "FILE NOT EXPORTED, USE THE dark.exe -x OPTION TO EXPORT BINARIES";
if (null != exportBasePath)
{
string relativeSourceFile = Path.Combine(tableName, row.GetPrimaryKey('.'));
sourceFile = Path.Combine(exportBasePath, relativeSourceFile);
// ensure the parent directory exists
System.IO.Directory.CreateDirectory(Path.Combine(exportBasePath, tableName));
using (FileStream fs = System.IO.File.Create(sourceFile))
{
int bytesRead;
byte[] buffer = new byte[512];
while (0 != (bytesRead = rowRecord.GetStream(i + 1, buffer, buffer.Length)))
{
fs.Write(buffer, 0, bytesRead);
}
}
}
row[i] = sourceFile;
break;
default:
string value = rowRecord.GetString(i + 1);
switch (row.Fields[i].Column.Category)
{
case ColumnCategory.Guid:
value = value.ToUpper(CultureInfo.InvariantCulture);
break;
}
// de-modularize
if (!this.suppressDemodularization && OutputType.Module == output.Type && ColumnModularizeType.None != row.Fields[i].Column.ModularizeType)
{
Regex modularization = new Regex(@"\.[0-9A-Fa-f]{8}_[0-9A-Fa-f]{4}_[0-9A-Fa-f]{4}_[0-9A-Fa-f]{4}_[0-9A-Fa-f]{12}");
if (null == modularizationGuid)
{
Match match = modularization.Match(value);
if (match.Success)
{
modularizationGuid = String.Concat('{', match.Value.Substring(1).Replace('_', '-'), '}');
}
}
value = modularization.Replace(value, String.Empty);
}
// escape "$(" for the preprocessor
value = value.Replace("$(", "$$(");
// escape things that look like wix variables
MatchCollection matches = Common.WixVariableRegex.Matches(value);
for (int j = matches.Count - 1; 0 <= j; j--)
{
value = value.Insert(matches[j].Index, "!");
}
row[i] = value;
break;
}
}
}
}
}
output.Tables.Add(table);
}
}
}
}
}
finally
{
if (null != validationView)
{
validationView.Close();
}
}
// set the modularization guid as the PackageCode
if (null != modularizationGuid)
{
Table table = output.Tables["_SummaryInformation"];
foreach (Row row in table.Rows)
{
if (9 == (int)row[0]) // PID_REVNUMBER
{
row[1] = modularizationGuid;
}
}
}
if (this.isAdminImage)
{
GenerateWixFileTable(databaseFile, output);
GenerateSectionIds(output);
}
return output;
}
///
/// Creates section ids on rows which form logical groupings of resources.
///
/// The Output that represents the msi database.
private void GenerateSectionIds(Output output)
{
// First assign and index section ids for the tables that are in their own sections.
AssignSectionIdsToTable(output.Tables["Binary"], 0);
Hashtable componentSectionIdIndex = AssignSectionIdsToTable(output.Tables["Component"], 0);
Hashtable customActionSectionIdIndex = AssignSectionIdsToTable(output.Tables["CustomAction"], 0);
AssignSectionIdsToTable(output.Tables["Directory"], 0);
Hashtable featureSectionIdIndex = AssignSectionIdsToTable(output.Tables["Feature"], 0);
AssignSectionIdsToTable(output.Tables["Icon"], 0);
Hashtable digitalCertificateSectionIdIndex = AssignSectionIdsToTable(output.Tables["MsiDigitalCertificate"], 0);
AssignSectionIdsToTable(output.Tables["Property"], 0);
// Now handle all the tables that rely on the first set of indexes but also produce their own indexes. Order matters here.
Hashtable fileSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["File"], componentSectionIdIndex, 1, 0);
Hashtable appIdSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["Class"], componentSectionIdIndex, 2, 5);
Hashtable odbcDataSourceSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["ODBCDataSource"], componentSectionIdIndex, 1, 0);
Hashtable odbcDriverSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["ODBCDriver"], componentSectionIdIndex, 1, 0);
Hashtable registrySectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["Registry"], componentSectionIdIndex, 5, 0);
Hashtable serviceInstallSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["ServiceInstall"], componentSectionIdIndex, 11, 0);
// Now handle all the tables which only rely on previous indexes and order does not matter.
foreach (Table table in output.Tables)
{
switch (table.Name)
{
case "WixFile":
case "MsiFileHash":
ConnectTableToSection(table, fileSectionIdIndex, 0);
break;
case "MsiAssembly":
case "MsiAssemblyName":
ConnectTableToSection(table, componentSectionIdIndex, 0);
break;
case "MsiPackageCertificate":
case "MsiPatchCertificate":
ConnectTableToSection(table, digitalCertificateSectionIdIndex, 1);
break;
case "CreateFolder":
case "FeatureComponents":
case "MoveFile":
case "ReserveCost":
case "ODBCTranslator":
ConnectTableToSection(table, componentSectionIdIndex, 1);
break;
case "TypeLib":
ConnectTableToSection(table, componentSectionIdIndex, 2);
break;
case "Shortcut":
case "Environment":
ConnectTableToSection(table, componentSectionIdIndex, 3);
break;
case "RemoveRegistry":
ConnectTableToSection(table, componentSectionIdIndex, 4);
break;
case "ServiceControl":
ConnectTableToSection(table, componentSectionIdIndex, 5);
break;
case "IniFile":
case "RemoveIniFile":
ConnectTableToSection(table, componentSectionIdIndex, 7);
break;
case "AppId":
ConnectTableToSection(table, appIdSectionIdIndex, 0);
break;
case "Condition":
ConnectTableToSection(table, featureSectionIdIndex, 0);
break;
case "ODBCSourceAttribute":
ConnectTableToSection(table, odbcDataSourceSectionIdIndex, 0);
break;
case "ODBCAttribute":
ConnectTableToSection(table, odbcDriverSectionIdIndex, 0);
break;
case "AdminExecuteSequence":
case "AdminUISequence":
case "AdvtExecuteSequence":
case "AdvtUISequence":
case "InstallExecuteSequence":
case "InstallUISequence":
ConnectTableToSection(table, customActionSectionIdIndex, 0);
break;
case "LockPermissions":
case "MsiLockPermissions":
foreach (Row row in table.Rows)
{
string lockObject = (string)row[0];
string tableName = (string)row[1];
switch (tableName)
{
case "File":
row.SectionId = (string)fileSectionIdIndex[lockObject];
break;
case "Registry":
row.SectionId = (string)registrySectionIdIndex[lockObject];
break;
case "ServiceInstall":
row.SectionId = (string)serviceInstallSectionIdIndex[lockObject];
break;
}
}
break;
}
}
// Now pass the output to each unbinder extension to allow them to analyze the output and determine thier proper section ids.
foreach (IUnbinderExtension extension in this.unbinderExtensions)
{
extension.GenerateSectionIds(output);
}
}
///
/// Creates new section ids on all the rows in a table.
///
/// The table to add sections to.
/// The index of the column which is used by other tables to reference this table.
/// A Hashtable containing the tables key for each row paired with its assigned section id.
private Hashtable AssignSectionIdsToTable(Table table, int rowPrimaryKeyIndex)
{
Hashtable hashtable = new Hashtable();
if (null != table)
{
foreach (Row row in table.Rows)
{
row.SectionId = GetNewSectionId();
hashtable.Add(row[rowPrimaryKeyIndex], row.SectionId);
}
}
return hashtable;
}
///
/// Connects a table's rows to an already sectioned table.
///
/// The table containing rows that need to be connected to sections.
/// A hashtable containing keys to map table to its section.
/// The index of the column which is used as the foreign key in to the sectionIdIndex.
private static void ConnectTableToSection(Table table, Hashtable sectionIdIndex, int rowIndex)
{
if (null != table)
{
foreach (Row row in table.Rows)
{
if (sectionIdIndex.ContainsKey(row[rowIndex]))
{
row.SectionId = (string)sectionIdIndex[row[rowIndex]];
}
}
}
}
///
/// Connects a table's rows to an already sectioned table and produces an index for other tables to connect to it.
///
/// The table containing rows that need to be connected to sections.
/// A hashtable containing keys to map table to its section.
/// The index of the column which is used as the foreign key in to the sectionIdIndex.
/// The index of the column which is used by other tables to reference this table.
/// A Hashtable containing the tables key for each row paired with its assigned section id.
private static Hashtable ConnectTableToSectionAndIndex(Table table, Hashtable sectionIdIndex, int rowIndex, int rowPrimaryKeyIndex)
{
Hashtable newHashTable = new Hashtable();
if (null != table)
{
foreach (Row row in table.Rows)
{
if (!sectionIdIndex.ContainsKey(row[rowIndex]))
{
continue;
}
row.SectionId = (string)sectionIdIndex[row[rowIndex]];
if (null != row[rowPrimaryKeyIndex])
{
newHashTable.Add(row[rowPrimaryKeyIndex], row.SectionId);
}
}
}
return newHashTable;
}
///
/// Creates a new section identifier to be used when adding a section to an output.
///
/// A string representing a new section id.
private string GetNewSectionId()
{
this.sectionCount++;
return "wix.section." + this.sectionCount.ToString(CultureInfo.InvariantCulture);
}
///
/// Generates the WixFile table based on a path to an admin image msi and an Output.
///
/// The path to the msi database file in an admin image.
/// The Output that represents the msi database.
private void GenerateWixFileTable(string databaseFile, Output output)
{
string adminRootPath = Path.GetDirectoryName(databaseFile);
Hashtable componentDirectoryIndex = new Hashtable();
Table componentTable = output.Tables["Component"];
foreach (Row row in componentTable.Rows)
{
componentDirectoryIndex.Add(row[0], row[2]);
}
// Index full source paths for all directories
Hashtable directoryDirectoryParentIndex = new Hashtable();
Hashtable directoryFullPathIndex = new Hashtable();
Hashtable directorySourceNameIndex = new Hashtable();
Table directoryTable = output.Tables["Directory"];
foreach (Row row in directoryTable.Rows)
{
directoryDirectoryParentIndex.Add(row[0], row[1]);
if (null == row[1])
{
directoryFullPathIndex.Add(row[0], adminRootPath);
}
else
{
directorySourceNameIndex.Add(row[0], GetAdminSourceName((string)row[2]));
}
}
foreach (DictionaryEntry directoryEntry in directoryDirectoryParentIndex)
{
if (!directoryFullPathIndex.ContainsKey(directoryEntry.Key))
{
GetAdminFullPath((string)directoryEntry.Key, directoryDirectoryParentIndex, directorySourceNameIndex, directoryFullPathIndex);
}
}
Table fileTable = output.Tables["File"];
Table wixFileTable = output.EnsureTable(this.tableDefinitions["WixFile"]);
foreach (Row row in fileTable.Rows)
{
WixFileRow wixFileRow = new WixFileRow(null, this.tableDefinitions["WixFile"]);
wixFileRow.File = (string)row[0];
wixFileRow.Directory = (string)componentDirectoryIndex[(string)row[1]];
wixFileRow.Source = Path.Combine((string)directoryFullPathIndex[wixFileRow.Directory], GetAdminSourceName((string)row[2]));
if (!File.Exists(wixFileRow.Source))
{
throw new WixException(WixErrors.WixFileNotFound(wixFileRow.Source));
}
wixFileTable.Rows.Add(wixFileRow);
}
}
///
/// Gets the full path of a directory. Populates the full path index with the directory's full path and all of its parent directorie's full paths.
///
/// The directory identifier.
/// The Hashtable containing all the directory to directory parent mapping.
/// The Hashtable containing all the directory to source name mapping.
/// The Hashtable containing a mapping between all of the directories and their previously calculated full paths.
/// The full path to the directory.
private string GetAdminFullPath(string directory, Hashtable directoryDirectoryParentIndex, Hashtable directorySourceNameIndex, Hashtable directoryFullPathIndex)
{
string parent = (string)directoryDirectoryParentIndex[directory];
string sourceName = (string)directorySourceNameIndex[directory];
string parentFullPath;
if (directoryFullPathIndex.ContainsKey(parent))
{
parentFullPath = (string)directoryFullPathIndex[parent];
}
else
{
parentFullPath = GetAdminFullPath(parent, directoryDirectoryParentIndex, directorySourceNameIndex, directoryFullPathIndex);
}
if (null == sourceName)
{
sourceName = String.Empty;
}
string fullPath = Path.Combine(parentFullPath, sourceName);
directoryFullPathIndex.Add(directory, fullPath);
return fullPath;
}
///
/// Get the source name in an admin image.
///
/// The Filename value.
/// The source name of the directory in an admin image.
private static string GetAdminSourceName(string value)
{
string name = null;
string[] names;
string shortname = null;
string shortsourcename = null;
string sourcename = null;
names = Installer.GetNames(value);
if (null != names[0] && "." != names[0])
{
if (null != names[1])
{
shortname = names[0];
}
else
{
name = names[0];
}
}
if (null != names[1])
{
name = names[1];
}
if (null != names[2])
{
if (null != names[3])
{
shortsourcename = names[2];
}
else
{
sourcename = names[2];
}
}
if (null != names[3])
{
sourcename = names[3];
}
if (null != sourcename)
{
return sourcename;
}
else if (null != shortsourcename)
{
return shortsourcename;
}
else if (null != name)
{
return name;
}
else
{
return shortname;
}
}
///
/// Unbind an MSP patch file.
///
/// The patch file.
/// The path where files should be exported.
/// The unbound patch.
private Output UnbindPatch(string patchFile, string exportBasePath)
{
Output patch;
// patch files are essentially database files (use a special flag to let the API know its a patch file)
try
{
using (Database database = new Database(patchFile, OpenDatabase.ReadOnly | OpenDatabase.OpenPatchFile))
{
patch = this.UnbindDatabase(patchFile, database, OutputType.Patch, exportBasePath, false);
}
}
catch (Win32Exception e)
{
if (0x6E == e.NativeErrorCode) // ERROR_OPEN_FAILED
{
throw new WixException(WixErrors.OpenDatabaseFailed(patchFile));
}
throw;
}
// retrieve the transforms (they are in substorages)
using (Storage storage = Storage.Open(patchFile, StorageMode.Read | StorageMode.ShareDenyWrite))
{
Table summaryInformationTable = patch.Tables["_SummaryInformation"];
foreach (Row row in summaryInformationTable.Rows)
{
if (8 == (int)row[0]) // PID_LASTAUTHOR
{
string value = (string)row[1];
foreach (string decoratedSubStorageName in value.Split(';'))
{
string subStorageName = decoratedSubStorageName.Substring(1);
string transformFile = Path.Combine(this.TempFilesLocation, String.Concat("Transform", Path.DirectorySeparatorChar, subStorageName, ".mst"));
// ensure the parent directory exists
System.IO.Directory.CreateDirectory(Path.GetDirectoryName(transformFile));
// copy the substorage to a new storage for the transform file
using (Storage subStorage = storage.OpenStorage(subStorageName))
{
using (Storage transformStorage = Storage.CreateDocFile(transformFile, StorageMode.ReadWrite | StorageMode.ShareExclusive | StorageMode.Create))
{
subStorage.CopyTo(transformStorage);
}
}
// unbind the transform
Output transform = this.UnbindTransform(transformFile, (null == exportBasePath ? null : Path.Combine(exportBasePath, subStorageName)));
patch.SubStorages.Add(new SubStorage(subStorageName, transform));
}
break;
}
}
}
// extract the files from the cabinets
// TODO: use per-transform export paths for support of multi-product patches
if (null != exportBasePath && !this.suppressExtractCabinets)
{
using (Database database = new Database(patchFile, OpenDatabase.ReadOnly | OpenDatabase.OpenPatchFile))
{
foreach (SubStorage subStorage in patch.SubStorages)
{
// only patch transforms should carry files
if (subStorage.Name.StartsWith("#", StringComparison.Ordinal))
{
this.ExtractCabinets(subStorage.Data, database, patchFile, exportBasePath);
}
}
}
}
return patch;
}
///
/// Unbind an MSI transform file.
///
/// The transform file.
/// The path where files should be exported.
/// The unbound transform.
private Output UnbindTransform(string transformFile, string exportBasePath)
{
Output transform = new Output(new SourceLineNumber(transformFile));
transform.Type = OutputType.Transform;
// get the summary information table
using (SummaryInformation summaryInformation = new SummaryInformation(transformFile))
{
Table table = transform.EnsureTable(this.tableDefinitions["_SummaryInformation"]);
for (int i = 1; 19 >= i; i++)
{
string value = summaryInformation.GetProperty(i);
if (0 < value.Length)
{
Row row = table.CreateRow(transform.SourceLineNumbers);
row[0] = i;
row[1] = value;
}
}
}
// create a schema msi which hopefully matches the table schemas in the transform
Output schemaOutput = new Output(null);
string msiDatabaseFile = Path.Combine(this.TempFilesLocation, "schema.msi");
foreach (TableDefinition tableDefinition in this.tableDefinitions)
{
// skip unreal tables and the Patch table
if (!tableDefinition.Unreal && "Patch" != tableDefinition.Name)
{
schemaOutput.EnsureTable(tableDefinition);
}
}
Hashtable addedRows = new Hashtable();
Table transformViewTable;
// Bind the schema msi.
this.GenerateDatabase(schemaOutput, msiDatabaseFile);
// apply the transform to the database and retrieve the modifications
using (Database msiDatabase = new Database(msiDatabaseFile, OpenDatabase.Transact))
{
// apply the transform with the ViewTransform option to collect all the modifications
msiDatabase.ApplyTransform(transformFile, TransformErrorConditions.All | TransformErrorConditions.ViewTransform);
// unbind the database
Output transformViewOutput = this.UnbindDatabase(msiDatabaseFile, msiDatabase, OutputType.Product, exportBasePath, true);
// index the added and possibly modified rows (added rows may also appears as modified rows)
transformViewTable = transformViewOutput.Tables["_TransformView"];
Hashtable modifiedRows = new Hashtable();
foreach (Row row in transformViewTable.Rows)
{
string tableName = (string)row[0];
string columnName = (string)row[1];
string primaryKeys = (string)row[2];
if ("INSERT" == columnName)
{
string index = String.Concat(tableName, ':', primaryKeys);
addedRows.Add(index, null);
}
else if ("CREATE" != columnName && "DELETE" != columnName && "DROP" != columnName && null != primaryKeys) // modified row
{
string index = String.Concat(tableName, ':', primaryKeys);
modifiedRows[index] = row;
}
}
// create placeholder rows for modified rows to make the transform insert the updated values when its applied
foreach (Row row in modifiedRows.Values)
{
string tableName = (string)row[0];
string columnName = (string)row[1];
string primaryKeys = (string)row[2];
string index = String.Concat(tableName, ':', primaryKeys);
// ignore information for added rows
if (!addedRows.Contains(index))
{
Table table = schemaOutput.Tables[tableName];
this.CreateRow(table, primaryKeys, true);
}
}
}
// Re-bind the schema output with the placeholder rows.
this.GenerateDatabase(schemaOutput, msiDatabaseFile);
// apply the transform to the database and retrieve the modifications
using (Database msiDatabase = new Database(msiDatabaseFile, OpenDatabase.Transact))
{
try
{
// apply the transform
msiDatabase.ApplyTransform(transformFile, TransformErrorConditions.All);
// commit the database to guard against weird errors with streams
msiDatabase.Commit();
}
catch (Win32Exception ex)
{
if (0x65B == ex.NativeErrorCode)
{
// this commonly happens when the transform was built
// against a database schema different from the internal
// table definitions
throw new WixException(WixErrors.TransformSchemaMismatch());
}
}
// unbind the database
Output output = this.UnbindDatabase(msiDatabaseFile, msiDatabase, OutputType.Product, exportBasePath, true);
// index all the rows to easily find modified rows
Hashtable rows = new Hashtable();
foreach (Table table in output.Tables)
{
foreach (Row row in table.Rows)
{
rows.Add(String.Concat(table.Name, ':', row.GetPrimaryKey('\t', " ")), row);
}
}
// process the _TransformView rows into transform rows
foreach (Row row in transformViewTable.Rows)
{
string tableName = (string)row[0];
string columnName = (string)row[1];
string primaryKeys = (string)row[2];
Table table = transform.EnsureTable(this.tableDefinitions[tableName]);
if ("CREATE" == columnName) // added table
{
table.Operation = TableOperation.Add;
}
else if ("DELETE" == columnName) // deleted row
{
Row deletedRow = this.CreateRow(table, primaryKeys, false);
deletedRow.Operation = RowOperation.Delete;
}
else if ("DROP" == columnName) // dropped table
{
table.Operation = TableOperation.Drop;
}
else if ("INSERT" == columnName) // added row
{
string index = String.Concat(tableName, ':', primaryKeys);
Row addedRow = (Row)rows[index];
addedRow.Operation = RowOperation.Add;
table.Rows.Add(addedRow);
}
else if (null != primaryKeys) // modified row
{
string index = String.Concat(tableName, ':', primaryKeys);
// the _TransformView table includes information for added rows
// that looks like modified rows so it sometimes needs to be ignored
if (!addedRows.Contains(index))
{
Row modifiedRow = (Row)rows[index];
// mark the field as modified
int indexOfModifiedValue = -1;
for (int i = 0; i < modifiedRow.TableDefinition.Columns.Count; ++i)
{
if (columnName.Equals(modifiedRow.TableDefinition.Columns[i].Name, StringComparison.Ordinal))
{
indexOfModifiedValue = i;
break;
}
}
modifiedRow.Fields[indexOfModifiedValue].Modified = true;
// move the modified row into the transform the first time its encountered
if (RowOperation.None == modifiedRow.Operation)
{
modifiedRow.Operation = RowOperation.Modify;
table.Rows.Add(modifiedRow);
}
}
}
else // added column
{
ColumnDefinition column = table.Definition.Columns.Single(c => c.Name.Equals(columnName, StringComparison.Ordinal));
column.Added = true;
}
}
}
return transform;
}
private void GenerateDatabase(Output output, string databaseFile)
{
GenerateDatabaseCommand command = new GenerateDatabaseCommand();
command.Extensions = Enumerable.Empty();
command.FileManagers = Enumerable.Empty();
command.Output = output;
command.OutputPath = databaseFile;
command.KeepAddedColumns = true;
command.UseSubDirectory = false;
command.SuppressAddingValidationRows = true;
command.TableDefinitions = this.tableDefinitions;
command.TempFilesLocation = this.TempFilesLocation;
command.Codepage = -1;
command.Execute();
}
///
/// Unbind a bundle.
///
/// The bundle file.
/// The path where files should be exported.
/// The unbound bundle.
private Output UnbindBundle(string bundleFile, string exportBasePath)
{
string uxExtractPath = Path.Combine(exportBasePath, "UX");
string acExtractPath = Path.Combine(exportBasePath, "AttachedContainer");
using (BurnReader reader = BurnReader.Open(bundleFile))
{
reader.ExtractUXContainer(uxExtractPath, this.TempFilesLocation);
reader.ExtractAttachedContainer(acExtractPath, this.TempFilesLocation);
}
return null;
}
///
/// Create a deleted or modified row.
///
/// The table containing the row.
/// The primary keys of the row.
/// Option to set all required fields with placeholder values.
/// The new row.
private Row CreateRow(Table table, string primaryKeys, bool setRequiredFields)
{
Row row = table.CreateRow(null);
string[] primaryKeyParts = primaryKeys.Split('\t');
int primaryKeyPartIndex = 0;
for (int i = 0; i < table.Definition.Columns.Count; i++)
{
ColumnDefinition columnDefinition = table.Definition.Columns[i];
if (columnDefinition.PrimaryKey)
{
if (ColumnType.Number == columnDefinition.Type && !columnDefinition.IsLocalizable)
{
row[i] = Convert.ToInt32(primaryKeyParts[primaryKeyPartIndex++], CultureInfo.InvariantCulture);
}
else
{
row[i] = primaryKeyParts[primaryKeyPartIndex++];
}
}
else if (setRequiredFields)
{
if (ColumnType.Number == columnDefinition.Type && !columnDefinition.IsLocalizable)
{
row[i] = 1;
}
else if (ColumnType.Object == columnDefinition.Type)
{
if (null == this.emptyFile)
{
this.emptyFile = Path.GetTempFileName() + ".empty";
using (FileStream fileStream = File.Create(this.emptyFile))
{
}
}
row[i] = this.emptyFile;
}
else
{
row[i] = "1";
}
}
}
return row;
}
///
/// Extract the cabinets from a database.
///
/// The output to use when finding cabinets.
/// The database containing the cabinets.
/// The location of the database file.
/// The path where the files should be exported.
private void ExtractCabinets(Output output, Database database, string databaseFile, string exportBasePath)
{
string databaseBasePath = Path.GetDirectoryName(databaseFile);
StringCollection cabinetFiles = new StringCollection();
SortedList embeddedCabinets = new SortedList();
// index all of the cabinet files
if (OutputType.Module == output.Type)
{
embeddedCabinets.Add(0, "MergeModule.CABinet");
}
else if (null != output.Tables["Media"])
{
foreach (MediaRow mediaRow in output.Tables["Media"].Rows)
{
if (null != mediaRow.Cabinet)
{
if (OutputType.Product == output.Type ||
(OutputType.Transform == output.Type && RowOperation.Add == mediaRow.Operation))
{
if (mediaRow.Cabinet.StartsWith("#", StringComparison.Ordinal))
{
embeddedCabinets.Add(mediaRow.DiskId, mediaRow.Cabinet.Substring(1));
}
else
{
cabinetFiles.Add(Path.Combine(databaseBasePath, mediaRow.Cabinet));
}
}
}
}
}
// extract the embedded cabinet files from the database
if (0 < embeddedCabinets.Count)
{
using (View streamsView = database.OpenView("SELECT `Data` FROM `_Streams` WHERE `Name` = ?"))
{
foreach (int diskId in embeddedCabinets.Keys)
{
using (Record record = new Record(1))
{
record.SetString(1, (string)embeddedCabinets[diskId]);
streamsView.Execute(record);
}
using (Record record = streamsView.Fetch())
{
if (null != record)
{
// since the cabinets are stored in case-sensitive streams inside the msi, but the file system is not case-sensitive,
// embedded cabinets must be extracted to a canonical file name (like their diskid) to ensure extraction will always work
string cabinetFile = Path.Combine(this.TempFilesLocation, String.Concat("Media", Path.DirectorySeparatorChar, diskId.ToString(CultureInfo.InvariantCulture), ".cab"));
// ensure the parent directory exists
System.IO.Directory.CreateDirectory(Path.GetDirectoryName(cabinetFile));
using (FileStream fs = System.IO.File.Create(cabinetFile))
{
int bytesRead;
byte[] buffer = new byte[512];
while (0 != (bytesRead = record.GetStream(1, buffer, buffer.Length)))
{
fs.Write(buffer, 0, bytesRead);
}
}
cabinetFiles.Add(cabinetFile);
}
else
{
// TODO: warning about missing embedded cabinet
}
}
}
}
}
// extract the cabinet files
if (0 < cabinetFiles.Count)
{
string fileDirectory = Path.Combine(exportBasePath, "File");
// delete the directory and its files to prevent cab extraction due to an existing file
if (Directory.Exists(fileDirectory))
{
Directory.Delete(fileDirectory, true);
}
// ensure the directory exists or extraction will fail
Directory.CreateDirectory(fileDirectory);
foreach (string cabinetFile in cabinetFiles)
{
using (WixExtractCab extractCab = new WixExtractCab())
{
try
{
extractCab.Extract(cabinetFile, fileDirectory);
}
catch (FileNotFoundException)
{
throw new WixException(WixErrors.FileNotFound(new SourceLineNumber(databaseFile), cabinetFile));
}
}
}
}
}
}
}