diff options
| author | Rob Mensching <rob@firegiant.com> | 2017-10-14 16:12:07 -0700 |
|---|---|---|
| committer | Rob Mensching <rob@firegiant.com> | 2017-10-14 16:12:07 -0700 |
| commit | dbde9e7104b907bbbaea17e21247d8cafc8b3a4c (patch) | |
| tree | 0f5fbbb6fe12c6b2e5e622a0e18ce4c5b4eb2b96 /src/WixToolset.Core.WindowsInstaller/Unbind | |
| parent | fbf986eb97f68396797a89fc7d40dec07b775440 (diff) | |
| download | wix-dbde9e7104b907bbbaea17e21247d8cafc8b3a4c.tar.gz wix-dbde9e7104b907bbbaea17e21247d8cafc8b3a4c.tar.bz2 wix-dbde9e7104b907bbbaea17e21247d8cafc8b3a4c.zip | |
Massive refactoring to introduce the concept of IBackend
Diffstat (limited to 'src/WixToolset.Core.WindowsInstaller/Unbind')
4 files changed, 1307 insertions, 0 deletions
diff --git a/src/WixToolset.Core.WindowsInstaller/Unbind/ExtractCabinetsCommand.cs b/src/WixToolset.Core.WindowsInstaller/Unbind/ExtractCabinetsCommand.cs new file mode 100644 index 00000000..229e75b4 --- /dev/null +++ b/src/WixToolset.Core.WindowsInstaller/Unbind/ExtractCabinetsCommand.cs | |||
| @@ -0,0 +1,146 @@ | |||
| 1 | // 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. | ||
| 2 | |||
| 3 | namespace WixToolset.Core.WindowsInstaller.Unbind | ||
| 4 | { | ||
| 5 | using System; | ||
| 6 | using System.Collections; | ||
| 7 | using System.Collections.Specialized; | ||
| 8 | using System.Globalization; | ||
| 9 | using System.IO; | ||
| 10 | using WixToolset.Core.Cab; | ||
| 11 | using WixToolset.Data; | ||
| 12 | using WixToolset.Data.Rows; | ||
| 13 | using WixToolset.Msi; | ||
| 14 | |||
| 15 | internal class ExtractCabinetsCommand | ||
| 16 | { | ||
| 17 | public ExtractCabinetsCommand(Output output, Database database, string inputFilePath, string exportBasePath, string intermediateFolder) | ||
| 18 | { | ||
| 19 | this.Output = output; | ||
| 20 | this.Database = database; | ||
| 21 | this.InputFilePath = inputFilePath; | ||
| 22 | this.ExportBasePath = exportBasePath; | ||
| 23 | this.IntermediateFolder = intermediateFolder; | ||
| 24 | } | ||
| 25 | |||
| 26 | private Output Output { get; } | ||
| 27 | |||
| 28 | private Database Database { get; } | ||
| 29 | |||
| 30 | private string InputFilePath { get; } | ||
| 31 | |||
| 32 | private string ExportBasePath { get; } | ||
| 33 | |||
| 34 | private string IntermediateFolder { get; } | ||
| 35 | |||
| 36 | public void Execute() | ||
| 37 | { | ||
| 38 | string databaseBasePath = Path.GetDirectoryName(this.InputFilePath); | ||
| 39 | StringCollection cabinetFiles = new StringCollection(); | ||
| 40 | SortedList embeddedCabinets = new SortedList(); | ||
| 41 | |||
| 42 | // index all of the cabinet files | ||
| 43 | if (OutputType.Module == this.Output.Type) | ||
| 44 | { | ||
| 45 | embeddedCabinets.Add(0, "MergeModule.CABinet"); | ||
| 46 | } | ||
| 47 | else if (null != this.Output.Tables["Media"]) | ||
| 48 | { | ||
| 49 | foreach (MediaRow mediaRow in this.Output.Tables["Media"].Rows) | ||
| 50 | { | ||
| 51 | if (null != mediaRow.Cabinet) | ||
| 52 | { | ||
| 53 | if (OutputType.Product == this.Output.Type || | ||
| 54 | (OutputType.Transform == this.Output.Type && RowOperation.Add == mediaRow.Operation)) | ||
| 55 | { | ||
| 56 | if (mediaRow.Cabinet.StartsWith("#", StringComparison.Ordinal)) | ||
| 57 | { | ||
| 58 | embeddedCabinets.Add(mediaRow.DiskId, mediaRow.Cabinet.Substring(1)); | ||
| 59 | } | ||
| 60 | else | ||
| 61 | { | ||
| 62 | cabinetFiles.Add(Path.Combine(databaseBasePath, mediaRow.Cabinet)); | ||
| 63 | } | ||
| 64 | } | ||
| 65 | } | ||
| 66 | } | ||
| 67 | } | ||
| 68 | |||
| 69 | // extract the embedded cabinet files from the database | ||
| 70 | if (0 < embeddedCabinets.Count) | ||
| 71 | { | ||
| 72 | using (View streamsView = this.Database.OpenView("SELECT `Data` FROM `_Streams` WHERE `Name` = ?")) | ||
| 73 | { | ||
| 74 | foreach (int diskId in embeddedCabinets.Keys) | ||
| 75 | { | ||
| 76 | using (Record record = new Record(1)) | ||
| 77 | { | ||
| 78 | record.SetString(1, (string)embeddedCabinets[diskId]); | ||
| 79 | streamsView.Execute(record); | ||
| 80 | } | ||
| 81 | |||
| 82 | using (Record record = streamsView.Fetch()) | ||
| 83 | { | ||
| 84 | if (null != record) | ||
| 85 | { | ||
| 86 | // since the cabinets are stored in case-sensitive streams inside the msi, but the file system is not case-sensitive, | ||
| 87 | // embedded cabinets must be extracted to a canonical file name (like their diskid) to ensure extraction will always work | ||
| 88 | string cabinetFile = Path.Combine(this.IntermediateFolder, String.Concat("Media", Path.DirectorySeparatorChar, diskId.ToString(CultureInfo.InvariantCulture), ".cab")); | ||
| 89 | |||
| 90 | // ensure the parent directory exists | ||
| 91 | System.IO.Directory.CreateDirectory(Path.GetDirectoryName(cabinetFile)); | ||
| 92 | |||
| 93 | using (FileStream fs = System.IO.File.Create(cabinetFile)) | ||
| 94 | { | ||
| 95 | int bytesRead; | ||
| 96 | byte[] buffer = new byte[512]; | ||
| 97 | |||
| 98 | while (0 != (bytesRead = record.GetStream(1, buffer, buffer.Length))) | ||
| 99 | { | ||
| 100 | fs.Write(buffer, 0, bytesRead); | ||
| 101 | } | ||
| 102 | } | ||
| 103 | |||
| 104 | cabinetFiles.Add(cabinetFile); | ||
| 105 | } | ||
| 106 | else | ||
| 107 | { | ||
| 108 | // TODO: warning about missing embedded cabinet | ||
| 109 | } | ||
| 110 | } | ||
| 111 | } | ||
| 112 | } | ||
| 113 | } | ||
| 114 | |||
| 115 | // extract the cabinet files | ||
| 116 | if (0 < cabinetFiles.Count) | ||
| 117 | { | ||
| 118 | string fileDirectory = Path.Combine(this.ExportBasePath, "File"); | ||
| 119 | |||
| 120 | // delete the directory and its files to prevent cab extraction due to an existing file | ||
| 121 | if (Directory.Exists(fileDirectory)) | ||
| 122 | { | ||
| 123 | Directory.Delete(fileDirectory, true); | ||
| 124 | } | ||
| 125 | |||
| 126 | // ensure the directory exists or extraction will fail | ||
| 127 | Directory.CreateDirectory(fileDirectory); | ||
| 128 | |||
| 129 | foreach (string cabinetFile in cabinetFiles) | ||
| 130 | { | ||
| 131 | using (var extractCab = new WixExtractCab()) | ||
| 132 | { | ||
| 133 | try | ||
| 134 | { | ||
| 135 | extractCab.Extract(cabinetFile, fileDirectory); | ||
| 136 | } | ||
| 137 | catch (FileNotFoundException) | ||
| 138 | { | ||
| 139 | throw new WixException(WixErrors.FileNotFound(new SourceLineNumber(this.InputFilePath), cabinetFile)); | ||
| 140 | } | ||
| 141 | } | ||
| 142 | } | ||
| 143 | } | ||
| 144 | } | ||
| 145 | } | ||
| 146 | } | ||
diff --git a/src/WixToolset.Core.WindowsInstaller/Unbind/UnbindDatabaseCommand.cs b/src/WixToolset.Core.WindowsInstaller/Unbind/UnbindDatabaseCommand.cs new file mode 100644 index 00000000..208be874 --- /dev/null +++ b/src/WixToolset.Core.WindowsInstaller/Unbind/UnbindDatabaseCommand.cs | |||
| @@ -0,0 +1,791 @@ | |||
| 1 | // 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. | ||
| 2 | |||
| 3 | namespace WixToolset.Core.WindowsInstaller.Unbind | ||
| 4 | { | ||
| 5 | using System; | ||
| 6 | using System.Collections; | ||
| 7 | using System.Collections.Generic; | ||
| 8 | using System.Globalization; | ||
| 9 | using System.IO; | ||
| 10 | using System.Text.RegularExpressions; | ||
| 11 | using WixToolset.Core.Native; | ||
| 12 | using WixToolset.Data; | ||
| 13 | using WixToolset.Data.Rows; | ||
| 14 | using WixToolset.Msi; | ||
| 15 | |||
| 16 | internal class UnbindDatabaseCommand | ||
| 17 | { | ||
| 18 | public UnbindDatabaseCommand(Messaging messaging, Database database, string databasePath, OutputType outputType, string exportBasePath, string intermediateFolder, bool isAdminImage, bool suppressDemodularization, bool skipSummaryInfo) | ||
| 19 | { | ||
| 20 | this.Messaging = messaging; | ||
| 21 | this.Database = database; | ||
| 22 | this.DatabasePath = databasePath; | ||
| 23 | this.OutputType = outputType; | ||
| 24 | this.ExportBasePath = exportBasePath; | ||
| 25 | this.IntermediateFolder = intermediateFolder; | ||
| 26 | this.IsAdminImage = isAdminImage; | ||
| 27 | this.SuppressDemodularization = suppressDemodularization; | ||
| 28 | this.SkipSummaryInfo = skipSummaryInfo; | ||
| 29 | |||
| 30 | this.TableDefinitions = WindowsInstallerStandard.GetTableDefinitions(); | ||
| 31 | } | ||
| 32 | |||
| 33 | public Messaging Messaging { get; } | ||
| 34 | |||
| 35 | public Database Database { get; } | ||
| 36 | |||
| 37 | public string DatabasePath { get; } | ||
| 38 | |||
| 39 | public OutputType OutputType { get; } | ||
| 40 | |||
| 41 | public string ExportBasePath { get; } | ||
| 42 | |||
| 43 | public string IntermediateFolder { get; } | ||
| 44 | |||
| 45 | public bool IsAdminImage { get; } | ||
| 46 | |||
| 47 | public bool SuppressDemodularization { get; } | ||
| 48 | |||
| 49 | public bool SkipSummaryInfo { get; } | ||
| 50 | |||
| 51 | public TableDefinitionCollection TableDefinitions { get; } | ||
| 52 | |||
| 53 | private int SectionCount { get; set; } | ||
| 54 | |||
| 55 | public Output Execute() | ||
| 56 | { | ||
| 57 | string modularizationGuid = null; | ||
| 58 | Output output = new Output(new SourceLineNumber(this.DatabasePath)); | ||
| 59 | View validationView = null; | ||
| 60 | |||
| 61 | // set the output type | ||
| 62 | output.Type = this.OutputType; | ||
| 63 | |||
| 64 | // get the codepage | ||
| 65 | this.Database.Export("_ForceCodepage", this.IntermediateFolder, "_ForceCodepage.idt"); | ||
| 66 | using (StreamReader sr = File.OpenText(Path.Combine(this.IntermediateFolder, "_ForceCodepage.idt"))) | ||
| 67 | { | ||
| 68 | string line; | ||
| 69 | |||
| 70 | while (null != (line = sr.ReadLine())) | ||
| 71 | { | ||
| 72 | string[] data = line.Split('\t'); | ||
| 73 | |||
| 74 | if (2 == data.Length) | ||
| 75 | { | ||
| 76 | output.Codepage = Convert.ToInt32(data[0], CultureInfo.InvariantCulture); | ||
| 77 | } | ||
| 78 | } | ||
| 79 | } | ||
| 80 | |||
| 81 | // get the summary information table if it exists; it won't if unbinding a transform | ||
| 82 | if (!this.SkipSummaryInfo) | ||
| 83 | { | ||
| 84 | using (SummaryInformation summaryInformation = new SummaryInformation(this.Database)) | ||
| 85 | { | ||
| 86 | Table table = new Table(null, this.TableDefinitions["_SummaryInformation"]); | ||
| 87 | |||
| 88 | for (int i = 1; 19 >= i; i++) | ||
| 89 | { | ||
| 90 | string value = summaryInformation.GetProperty(i); | ||
| 91 | |||
| 92 | if (0 < value.Length) | ||
| 93 | { | ||
| 94 | Row row = table.CreateRow(output.SourceLineNumbers); | ||
| 95 | row[0] = i; | ||
| 96 | row[1] = value; | ||
| 97 | } | ||
| 98 | } | ||
| 99 | |||
| 100 | output.Tables.Add(table); | ||
| 101 | } | ||
| 102 | } | ||
| 103 | |||
| 104 | try | ||
| 105 | { | ||
| 106 | // open a view on the validation table if it exists | ||
| 107 | if (this.Database.TableExists("_Validation")) | ||
| 108 | { | ||
| 109 | validationView = this.Database.OpenView("SELECT * FROM `_Validation` WHERE `Table` = ? AND `Column` = ?"); | ||
| 110 | } | ||
| 111 | |||
| 112 | // get the normal tables | ||
| 113 | using (View tablesView = this.Database.OpenExecuteView("SELECT * FROM _Tables")) | ||
| 114 | { | ||
| 115 | while (true) | ||
| 116 | { | ||
| 117 | using (Record tableRecord = tablesView.Fetch()) | ||
| 118 | { | ||
| 119 | if (null == tableRecord) | ||
| 120 | { | ||
| 121 | break; | ||
| 122 | } | ||
| 123 | |||
| 124 | string tableName = tableRecord.GetString(1); | ||
| 125 | |||
| 126 | using (View tableView = this.Database.OpenExecuteView(String.Format(CultureInfo.InvariantCulture, "SELECT * FROM `{0}`", tableName))) | ||
| 127 | { | ||
| 128 | List<ColumnDefinition> columns; | ||
| 129 | using (Record columnNameRecord = tableView.GetColumnInfo(MsiInterop.MSICOLINFONAMES), | ||
| 130 | columnTypeRecord = tableView.GetColumnInfo(MsiInterop.MSICOLINFOTYPES)) | ||
| 131 | { | ||
| 132 | // index the primary keys | ||
| 133 | HashSet<string> tablePrimaryKeys = new HashSet<string>(); | ||
| 134 | using (Record primaryKeysRecord = this.Database.PrimaryKeys(tableName)) | ||
| 135 | { | ||
| 136 | int primaryKeysFieldCount = primaryKeysRecord.GetFieldCount(); | ||
| 137 | |||
| 138 | for (int i = 1; i <= primaryKeysFieldCount; i++) | ||
| 139 | { | ||
| 140 | tablePrimaryKeys.Add(primaryKeysRecord.GetString(i)); | ||
| 141 | } | ||
| 142 | } | ||
| 143 | |||
| 144 | int columnCount = columnNameRecord.GetFieldCount(); | ||
| 145 | columns = new List<ColumnDefinition>(columnCount); | ||
| 146 | for (int i = 1; i <= columnCount; i++) | ||
| 147 | { | ||
| 148 | string columnName = columnNameRecord.GetString(i); | ||
| 149 | string idtType = columnTypeRecord.GetString(i); | ||
| 150 | |||
| 151 | ColumnType columnType; | ||
| 152 | int length; | ||
| 153 | bool nullable; | ||
| 154 | |||
| 155 | ColumnCategory columnCategory = ColumnCategory.Unknown; | ||
| 156 | ColumnModularizeType columnModularizeType = ColumnModularizeType.None; | ||
| 157 | bool primary = tablePrimaryKeys.Contains(columnName); | ||
| 158 | bool minValueSet = false; | ||
| 159 | int minValue = -1; | ||
| 160 | bool maxValueSet = false; | ||
| 161 | int maxValue = -1; | ||
| 162 | string keyTable = null; | ||
| 163 | bool keyColumnSet = false; | ||
| 164 | int keyColumn = -1; | ||
| 165 | string category = null; | ||
| 166 | string set = null; | ||
| 167 | string description = null; | ||
| 168 | |||
| 169 | // get the column type, length, and whether its nullable | ||
| 170 | switch (Char.ToLower(idtType[0], CultureInfo.InvariantCulture)) | ||
| 171 | { | ||
| 172 | case 'i': | ||
| 173 | columnType = ColumnType.Number; | ||
| 174 | break; | ||
| 175 | case 'l': | ||
| 176 | columnType = ColumnType.Localized; | ||
| 177 | break; | ||
| 178 | case 's': | ||
| 179 | columnType = ColumnType.String; | ||
| 180 | break; | ||
| 181 | case 'v': | ||
| 182 | columnType = ColumnType.Object; | ||
| 183 | break; | ||
| 184 | default: | ||
| 185 | // TODO: error | ||
| 186 | columnType = ColumnType.Unknown; | ||
| 187 | break; | ||
| 188 | } | ||
| 189 | length = Convert.ToInt32(idtType.Substring(1), CultureInfo.InvariantCulture); | ||
| 190 | nullable = Char.IsUpper(idtType[0]); | ||
| 191 | |||
| 192 | // try to get validation information | ||
| 193 | if (null != validationView) | ||
| 194 | { | ||
| 195 | using (Record validationRecord = new Record(2)) | ||
| 196 | { | ||
| 197 | validationRecord.SetString(1, tableName); | ||
| 198 | validationRecord.SetString(2, columnName); | ||
| 199 | |||
| 200 | validationView.Execute(validationRecord); | ||
| 201 | } | ||
| 202 | |||
| 203 | using (Record validationRecord = validationView.Fetch()) | ||
| 204 | { | ||
| 205 | if (null != validationRecord) | ||
| 206 | { | ||
| 207 | string validationNullable = validationRecord.GetString(3); | ||
| 208 | minValueSet = !validationRecord.IsNull(4); | ||
| 209 | minValue = (minValueSet ? validationRecord.GetInteger(4) : -1); | ||
| 210 | maxValueSet = !validationRecord.IsNull(5); | ||
| 211 | maxValue = (maxValueSet ? validationRecord.GetInteger(5) : -1); | ||
| 212 | keyTable = (!validationRecord.IsNull(6) ? validationRecord.GetString(6) : null); | ||
| 213 | keyColumnSet = !validationRecord.IsNull(7); | ||
| 214 | keyColumn = (keyColumnSet ? validationRecord.GetInteger(7) : -1); | ||
| 215 | category = (!validationRecord.IsNull(8) ? validationRecord.GetString(8) : null); | ||
| 216 | set = (!validationRecord.IsNull(9) ? validationRecord.GetString(9) : null); | ||
| 217 | description = (!validationRecord.IsNull(10) ? validationRecord.GetString(10) : null); | ||
| 218 | |||
| 219 | // check the validation nullable value against the column definition | ||
| 220 | if (null == validationNullable) | ||
| 221 | { | ||
| 222 | // TODO: warn for illegal validation nullable column | ||
| 223 | } | ||
| 224 | else if ((nullable && "Y" != validationNullable) || (!nullable && "N" != validationNullable)) | ||
| 225 | { | ||
| 226 | // TODO: warn for mismatch between column definition and validation nullable | ||
| 227 | } | ||
| 228 | |||
| 229 | // convert category to ColumnCategory | ||
| 230 | if (null != category) | ||
| 231 | { | ||
| 232 | try | ||
| 233 | { | ||
| 234 | columnCategory = (ColumnCategory)Enum.Parse(typeof(ColumnCategory), category, true); | ||
| 235 | } | ||
| 236 | catch (ArgumentException) | ||
| 237 | { | ||
| 238 | columnCategory = ColumnCategory.Unknown; | ||
| 239 | } | ||
| 240 | } | ||
| 241 | } | ||
| 242 | else | ||
| 243 | { | ||
| 244 | // TODO: warn about no validation information | ||
| 245 | } | ||
| 246 | } | ||
| 247 | } | ||
| 248 | |||
| 249 | // guess the modularization type | ||
| 250 | if ("Icon" == keyTable && 1 == keyColumn) | ||
| 251 | { | ||
| 252 | columnModularizeType = ColumnModularizeType.Icon; | ||
| 253 | } | ||
| 254 | else if ("Condition" == columnName) | ||
| 255 | { | ||
| 256 | columnModularizeType = ColumnModularizeType.Condition; | ||
| 257 | } | ||
| 258 | else if (ColumnCategory.Formatted == columnCategory || ColumnCategory.FormattedSDDLText == columnCategory) | ||
| 259 | { | ||
| 260 | columnModularizeType = ColumnModularizeType.Property; | ||
| 261 | } | ||
| 262 | else if (ColumnCategory.Identifier == columnCategory) | ||
| 263 | { | ||
| 264 | columnModularizeType = ColumnModularizeType.Column; | ||
| 265 | } | ||
| 266 | |||
| 267 | 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)); | ||
| 268 | } | ||
| 269 | } | ||
| 270 | |||
| 271 | TableDefinition tableDefinition = new TableDefinition(tableName, columns, false, false); | ||
| 272 | |||
| 273 | // use our table definitions if core properties are the same; this allows us to take advantage | ||
| 274 | // of wix concepts like localizable columns which current code assumes | ||
| 275 | if (this.TableDefinitions.Contains(tableName) && 0 == tableDefinition.CompareTo(this.TableDefinitions[tableName])) | ||
| 276 | { | ||
| 277 | tableDefinition = this.TableDefinitions[tableName]; | ||
| 278 | } | ||
| 279 | |||
| 280 | Table table = new Table(null, tableDefinition); | ||
| 281 | |||
| 282 | while (true) | ||
| 283 | { | ||
| 284 | using (Record rowRecord = tableView.Fetch()) | ||
| 285 | { | ||
| 286 | if (null == rowRecord) | ||
| 287 | { | ||
| 288 | break; | ||
| 289 | } | ||
| 290 | |||
| 291 | int recordCount = rowRecord.GetFieldCount(); | ||
| 292 | Row row = table.CreateRow(output.SourceLineNumbers); | ||
| 293 | |||
| 294 | for (int i = 0; recordCount > i && row.Fields.Length > i; i++) | ||
| 295 | { | ||
| 296 | if (rowRecord.IsNull(i + 1)) | ||
| 297 | { | ||
| 298 | if (!row.Fields[i].Column.Nullable) | ||
| 299 | { | ||
| 300 | // TODO: display an error for a null value in a non-nullable field OR | ||
| 301 | // display a warning and put an empty string in the value to let the compiler handle it | ||
| 302 | // (the second option is risky because the later code may make certain assumptions about | ||
| 303 | // the contents of a row value) | ||
| 304 | } | ||
| 305 | } | ||
| 306 | else | ||
| 307 | { | ||
| 308 | switch (row.Fields[i].Column.Type) | ||
| 309 | { | ||
| 310 | case ColumnType.Number: | ||
| 311 | bool success = false; | ||
| 312 | int intValue = rowRecord.GetInteger(i + 1); | ||
| 313 | if (row.Fields[i].Column.IsLocalizable) | ||
| 314 | { | ||
| 315 | success = row.BestEffortSetField(i, Convert.ToString(intValue, CultureInfo.InvariantCulture)); | ||
| 316 | } | ||
| 317 | else | ||
| 318 | { | ||
| 319 | success = row.BestEffortSetField(i, intValue); | ||
| 320 | } | ||
| 321 | |||
| 322 | if (!success) | ||
| 323 | { | ||
| 324 | this.Messaging.OnMessage(WixWarnings.BadColumnDataIgnored(row.SourceLineNumbers, Convert.ToString(intValue, CultureInfo.InvariantCulture), tableName, row.Fields[i].Column.Name)); | ||
| 325 | } | ||
| 326 | break; | ||
| 327 | case ColumnType.Object: | ||
| 328 | string sourceFile = "FILE NOT EXPORTED, USE THE dark.exe -x OPTION TO EXPORT BINARIES"; | ||
| 329 | |||
| 330 | if (null != this.ExportBasePath) | ||
| 331 | { | ||
| 332 | string relativeSourceFile = Path.Combine(tableName, row.GetPrimaryKey('.')); | ||
| 333 | sourceFile = Path.Combine(this.ExportBasePath, relativeSourceFile); | ||
| 334 | |||
| 335 | // ensure the parent directory exists | ||
| 336 | System.IO.Directory.CreateDirectory(Path.Combine(this.ExportBasePath, tableName)); | ||
| 337 | |||
| 338 | using (FileStream fs = System.IO.File.Create(sourceFile)) | ||
| 339 | { | ||
| 340 | int bytesRead; | ||
| 341 | byte[] buffer = new byte[512]; | ||
| 342 | |||
| 343 | while (0 != (bytesRead = rowRecord.GetStream(i + 1, buffer, buffer.Length))) | ||
| 344 | { | ||
| 345 | fs.Write(buffer, 0, bytesRead); | ||
| 346 | } | ||
| 347 | } | ||
| 348 | } | ||
| 349 | |||
| 350 | row[i] = sourceFile; | ||
| 351 | break; | ||
| 352 | default: | ||
| 353 | string value = rowRecord.GetString(i + 1); | ||
| 354 | |||
| 355 | switch (row.Fields[i].Column.Category) | ||
| 356 | { | ||
| 357 | case ColumnCategory.Guid: | ||
| 358 | value = value.ToUpper(CultureInfo.InvariantCulture); | ||
| 359 | break; | ||
| 360 | } | ||
| 361 | |||
| 362 | // de-modularize | ||
| 363 | if (!this.SuppressDemodularization && OutputType.Module == output.Type && ColumnModularizeType.None != row.Fields[i].Column.ModularizeType) | ||
| 364 | { | ||
| 365 | 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}"); | ||
| 366 | |||
| 367 | if (null == modularizationGuid) | ||
| 368 | { | ||
| 369 | Match match = modularization.Match(value); | ||
| 370 | if (match.Success) | ||
| 371 | { | ||
| 372 | modularizationGuid = String.Concat('{', match.Value.Substring(1).Replace('_', '-'), '}'); | ||
| 373 | } | ||
| 374 | } | ||
| 375 | |||
| 376 | value = modularization.Replace(value, String.Empty); | ||
| 377 | } | ||
| 378 | |||
| 379 | // escape "$(" for the preprocessor | ||
| 380 | value = value.Replace("$(", "$$("); | ||
| 381 | |||
| 382 | // escape things that look like wix variables | ||
| 383 | MatchCollection matches = Common.WixVariableRegex.Matches(value); | ||
| 384 | for (int j = matches.Count - 1; 0 <= j; j--) | ||
| 385 | { | ||
| 386 | value = value.Insert(matches[j].Index, "!"); | ||
| 387 | } | ||
| 388 | |||
| 389 | row[i] = value; | ||
| 390 | break; | ||
| 391 | } | ||
| 392 | } | ||
| 393 | } | ||
| 394 | } | ||
| 395 | } | ||
| 396 | |||
| 397 | output.Tables.Add(table); | ||
| 398 | } | ||
| 399 | |||
| 400 | } | ||
| 401 | } | ||
| 402 | } | ||
| 403 | } | ||
| 404 | finally | ||
| 405 | { | ||
| 406 | if (null != validationView) | ||
| 407 | { | ||
| 408 | validationView.Close(); | ||
| 409 | } | ||
| 410 | } | ||
| 411 | |||
| 412 | // set the modularization guid as the PackageCode | ||
| 413 | if (null != modularizationGuid) | ||
| 414 | { | ||
| 415 | Table table = output.Tables["_SummaryInformation"]; | ||
| 416 | |||
| 417 | foreach (Row row in table.Rows) | ||
| 418 | { | ||
| 419 | if (9 == (int)row[0]) // PID_REVNUMBER | ||
| 420 | { | ||
| 421 | row[1] = modularizationGuid; | ||
| 422 | } | ||
| 423 | } | ||
| 424 | } | ||
| 425 | |||
| 426 | if (this.IsAdminImage) | ||
| 427 | { | ||
| 428 | GenerateWixFileTable(this.DatabasePath, output); | ||
| 429 | GenerateSectionIds(output); | ||
| 430 | } | ||
| 431 | |||
| 432 | return output; | ||
| 433 | } | ||
| 434 | |||
| 435 | /// <summary> | ||
| 436 | /// Generates the WixFile table based on a path to an admin image msi and an Output. | ||
| 437 | /// </summary> | ||
| 438 | /// <param name="databaseFile">The path to the msi database file in an admin image.</param> | ||
| 439 | /// <param name="output">The Output that represents the msi database.</param> | ||
| 440 | private void GenerateWixFileTable(string databaseFile, Output output) | ||
| 441 | { | ||
| 442 | string adminRootPath = Path.GetDirectoryName(databaseFile); | ||
| 443 | |||
| 444 | Hashtable componentDirectoryIndex = new Hashtable(); | ||
| 445 | Table componentTable = output.Tables["Component"]; | ||
| 446 | foreach (Row row in componentTable.Rows) | ||
| 447 | { | ||
| 448 | componentDirectoryIndex.Add(row[0], row[2]); | ||
| 449 | } | ||
| 450 | |||
| 451 | // Index full source paths for all directories | ||
| 452 | Hashtable directoryDirectoryParentIndex = new Hashtable(); | ||
| 453 | Hashtable directoryFullPathIndex = new Hashtable(); | ||
| 454 | Hashtable directorySourceNameIndex = new Hashtable(); | ||
| 455 | Table directoryTable = output.Tables["Directory"]; | ||
| 456 | foreach (Row row in directoryTable.Rows) | ||
| 457 | { | ||
| 458 | directoryDirectoryParentIndex.Add(row[0], row[1]); | ||
| 459 | if (null == row[1]) | ||
| 460 | { | ||
| 461 | directoryFullPathIndex.Add(row[0], adminRootPath); | ||
| 462 | } | ||
| 463 | else | ||
| 464 | { | ||
| 465 | directorySourceNameIndex.Add(row[0], GetAdminSourceName((string)row[2])); | ||
| 466 | } | ||
| 467 | } | ||
| 468 | |||
| 469 | foreach (DictionaryEntry directoryEntry in directoryDirectoryParentIndex) | ||
| 470 | { | ||
| 471 | if (!directoryFullPathIndex.ContainsKey(directoryEntry.Key)) | ||
| 472 | { | ||
| 473 | GetAdminFullPath((string)directoryEntry.Key, directoryDirectoryParentIndex, directorySourceNameIndex, directoryFullPathIndex); | ||
| 474 | } | ||
| 475 | } | ||
| 476 | |||
| 477 | Table fileTable = output.Tables["File"]; | ||
| 478 | Table wixFileTable = output.EnsureTable(this.TableDefinitions["WixFile"]); | ||
| 479 | foreach (Row row in fileTable.Rows) | ||
| 480 | { | ||
| 481 | WixFileRow wixFileRow = new WixFileRow(null, this.TableDefinitions["WixFile"]); | ||
| 482 | wixFileRow.File = (string)row[0]; | ||
| 483 | wixFileRow.Directory = (string)componentDirectoryIndex[(string)row[1]]; | ||
| 484 | wixFileRow.Source = Path.Combine((string)directoryFullPathIndex[wixFileRow.Directory], GetAdminSourceName((string)row[2])); | ||
| 485 | |||
| 486 | if (!File.Exists(wixFileRow.Source)) | ||
| 487 | { | ||
| 488 | throw new WixException(WixErrors.WixFileNotFound(wixFileRow.Source)); | ||
| 489 | } | ||
| 490 | |||
| 491 | wixFileTable.Rows.Add(wixFileRow); | ||
| 492 | } | ||
| 493 | } | ||
| 494 | |||
| 495 | /// <summary> | ||
| 496 | /// 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. | ||
| 497 | /// </summary> | ||
| 498 | /// <param name="directory">The directory identifier.</param> | ||
| 499 | /// <param name="directoryDirectoryParentIndex">The Hashtable containing all the directory to directory parent mapping.</param> | ||
| 500 | /// <param name="directorySourceNameIndex">The Hashtable containing all the directory to source name mapping.</param> | ||
| 501 | /// <param name="directoryFullPathIndex">The Hashtable containing a mapping between all of the directories and their previously calculated full paths.</param> | ||
| 502 | /// <returns>The full path to the directory.</returns> | ||
| 503 | private string GetAdminFullPath(string directory, Hashtable directoryDirectoryParentIndex, Hashtable directorySourceNameIndex, Hashtable directoryFullPathIndex) | ||
| 504 | { | ||
| 505 | string parent = (string)directoryDirectoryParentIndex[directory]; | ||
| 506 | string sourceName = (string)directorySourceNameIndex[directory]; | ||
| 507 | |||
| 508 | string parentFullPath; | ||
| 509 | if (directoryFullPathIndex.ContainsKey(parent)) | ||
| 510 | { | ||
| 511 | parentFullPath = (string)directoryFullPathIndex[parent]; | ||
| 512 | } | ||
| 513 | else | ||
| 514 | { | ||
| 515 | parentFullPath = GetAdminFullPath(parent, directoryDirectoryParentIndex, directorySourceNameIndex, directoryFullPathIndex); | ||
| 516 | } | ||
| 517 | |||
| 518 | if (null == sourceName) | ||
| 519 | { | ||
| 520 | sourceName = String.Empty; | ||
| 521 | } | ||
| 522 | |||
| 523 | string fullPath = Path.Combine(parentFullPath, sourceName); | ||
| 524 | directoryFullPathIndex.Add(directory, fullPath); | ||
| 525 | |||
| 526 | return fullPath; | ||
| 527 | } | ||
| 528 | |||
| 529 | /// <summary> | ||
| 530 | /// Get the source name in an admin image. | ||
| 531 | /// </summary> | ||
| 532 | /// <param name="value">The Filename value.</param> | ||
| 533 | /// <returns>The source name of the directory in an admin image.</returns> | ||
| 534 | private static string GetAdminSourceName(string value) | ||
| 535 | { | ||
| 536 | string name = null; | ||
| 537 | string[] names; | ||
| 538 | string shortname = null; | ||
| 539 | string shortsourcename = null; | ||
| 540 | string sourcename = null; | ||
| 541 | |||
| 542 | names = Common.GetNames(value); | ||
| 543 | |||
| 544 | if (null != names[0] && "." != names[0]) | ||
| 545 | { | ||
| 546 | if (null != names[1]) | ||
| 547 | { | ||
| 548 | shortname = names[0]; | ||
| 549 | } | ||
| 550 | else | ||
| 551 | { | ||
| 552 | name = names[0]; | ||
| 553 | } | ||
| 554 | } | ||
| 555 | |||
| 556 | if (null != names[1]) | ||
| 557 | { | ||
| 558 | name = names[1]; | ||
| 559 | } | ||
| 560 | |||
| 561 | if (null != names[2]) | ||
| 562 | { | ||
| 563 | if (null != names[3]) | ||
| 564 | { | ||
| 565 | shortsourcename = names[2]; | ||
| 566 | } | ||
| 567 | else | ||
| 568 | { | ||
| 569 | sourcename = names[2]; | ||
| 570 | } | ||
| 571 | } | ||
| 572 | |||
| 573 | if (null != names[3]) | ||
| 574 | { | ||
| 575 | sourcename = names[3]; | ||
| 576 | } | ||
| 577 | |||
| 578 | if (null != sourcename) | ||
| 579 | { | ||
| 580 | return sourcename; | ||
| 581 | } | ||
| 582 | else if (null != shortsourcename) | ||
| 583 | { | ||
| 584 | return shortsourcename; | ||
| 585 | } | ||
| 586 | else if (null != name) | ||
| 587 | { | ||
| 588 | return name; | ||
| 589 | } | ||
| 590 | else | ||
| 591 | { | ||
| 592 | return shortname; | ||
| 593 | } | ||
| 594 | } | ||
| 595 | |||
| 596 | /// <summary> | ||
| 597 | /// Creates section ids on rows which form logical groupings of resources. | ||
| 598 | /// </summary> | ||
| 599 | /// <param name="output">The Output that represents the msi database.</param> | ||
| 600 | private void GenerateSectionIds(Output output) | ||
| 601 | { | ||
| 602 | // First assign and index section ids for the tables that are in their own sections. | ||
| 603 | AssignSectionIdsToTable(output.Tables["Binary"], 0); | ||
| 604 | Hashtable componentSectionIdIndex = AssignSectionIdsToTable(output.Tables["Component"], 0); | ||
| 605 | Hashtable customActionSectionIdIndex = AssignSectionIdsToTable(output.Tables["CustomAction"], 0); | ||
| 606 | AssignSectionIdsToTable(output.Tables["Directory"], 0); | ||
| 607 | Hashtable featureSectionIdIndex = AssignSectionIdsToTable(output.Tables["Feature"], 0); | ||
| 608 | AssignSectionIdsToTable(output.Tables["Icon"], 0); | ||
| 609 | Hashtable digitalCertificateSectionIdIndex = AssignSectionIdsToTable(output.Tables["MsiDigitalCertificate"], 0); | ||
| 610 | AssignSectionIdsToTable(output.Tables["Property"], 0); | ||
| 611 | |||
| 612 | // Now handle all the tables that rely on the first set of indexes but also produce their own indexes. Order matters here. | ||
| 613 | Hashtable fileSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["File"], componentSectionIdIndex, 1, 0); | ||
| 614 | Hashtable appIdSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["Class"], componentSectionIdIndex, 2, 5); | ||
| 615 | Hashtable odbcDataSourceSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["ODBCDataSource"], componentSectionIdIndex, 1, 0); | ||
| 616 | Hashtable odbcDriverSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["ODBCDriver"], componentSectionIdIndex, 1, 0); | ||
| 617 | Hashtable registrySectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["Registry"], componentSectionIdIndex, 5, 0); | ||
| 618 | Hashtable serviceInstallSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["ServiceInstall"], componentSectionIdIndex, 11, 0); | ||
| 619 | |||
| 620 | // Now handle all the tables which only rely on previous indexes and order does not matter. | ||
| 621 | foreach (Table table in output.Tables) | ||
| 622 | { | ||
| 623 | switch (table.Name) | ||
| 624 | { | ||
| 625 | case "WixFile": | ||
| 626 | case "MsiFileHash": | ||
| 627 | ConnectTableToSection(table, fileSectionIdIndex, 0); | ||
| 628 | break; | ||
| 629 | case "MsiAssembly": | ||
| 630 | case "MsiAssemblyName": | ||
| 631 | ConnectTableToSection(table, componentSectionIdIndex, 0); | ||
| 632 | break; | ||
| 633 | case "MsiPackageCertificate": | ||
| 634 | case "MsiPatchCertificate": | ||
| 635 | ConnectTableToSection(table, digitalCertificateSectionIdIndex, 1); | ||
| 636 | break; | ||
| 637 | case "CreateFolder": | ||
| 638 | case "FeatureComponents": | ||
| 639 | case "MoveFile": | ||
| 640 | case "ReserveCost": | ||
| 641 | case "ODBCTranslator": | ||
| 642 | ConnectTableToSection(table, componentSectionIdIndex, 1); | ||
| 643 | break; | ||
| 644 | case "TypeLib": | ||
| 645 | ConnectTableToSection(table, componentSectionIdIndex, 2); | ||
| 646 | break; | ||
| 647 | case "Shortcut": | ||
| 648 | case "Environment": | ||
| 649 | ConnectTableToSection(table, componentSectionIdIndex, 3); | ||
| 650 | break; | ||
| 651 | case "RemoveRegistry": | ||
| 652 | ConnectTableToSection(table, componentSectionIdIndex, 4); | ||
| 653 | break; | ||
| 654 | case "ServiceControl": | ||
| 655 | ConnectTableToSection(table, componentSectionIdIndex, 5); | ||
| 656 | break; | ||
| 657 | case "IniFile": | ||
| 658 | case "RemoveIniFile": | ||
| 659 | ConnectTableToSection(table, componentSectionIdIndex, 7); | ||
| 660 | break; | ||
| 661 | case "AppId": | ||
| 662 | ConnectTableToSection(table, appIdSectionIdIndex, 0); | ||
| 663 | break; | ||
| 664 | case "Condition": | ||
| 665 | ConnectTableToSection(table, featureSectionIdIndex, 0); | ||
| 666 | break; | ||
| 667 | case "ODBCSourceAttribute": | ||
| 668 | ConnectTableToSection(table, odbcDataSourceSectionIdIndex, 0); | ||
| 669 | break; | ||
| 670 | case "ODBCAttribute": | ||
| 671 | ConnectTableToSection(table, odbcDriverSectionIdIndex, 0); | ||
| 672 | break; | ||
| 673 | case "AdminExecuteSequence": | ||
| 674 | case "AdminUISequence": | ||
| 675 | case "AdvtExecuteSequence": | ||
| 676 | case "AdvtUISequence": | ||
| 677 | case "InstallExecuteSequence": | ||
| 678 | case "InstallUISequence": | ||
| 679 | ConnectTableToSection(table, customActionSectionIdIndex, 0); | ||
| 680 | break; | ||
| 681 | case "LockPermissions": | ||
| 682 | case "MsiLockPermissions": | ||
| 683 | foreach (Row row in table.Rows) | ||
| 684 | { | ||
| 685 | string lockObject = (string)row[0]; | ||
| 686 | string tableName = (string)row[1]; | ||
| 687 | switch (tableName) | ||
| 688 | { | ||
| 689 | case "File": | ||
| 690 | row.SectionId = (string)fileSectionIdIndex[lockObject]; | ||
| 691 | break; | ||
| 692 | case "Registry": | ||
| 693 | row.SectionId = (string)registrySectionIdIndex[lockObject]; | ||
| 694 | break; | ||
| 695 | case "ServiceInstall": | ||
| 696 | row.SectionId = (string)serviceInstallSectionIdIndex[lockObject]; | ||
| 697 | break; | ||
| 698 | } | ||
| 699 | } | ||
| 700 | break; | ||
| 701 | } | ||
| 702 | } | ||
| 703 | |||
| 704 | // Now pass the output to each unbinder extension to allow them to analyze the output and determine thier proper section ids. | ||
| 705 | //foreach (IUnbinderExtension extension in this.unbinderExtensions) | ||
| 706 | //{ | ||
| 707 | // extension.GenerateSectionIds(output); | ||
| 708 | //} | ||
| 709 | } | ||
| 710 | |||
| 711 | /// <summary> | ||
| 712 | /// Creates new section ids on all the rows in a table. | ||
| 713 | /// </summary> | ||
| 714 | /// <param name="table">The table to add sections to.</param> | ||
| 715 | /// <param name="rowPrimaryKeyIndex">The index of the column which is used by other tables to reference this table.</param> | ||
| 716 | /// <returns>A Hashtable containing the tables key for each row paired with its assigned section id.</returns> | ||
| 717 | private Hashtable AssignSectionIdsToTable(Table table, int rowPrimaryKeyIndex) | ||
| 718 | { | ||
| 719 | Hashtable hashtable = new Hashtable(); | ||
| 720 | if (null != table) | ||
| 721 | { | ||
| 722 | foreach (Row row in table.Rows) | ||
| 723 | { | ||
| 724 | row.SectionId = GetNewSectionId(); | ||
| 725 | hashtable.Add(row[rowPrimaryKeyIndex], row.SectionId); | ||
| 726 | } | ||
| 727 | } | ||
| 728 | return hashtable; | ||
| 729 | } | ||
| 730 | |||
| 731 | /// <summary> | ||
| 732 | /// Connects a table's rows to an already sectioned table. | ||
| 733 | /// </summary> | ||
| 734 | /// <param name="table">The table containing rows that need to be connected to sections.</param> | ||
| 735 | /// <param name="sectionIdIndex">A hashtable containing keys to map table to its section.</param> | ||
| 736 | /// <param name="rowIndex">The index of the column which is used as the foreign key in to the sectionIdIndex.</param> | ||
| 737 | private static void ConnectTableToSection(Table table, Hashtable sectionIdIndex, int rowIndex) | ||
| 738 | { | ||
| 739 | if (null != table) | ||
| 740 | { | ||
| 741 | foreach (Row row in table.Rows) | ||
| 742 | { | ||
| 743 | if (sectionIdIndex.ContainsKey(row[rowIndex])) | ||
| 744 | { | ||
| 745 | row.SectionId = (string)sectionIdIndex[row[rowIndex]]; | ||
| 746 | } | ||
| 747 | } | ||
| 748 | } | ||
| 749 | } | ||
| 750 | |||
| 751 | /// <summary> | ||
| 752 | /// Connects a table's rows to an already sectioned table and produces an index for other tables to connect to it. | ||
| 753 | /// </summary> | ||
| 754 | /// <param name="table">The table containing rows that need to be connected to sections.</param> | ||
| 755 | /// <param name="sectionIdIndex">A hashtable containing keys to map table to its section.</param> | ||
| 756 | /// <param name="rowIndex">The index of the column which is used as the foreign key in to the sectionIdIndex.</param> | ||
| 757 | /// <param name="rowPrimaryKeyIndex">The index of the column which is used by other tables to reference this table.</param> | ||
| 758 | /// <returns>A Hashtable containing the tables key for each row paired with its assigned section id.</returns> | ||
| 759 | private static Hashtable ConnectTableToSectionAndIndex(Table table, Hashtable sectionIdIndex, int rowIndex, int rowPrimaryKeyIndex) | ||
| 760 | { | ||
| 761 | Hashtable newHashTable = new Hashtable(); | ||
| 762 | if (null != table) | ||
| 763 | { | ||
| 764 | foreach (Row row in table.Rows) | ||
| 765 | { | ||
| 766 | if (!sectionIdIndex.ContainsKey(row[rowIndex])) | ||
| 767 | { | ||
| 768 | continue; | ||
| 769 | } | ||
| 770 | |||
| 771 | row.SectionId = (string)sectionIdIndex[row[rowIndex]]; | ||
| 772 | if (null != row[rowPrimaryKeyIndex]) | ||
| 773 | { | ||
| 774 | newHashTable.Add(row[rowPrimaryKeyIndex], row.SectionId); | ||
| 775 | } | ||
| 776 | } | ||
| 777 | } | ||
| 778 | return newHashTable; | ||
| 779 | } | ||
| 780 | |||
| 781 | /// <summary> | ||
| 782 | /// Creates a new section identifier to be used when adding a section to an output. | ||
| 783 | /// </summary> | ||
| 784 | /// <returns>A string representing a new section id.</returns> | ||
| 785 | private string GetNewSectionId() | ||
| 786 | { | ||
| 787 | this.SectionCount++; | ||
| 788 | return "wix.section." + this.SectionCount.ToString(CultureInfo.InvariantCulture); | ||
| 789 | } | ||
| 790 | } | ||
| 791 | } | ||
diff --git a/src/WixToolset.Core.WindowsInstaller/Unbind/UnbindMsiOrMsmCommand.cs b/src/WixToolset.Core.WindowsInstaller/Unbind/UnbindMsiOrMsmCommand.cs new file mode 100644 index 00000000..f04dcefe --- /dev/null +++ b/src/WixToolset.Core.WindowsInstaller/Unbind/UnbindMsiOrMsmCommand.cs | |||
| @@ -0,0 +1,53 @@ | |||
| 1 | // 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. | ||
| 2 | |||
| 3 | namespace WixToolset.Core.WindowsInstaller.Unbind | ||
| 4 | { | ||
| 5 | using System; | ||
| 6 | using System.ComponentModel; | ||
| 7 | using WixToolset.Core.Native; | ||
| 8 | using WixToolset.Data; | ||
| 9 | using WixToolset.Extensibility; | ||
| 10 | using WixToolset.Msi; | ||
| 11 | |||
| 12 | internal class UnbindMsiOrMsmCommand | ||
| 13 | { | ||
| 14 | public UnbindMsiOrMsmCommand(IUnbindContext context) | ||
| 15 | { | ||
| 16 | this.Context = context; | ||
| 17 | } | ||
| 18 | |||
| 19 | public IUnbindContext Context { get; } | ||
| 20 | |||
| 21 | public Output Execute() | ||
| 22 | { | ||
| 23 | Output output; | ||
| 24 | |||
| 25 | try | ||
| 26 | { | ||
| 27 | using (Database database = new Database(this.Context.InputFilePath, OpenDatabase.ReadOnly)) | ||
| 28 | { | ||
| 29 | var unbindCommand = new UnbindDatabaseCommand(this.Context.Messaging, database, this.Context.InputFilePath, OutputType.Product, this.Context.ExportBasePath, this.Context.IntermediateFolder, this.Context.IsAdminImage, this.Context.SuppressDemodularization, skipSummaryInfo: false); | ||
| 30 | output = unbindCommand.Execute(); | ||
| 31 | |||
| 32 | // extract the files from the cabinets | ||
| 33 | if (!String.IsNullOrEmpty(this.Context.ExportBasePath) && !this.Context.SuppressExtractCabinets) | ||
| 34 | { | ||
| 35 | var extractCommand = new ExtractCabinetsCommand(output, database, this.Context.InputFilePath, this.Context.ExportBasePath, this.Context.IntermediateFolder); | ||
| 36 | extractCommand.Execute(); | ||
| 37 | } | ||
| 38 | } | ||
| 39 | } | ||
| 40 | catch (Win32Exception e) | ||
| 41 | { | ||
| 42 | if (0x6E == e.NativeErrorCode) // ERROR_OPEN_FAILED | ||
| 43 | { | ||
| 44 | throw new WixException(WixErrors.OpenDatabaseFailed(this.Context.InputFilePath)); | ||
| 45 | } | ||
| 46 | |||
| 47 | throw; | ||
| 48 | } | ||
| 49 | |||
| 50 | return output; | ||
| 51 | } | ||
| 52 | } | ||
| 53 | } | ||
diff --git a/src/WixToolset.Core.WindowsInstaller/Unbind/UnbindTranformCommand.cs b/src/WixToolset.Core.WindowsInstaller/Unbind/UnbindTranformCommand.cs new file mode 100644 index 00000000..c0eda9c7 --- /dev/null +++ b/src/WixToolset.Core.WindowsInstaller/Unbind/UnbindTranformCommand.cs | |||
| @@ -0,0 +1,317 @@ | |||
| 1 | // 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. | ||
| 2 | |||
| 3 | namespace WixToolset.Core.WindowsInstaller.Unbind | ||
| 4 | { | ||
| 5 | using System; | ||
| 6 | using System.Collections; | ||
| 7 | using System.Collections.Generic; | ||
| 8 | using System.ComponentModel; | ||
| 9 | using System.Globalization; | ||
| 10 | using System.IO; | ||
| 11 | using System.Linq; | ||
| 12 | using System.Text.RegularExpressions; | ||
| 13 | using WixToolset.Core.Native; | ||
| 14 | using WixToolset.Core.WindowsInstaller.Databases; | ||
| 15 | using WixToolset.Data; | ||
| 16 | using WixToolset.Data.Rows; | ||
| 17 | using WixToolset.Extensibility; | ||
| 18 | using WixToolset.Msi; | ||
| 19 | |||
| 20 | internal class UnbindTransformCommand | ||
| 21 | { | ||
| 22 | public UnbindTransformCommand(Messaging messaging, string transformFile, string exportBasePath, string intermediateFolder) | ||
| 23 | { | ||
| 24 | this.Messaging = messaging; | ||
| 25 | this.TransformFile = transformFile; | ||
| 26 | this.ExportBasePath = exportBasePath; | ||
| 27 | this.IntermediateFolder = intermediateFolder; | ||
| 28 | |||
| 29 | this.TableDefinitions = WindowsInstallerStandard.GetTableDefinitions(); | ||
| 30 | } | ||
| 31 | |||
| 32 | private Messaging Messaging { get; } | ||
| 33 | |||
| 34 | private string TransformFile { get; } | ||
| 35 | |||
| 36 | private string ExportBasePath { get; } | ||
| 37 | |||
| 38 | private string IntermediateFolder { get; } | ||
| 39 | |||
| 40 | private TableDefinitionCollection TableDefinitions { get; } | ||
| 41 | |||
| 42 | private string EmptyFile { get; set; } | ||
| 43 | |||
| 44 | public Output Execute() | ||
| 45 | { | ||
| 46 | Output transform = new Output(new SourceLineNumber(this.TransformFile)); | ||
| 47 | transform.Type = OutputType.Transform; | ||
| 48 | |||
| 49 | // get the summary information table | ||
| 50 | using (SummaryInformation summaryInformation = new SummaryInformation(this.TransformFile)) | ||
| 51 | { | ||
| 52 | Table table = transform.EnsureTable(this.TableDefinitions["_SummaryInformation"]); | ||
| 53 | |||
| 54 | for (int i = 1; 19 >= i; i++) | ||
| 55 | { | ||
| 56 | string value = summaryInformation.GetProperty(i); | ||
| 57 | |||
| 58 | if (0 < value.Length) | ||
| 59 | { | ||
| 60 | Row row = table.CreateRow(transform.SourceLineNumbers); | ||
| 61 | row[0] = i; | ||
| 62 | row[1] = value; | ||
| 63 | } | ||
| 64 | } | ||
| 65 | } | ||
| 66 | |||
| 67 | // create a schema msi which hopefully matches the table schemas in the transform | ||
| 68 | Output schemaOutput = new Output(null); | ||
| 69 | string msiDatabaseFile = Path.Combine(this.IntermediateFolder, "schema.msi"); | ||
| 70 | foreach (TableDefinition tableDefinition in this.TableDefinitions) | ||
| 71 | { | ||
| 72 | // skip unreal tables and the Patch table | ||
| 73 | if (!tableDefinition.Unreal && "Patch" != tableDefinition.Name) | ||
| 74 | { | ||
| 75 | schemaOutput.EnsureTable(tableDefinition); | ||
| 76 | } | ||
| 77 | } | ||
| 78 | |||
| 79 | Hashtable addedRows = new Hashtable(); | ||
| 80 | Table transformViewTable; | ||
| 81 | |||
| 82 | // Bind the schema msi. | ||
| 83 | this.GenerateDatabase(schemaOutput, msiDatabaseFile); | ||
| 84 | |||
| 85 | // apply the transform to the database and retrieve the modifications | ||
| 86 | using (Database msiDatabase = new Database(msiDatabaseFile, OpenDatabase.Transact)) | ||
| 87 | { | ||
| 88 | // apply the transform with the ViewTransform option to collect all the modifications | ||
| 89 | msiDatabase.ApplyTransform(this.TransformFile, TransformErrorConditions.All | TransformErrorConditions.ViewTransform); | ||
| 90 | |||
| 91 | // unbind the database | ||
| 92 | var unbindCommand = new UnbindDatabaseCommand(this.Messaging, msiDatabase, msiDatabaseFile, OutputType.Product, this.ExportBasePath, this.IntermediateFolder, false, false, skipSummaryInfo: true); | ||
| 93 | Output transformViewOutput = unbindCommand.Execute(); | ||
| 94 | |||
| 95 | // index the added and possibly modified rows (added rows may also appears as modified rows) | ||
| 96 | transformViewTable = transformViewOutput.Tables["_TransformView"]; | ||
| 97 | Hashtable modifiedRows = new Hashtable(); | ||
| 98 | foreach (Row row in transformViewTable.Rows) | ||
| 99 | { | ||
| 100 | string tableName = (string)row[0]; | ||
| 101 | string columnName = (string)row[1]; | ||
| 102 | string primaryKeys = (string)row[2]; | ||
| 103 | |||
| 104 | if ("INSERT" == columnName) | ||
| 105 | { | ||
| 106 | string index = String.Concat(tableName, ':', primaryKeys); | ||
| 107 | |||
| 108 | addedRows.Add(index, null); | ||
| 109 | } | ||
| 110 | else if ("CREATE" != columnName && "DELETE" != columnName && "DROP" != columnName && null != primaryKeys) // modified row | ||
| 111 | { | ||
| 112 | string index = String.Concat(tableName, ':', primaryKeys); | ||
| 113 | |||
| 114 | modifiedRows[index] = row; | ||
| 115 | } | ||
| 116 | } | ||
| 117 | |||
| 118 | // create placeholder rows for modified rows to make the transform insert the updated values when its applied | ||
| 119 | foreach (Row row in modifiedRows.Values) | ||
| 120 | { | ||
| 121 | string tableName = (string)row[0]; | ||
| 122 | string columnName = (string)row[1]; | ||
| 123 | string primaryKeys = (string)row[2]; | ||
| 124 | |||
| 125 | string index = String.Concat(tableName, ':', primaryKeys); | ||
| 126 | |||
| 127 | // ignore information for added rows | ||
| 128 | if (!addedRows.Contains(index)) | ||
| 129 | { | ||
| 130 | Table table = schemaOutput.Tables[tableName]; | ||
| 131 | this.CreateRow(table, primaryKeys, true); | ||
| 132 | } | ||
| 133 | } | ||
| 134 | } | ||
| 135 | |||
| 136 | // Re-bind the schema output with the placeholder rows. | ||
| 137 | this.GenerateDatabase(schemaOutput, msiDatabaseFile); | ||
| 138 | |||
| 139 | // apply the transform to the database and retrieve the modifications | ||
| 140 | using (Database msiDatabase = new Database(msiDatabaseFile, OpenDatabase.Transact)) | ||
| 141 | { | ||
| 142 | try | ||
| 143 | { | ||
| 144 | // apply the transform | ||
| 145 | msiDatabase.ApplyTransform(this.TransformFile, TransformErrorConditions.All); | ||
| 146 | |||
| 147 | // commit the database to guard against weird errors with streams | ||
| 148 | msiDatabase.Commit(); | ||
| 149 | } | ||
| 150 | catch (Win32Exception ex) | ||
| 151 | { | ||
| 152 | if (0x65B == ex.NativeErrorCode) | ||
| 153 | { | ||
| 154 | // this commonly happens when the transform was built | ||
| 155 | // against a database schema different from the internal | ||
| 156 | // table definitions | ||
| 157 | throw new WixException(WixErrors.TransformSchemaMismatch()); | ||
| 158 | } | ||
| 159 | } | ||
| 160 | |||
| 161 | // unbind the database | ||
| 162 | var unbindCommand = new UnbindDatabaseCommand(this.Messaging, msiDatabase, msiDatabaseFile, OutputType.Product, this.ExportBasePath, this.IntermediateFolder, false, false, skipSummaryInfo: true); | ||
| 163 | Output output = unbindCommand.Execute(); | ||
| 164 | |||
| 165 | // index all the rows to easily find modified rows | ||
| 166 | Hashtable rows = new Hashtable(); | ||
| 167 | foreach (Table table in output.Tables) | ||
| 168 | { | ||
| 169 | foreach (Row row in table.Rows) | ||
| 170 | { | ||
| 171 | rows.Add(String.Concat(table.Name, ':', row.GetPrimaryKey('\t', " ")), row); | ||
| 172 | } | ||
| 173 | } | ||
| 174 | |||
| 175 | // process the _TransformView rows into transform rows | ||
| 176 | foreach (Row row in transformViewTable.Rows) | ||
| 177 | { | ||
| 178 | string tableName = (string)row[0]; | ||
| 179 | string columnName = (string)row[1]; | ||
| 180 | string primaryKeys = (string)row[2]; | ||
| 181 | |||
| 182 | Table table = transform.EnsureTable(this.TableDefinitions[tableName]); | ||
| 183 | |||
| 184 | if ("CREATE" == columnName) // added table | ||
| 185 | { | ||
| 186 | table.Operation = TableOperation.Add; | ||
| 187 | } | ||
| 188 | else if ("DELETE" == columnName) // deleted row | ||
| 189 | { | ||
| 190 | Row deletedRow = this.CreateRow(table, primaryKeys, false); | ||
| 191 | deletedRow.Operation = RowOperation.Delete; | ||
| 192 | } | ||
| 193 | else if ("DROP" == columnName) // dropped table | ||
| 194 | { | ||
| 195 | table.Operation = TableOperation.Drop; | ||
| 196 | } | ||
| 197 | else if ("INSERT" == columnName) // added row | ||
| 198 | { | ||
| 199 | string index = String.Concat(tableName, ':', primaryKeys); | ||
| 200 | Row addedRow = (Row)rows[index]; | ||
| 201 | addedRow.Operation = RowOperation.Add; | ||
| 202 | table.Rows.Add(addedRow); | ||
| 203 | } | ||
| 204 | else if (null != primaryKeys) // modified row | ||
| 205 | { | ||
| 206 | string index = String.Concat(tableName, ':', primaryKeys); | ||
| 207 | |||
| 208 | // the _TransformView table includes information for added rows | ||
| 209 | // that looks like modified rows so it sometimes needs to be ignored | ||
| 210 | if (!addedRows.Contains(index)) | ||
| 211 | { | ||
| 212 | Row modifiedRow = (Row)rows[index]; | ||
| 213 | |||
| 214 | // mark the field as modified | ||
| 215 | int indexOfModifiedValue = -1; | ||
| 216 | for (int i = 0; i < modifiedRow.TableDefinition.Columns.Count; ++i) | ||
| 217 | { | ||
| 218 | if (columnName.Equals(modifiedRow.TableDefinition.Columns[i].Name, StringComparison.Ordinal)) | ||
| 219 | { | ||
| 220 | indexOfModifiedValue = i; | ||
| 221 | break; | ||
| 222 | } | ||
| 223 | } | ||
| 224 | modifiedRow.Fields[indexOfModifiedValue].Modified = true; | ||
| 225 | |||
| 226 | // move the modified row into the transform the first time its encountered | ||
| 227 | if (RowOperation.None == modifiedRow.Operation) | ||
| 228 | { | ||
| 229 | modifiedRow.Operation = RowOperation.Modify; | ||
| 230 | table.Rows.Add(modifiedRow); | ||
| 231 | } | ||
| 232 | } | ||
| 233 | } | ||
| 234 | else // added column | ||
| 235 | { | ||
| 236 | ColumnDefinition column = table.Definition.Columns.Single(c => c.Name.Equals(columnName, StringComparison.Ordinal)); | ||
| 237 | column.Added = true; | ||
| 238 | } | ||
| 239 | } | ||
| 240 | } | ||
| 241 | |||
| 242 | return transform; | ||
| 243 | } | ||
| 244 | |||
| 245 | private void GenerateDatabase(Output output, string databaseFile) | ||
| 246 | { | ||
| 247 | var command = new GenerateDatabaseCommand(); | ||
| 248 | command.Extensions = Array.Empty<IBinderExtension>(); | ||
| 249 | command.Output = output; | ||
| 250 | command.OutputPath = databaseFile; | ||
| 251 | command.KeepAddedColumns = true; | ||
| 252 | command.UseSubDirectory = false; | ||
| 253 | command.SuppressAddingValidationRows = true; | ||
| 254 | command.TableDefinitions = this.TableDefinitions; | ||
| 255 | command.TempFilesLocation = this.IntermediateFolder; | ||
| 256 | command.Codepage = -1; | ||
| 257 | command.Execute(); | ||
| 258 | } | ||
| 259 | |||
| 260 | /// <summary> | ||
| 261 | /// Create a deleted or modified row. | ||
| 262 | /// </summary> | ||
| 263 | /// <param name="table">The table containing the row.</param> | ||
| 264 | /// <param name="primaryKeys">The primary keys of the row.</param> | ||
| 265 | /// <param name="setRequiredFields">Option to set all required fields with placeholder values.</param> | ||
| 266 | /// <returns>The new row.</returns> | ||
| 267 | private Row CreateRow(Table table, string primaryKeys, bool setRequiredFields) | ||
| 268 | { | ||
| 269 | Row row = table.CreateRow(null); | ||
| 270 | |||
| 271 | string[] primaryKeyParts = primaryKeys.Split('\t'); | ||
| 272 | int primaryKeyPartIndex = 0; | ||
| 273 | |||
| 274 | for (int i = 0; i < table.Definition.Columns.Count; i++) | ||
| 275 | { | ||
| 276 | ColumnDefinition columnDefinition = table.Definition.Columns[i]; | ||
| 277 | |||
| 278 | if (columnDefinition.PrimaryKey) | ||
| 279 | { | ||
| 280 | if (ColumnType.Number == columnDefinition.Type && !columnDefinition.IsLocalizable) | ||
| 281 | { | ||
| 282 | row[i] = Convert.ToInt32(primaryKeyParts[primaryKeyPartIndex++], CultureInfo.InvariantCulture); | ||
| 283 | } | ||
| 284 | else | ||
| 285 | { | ||
| 286 | row[i] = primaryKeyParts[primaryKeyPartIndex++]; | ||
| 287 | } | ||
| 288 | } | ||
| 289 | else if (setRequiredFields) | ||
| 290 | { | ||
| 291 | if (ColumnType.Number == columnDefinition.Type && !columnDefinition.IsLocalizable) | ||
| 292 | { | ||
| 293 | row[i] = 1; | ||
| 294 | } | ||
| 295 | else if (ColumnType.Object == columnDefinition.Type) | ||
| 296 | { | ||
| 297 | if (null == this.EmptyFile) | ||
| 298 | { | ||
| 299 | this.EmptyFile = Path.GetTempFileName() + ".empty"; | ||
| 300 | using (FileStream fileStream = File.Create(this.EmptyFile)) | ||
| 301 | { | ||
| 302 | } | ||
| 303 | } | ||
| 304 | |||
| 305 | row[i] = this.EmptyFile; | ||
| 306 | } | ||
| 307 | else | ||
| 308 | { | ||
| 309 | row[i] = "1"; | ||
| 310 | } | ||
| 311 | } | ||
| 312 | } | ||
| 313 | |||
| 314 | return row; | ||
| 315 | } | ||
| 316 | } | ||
| 317 | } | ||
