From dbde9e7104b907bbbaea17e21247d8cafc8b3a4c Mon Sep 17 00:00:00 2001 From: Rob Mensching Date: Sat, 14 Oct 2017 16:12:07 -0700 Subject: Massive refactoring to introduce the concept of IBackend --- src/WixToolset.Core/Unbinder.cs | 1399 +-------------------------------------- 1 file changed, 20 insertions(+), 1379 deletions(-) (limited to 'src/WixToolset.Core/Unbinder.cs') diff --git a/src/WixToolset.Core/Unbinder.cs b/src/WixToolset.Core/Unbinder.cs index 744d5536..2ff51997 100644 --- a/src/WixToolset.Core/Unbinder.cs +++ b/src/WixToolset.Core/Unbinder.cs @@ -1,37 +1,18 @@ // 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 +namespace WixToolset.Core { - 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; + using System.Collections.Generic; /// /// Unbinder core of the WiX toolset. /// - public sealed class Unbinder : IMessageHandler + public sealed class Unbinder { - private string emptyFile; - private bool isAdminImage; - private int sectionCount; - private bool suppressDemodularization; - private bool suppressExtractCabinets; private TableDefinitionCollection tableDefinitions; private ArrayList unbinderExtensions; // private TempFileCollection tempFiles; @@ -45,61 +26,32 @@ namespace WixToolset this.unbinderExtensions = new ArrayList(); } + public IEnumerable BackendFactories { get; } + /// /// 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; } - } + public bool IsAdminImage { get; set; } /// /// 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; } - } + public bool SuppressDemodularization { get; set; } /// /// 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; } - } + public bool SuppressExtractCabinets { get; set; } /// /// 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); - // } - // } - } + public string TempFilesLocation => Path.GetTempPath(); /// /// Adds extension data. @@ -156,1336 +108,25 @@ namespace WixToolset // 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); + var context = new UnbindContext(); + context.InputFilePath = file; + context.ExportBasePath = exportBasePath; + context.IntermediateFolder = this.TempFilesLocation; + context.IsAdminImage = this.IsAdminImage; + context.SuppressDemodularization = this.SuppressDemodularization; + context.SuppressExtractCabinets = this.SuppressExtractCabinets; - 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) + foreach (var factory in this.BackendFactories) { - Table table = output.Tables["_SummaryInformation"]; - - foreach (Row row in table.Rows) + if (factory.TryCreateBackend(outputType.ToString(), file, null, out var backend)) { - if (9 == (int)row[0]) // PID_REVNUMBER - { - row[1] = modularizationGuid; - } + return backend.Unbind(context); } } - 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); - } + // TODO: Display message that could not find a unbinder for output type? 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)); - } - } - } - } - } } } -- cgit v1.2.3-55-g6feb