main
cs 685 lines 30.1 KB
Raw
1 // Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information.
2
3 namespace WixToolset.Core.WindowsInstaller.Unbind
4 {
5 using System;
6 using System.Collections;
7 using System.Collections.Generic;
8 using System.ComponentModel;
9 using System.Globalization;
10 using System.IO;
11 using System.Linq;
12 using System.Text.RegularExpressions;
13 using WixToolset.Core.Native.Msi;
14 using WixToolset.Data;
15 using WixToolset.Data.WindowsInstaller;
16 using WixToolset.Data.WindowsInstaller.Rows;
17 using WixToolset.Extensibility.Data;
18 using WixToolset.Extensibility.Services;
19
20 internal class UnbindDatabaseCommand
21 {
22 private static readonly 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}");
23
24 public UnbindDatabaseCommand(IMessaging messaging, IBackendHelper backendHelper, IFileSystem fileSystem, IPathResolver pathResolver, string databasePath, Database database, OutputType outputType, string exportBasePath, string extractFilesFolder, string intermediateFolder, bool enableDemodularization, bool skipSummaryInfo)
25 {
26 this.Messaging = messaging;
27 this.BackendHelper = backendHelper;
28 this.FileSystem = fileSystem;
29 this.PathResolver = pathResolver;
30 this.DatabasePath = databasePath;
31 this.Database = database;
32 this.OutputType = outputType;
33 this.ExportBasePath = exportBasePath;
34 this.ExtractFilesFolder = extractFilesFolder;
35 this.IntermediateFolder = intermediateFolder;
36 this.EnableDemodularization = enableDemodularization;
37 this.SkipSummaryInfo = skipSummaryInfo;
38
39 this.TableDefinitions = new TableDefinitionCollection(WindowsInstallerTableDefinitions.All);
40 }
41
42 private IMessaging Messaging { get; }
43
44 private IBackendHelper BackendHelper { get; }
45
46 private IFileSystem FileSystem { get; }
47
48 private IPathResolver PathResolver { get; }
49
50 private Database Database { get; set; }
51
52 private string DatabasePath { get; }
53
54 private OutputType OutputType { get; }
55
56 private string ExportBasePath { get; }
57
58 private string ExtractFilesFolder { get; }
59
60 private string IntermediateFolder { get; }
61
62 private bool EnableDemodularization { get; }
63
64 private bool SkipSummaryInfo { get; }
65
66 private TableDefinitionCollection TableDefinitions { get; }
67
68 public bool AdminImage { get; private set; }
69
70 public WindowsInstallerData Data { get; private set; }
71
72 public IEnumerable<string> ExportedFiles { get; private set; }
73
74 public WindowsInstallerData Execute()
75 {
76 var adminImage = false;
77 var exportedFiles = new List<string>();
78
79 var data = new WindowsInstallerData(new SourceLineNumber(this.DatabasePath))
80 {
81 Type = this.OutputType
82 };
83
84 Database database = null;
85 try
86 {
87 if (this.Database == null)
88 {
89 database = new Database(this.DatabasePath, OpenDatabase.ReadOnly);
90 this.Database = database;
91 }
92
93 Directory.CreateDirectory(this.IntermediateFolder);
94
95 data.Codepage = this.GetCodePage();
96
97 var modularizationGuid = this.ProcessTables(data, exportedFiles);
98
99 var summaryInfo = this.ProcessSummaryInfo(data, modularizationGuid);
100
101 this.UpdateUnrealFileColumns(this.DatabasePath, data, summaryInfo, exportedFiles);
102 }
103 catch (Win32Exception e)
104 {
105 if (0x6E == e.NativeErrorCode) // ERROR_OPEN_FAILED
106 {
107 throw new WixException(ErrorMessages.OpenDatabaseFailed(this.DatabasePath));
108 }
109
110 throw;
111 }
112 finally
113 {
114 database?.Dispose();
115 }
116
117 this.AdminImage = adminImage;
118 this.Data = data;
119 this.ExportedFiles = exportedFiles;
120
121 return data;
122 }
123
124 private int GetCodePage()
125 {
126 var codepage = 0;
127
128 this.Database.Export("_ForceCodepage", this.IntermediateFolder, "_ForceCodepage.idt");
129
130 var lines = File.ReadAllLines(Path.Combine(this.IntermediateFolder, "_ForceCodepage.idt"));
131
132 if (lines.Length == 3)
133 {
134 var data = lines[2].Split('\t');
135
136 if (2 == data.Length)
137 {
138 codepage = Convert.ToInt32(data[0], CultureInfo.InvariantCulture);
139 }
140 }
141
142 return codepage;
143 }
144
145 private string ProcessTables(WindowsInstallerData output, List<string> exportedFiles)
146 {
147 View validationView = null;
148 string modularizationGuid = null;
149 string modularizationSuffix = null;
150
151 try
152 {
153 // open a view on the validation table if it exists
154 if (this.Database.TableExists("_Validation"))
155 {
156 validationView = this.Database.OpenView("SELECT * FROM `_Validation` WHERE `Table` = ? AND `Column` = ?");
157 }
158
159 // get the normal tables
160 using (var tablesView = this.Database.OpenExecuteView("SELECT * FROM _Tables"))
161 {
162 foreach (var tableRecord in tablesView.Records)
163 {
164 var tableName = tableRecord.GetString(1);
165
166 using (var tableView = this.Database.OpenExecuteView($"SELECT * FROM `{tableName}`"))
167 {
168 var tableDefinition = this.GetTableDefinition(tableName, tableView, validationView);
169 var table = new Table(tableDefinition);
170
171 foreach (var rowRecord in tableView.Records)
172 {
173 var recordCount = rowRecord.GetFieldCount();
174 var row = table.CreateRow(output.SourceLineNumbers);
175
176 for (var i = 0; recordCount > i && row.Fields.Length > i; i++)
177 {
178 if (rowRecord.IsNull(i + 1))
179 {
180 if (!row.Fields[i].Column.Nullable)
181 {
182 // TODO: display an error for a null value in a non-nullable field OR
183 // display a warning and put an empty string in the value to let the compiler handle it
184 // (the second option is risky because the later code may make certain assumptions about
185 // the contents of a row value)
186 }
187 }
188 else
189 {
190 switch (row.Fields[i].Column.Type)
191 {
192 case ColumnType.Number:
193 var intValue = rowRecord.GetInteger(i + 1);
194 var success = row.Fields[i].Column.IsLocalizable ? row.BestEffortSetField(i, Convert.ToString(intValue, CultureInfo.InvariantCulture)) : row.BestEffortSetField(i, intValue);
195
196 if (!success)
197 {
198 this.Messaging.Write(WarningMessages.BadColumnDataIgnored(row.SourceLineNumbers, Convert.ToString(intValue, CultureInfo.InvariantCulture), tableName, row.Fields[i].Column.Name));
199 }
200 break;
201 case ColumnType.Object:
202 var source = "FILE NOT EXPORTED";
203
204 if (null != this.ExportBasePath)
205 {
206 source = Path.Combine(this.ExportBasePath, tableName, row.GetPrimaryKey('.'));
207
208 if (!String.IsNullOrEmpty(modularizationSuffix))
209 {
210 source += modularizationSuffix;
211 }
212
213 Directory.CreateDirectory(Path.Combine(this.ExportBasePath, tableName));
214
215 using (var fs = this.FileSystem.OpenFile(null, source, FileMode.Create, FileAccess.Write, FileShare.None))
216 {
217 int bytesRead;
218 var buffer = new byte[4096];
219
220 while (0 != (bytesRead = rowRecord.GetStream(i + 1, buffer, buffer.Length)))
221 {
222 fs.Write(buffer, 0, bytesRead);
223 }
224 }
225
226 exportedFiles.Add(source);
227 }
228
229 row[i] = source;
230 break;
231 default:
232 var value = rowRecord.GetString(i + 1);
233
234 switch (row.Fields[i].Column.Category)
235 {
236 case ColumnCategory.Guid:
237 value = value.ToUpperInvariant();
238 break;
239 }
240
241 // De-modularize
242 if (this.EnableDemodularization && OutputType.Module == output.Type && ColumnModularizeType.None != row.Fields[i].Column.ModularizeType)
243 {
244 if (null == modularizationGuid)
245 {
246 var match = Modularization.Match(value);
247 modularizationSuffix = match.Value;
248
249 if (match.Success)
250 {
251 modularizationGuid = String.Concat('{', match.Value.Substring(1).Replace('_', '-'), '}');
252 }
253 }
254
255 value = Modularization.Replace(value, String.Empty);
256 }
257
258 #if TODO_MOVE_TO_DECOMPILER
259 // escape "$(" for the preprocessor
260 value = value.Replace("$(", "$$(");
261
262 // escape things that look like wix variables
263 // TODO: Evaluate this requirement.
264 //var matches = Common.WixVariableRegex.Matches(value);
265 //for (var j = matches.Count - 1; 0 <= j; j--)
266 //{
267 // value = value.Insert(matches[j].Index, "!");
268 //}
269 #endif
270
271 row[i] = value;
272 break;
273 }
274 }
275 }
276 }
277
278 output.Tables.Add(table);
279 }
280 }
281 }
282 }
283 finally
284 {
285 validationView?.Close();
286 }
287
288 return modularizationGuid;
289 }
290
291 private SummaryInformationBits ProcessSummaryInfo(WindowsInstallerData output, string modularizationGuid)
292 {
293 var result = new SummaryInformationBits();
294
295 if (!this.SkipSummaryInfo)
296 {
297 using (var summaryInformation = new SummaryInformation(this.Database))
298 {
299 var table = new Table(this.TableDefinitions["_SummaryInformation"]);
300
301 for (var i = 1; 19 >= i; i++)
302 {
303 var value = summaryInformation.GetProperty(i);
304
305 // Set the modularization guid as the PackageCode, for merge modules.
306 if (i == (int)SummaryInformation.Package.PackageCode && !String.IsNullOrEmpty(modularizationGuid))
307 {
308 var row = table.CreateRow(output.SourceLineNumbers);
309 row[0] = i;
310 row[1] = modularizationGuid;
311 }
312 else if (0 < value.Length)
313 {
314 var row = table.CreateRow(output.SourceLineNumbers);
315 row[0] = i;
316 row[1] = value;
317
318 if (i == (int)SummaryInformation.Package.FileAndElevatedFlags)
319 {
320 var wordcount = Convert.ToInt32(value, CultureInfo.InvariantCulture);
321 result.LongFilenames = (wordcount & 0x1) != 0x1;
322 result.Compressed = (wordcount & 0x2) == 0x2;
323 result.AdminImage = (wordcount & 0x4) == 0x4;
324 }
325 }
326 }
327
328 output.Tables.Add(table);
329 }
330 }
331
332 return result;
333 }
334
335 private TableDefinition GetTableDefinition(string tableName, View tableView, View validationView)
336 {
337 // Use our table definitions whenever possible since they will be used when compiling the source code anyway.
338 // This also allows us to take advantage of WiX concepts like localizable columns which current code assumes.
339 if (this.TableDefinitions.Contains(tableName))
340 {
341 return this.TableDefinitions[tableName];
342 }
343
344 ColumnDefinition[] columns;
345 using (Record columnNameRecord = tableView.GetColumnNames(),
346 columnTypeRecord = tableView.GetColumnTypes())
347 {
348 // index the primary keys
349 var tablePrimaryKeys = new HashSet<string>();
350 using (var primaryKeysRecord = this.Database.PrimaryKeys(tableName))
351 {
352 var primaryKeysFieldCount = primaryKeysRecord.GetFieldCount();
353
354 for (var i = 1; i <= primaryKeysFieldCount; i++)
355 {
356 tablePrimaryKeys.Add(primaryKeysRecord.GetString(i));
357 }
358 }
359
360 var columnCount = columnNameRecord.GetFieldCount();
361 columns = new ColumnDefinition[columnCount];
362 for (var i = 1; i <= columnCount; i++)
363 {
364 var columnName = columnNameRecord.GetString(i);
365 var idtType = columnTypeRecord.GetString(i);
366
367 var columnCategory = ColumnCategory.Unknown;
368 var columnModularizeType = ColumnModularizeType.None;
369 var primary = tablePrimaryKeys.Contains(columnName);
370 int? minValue = null;
371 int? maxValue = null;
372 string keyTable = null;
373 int? keyColumn = null;
374 string category = null;
375 string set = null;
376 string description = null;
377
378 // get the column type, length, and whether its nullable
379 ColumnType columnType;
380 switch (Char.ToLowerInvariant(idtType[0]))
381 {
382 case 'i':
383 columnType = ColumnType.Number;
384 break;
385 case 'l':
386 columnType = ColumnType.Localized;
387 break;
388 case 's':
389 columnType = ColumnType.String;
390 break;
391 case 'v':
392 columnType = ColumnType.Object;
393 break;
394 default:
395 // TODO: error
396 columnType = ColumnType.Unknown;
397 break;
398 }
399 var length = Convert.ToInt32(idtType.Substring(1), CultureInfo.InvariantCulture);
400 var nullable = Char.IsUpper(idtType[0]);
401
402 // try to get validation information
403 if (null != validationView)
404 {
405 using (var validationRecord = new Record(2))
406 {
407 validationRecord.SetString(1, tableName);
408 validationRecord.SetString(2, columnName);
409
410 validationView.Execute(validationRecord);
411 }
412
413 using (var validationRecord = validationView.Fetch())
414 {
415 if (null != validationRecord)
416 {
417 var validationNullable = validationRecord.GetString(3);
418 minValue = validationRecord.IsNull(4) ? null : (int?)validationRecord.GetInteger(4);
419 maxValue = validationRecord.IsNull(5) ? null : (int?)validationRecord.GetInteger(5);
420 keyTable = validationRecord.IsNull(6) ? null : validationRecord.GetString(6);
421 keyColumn = validationRecord.IsNull(7) ? null : (int?)validationRecord.GetInteger(7);
422 category = validationRecord.IsNull(8) ? null : validationRecord.GetString(8);
423 set = validationRecord.IsNull(9) ? null : validationRecord.GetString(9);
424 description = validationRecord.IsNull(10) ? null : validationRecord.GetString(10);
425
426 // check the validation nullable value against the column definition
427 if (null == validationNullable)
428 {
429 // TODO: warn for illegal validation nullable column
430 }
431 else if ((nullable && "Y" != validationNullable) || (!nullable && "N" != validationNullable))
432 {
433 // TODO: warn for mismatch between column definition and validation nullable
434 }
435
436 // convert category to ColumnCategory
437 if (null != category)
438 {
439 if (!Enum.TryParse(category, true, out columnCategory))
440 {
441 columnCategory = ColumnCategory.Unknown;
442 }
443 }
444 }
445 else
446 {
447 // TODO: warn about no validation information
448 }
449 }
450 }
451
452 // guess the modularization type
453 if ("Icon" == keyTable && 1 == keyColumn)
454 {
455 columnModularizeType = ColumnModularizeType.Icon;
456 }
457 else if ("Condition" == columnName)
458 {
459 columnModularizeType = ColumnModularizeType.Condition;
460 }
461 else if (ColumnCategory.Formatted == columnCategory || ColumnCategory.FormattedSDDLText == columnCategory)
462 {
463 columnModularizeType = ColumnModularizeType.Property;
464 }
465 else if (ColumnCategory.Identifier == columnCategory)
466 {
467 columnModularizeType = ColumnModularizeType.Column;
468 }
469
470 columns[i - 1] = new ColumnDefinition(columnName, columnType, length, primary, nullable, columnCategory, minValue, maxValue, keyTable, keyColumn, set, description, columnModularizeType, (ColumnType.Localized == columnType), true);
471 }
472 }
473
474 return new TableDefinition(tableName, null, columns, false);
475 }
476
477 private void UpdateUnrealFileColumns(string databaseFile, WindowsInstallerData output, SummaryInformationBits summaryInformation, List<string> exportedFiles)
478 {
479 var fileRows = output.Tables["File"]?.Rows;
480
481 if (fileRows == null || fileRows.Count == 0)
482 {
483 return;
484 }
485
486 this.UpdateFileRowsDiskId(output, fileRows);
487
488 this.UpdateFileRowsSource(databaseFile, output, fileRows, summaryInformation, exportedFiles);
489 }
490
491 private void UpdateFileRowsDiskId(WindowsInstallerData output, IList<Row> fileRows)
492 {
493 var mediaRows = output.Tables["Media"]?.Rows?.Cast<MediaRow>()?.OrderBy(r => r.LastSequence)?.ToList();
494
495 var lastMediaRowIndex = 0;
496 var lastMediaRow = (mediaRows == null || mediaRows.Count == 0) ? null : mediaRows[lastMediaRowIndex];
497
498 foreach (var fileRow in fileRows.Cast<FileRow>()?.OrderBy(r => r.Sequence))
499 {
500 while (lastMediaRow != null && fileRow.Sequence > lastMediaRow.LastSequence)
501 {
502 ++lastMediaRowIndex;
503
504 lastMediaRow = lastMediaRowIndex < mediaRows.Count ? mediaRows[lastMediaRowIndex] : null;
505 }
506
507 fileRow.DiskId = lastMediaRow?.DiskId ?? 1;
508 }
509 }
510
511 private void UpdateFileRowsSource(string databasePath, WindowsInstallerData output, IList<Row> fileRows, SummaryInformationBits summaryInformation, List<string> exportedFiles)
512 {
513 var databaseFolder = Path.GetDirectoryName(databasePath);
514
515 var componentDirectoryIndex = output.Tables["Component"].Rows.Cast<ComponentRow>().ToDictionary(r => r.Component, r => r.Directory);
516
517 // Index full source paths for all directories
518 var directories = new Dictionary<string, IResolvedDirectory>();
519
520 var directoryTable = output.Tables["Directory"];
521 foreach (var row in directoryTable.Rows)
522 {
523 var sourceName = this.BackendHelper.GetMsiFileName(row.FieldAsString(2), source: true, longName: summaryInformation.LongFilenames);
524 var resolvedDirectory = this.BackendHelper.CreateResolvedDirectory(row.FieldAsString(1), sourceName);
525
526 directories.Add(row.FieldAsString(0), resolvedDirectory);
527 }
528
529 if (summaryInformation.AdminImage)
530 {
531 foreach (var fileRow in fileRows.Cast<FileRow>())
532 {
533 var directoryId = componentDirectoryIndex[fileRow.Component];
534 var relativeFileLayoutPath = this.PathResolver.GetFileSourcePath(directories, directoryId, fileRow.FileName, compressed: false, useLongName: summaryInformation.LongFilenames);
535
536 fileRow.Source = Path.Combine(databaseFolder, relativeFileLayoutPath);
537 }
538 }
539 else
540 {
541 var extractedFileIds = new HashSet<string>();
542
543 if (!String.IsNullOrEmpty(this.ExtractFilesFolder))
544 {
545 var extractCommand = new ExtractCabinetsCommand(this.FileSystem, output, this.Database, this.DatabasePath, this.ExtractFilesFolder, this.IntermediateFolder);
546 extractCommand.Execute();
547
548 extractedFileIds = new HashSet<string>(extractCommand.ExtractedFileIdsWithMediaRow.Keys, StringComparer.OrdinalIgnoreCase);
549 exportedFiles.AddRange(extractedFileIds);
550 }
551
552 foreach (var fileRow in fileRows.Cast<FileRow>())
553 {
554 var source = "FILE NOT EXPORTED";
555
556 if (fileRow.Compressed == YesNoType.Yes || (fileRow.Compressed == YesNoType.NotSet && summaryInformation.Compressed))
557 {
558 if (extractedFileIds.Contains(fileRow.File))
559 {
560 source = Path.Combine(this.ExtractFilesFolder, fileRow.File);
561 }
562 }
563 else if (componentDirectoryIndex.TryGetValue(fileRow.Component, out var directoryId)) // this can happen when unbinding an invalid MSI file or MST file with select table modifications.
564 {
565 var relativeFileLayoutPath = this.PathResolver.GetFileSourcePath(directories, directoryId, fileRow.FileName, compressed: false, useLongName: summaryInformation.LongFilenames);
566
567 source = Path.Combine(databaseFolder, relativeFileLayoutPath);
568 }
569
570 fileRow.Source = source;
571 }
572 }
573 }
574
575 /// <summary>
576 /// 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.
577 /// </summary>
578 /// <param name="directory">The directory identifier.</param>
579 /// <param name="directoryDirectoryParentIndex">The Hashtable containing all the directory to directory parent mapping.</param>
580 /// <param name="directorySourceNameIndex">The Hashtable containing all the directory to source name mapping.</param>
581 /// <param name="directoryFullPathIndex">The Hashtable containing a mapping between all of the directories and their previously calculated full paths.</param>
582 /// <returns>The full path to the directory.</returns>
583 private string GetAdminFullPath(string directory, Hashtable directoryDirectoryParentIndex, Hashtable directorySourceNameIndex, Hashtable directoryFullPathIndex)
584 {
585 var parent = (string)directoryDirectoryParentIndex[directory];
586 var sourceName = (string)directorySourceNameIndex[directory];
587
588 string parentFullPath;
589 if (directoryFullPathIndex.ContainsKey(parent))
590 {
591 parentFullPath = (string)directoryFullPathIndex[parent];
592 }
593 else
594 {
595 parentFullPath = this.GetAdminFullPath(parent, directoryDirectoryParentIndex, directorySourceNameIndex, directoryFullPathIndex);
596 }
597
598 if (null == sourceName)
599 {
600 sourceName = String.Empty;
601 }
602
603 var fullPath = Path.Combine(parentFullPath, sourceName);
604 directoryFullPathIndex.Add(directory, fullPath);
605
606 return fullPath;
607 }
608
609 /// <summary>
610 /// Get the source name in an admin image.
611 /// </summary>
612 /// <param name="value">The Filename value.</param>
613 /// <returns>The source name of the directory in an admin image.</returns>
614 private string GetAdminSourceName(string value)
615 {
616 string name = null;
617 string[] names;
618 string shortname = null;
619 string shortsourcename = null;
620 string sourcename = null;
621
622 names = this.BackendHelper.SplitMsiFileName(value);
623
624 if (null != names[0] && "." != names[0])
625 {
626 if (null != names[1])
627 {
628 shortname = names[0];
629 }
630 else
631 {
632 name = names[0];
633 }
634 }
635
636 if (null != names[1])
637 {
638 name = names[1];
639 }
640
641 if (null != names[2])
642 {
643 if (null != names[3])
644 {
645 shortsourcename = names[2];
646 }
647 else
648 {
649 sourcename = names[2];
650 }
651 }
652
653 if (null != names[3])
654 {
655 sourcename = names[3];
656 }
657
658 if (null != sourcename)
659 {
660 return sourcename;
661 }
662 else if (null != shortsourcename)
663 {
664 return shortsourcename;
665 }
666 else if (null != name)
667 {
668 return name;
669 }
670 else
671 {
672 return shortname;
673 }
674 }
675
676 private class SummaryInformationBits
677 {
678 public bool AdminImage { get; set; }
679
680 public bool Compressed { get; set; }
681
682 public bool LongFilenames { get; set; }
683 }
684 }
685 }