diff options
Diffstat (limited to 'src/WixToolset.Core/Unbinder.cs')
-rw-r--r-- | src/WixToolset.Core/Unbinder.cs | 1399 |
1 files changed, 20 insertions, 1379 deletions
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 @@ | |||
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. | 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 | 2 | ||
3 | namespace WixToolset | 3 | namespace WixToolset.Core |
4 | { | 4 | { |
5 | using System; | ||
6 | using System.CodeDom.Compiler; | ||
7 | using System.Collections; | 5 | using System.Collections; |
8 | using System.Collections.Generic; | ||
9 | using System.Collections.Specialized; | ||
10 | using System.ComponentModel; | ||
11 | using System.Globalization; | ||
12 | using System.IO; | 6 | using System.IO; |
13 | using System.Linq; | ||
14 | using System.Text.RegularExpressions; | ||
15 | using WixToolset.Bind; | ||
16 | using WixToolset.Bind.Bundles; | ||
17 | using WixToolset.Cab; | ||
18 | using WixToolset.Data; | 7 | using WixToolset.Data; |
19 | using WixToolset.Data.Rows; | ||
20 | using WixToolset.Extensibility; | 8 | using WixToolset.Extensibility; |
21 | using WixToolset.Msi; | 9 | using System.Collections.Generic; |
22 | using WixToolset.Core.Native; | ||
23 | using WixToolset.Ole32; | ||
24 | 10 | ||
25 | /// <summary> | 11 | /// <summary> |
26 | /// Unbinder core of the WiX toolset. | 12 | /// Unbinder core of the WiX toolset. |
27 | /// </summary> | 13 | /// </summary> |
28 | public sealed class Unbinder : IMessageHandler | 14 | public sealed class Unbinder |
29 | { | 15 | { |
30 | private string emptyFile; | ||
31 | private bool isAdminImage; | ||
32 | private int sectionCount; | ||
33 | private bool suppressDemodularization; | ||
34 | private bool suppressExtractCabinets; | ||
35 | private TableDefinitionCollection tableDefinitions; | 16 | private TableDefinitionCollection tableDefinitions; |
36 | private ArrayList unbinderExtensions; | 17 | private ArrayList unbinderExtensions; |
37 | // private TempFileCollection tempFiles; | 18 | // private TempFileCollection tempFiles; |
@@ -45,61 +26,32 @@ namespace WixToolset | |||
45 | this.unbinderExtensions = new ArrayList(); | 26 | this.unbinderExtensions = new ArrayList(); |
46 | } | 27 | } |
47 | 28 | ||
29 | public IEnumerable<IBackendFactory> BackendFactories { get; } | ||
30 | |||
48 | /// <summary> | 31 | /// <summary> |
49 | /// Gets or sets whether the input msi is an admin image. | 32 | /// Gets or sets whether the input msi is an admin image. |
50 | /// </summary> | 33 | /// </summary> |
51 | /// <value>Set to true if the input msi is part of an admin image.</value> | 34 | /// <value>Set to true if the input msi is part of an admin image.</value> |
52 | public bool IsAdminImage | 35 | public bool IsAdminImage { get; set; } |
53 | { | ||
54 | get { return this.isAdminImage; } | ||
55 | set { this.isAdminImage = value; } | ||
56 | } | ||
57 | 36 | ||
58 | /// <summary> | 37 | /// <summary> |
59 | /// Gets or sets the option to suppress demodularizing values. | 38 | /// Gets or sets the option to suppress demodularizing values. |
60 | /// </summary> | 39 | /// </summary> |
61 | /// <value>The option to suppress demodularizing values.</value> | 40 | /// <value>The option to suppress demodularizing values.</value> |
62 | public bool SuppressDemodularization | 41 | public bool SuppressDemodularization { get; set; } |
63 | { | ||
64 | get { return this.suppressDemodularization; } | ||
65 | set { this.suppressDemodularization = value; } | ||
66 | } | ||
67 | 42 | ||
68 | /// <summary> | 43 | /// <summary> |
69 | /// Gets or sets the option to suppress extracting cabinets. | 44 | /// Gets or sets the option to suppress extracting cabinets. |
70 | /// </summary> | 45 | /// </summary> |
71 | /// <value>The option to suppress extracting cabinets.</value> | 46 | /// <value>The option to suppress extracting cabinets.</value> |
72 | public bool SuppressExtractCabinets | 47 | public bool SuppressExtractCabinets { get; set; } |
73 | { | ||
74 | get { return this.suppressExtractCabinets; } | ||
75 | set { this.suppressExtractCabinets = value; } | ||
76 | } | ||
77 | 48 | ||
78 | /// <summary> | 49 | /// <summary> |
79 | /// Gets or sets the temporary path for the Binder. If left null, the binder | 50 | /// Gets or sets the temporary path for the Binder. If left null, the binder |
80 | /// will use %TEMP% environment variable. | 51 | /// will use %TEMP% environment variable. |
81 | /// </summary> | 52 | /// </summary> |
82 | /// <value>Path to temp files.</value> | 53 | /// <value>Path to temp files.</value> |
83 | public string TempFilesLocation | 54 | public string TempFilesLocation => Path.GetTempPath(); |
84 | { | ||
85 | get | ||
86 | { | ||
87 | // return null == this.tempFiles ? String.Empty : this.tempFiles.BasePath; | ||
88 | return Path.GetTempPath(); | ||
89 | } | ||
90 | |||
91 | // set | ||
92 | // { | ||
93 | // if (null == value) | ||
94 | // { | ||
95 | // this.tempFiles = new TempFileCollection(); | ||
96 | // } | ||
97 | // else | ||
98 | // { | ||
99 | // this.tempFiles = new TempFileCollection(value); | ||
100 | // } | ||
101 | // } | ||
102 | } | ||
103 | 55 | ||
104 | /// <summary> | 56 | /// <summary> |
105 | /// Adds extension data. | 57 | /// Adds extension data. |
@@ -156,1336 +108,25 @@ namespace WixToolset | |||
156 | // if we don't have the temporary files object yet, get one | 108 | // if we don't have the temporary files object yet, get one |
157 | Directory.CreateDirectory(this.TempFilesLocation); // ensure the base path is there | 109 | Directory.CreateDirectory(this.TempFilesLocation); // ensure the base path is there |
158 | 110 | ||
159 | if (OutputType.Patch == outputType) | 111 | var context = new UnbindContext(); |
160 | { | 112 | context.InputFilePath = file; |
161 | return this.UnbindPatch(file, exportBasePath); | 113 | context.ExportBasePath = exportBasePath; |
162 | } | 114 | context.IntermediateFolder = this.TempFilesLocation; |
163 | else if (OutputType.Transform == outputType) | 115 | context.IsAdminImage = this.IsAdminImage; |
164 | { | 116 | context.SuppressDemodularization = this.SuppressDemodularization; |
165 | return this.UnbindTransform(file, exportBasePath); | 117 | context.SuppressExtractCabinets = this.SuppressExtractCabinets; |
166 | } | ||
167 | else if (OutputType.Bundle == outputType) | ||
168 | { | ||
169 | return this.UnbindBundle(file, exportBasePath); | ||
170 | } | ||
171 | else // other database types | ||
172 | { | ||
173 | return this.UnbindDatabase(file, outputType, exportBasePath); | ||
174 | } | ||
175 | } | ||
176 | |||
177 | /// <summary> | ||
178 | /// Cleans up the temp files used by the Decompiler. | ||
179 | /// </summary> | ||
180 | /// <returns>True if all files were deleted, false otherwise.</returns> | ||
181 | /// <remarks> | ||
182 | /// This should be called after every call to Decompile to ensure there | ||
183 | /// are no conflicts between each decompiled database. | ||
184 | /// </remarks> | ||
185 | public bool DeleteTempFiles() | ||
186 | { | ||
187 | #if REDO_IN_NETCORE | ||
188 | bool deleted = Common.DeleteTempFiles(this.tempFiles.BasePath, this); | ||
189 | |||
190 | if (deleted) | ||
191 | { | ||
192 | this.tempFiles = null; // temp files have been deleted, no need to remember this now | ||
193 | } | ||
194 | |||
195 | return deleted; | ||
196 | #endif | ||
197 | return true; | ||
198 | } | ||
199 | |||
200 | /// <summary> | ||
201 | /// Sends a message to the message delegate if there is one. | ||
202 | /// </summary> | ||
203 | /// <param name="mea">Message event arguments.</param> | ||
204 | public void OnMessage(MessageEventArgs e) | ||
205 | { | ||
206 | Messaging.Instance.OnMessage(e); | ||
207 | } | ||
208 | |||
209 | /// <summary> | ||
210 | /// Unbind an MSI database file. | ||
211 | /// </summary> | ||
212 | /// <param name="databaseFile">The database file.</param> | ||
213 | /// <param name="outputType">The output type.</param> | ||
214 | /// <param name="exportBasePath">The path where files should be exported.</param> | ||
215 | /// <returns>The unbound database.</returns> | ||
216 | private Output UnbindDatabase(string databaseFile, OutputType outputType, string exportBasePath) | ||
217 | { | ||
218 | Output output; | ||
219 | |||
220 | try | ||
221 | { | ||
222 | using (Database database = new Database(databaseFile, OpenDatabase.ReadOnly)) | ||
223 | { | ||
224 | output = this.UnbindDatabase(databaseFile, database, outputType, exportBasePath, false); | ||
225 | |||
226 | // extract the files from the cabinets | ||
227 | if (null != exportBasePath && !this.suppressExtractCabinets) | ||
228 | { | ||
229 | this.ExtractCabinets(output, database, databaseFile, exportBasePath); | ||
230 | } | ||
231 | } | ||
232 | } | ||
233 | catch (Win32Exception e) | ||
234 | { | ||
235 | if (0x6E == e.NativeErrorCode) // ERROR_OPEN_FAILED | ||
236 | { | ||
237 | throw new WixException(WixErrors.OpenDatabaseFailed(databaseFile)); | ||
238 | } | ||
239 | |||
240 | throw; | ||
241 | } | ||
242 | |||
243 | return output; | ||
244 | } | ||
245 | |||
246 | /// <summary> | ||
247 | /// Unbind an MSI database file. | ||
248 | /// </summary> | ||
249 | /// <param name="databaseFile">The database file.</param> | ||
250 | /// <param name="database">The opened database.</param> | ||
251 | /// <param name="outputType">The type of output to create.</param> | ||
252 | /// <param name="exportBasePath">The path where files should be exported.</param> | ||
253 | /// <param name="skipSummaryInfo">Option to skip unbinding the _SummaryInformation table.</param> | ||
254 | /// <returns>The output representing the database.</returns> | ||
255 | private Output UnbindDatabase(string databaseFile, Database database, OutputType outputType, string exportBasePath, bool skipSummaryInfo) | ||
256 | { | ||
257 | string modularizationGuid = null; | ||
258 | Output output = new Output(new SourceLineNumber(databaseFile)); | ||
259 | View validationView = null; | ||
260 | |||
261 | // set the output type | ||
262 | output.Type = outputType; | ||
263 | |||
264 | // get the codepage | ||
265 | database.Export("_ForceCodepage", this.TempFilesLocation, "_ForceCodepage.idt"); | ||
266 | using (StreamReader sr = File.OpenText(Path.Combine(this.TempFilesLocation, "_ForceCodepage.idt"))) | ||
267 | { | ||
268 | string line; | ||
269 | |||
270 | while (null != (line = sr.ReadLine())) | ||
271 | { | ||
272 | string[] data = line.Split('\t'); | ||
273 | |||
274 | if (2 == data.Length) | ||
275 | { | ||
276 | output.Codepage = Convert.ToInt32(data[0], CultureInfo.InvariantCulture); | ||
277 | } | ||
278 | } | ||
279 | } | ||
280 | |||
281 | // get the summary information table if it exists; it won't if unbinding a transform | ||
282 | if (!skipSummaryInfo) | ||
283 | { | ||
284 | using (SummaryInformation summaryInformation = new SummaryInformation(database)) | ||
285 | { | ||
286 | Table table = new Table(null, this.tableDefinitions["_SummaryInformation"]); | ||
287 | |||
288 | for (int i = 1; 19 >= i; i++) | ||
289 | { | ||
290 | string value = summaryInformation.GetProperty(i); | ||
291 | |||
292 | if (0 < value.Length) | ||
293 | { | ||
294 | Row row = table.CreateRow(output.SourceLineNumbers); | ||
295 | row[0] = i; | ||
296 | row[1] = value; | ||
297 | } | ||
298 | } | ||
299 | |||
300 | output.Tables.Add(table); | ||
301 | } | ||
302 | } | ||
303 | |||
304 | try | ||
305 | { | ||
306 | // open a view on the validation table if it exists | ||
307 | if (database.TableExists("_Validation")) | ||
308 | { | ||
309 | validationView = database.OpenView("SELECT * FROM `_Validation` WHERE `Table` = ? AND `Column` = ?"); | ||
310 | } | ||
311 | |||
312 | // get the normal tables | ||
313 | using (View tablesView = database.OpenExecuteView("SELECT * FROM _Tables")) | ||
314 | { | ||
315 | while (true) | ||
316 | { | ||
317 | using (Record tableRecord = tablesView.Fetch()) | ||
318 | { | ||
319 | if (null == tableRecord) | ||
320 | { | ||
321 | break; | ||
322 | } | ||
323 | |||
324 | string tableName = tableRecord.GetString(1); | ||
325 | |||
326 | using (View tableView = database.OpenExecuteView(String.Format(CultureInfo.InvariantCulture, "SELECT * FROM `{0}`", tableName))) | ||
327 | { | ||
328 | List<ColumnDefinition> columns; | ||
329 | using (Record columnNameRecord = tableView.GetColumnInfo(MsiInterop.MSICOLINFONAMES), | ||
330 | columnTypeRecord = tableView.GetColumnInfo(MsiInterop.MSICOLINFOTYPES)) | ||
331 | { | ||
332 | // index the primary keys | ||
333 | HashSet<string> tablePrimaryKeys = new HashSet<string>(); | ||
334 | using (Record primaryKeysRecord = database.PrimaryKeys(tableName)) | ||
335 | { | ||
336 | int primaryKeysFieldCount = primaryKeysRecord.GetFieldCount(); | ||
337 | |||
338 | for (int i = 1; i <= primaryKeysFieldCount; i++) | ||
339 | { | ||
340 | tablePrimaryKeys.Add(primaryKeysRecord.GetString(i)); | ||
341 | } | ||
342 | } | ||
343 | |||
344 | int columnCount = columnNameRecord.GetFieldCount(); | ||
345 | columns = new List<ColumnDefinition>(columnCount); | ||
346 | for (int i = 1; i <= columnCount; i++) | ||
347 | { | ||
348 | string columnName = columnNameRecord.GetString(i); | ||
349 | string idtType = columnTypeRecord.GetString(i); | ||
350 | |||
351 | ColumnType columnType; | ||
352 | int length; | ||
353 | bool nullable; | ||
354 | |||
355 | ColumnCategory columnCategory = ColumnCategory.Unknown; | ||
356 | ColumnModularizeType columnModularizeType = ColumnModularizeType.None; | ||
357 | bool primary = tablePrimaryKeys.Contains(columnName); | ||
358 | bool minValueSet = false; | ||
359 | int minValue = -1; | ||
360 | bool maxValueSet = false; | ||
361 | int maxValue = -1; | ||
362 | string keyTable = null; | ||
363 | bool keyColumnSet = false; | ||
364 | int keyColumn = -1; | ||
365 | string category = null; | ||
366 | string set = null; | ||
367 | string description = null; | ||
368 | |||
369 | // get the column type, length, and whether its nullable | ||
370 | switch (Char.ToLower(idtType[0], CultureInfo.InvariantCulture)) | ||
371 | { | ||
372 | case 'i': | ||
373 | columnType = ColumnType.Number; | ||
374 | break; | ||
375 | case 'l': | ||
376 | columnType = ColumnType.Localized; | ||
377 | break; | ||
378 | case 's': | ||
379 | columnType = ColumnType.String; | ||
380 | break; | ||
381 | case 'v': | ||
382 | columnType = ColumnType.Object; | ||
383 | break; | ||
384 | default: | ||
385 | // TODO: error | ||
386 | columnType = ColumnType.Unknown; | ||
387 | break; | ||
388 | } | ||
389 | length = Convert.ToInt32(idtType.Substring(1), CultureInfo.InvariantCulture); | ||
390 | nullable = Char.IsUpper(idtType[0]); | ||
391 | |||
392 | // try to get validation information | ||
393 | if (null != validationView) | ||
394 | { | ||
395 | using (Record validationRecord = new Record(2)) | ||
396 | { | ||
397 | validationRecord.SetString(1, tableName); | ||
398 | validationRecord.SetString(2, columnName); | ||
399 | |||
400 | validationView.Execute(validationRecord); | ||
401 | } | ||
402 | |||
403 | using (Record validationRecord = validationView.Fetch()) | ||
404 | { | ||
405 | if (null != validationRecord) | ||
406 | { | ||
407 | string validationNullable = validationRecord.GetString(3); | ||
408 | minValueSet = !validationRecord.IsNull(4); | ||
409 | minValue = (minValueSet ? validationRecord.GetInteger(4) : -1); | ||
410 | maxValueSet = !validationRecord.IsNull(5); | ||
411 | maxValue = (maxValueSet ? validationRecord.GetInteger(5) : -1); | ||
412 | keyTable = (!validationRecord.IsNull(6) ? validationRecord.GetString(6) : null); | ||
413 | keyColumnSet = !validationRecord.IsNull(7); | ||
414 | keyColumn = (keyColumnSet ? validationRecord.GetInteger(7) : -1); | ||
415 | category = (!validationRecord.IsNull(8) ? validationRecord.GetString(8) : null); | ||
416 | set = (!validationRecord.IsNull(9) ? validationRecord.GetString(9) : null); | ||
417 | description = (!validationRecord.IsNull(10) ? validationRecord.GetString(10) : null); | ||
418 | |||
419 | // check the validation nullable value against the column definition | ||
420 | if (null == validationNullable) | ||
421 | { | ||
422 | // TODO: warn for illegal validation nullable column | ||
423 | } | ||
424 | else if ((nullable && "Y" != validationNullable) || (!nullable && "N" != validationNullable)) | ||
425 | { | ||
426 | // TODO: warn for mismatch between column definition and validation nullable | ||
427 | } | ||
428 | |||
429 | // convert category to ColumnCategory | ||
430 | if (null != category) | ||
431 | { | ||
432 | try | ||
433 | { | ||
434 | columnCategory = (ColumnCategory)Enum.Parse(typeof(ColumnCategory), category, true); | ||
435 | } | ||
436 | catch (ArgumentException) | ||
437 | { | ||
438 | columnCategory = ColumnCategory.Unknown; | ||
439 | } | ||
440 | } | ||
441 | } | ||
442 | else | ||
443 | { | ||
444 | // TODO: warn about no validation information | ||
445 | } | ||
446 | } | ||
447 | } | ||
448 | |||
449 | // guess the modularization type | ||
450 | if ("Icon" == keyTable && 1 == keyColumn) | ||
451 | { | ||
452 | columnModularizeType = ColumnModularizeType.Icon; | ||
453 | } | ||
454 | else if ("Condition" == columnName) | ||
455 | { | ||
456 | columnModularizeType = ColumnModularizeType.Condition; | ||
457 | } | ||
458 | else if (ColumnCategory.Formatted == columnCategory || ColumnCategory.FormattedSDDLText == columnCategory) | ||
459 | { | ||
460 | columnModularizeType = ColumnModularizeType.Property; | ||
461 | } | ||
462 | else if (ColumnCategory.Identifier == columnCategory) | ||
463 | { | ||
464 | columnModularizeType = ColumnModularizeType.Column; | ||
465 | } | ||
466 | |||
467 | 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)); | ||
468 | } | ||
469 | } | ||
470 | |||
471 | TableDefinition tableDefinition = new TableDefinition(tableName, columns, false, false); | ||
472 | |||
473 | // use our table definitions if core properties are the same; this allows us to take advantage | ||
474 | // of wix concepts like localizable columns which current code assumes | ||
475 | if (this.tableDefinitions.Contains(tableName) && 0 == tableDefinition.CompareTo(this.tableDefinitions[tableName])) | ||
476 | { | ||
477 | tableDefinition = this.tableDefinitions[tableName]; | ||
478 | } | ||
479 | |||
480 | Table table = new Table(null, tableDefinition); | ||
481 | |||
482 | while (true) | ||
483 | { | ||
484 | using (Record rowRecord = tableView.Fetch()) | ||
485 | { | ||
486 | if (null == rowRecord) | ||
487 | { | ||
488 | break; | ||
489 | } | ||
490 | |||
491 | int recordCount = rowRecord.GetFieldCount(); | ||
492 | Row row = table.CreateRow(output.SourceLineNumbers); | ||
493 | |||
494 | for (int i = 0; recordCount > i && row.Fields.Length > i; i++) | ||
495 | { | ||
496 | if (rowRecord.IsNull(i + 1)) | ||
497 | { | ||
498 | if (!row.Fields[i].Column.Nullable) | ||
499 | { | ||
500 | // TODO: display an error for a null value in a non-nullable field OR | ||
501 | // display a warning and put an empty string in the value to let the compiler handle it | ||
502 | // (the second option is risky because the later code may make certain assumptions about | ||
503 | // the contents of a row value) | ||
504 | } | ||
505 | } | ||
506 | else | ||
507 | { | ||
508 | switch (row.Fields[i].Column.Type) | ||
509 | { | ||
510 | case ColumnType.Number: | ||
511 | bool success = false; | ||
512 | int intValue = rowRecord.GetInteger(i + 1); | ||
513 | if (row.Fields[i].Column.IsLocalizable) | ||
514 | { | ||
515 | success = row.BestEffortSetField(i, Convert.ToString(intValue, CultureInfo.InvariantCulture)); | ||
516 | } | ||
517 | else | ||
518 | { | ||
519 | success = row.BestEffortSetField(i, intValue); | ||
520 | } | ||
521 | |||
522 | if (!success) | ||
523 | { | ||
524 | this.OnMessage(WixWarnings.BadColumnDataIgnored(row.SourceLineNumbers, Convert.ToString(intValue, CultureInfo.InvariantCulture), tableName, row.Fields[i].Column.Name)); | ||
525 | } | ||
526 | break; | ||
527 | case ColumnType.Object: | ||
528 | string sourceFile = "FILE NOT EXPORTED, USE THE dark.exe -x OPTION TO EXPORT BINARIES"; | ||
529 | |||
530 | if (null != exportBasePath) | ||
531 | { | ||
532 | string relativeSourceFile = Path.Combine(tableName, row.GetPrimaryKey('.')); | ||
533 | sourceFile = Path.Combine(exportBasePath, relativeSourceFile); | ||
534 | |||
535 | // ensure the parent directory exists | ||
536 | System.IO.Directory.CreateDirectory(Path.Combine(exportBasePath, tableName)); | ||
537 | |||
538 | using (FileStream fs = System.IO.File.Create(sourceFile)) | ||
539 | { | ||
540 | int bytesRead; | ||
541 | byte[] buffer = new byte[512]; | ||
542 | |||
543 | while (0 != (bytesRead = rowRecord.GetStream(i + 1, buffer, buffer.Length))) | ||
544 | { | ||
545 | fs.Write(buffer, 0, bytesRead); | ||
546 | } | ||
547 | } | ||
548 | } | ||
549 | |||
550 | row[i] = sourceFile; | ||
551 | break; | ||
552 | default: | ||
553 | string value = rowRecord.GetString(i + 1); | ||
554 | 118 | ||
555 | switch (row.Fields[i].Column.Category) | 119 | foreach (var factory in this.BackendFactories) |
556 | { | ||
557 | case ColumnCategory.Guid: | ||
558 | value = value.ToUpper(CultureInfo.InvariantCulture); | ||
559 | break; | ||
560 | } | ||
561 | |||
562 | // de-modularize | ||
563 | if (!this.suppressDemodularization && OutputType.Module == output.Type && ColumnModularizeType.None != row.Fields[i].Column.ModularizeType) | ||
564 | { | ||
565 | 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}"); | ||
566 | |||
567 | if (null == modularizationGuid) | ||
568 | { | ||
569 | Match match = modularization.Match(value); | ||
570 | if (match.Success) | ||
571 | { | ||
572 | modularizationGuid = String.Concat('{', match.Value.Substring(1).Replace('_', '-'), '}'); | ||
573 | } | ||
574 | } | ||
575 | |||
576 | value = modularization.Replace(value, String.Empty); | ||
577 | } | ||
578 | |||
579 | // escape "$(" for the preprocessor | ||
580 | value = value.Replace("$(", "$$("); | ||
581 | |||
582 | // escape things that look like wix variables | ||
583 | MatchCollection matches = Common.WixVariableRegex.Matches(value); | ||
584 | for (int j = matches.Count - 1; 0 <= j; j--) | ||
585 | { | ||
586 | value = value.Insert(matches[j].Index, "!"); | ||
587 | } | ||
588 | |||
589 | row[i] = value; | ||
590 | break; | ||
591 | } | ||
592 | } | ||
593 | } | ||
594 | } | ||
595 | } | ||
596 | |||
597 | output.Tables.Add(table); | ||
598 | } | ||
599 | |||
600 | } | ||
601 | } | ||
602 | } | ||
603 | } | ||
604 | finally | ||
605 | { | ||
606 | if (null != validationView) | ||
607 | { | ||
608 | validationView.Close(); | ||
609 | } | ||
610 | } | ||
611 | |||
612 | // set the modularization guid as the PackageCode | ||
613 | if (null != modularizationGuid) | ||
614 | { | 120 | { |
615 | Table table = output.Tables["_SummaryInformation"]; | 121 | if (factory.TryCreateBackend(outputType.ToString(), file, null, out var backend)) |
616 | |||
617 | foreach (Row row in table.Rows) | ||
618 | { | 122 | { |
619 | if (9 == (int)row[0]) // PID_REVNUMBER | 123 | return backend.Unbind(context); |
620 | { | ||
621 | row[1] = modularizationGuid; | ||
622 | } | ||
623 | } | 124 | } |
624 | } | 125 | } |
625 | 126 | ||
626 | if (this.isAdminImage) | 127 | // TODO: Display message that could not find a unbinder for output type? |
627 | { | ||
628 | GenerateWixFileTable(databaseFile, output); | ||
629 | GenerateSectionIds(output); | ||
630 | } | ||
631 | |||
632 | return output; | ||
633 | } | ||
634 | |||
635 | /// <summary> | ||
636 | /// Creates section ids on rows which form logical groupings of resources. | ||
637 | /// </summary> | ||
638 | /// <param name="output">The Output that represents the msi database.</param> | ||
639 | private void GenerateSectionIds(Output output) | ||
640 | { | ||
641 | // First assign and index section ids for the tables that are in their own sections. | ||
642 | AssignSectionIdsToTable(output.Tables["Binary"], 0); | ||
643 | Hashtable componentSectionIdIndex = AssignSectionIdsToTable(output.Tables["Component"], 0); | ||
644 | Hashtable customActionSectionIdIndex = AssignSectionIdsToTable(output.Tables["CustomAction"], 0); | ||
645 | AssignSectionIdsToTable(output.Tables["Directory"], 0); | ||
646 | Hashtable featureSectionIdIndex = AssignSectionIdsToTable(output.Tables["Feature"], 0); | ||
647 | AssignSectionIdsToTable(output.Tables["Icon"], 0); | ||
648 | Hashtable digitalCertificateSectionIdIndex = AssignSectionIdsToTable(output.Tables["MsiDigitalCertificate"], 0); | ||
649 | AssignSectionIdsToTable(output.Tables["Property"], 0); | ||
650 | |||
651 | // Now handle all the tables that rely on the first set of indexes but also produce their own indexes. Order matters here. | ||
652 | Hashtable fileSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["File"], componentSectionIdIndex, 1, 0); | ||
653 | Hashtable appIdSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["Class"], componentSectionIdIndex, 2, 5); | ||
654 | Hashtable odbcDataSourceSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["ODBCDataSource"], componentSectionIdIndex, 1, 0); | ||
655 | Hashtable odbcDriverSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["ODBCDriver"], componentSectionIdIndex, 1, 0); | ||
656 | Hashtable registrySectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["Registry"], componentSectionIdIndex, 5, 0); | ||
657 | Hashtable serviceInstallSectionIdIndex = ConnectTableToSectionAndIndex(output.Tables["ServiceInstall"], componentSectionIdIndex, 11, 0); | ||
658 | |||
659 | // Now handle all the tables which only rely on previous indexes and order does not matter. | ||
660 | foreach (Table table in output.Tables) | ||
661 | { | ||
662 | switch (table.Name) | ||
663 | { | ||
664 | case "WixFile": | ||
665 | case "MsiFileHash": | ||
666 | ConnectTableToSection(table, fileSectionIdIndex, 0); | ||
667 | break; | ||
668 | case "MsiAssembly": | ||
669 | case "MsiAssemblyName": | ||
670 | ConnectTableToSection(table, componentSectionIdIndex, 0); | ||
671 | break; | ||
672 | case "MsiPackageCertificate": | ||
673 | case "MsiPatchCertificate": | ||
674 | ConnectTableToSection(table, digitalCertificateSectionIdIndex, 1); | ||
675 | break; | ||
676 | case "CreateFolder": | ||
677 | case "FeatureComponents": | ||
678 | case "MoveFile": | ||
679 | case "ReserveCost": | ||
680 | case "ODBCTranslator": | ||
681 | ConnectTableToSection(table, componentSectionIdIndex, 1); | ||
682 | break; | ||
683 | case "TypeLib": | ||
684 | ConnectTableToSection(table, componentSectionIdIndex, 2); | ||
685 | break; | ||
686 | case "Shortcut": | ||
687 | case "Environment": | ||
688 | ConnectTableToSection(table, componentSectionIdIndex, 3); | ||
689 | break; | ||
690 | case "RemoveRegistry": | ||
691 | ConnectTableToSection(table, componentSectionIdIndex, 4); | ||
692 | break; | ||
693 | case "ServiceControl": | ||
694 | ConnectTableToSection(table, componentSectionIdIndex, 5); | ||
695 | break; | ||
696 | case "IniFile": | ||
697 | case "RemoveIniFile": | ||
698 | ConnectTableToSection(table, componentSectionIdIndex, 7); | ||
699 | break; | ||
700 | case "AppId": | ||
701 | ConnectTableToSection(table, appIdSectionIdIndex, 0); | ||
702 | break; | ||
703 | case "Condition": | ||
704 | ConnectTableToSection(table, featureSectionIdIndex, 0); | ||
705 | break; | ||
706 | case "ODBCSourceAttribute": | ||
707 | ConnectTableToSection(table, odbcDataSourceSectionIdIndex, 0); | ||
708 | break; | ||
709 | case "ODBCAttribute": | ||
710 | ConnectTableToSection(table, odbcDriverSectionIdIndex, 0); | ||
711 | break; | ||
712 | case "AdminExecuteSequence": | ||
713 | case "AdminUISequence": | ||
714 | case "AdvtExecuteSequence": | ||
715 | case "AdvtUISequence": | ||
716 | case "InstallExecuteSequence": | ||
717 | case "InstallUISequence": | ||
718 | ConnectTableToSection(table, customActionSectionIdIndex, 0); | ||
719 | break; | ||
720 | case "LockPermissions": | ||
721 | case "MsiLockPermissions": | ||
722 | foreach (Row row in table.Rows) | ||
723 | { | ||
724 | string lockObject = (string)row[0]; | ||
725 | string tableName = (string)row[1]; | ||
726 | switch (tableName) | ||
727 | { | ||
728 | case "File": | ||
729 | row.SectionId = (string)fileSectionIdIndex[lockObject]; | ||
730 | break; | ||
731 | case "Registry": | ||
732 | row.SectionId = (string)registrySectionIdIndex[lockObject]; | ||
733 | break; | ||
734 | case "ServiceInstall": | ||
735 | row.SectionId = (string)serviceInstallSectionIdIndex[lockObject]; | ||
736 | break; | ||
737 | } | ||
738 | } | ||
739 | break; | ||
740 | } | ||
741 | } | ||
742 | |||
743 | // Now pass the output to each unbinder extension to allow them to analyze the output and determine thier proper section ids. | ||
744 | foreach (IUnbinderExtension extension in this.unbinderExtensions) | ||
745 | { | ||
746 | extension.GenerateSectionIds(output); | ||
747 | } | ||
748 | } | ||
749 | |||
750 | /// <summary> | ||
751 | /// Creates new section ids on all the rows in a table. | ||
752 | /// </summary> | ||
753 | /// <param name="table">The table to add sections to.</param> | ||
754 | /// <param name="rowPrimaryKeyIndex">The index of the column which is used by other tables to reference this table.</param> | ||
755 | /// <returns>A Hashtable containing the tables key for each row paired with its assigned section id.</returns> | ||
756 | private Hashtable AssignSectionIdsToTable(Table table, int rowPrimaryKeyIndex) | ||
757 | { | ||
758 | Hashtable hashtable = new Hashtable(); | ||
759 | if (null != table) | ||
760 | { | ||
761 | foreach (Row row in table.Rows) | ||
762 | { | ||
763 | row.SectionId = GetNewSectionId(); | ||
764 | hashtable.Add(row[rowPrimaryKeyIndex], row.SectionId); | ||
765 | } | ||
766 | } | ||
767 | return hashtable; | ||
768 | } | ||
769 | |||
770 | /// <summary> | ||
771 | /// Connects a table's rows to an already sectioned table. | ||
772 | /// </summary> | ||
773 | /// <param name="table">The table containing rows that need to be connected to sections.</param> | ||
774 | /// <param name="sectionIdIndex">A hashtable containing keys to map table to its section.</param> | ||
775 | /// <param name="rowIndex">The index of the column which is used as the foreign key in to the sectionIdIndex.</param> | ||
776 | private static void ConnectTableToSection(Table table, Hashtable sectionIdIndex, int rowIndex) | ||
777 | { | ||
778 | if (null != table) | ||
779 | { | ||
780 | foreach (Row row in table.Rows) | ||
781 | { | ||
782 | if (sectionIdIndex.ContainsKey(row[rowIndex])) | ||
783 | { | ||
784 | row.SectionId = (string)sectionIdIndex[row[rowIndex]]; | ||
785 | } | ||
786 | } | ||
787 | } | ||
788 | } | ||
789 | |||
790 | /// <summary> | ||
791 | /// Connects a table's rows to an already sectioned table and produces an index for other tables to connect to it. | ||
792 | /// </summary> | ||
793 | /// <param name="table">The table containing rows that need to be connected to sections.</param> | ||
794 | /// <param name="sectionIdIndex">A hashtable containing keys to map table to its section.</param> | ||
795 | /// <param name="rowIndex">The index of the column which is used as the foreign key in to the sectionIdIndex.</param> | ||
796 | /// <param name="rowPrimaryKeyIndex">The index of the column which is used by other tables to reference this table.</param> | ||
797 | /// <returns>A Hashtable containing the tables key for each row paired with its assigned section id.</returns> | ||
798 | private static Hashtable ConnectTableToSectionAndIndex(Table table, Hashtable sectionIdIndex, int rowIndex, int rowPrimaryKeyIndex) | ||
799 | { | ||
800 | Hashtable newHashTable = new Hashtable(); | ||
801 | if (null != table) | ||
802 | { | ||
803 | foreach (Row row in table.Rows) | ||
804 | { | ||
805 | if (!sectionIdIndex.ContainsKey(row[rowIndex])) | ||
806 | { | ||
807 | continue; | ||
808 | } | ||
809 | |||
810 | row.SectionId = (string)sectionIdIndex[row[rowIndex]]; | ||
811 | if (null != row[rowPrimaryKeyIndex]) | ||
812 | { | ||
813 | newHashTable.Add(row[rowPrimaryKeyIndex], row.SectionId); | ||
814 | } | ||
815 | } | ||
816 | } | ||
817 | return newHashTable; | ||
818 | } | ||
819 | |||
820 | /// <summary> | ||
821 | /// Creates a new section identifier to be used when adding a section to an output. | ||
822 | /// </summary> | ||
823 | /// <returns>A string representing a new section id.</returns> | ||
824 | private string GetNewSectionId() | ||
825 | { | ||
826 | this.sectionCount++; | ||
827 | return "wix.section." + this.sectionCount.ToString(CultureInfo.InvariantCulture); | ||
828 | } | ||
829 | |||
830 | /// <summary> | ||
831 | /// Generates the WixFile table based on a path to an admin image msi and an Output. | ||
832 | /// </summary> | ||
833 | /// <param name="databaseFile">The path to the msi database file in an admin image.</param> | ||
834 | /// <param name="output">The Output that represents the msi database.</param> | ||
835 | private void GenerateWixFileTable(string databaseFile, Output output) | ||
836 | { | ||
837 | string adminRootPath = Path.GetDirectoryName(databaseFile); | ||
838 | |||
839 | Hashtable componentDirectoryIndex = new Hashtable(); | ||
840 | Table componentTable = output.Tables["Component"]; | ||
841 | foreach (Row row in componentTable.Rows) | ||
842 | { | ||
843 | componentDirectoryIndex.Add(row[0], row[2]); | ||
844 | } | ||
845 | |||
846 | // Index full source paths for all directories | ||
847 | Hashtable directoryDirectoryParentIndex = new Hashtable(); | ||
848 | Hashtable directoryFullPathIndex = new Hashtable(); | ||
849 | Hashtable directorySourceNameIndex = new Hashtable(); | ||
850 | Table directoryTable = output.Tables["Directory"]; | ||
851 | foreach (Row row in directoryTable.Rows) | ||
852 | { | ||
853 | directoryDirectoryParentIndex.Add(row[0], row[1]); | ||
854 | if (null == row[1]) | ||
855 | { | ||
856 | directoryFullPathIndex.Add(row[0], adminRootPath); | ||
857 | } | ||
858 | else | ||
859 | { | ||
860 | directorySourceNameIndex.Add(row[0], GetAdminSourceName((string)row[2])); | ||
861 | } | ||
862 | } | ||
863 | |||
864 | foreach (DictionaryEntry directoryEntry in directoryDirectoryParentIndex) | ||
865 | { | ||
866 | if (!directoryFullPathIndex.ContainsKey(directoryEntry.Key)) | ||
867 | { | ||
868 | GetAdminFullPath((string)directoryEntry.Key, directoryDirectoryParentIndex, directorySourceNameIndex, directoryFullPathIndex); | ||
869 | } | ||
870 | } | ||
871 | |||
872 | Table fileTable = output.Tables["File"]; | ||
873 | Table wixFileTable = output.EnsureTable(this.tableDefinitions["WixFile"]); | ||
874 | foreach (Row row in fileTable.Rows) | ||
875 | { | ||
876 | WixFileRow wixFileRow = new WixFileRow(null, this.tableDefinitions["WixFile"]); | ||
877 | wixFileRow.File = (string)row[0]; | ||
878 | wixFileRow.Directory = (string)componentDirectoryIndex[(string)row[1]]; | ||
879 | wixFileRow.Source = Path.Combine((string)directoryFullPathIndex[wixFileRow.Directory], GetAdminSourceName((string)row[2])); | ||
880 | |||
881 | if (!File.Exists(wixFileRow.Source)) | ||
882 | { | ||
883 | throw new WixException(WixErrors.WixFileNotFound(wixFileRow.Source)); | ||
884 | } | ||
885 | |||
886 | wixFileTable.Rows.Add(wixFileRow); | ||
887 | } | ||
888 | } | ||
889 | |||
890 | /// <summary> | ||
891 | /// 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. | ||
892 | /// </summary> | ||
893 | /// <param name="directory">The directory identifier.</param> | ||
894 | /// <param name="directoryDirectoryParentIndex">The Hashtable containing all the directory to directory parent mapping.</param> | ||
895 | /// <param name="directorySourceNameIndex">The Hashtable containing all the directory to source name mapping.</param> | ||
896 | /// <param name="directoryFullPathIndex">The Hashtable containing a mapping between all of the directories and their previously calculated full paths.</param> | ||
897 | /// <returns>The full path to the directory.</returns> | ||
898 | private string GetAdminFullPath(string directory, Hashtable directoryDirectoryParentIndex, Hashtable directorySourceNameIndex, Hashtable directoryFullPathIndex) | ||
899 | { | ||
900 | string parent = (string)directoryDirectoryParentIndex[directory]; | ||
901 | string sourceName = (string)directorySourceNameIndex[directory]; | ||
902 | |||
903 | string parentFullPath; | ||
904 | if (directoryFullPathIndex.ContainsKey(parent)) | ||
905 | { | ||
906 | parentFullPath = (string)directoryFullPathIndex[parent]; | ||
907 | } | ||
908 | else | ||
909 | { | ||
910 | parentFullPath = GetAdminFullPath(parent, directoryDirectoryParentIndex, directorySourceNameIndex, directoryFullPathIndex); | ||
911 | } | ||
912 | |||
913 | if (null == sourceName) | ||
914 | { | ||
915 | sourceName = String.Empty; | ||
916 | } | ||
917 | |||
918 | string fullPath = Path.Combine(parentFullPath, sourceName); | ||
919 | directoryFullPathIndex.Add(directory, fullPath); | ||
920 | |||
921 | return fullPath; | ||
922 | } | ||
923 | |||
924 | /// <summary> | ||
925 | /// Get the source name in an admin image. | ||
926 | /// </summary> | ||
927 | /// <param name="value">The Filename value.</param> | ||
928 | /// <returns>The source name of the directory in an admin image.</returns> | ||
929 | private static string GetAdminSourceName(string value) | ||
930 | { | ||
931 | string name = null; | ||
932 | string[] names; | ||
933 | string shortname = null; | ||
934 | string shortsourcename = null; | ||
935 | string sourcename = null; | ||
936 | |||
937 | names = Installer.GetNames(value); | ||
938 | |||
939 | if (null != names[0] && "." != names[0]) | ||
940 | { | ||
941 | if (null != names[1]) | ||
942 | { | ||
943 | shortname = names[0]; | ||
944 | } | ||
945 | else | ||
946 | { | ||
947 | name = names[0]; | ||
948 | } | ||
949 | } | ||
950 | |||
951 | if (null != names[1]) | ||
952 | { | ||
953 | name = names[1]; | ||
954 | } | ||
955 | |||
956 | if (null != names[2]) | ||
957 | { | ||
958 | if (null != names[3]) | ||
959 | { | ||
960 | shortsourcename = names[2]; | ||
961 | } | ||
962 | else | ||
963 | { | ||
964 | sourcename = names[2]; | ||
965 | } | ||
966 | } | ||
967 | |||
968 | if (null != names[3]) | ||
969 | { | ||
970 | sourcename = names[3]; | ||
971 | } | ||
972 | |||
973 | if (null != sourcename) | ||
974 | { | ||
975 | return sourcename; | ||
976 | } | ||
977 | else if (null != shortsourcename) | ||
978 | { | ||
979 | return shortsourcename; | ||
980 | } | ||
981 | else if (null != name) | ||
982 | { | ||
983 | return name; | ||
984 | } | ||
985 | else | ||
986 | { | ||
987 | return shortname; | ||
988 | } | ||
989 | } | ||
990 | |||
991 | /// <summary> | ||
992 | /// Unbind an MSP patch file. | ||
993 | /// </summary> | ||
994 | /// <param name="patchFile">The patch file.</param> | ||
995 | /// <param name="exportBasePath">The path where files should be exported.</param> | ||
996 | /// <returns>The unbound patch.</returns> | ||
997 | private Output UnbindPatch(string patchFile, string exportBasePath) | ||
998 | { | ||
999 | Output patch; | ||
1000 | |||
1001 | // patch files are essentially database files (use a special flag to let the API know its a patch file) | ||
1002 | try | ||
1003 | { | ||
1004 | using (Database database = new Database(patchFile, OpenDatabase.ReadOnly | OpenDatabase.OpenPatchFile)) | ||
1005 | { | ||
1006 | patch = this.UnbindDatabase(patchFile, database, OutputType.Patch, exportBasePath, false); | ||
1007 | } | ||
1008 | } | ||
1009 | catch (Win32Exception e) | ||
1010 | { | ||
1011 | if (0x6E == e.NativeErrorCode) // ERROR_OPEN_FAILED | ||
1012 | { | ||
1013 | throw new WixException(WixErrors.OpenDatabaseFailed(patchFile)); | ||
1014 | } | ||
1015 | |||
1016 | throw; | ||
1017 | } | ||
1018 | |||
1019 | // retrieve the transforms (they are in substorages) | ||
1020 | using (Storage storage = Storage.Open(patchFile, StorageMode.Read | StorageMode.ShareDenyWrite)) | ||
1021 | { | ||
1022 | Table summaryInformationTable = patch.Tables["_SummaryInformation"]; | ||
1023 | foreach (Row row in summaryInformationTable.Rows) | ||
1024 | { | ||
1025 | if (8 == (int)row[0]) // PID_LASTAUTHOR | ||
1026 | { | ||
1027 | string value = (string)row[1]; | ||
1028 | |||
1029 | foreach (string decoratedSubStorageName in value.Split(';')) | ||
1030 | { | ||
1031 | string subStorageName = decoratedSubStorageName.Substring(1); | ||
1032 | string transformFile = Path.Combine(this.TempFilesLocation, String.Concat("Transform", Path.DirectorySeparatorChar, subStorageName, ".mst")); | ||
1033 | |||
1034 | // ensure the parent directory exists | ||
1035 | System.IO.Directory.CreateDirectory(Path.GetDirectoryName(transformFile)); | ||
1036 | |||
1037 | // copy the substorage to a new storage for the transform file | ||
1038 | using (Storage subStorage = storage.OpenStorage(subStorageName)) | ||
1039 | { | ||
1040 | using (Storage transformStorage = Storage.CreateDocFile(transformFile, StorageMode.ReadWrite | StorageMode.ShareExclusive | StorageMode.Create)) | ||
1041 | { | ||
1042 | subStorage.CopyTo(transformStorage); | ||
1043 | } | ||
1044 | } | ||
1045 | |||
1046 | // unbind the transform | ||
1047 | Output transform = this.UnbindTransform(transformFile, (null == exportBasePath ? null : Path.Combine(exportBasePath, subStorageName))); | ||
1048 | patch.SubStorages.Add(new SubStorage(subStorageName, transform)); | ||
1049 | } | ||
1050 | |||
1051 | break; | ||
1052 | } | ||
1053 | } | ||
1054 | } | ||
1055 | |||
1056 | // extract the files from the cabinets | ||
1057 | // TODO: use per-transform export paths for support of multi-product patches | ||
1058 | if (null != exportBasePath && !this.suppressExtractCabinets) | ||
1059 | { | ||
1060 | using (Database database = new Database(patchFile, OpenDatabase.ReadOnly | OpenDatabase.OpenPatchFile)) | ||
1061 | { | ||
1062 | foreach (SubStorage subStorage in patch.SubStorages) | ||
1063 | { | ||
1064 | // only patch transforms should carry files | ||
1065 | if (subStorage.Name.StartsWith("#", StringComparison.Ordinal)) | ||
1066 | { | ||
1067 | this.ExtractCabinets(subStorage.Data, database, patchFile, exportBasePath); | ||
1068 | } | ||
1069 | } | ||
1070 | } | ||
1071 | } | ||
1072 | |||
1073 | return patch; | ||
1074 | } | ||
1075 | |||
1076 | /// <summary> | ||
1077 | /// Unbind an MSI transform file. | ||
1078 | /// </summary> | ||
1079 | /// <param name="transformFile">The transform file.</param> | ||
1080 | /// <param name="exportBasePath">The path where files should be exported.</param> | ||
1081 | /// <returns>The unbound transform.</returns> | ||
1082 | private Output UnbindTransform(string transformFile, string exportBasePath) | ||
1083 | { | ||
1084 | Output transform = new Output(new SourceLineNumber(transformFile)); | ||
1085 | transform.Type = OutputType.Transform; | ||
1086 | |||
1087 | // get the summary information table | ||
1088 | using (SummaryInformation summaryInformation = new SummaryInformation(transformFile)) | ||
1089 | { | ||
1090 | Table table = transform.EnsureTable(this.tableDefinitions["_SummaryInformation"]); | ||
1091 | |||
1092 | for (int i = 1; 19 >= i; i++) | ||
1093 | { | ||
1094 | string value = summaryInformation.GetProperty(i); | ||
1095 | |||
1096 | if (0 < value.Length) | ||
1097 | { | ||
1098 | Row row = table.CreateRow(transform.SourceLineNumbers); | ||
1099 | row[0] = i; | ||
1100 | row[1] = value; | ||
1101 | } | ||
1102 | } | ||
1103 | } | ||
1104 | |||
1105 | // create a schema msi which hopefully matches the table schemas in the transform | ||
1106 | Output schemaOutput = new Output(null); | ||
1107 | string msiDatabaseFile = Path.Combine(this.TempFilesLocation, "schema.msi"); | ||
1108 | foreach (TableDefinition tableDefinition in this.tableDefinitions) | ||
1109 | { | ||
1110 | // skip unreal tables and the Patch table | ||
1111 | if (!tableDefinition.Unreal && "Patch" != tableDefinition.Name) | ||
1112 | { | ||
1113 | schemaOutput.EnsureTable(tableDefinition); | ||
1114 | } | ||
1115 | } | ||
1116 | |||
1117 | Hashtable addedRows = new Hashtable(); | ||
1118 | Table transformViewTable; | ||
1119 | |||
1120 | // Bind the schema msi. | ||
1121 | this.GenerateDatabase(schemaOutput, msiDatabaseFile); | ||
1122 | |||
1123 | // apply the transform to the database and retrieve the modifications | ||
1124 | using (Database msiDatabase = new Database(msiDatabaseFile, OpenDatabase.Transact)) | ||
1125 | { | ||
1126 | // apply the transform with the ViewTransform option to collect all the modifications | ||
1127 | msiDatabase.ApplyTransform(transformFile, TransformErrorConditions.All | TransformErrorConditions.ViewTransform); | ||
1128 | |||
1129 | // unbind the database | ||
1130 | Output transformViewOutput = this.UnbindDatabase(msiDatabaseFile, msiDatabase, OutputType.Product, exportBasePath, true); | ||
1131 | |||
1132 | // index the added and possibly modified rows (added rows may also appears as modified rows) | ||
1133 | transformViewTable = transformViewOutput.Tables["_TransformView"]; | ||
1134 | Hashtable modifiedRows = new Hashtable(); | ||
1135 | foreach (Row row in transformViewTable.Rows) | ||
1136 | { | ||
1137 | string tableName = (string)row[0]; | ||
1138 | string columnName = (string)row[1]; | ||
1139 | string primaryKeys = (string)row[2]; | ||
1140 | |||
1141 | if ("INSERT" == columnName) | ||
1142 | { | ||
1143 | string index = String.Concat(tableName, ':', primaryKeys); | ||
1144 | |||
1145 | addedRows.Add(index, null); | ||
1146 | } | ||
1147 | else if ("CREATE" != columnName && "DELETE" != columnName && "DROP" != columnName && null != primaryKeys) // modified row | ||
1148 | { | ||
1149 | string index = String.Concat(tableName, ':', primaryKeys); | ||
1150 | |||
1151 | modifiedRows[index] = row; | ||
1152 | } | ||
1153 | } | ||
1154 | |||
1155 | // create placeholder rows for modified rows to make the transform insert the updated values when its applied | ||
1156 | foreach (Row row in modifiedRows.Values) | ||
1157 | { | ||
1158 | string tableName = (string)row[0]; | ||
1159 | string columnName = (string)row[1]; | ||
1160 | string primaryKeys = (string)row[2]; | ||
1161 | |||
1162 | string index = String.Concat(tableName, ':', primaryKeys); | ||
1163 | |||
1164 | // ignore information for added rows | ||
1165 | if (!addedRows.Contains(index)) | ||
1166 | { | ||
1167 | Table table = schemaOutput.Tables[tableName]; | ||
1168 | this.CreateRow(table, primaryKeys, true); | ||
1169 | } | ||
1170 | } | ||
1171 | } | ||
1172 | |||
1173 | // Re-bind the schema output with the placeholder rows. | ||
1174 | this.GenerateDatabase(schemaOutput, msiDatabaseFile); | ||
1175 | |||
1176 | // apply the transform to the database and retrieve the modifications | ||
1177 | using (Database msiDatabase = new Database(msiDatabaseFile, OpenDatabase.Transact)) | ||
1178 | { | ||
1179 | try | ||
1180 | { | ||
1181 | // apply the transform | ||
1182 | msiDatabase.ApplyTransform(transformFile, TransformErrorConditions.All); | ||
1183 | |||
1184 | // commit the database to guard against weird errors with streams | ||
1185 | msiDatabase.Commit(); | ||
1186 | } | ||
1187 | catch (Win32Exception ex) | ||
1188 | { | ||
1189 | if (0x65B == ex.NativeErrorCode) | ||
1190 | { | ||
1191 | // this commonly happens when the transform was built | ||
1192 | // against a database schema different from the internal | ||
1193 | // table definitions | ||
1194 | throw new WixException(WixErrors.TransformSchemaMismatch()); | ||
1195 | } | ||
1196 | } | ||
1197 | |||
1198 | // unbind the database | ||
1199 | Output output = this.UnbindDatabase(msiDatabaseFile, msiDatabase, OutputType.Product, exportBasePath, true); | ||
1200 | |||
1201 | // index all the rows to easily find modified rows | ||
1202 | Hashtable rows = new Hashtable(); | ||
1203 | foreach (Table table in output.Tables) | ||
1204 | { | ||
1205 | foreach (Row row in table.Rows) | ||
1206 | { | ||
1207 | rows.Add(String.Concat(table.Name, ':', row.GetPrimaryKey('\t', " ")), row); | ||
1208 | } | ||
1209 | } | ||
1210 | |||
1211 | // process the _TransformView rows into transform rows | ||
1212 | foreach (Row row in transformViewTable.Rows) | ||
1213 | { | ||
1214 | string tableName = (string)row[0]; | ||
1215 | string columnName = (string)row[1]; | ||
1216 | string primaryKeys = (string)row[2]; | ||
1217 | |||
1218 | Table table = transform.EnsureTable(this.tableDefinitions[tableName]); | ||
1219 | |||
1220 | if ("CREATE" == columnName) // added table | ||
1221 | { | ||
1222 | table.Operation = TableOperation.Add; | ||
1223 | } | ||
1224 | else if ("DELETE" == columnName) // deleted row | ||
1225 | { | ||
1226 | Row deletedRow = this.CreateRow(table, primaryKeys, false); | ||
1227 | deletedRow.Operation = RowOperation.Delete; | ||
1228 | } | ||
1229 | else if ("DROP" == columnName) // dropped table | ||
1230 | { | ||
1231 | table.Operation = TableOperation.Drop; | ||
1232 | } | ||
1233 | else if ("INSERT" == columnName) // added row | ||
1234 | { | ||
1235 | string index = String.Concat(tableName, ':', primaryKeys); | ||
1236 | Row addedRow = (Row)rows[index]; | ||
1237 | addedRow.Operation = RowOperation.Add; | ||
1238 | table.Rows.Add(addedRow); | ||
1239 | } | ||
1240 | else if (null != primaryKeys) // modified row | ||
1241 | { | ||
1242 | string index = String.Concat(tableName, ':', primaryKeys); | ||
1243 | |||
1244 | // the _TransformView table includes information for added rows | ||
1245 | // that looks like modified rows so it sometimes needs to be ignored | ||
1246 | if (!addedRows.Contains(index)) | ||
1247 | { | ||
1248 | Row modifiedRow = (Row)rows[index]; | ||
1249 | |||
1250 | // mark the field as modified | ||
1251 | int indexOfModifiedValue = -1; | ||
1252 | for (int i = 0; i < modifiedRow.TableDefinition.Columns.Count; ++i) | ||
1253 | { | ||
1254 | if (columnName.Equals(modifiedRow.TableDefinition.Columns[i].Name, StringComparison.Ordinal)) | ||
1255 | { | ||
1256 | indexOfModifiedValue = i; | ||
1257 | break; | ||
1258 | } | ||
1259 | } | ||
1260 | modifiedRow.Fields[indexOfModifiedValue].Modified = true; | ||
1261 | |||
1262 | // move the modified row into the transform the first time its encountered | ||
1263 | if (RowOperation.None == modifiedRow.Operation) | ||
1264 | { | ||
1265 | modifiedRow.Operation = RowOperation.Modify; | ||
1266 | table.Rows.Add(modifiedRow); | ||
1267 | } | ||
1268 | } | ||
1269 | } | ||
1270 | else // added column | ||
1271 | { | ||
1272 | ColumnDefinition column = table.Definition.Columns.Single(c => c.Name.Equals(columnName, StringComparison.Ordinal)); | ||
1273 | column.Added = true; | ||
1274 | } | ||
1275 | } | ||
1276 | } | ||
1277 | |||
1278 | return transform; | ||
1279 | } | ||
1280 | |||
1281 | private void GenerateDatabase(Output output, string databaseFile) | ||
1282 | { | ||
1283 | GenerateDatabaseCommand command = new GenerateDatabaseCommand(); | ||
1284 | command.Extensions = Enumerable.Empty<IBinderExtension>(); | ||
1285 | command.FileManagers = Enumerable.Empty<IBinderFileManager>(); | ||
1286 | command.Output = output; | ||
1287 | command.OutputPath = databaseFile; | ||
1288 | command.KeepAddedColumns = true; | ||
1289 | command.UseSubDirectory = false; | ||
1290 | command.SuppressAddingValidationRows = true; | ||
1291 | command.TableDefinitions = this.tableDefinitions; | ||
1292 | command.TempFilesLocation = this.TempFilesLocation; | ||
1293 | command.Codepage = -1; | ||
1294 | command.Execute(); | ||
1295 | } | ||
1296 | |||
1297 | /// <summary> | ||
1298 | /// Unbind a bundle. | ||
1299 | /// </summary> | ||
1300 | /// <param name="bundleFile">The bundle file.</param> | ||
1301 | /// <param name="exportBasePath">The path where files should be exported.</param> | ||
1302 | /// <returns>The unbound bundle.</returns> | ||
1303 | private Output UnbindBundle(string bundleFile, string exportBasePath) | ||
1304 | { | ||
1305 | string uxExtractPath = Path.Combine(exportBasePath, "UX"); | ||
1306 | string acExtractPath = Path.Combine(exportBasePath, "AttachedContainer"); | ||
1307 | |||
1308 | using (BurnReader reader = BurnReader.Open(bundleFile)) | ||
1309 | { | ||
1310 | reader.ExtractUXContainer(uxExtractPath, this.TempFilesLocation); | ||
1311 | reader.ExtractAttachedContainer(acExtractPath, this.TempFilesLocation); | ||
1312 | } | ||
1313 | 128 | ||
1314 | return null; | 129 | return null; |
1315 | } | 130 | } |
1316 | |||
1317 | /// <summary> | ||
1318 | /// Create a deleted or modified row. | ||
1319 | /// </summary> | ||
1320 | /// <param name="table">The table containing the row.</param> | ||
1321 | /// <param name="primaryKeys">The primary keys of the row.</param> | ||
1322 | /// <param name="setRequiredFields">Option to set all required fields with placeholder values.</param> | ||
1323 | /// <returns>The new row.</returns> | ||
1324 | private Row CreateRow(Table table, string primaryKeys, bool setRequiredFields) | ||
1325 | { | ||
1326 | Row row = table.CreateRow(null); | ||
1327 | |||
1328 | string[] primaryKeyParts = primaryKeys.Split('\t'); | ||
1329 | int primaryKeyPartIndex = 0; | ||
1330 | |||
1331 | for (int i = 0; i < table.Definition.Columns.Count; i++) | ||
1332 | { | ||
1333 | ColumnDefinition columnDefinition = table.Definition.Columns[i]; | ||
1334 | |||
1335 | if (columnDefinition.PrimaryKey) | ||
1336 | { | ||
1337 | if (ColumnType.Number == columnDefinition.Type && !columnDefinition.IsLocalizable) | ||
1338 | { | ||
1339 | row[i] = Convert.ToInt32(primaryKeyParts[primaryKeyPartIndex++], CultureInfo.InvariantCulture); | ||
1340 | } | ||
1341 | else | ||
1342 | { | ||
1343 | row[i] = primaryKeyParts[primaryKeyPartIndex++]; | ||
1344 | } | ||
1345 | } | ||
1346 | else if (setRequiredFields) | ||
1347 | { | ||
1348 | if (ColumnType.Number == columnDefinition.Type && !columnDefinition.IsLocalizable) | ||
1349 | { | ||
1350 | row[i] = 1; | ||
1351 | } | ||
1352 | else if (ColumnType.Object == columnDefinition.Type) | ||
1353 | { | ||
1354 | if (null == this.emptyFile) | ||
1355 | { | ||
1356 | this.emptyFile = Path.GetTempFileName() + ".empty"; | ||
1357 | using (FileStream fileStream = File.Create(this.emptyFile)) | ||
1358 | { | ||
1359 | } | ||
1360 | } | ||
1361 | |||
1362 | row[i] = this.emptyFile; | ||
1363 | } | ||
1364 | else | ||
1365 | { | ||
1366 | row[i] = "1"; | ||
1367 | } | ||
1368 | } | ||
1369 | } | ||
1370 | |||
1371 | return row; | ||
1372 | } | ||
1373 | |||
1374 | /// <summary> | ||
1375 | /// Extract the cabinets from a database. | ||
1376 | /// </summary> | ||
1377 | /// <param name="output">The output to use when finding cabinets.</param> | ||
1378 | /// <param name="database">The database containing the cabinets.</param> | ||
1379 | /// <param name="databaseFile">The location of the database file.</param> | ||
1380 | /// <param name="exportBasePath">The path where the files should be exported.</param> | ||
1381 | private void ExtractCabinets(Output output, Database database, string databaseFile, string exportBasePath) | ||
1382 | { | ||
1383 | string databaseBasePath = Path.GetDirectoryName(databaseFile); | ||
1384 | StringCollection cabinetFiles = new StringCollection(); | ||
1385 | SortedList embeddedCabinets = new SortedList(); | ||
1386 | |||
1387 | // index all of the cabinet files | ||
1388 | if (OutputType.Module == output.Type) | ||
1389 | { | ||
1390 | embeddedCabinets.Add(0, "MergeModule.CABinet"); | ||
1391 | } | ||
1392 | else if (null != output.Tables["Media"]) | ||
1393 | { | ||
1394 | foreach (MediaRow mediaRow in output.Tables["Media"].Rows) | ||
1395 | { | ||
1396 | if (null != mediaRow.Cabinet) | ||
1397 | { | ||
1398 | if (OutputType.Product == output.Type || | ||
1399 | (OutputType.Transform == output.Type && RowOperation.Add == mediaRow.Operation)) | ||
1400 | { | ||
1401 | if (mediaRow.Cabinet.StartsWith("#", StringComparison.Ordinal)) | ||
1402 | { | ||
1403 | embeddedCabinets.Add(mediaRow.DiskId, mediaRow.Cabinet.Substring(1)); | ||
1404 | } | ||
1405 | else | ||
1406 | { | ||
1407 | cabinetFiles.Add(Path.Combine(databaseBasePath, mediaRow.Cabinet)); | ||
1408 | } | ||
1409 | } | ||
1410 | } | ||
1411 | } | ||
1412 | } | ||
1413 | |||
1414 | // extract the embedded cabinet files from the database | ||
1415 | if (0 < embeddedCabinets.Count) | ||
1416 | { | ||
1417 | using (View streamsView = database.OpenView("SELECT `Data` FROM `_Streams` WHERE `Name` = ?")) | ||
1418 | { | ||
1419 | foreach (int diskId in embeddedCabinets.Keys) | ||
1420 | { | ||
1421 | using (Record record = new Record(1)) | ||
1422 | { | ||
1423 | record.SetString(1, (string)embeddedCabinets[diskId]); | ||
1424 | streamsView.Execute(record); | ||
1425 | } | ||
1426 | |||
1427 | using (Record record = streamsView.Fetch()) | ||
1428 | { | ||
1429 | if (null != record) | ||
1430 | { | ||
1431 | // since the cabinets are stored in case-sensitive streams inside the msi, but the file system is not case-sensitive, | ||
1432 | // embedded cabinets must be extracted to a canonical file name (like their diskid) to ensure extraction will always work | ||
1433 | string cabinetFile = Path.Combine(this.TempFilesLocation, String.Concat("Media", Path.DirectorySeparatorChar, diskId.ToString(CultureInfo.InvariantCulture), ".cab")); | ||
1434 | |||
1435 | // ensure the parent directory exists | ||
1436 | System.IO.Directory.CreateDirectory(Path.GetDirectoryName(cabinetFile)); | ||
1437 | |||
1438 | using (FileStream fs = System.IO.File.Create(cabinetFile)) | ||
1439 | { | ||
1440 | int bytesRead; | ||
1441 | byte[] buffer = new byte[512]; | ||
1442 | |||
1443 | while (0 != (bytesRead = record.GetStream(1, buffer, buffer.Length))) | ||
1444 | { | ||
1445 | fs.Write(buffer, 0, bytesRead); | ||
1446 | } | ||
1447 | } | ||
1448 | |||
1449 | cabinetFiles.Add(cabinetFile); | ||
1450 | } | ||
1451 | else | ||
1452 | { | ||
1453 | // TODO: warning about missing embedded cabinet | ||
1454 | } | ||
1455 | } | ||
1456 | } | ||
1457 | } | ||
1458 | } | ||
1459 | |||
1460 | // extract the cabinet files | ||
1461 | if (0 < cabinetFiles.Count) | ||
1462 | { | ||
1463 | string fileDirectory = Path.Combine(exportBasePath, "File"); | ||
1464 | |||
1465 | // delete the directory and its files to prevent cab extraction due to an existing file | ||
1466 | if (Directory.Exists(fileDirectory)) | ||
1467 | { | ||
1468 | Directory.Delete(fileDirectory, true); | ||
1469 | } | ||
1470 | |||
1471 | // ensure the directory exists or extraction will fail | ||
1472 | Directory.CreateDirectory(fileDirectory); | ||
1473 | |||
1474 | foreach (string cabinetFile in cabinetFiles) | ||
1475 | { | ||
1476 | using (WixExtractCab extractCab = new WixExtractCab()) | ||
1477 | { | ||
1478 | try | ||
1479 | { | ||
1480 | extractCab.Extract(cabinetFile, fileDirectory); | ||
1481 | } | ||
1482 | catch (FileNotFoundException) | ||
1483 | { | ||
1484 | throw new WixException(WixErrors.FileNotFound(new SourceLineNumber(databaseFile), cabinetFile)); | ||
1485 | } | ||
1486 | } | ||
1487 | } | ||
1488 | } | ||
1489 | } | ||
1490 | } | 131 | } |
1491 | } | 132 | } |