aboutsummaryrefslogtreecommitdiff
path: root/src/WixToolset.Core/Unbinder.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/WixToolset.Core/Unbinder.cs')
-rw-r--r--src/WixToolset.Core/Unbinder.cs1399
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
3namespace WixToolset 3namespace 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}