aboutsummaryrefslogtreecommitdiff
path: root/src/WixToolset.Core.WindowsInstaller/Bind/ValidateDatabaseCommand.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/WixToolset.Core.WindowsInstaller/Bind/ValidateDatabaseCommand.cs')
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/ValidateDatabaseCommand.cs177
1 files changed, 177 insertions, 0 deletions
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/ValidateDatabaseCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/ValidateDatabaseCommand.cs
new file mode 100644
index 00000000..35b6018c
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/ValidateDatabaseCommand.cs
@@ -0,0 +1,177 @@
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
3namespace WixToolset.Core.WindowsInstaller.Bind
4{
5 using System;
6 using System.Collections.Generic;
7 using System.ComponentModel;
8 using System.Diagnostics;
9 using System.IO;
10 using System.Linq;
11 using System.Threading;
12 using WixToolset.Core.Native;
13 using WixToolset.Core.Native.Msi;
14 using WixToolset.Data;
15 using WixToolset.Data.WindowsInstaller;
16 using WixToolset.Extensibility.Services;
17
18 internal class ValidateDatabaseCommand : IWindowsInstallerValidatorCallback
19 {
20 // Set of ICEs that have equivalent-or-better checks in WiX.
21 private static readonly string[] WellKnownSuppressedIces = new[] { "ICE08", "ICE33", "ICE47", "ICE66" };
22
23 public ValidateDatabaseCommand(IMessaging messaging, string intermediateFolder, WindowsInstallerData data, string outputPath, IEnumerable<string> cubeFiles, IEnumerable<string> ices, IEnumerable<string> suppressedIces)
24 {
25 this.Messaging = messaging;
26 this.Data = data;
27 this.OutputPath = outputPath;
28 this.CubeFiles = cubeFiles;
29 this.Ices = ices;
30 this.SuppressedIces = suppressedIces == null ? WellKnownSuppressedIces : suppressedIces.Union(WellKnownSuppressedIces);
31
32 this.IntermediateFolder = intermediateFolder;
33 this.OutputSourceLineNumber = new SourceLineNumber(outputPath);
34 }
35
36 /// <summary>
37 /// Encountered error implementation for <see cref="IWindowsInstallerValidatorCallback"/>.
38 /// </summary>
39 public bool EncounteredError => this.Messaging.EncounteredError;
40
41 private IMessaging Messaging { get; }
42
43 private WindowsInstallerData Data { get; }
44
45 private string OutputPath { get; }
46
47 private IEnumerable<string> CubeFiles { get; }
48
49 private IEnumerable<string> Ices { get; }
50
51 private IEnumerable<string> SuppressedIces { get; }
52
53 private string IntermediateFolder { get; }
54
55 /// <summary>
56 /// Fallback when an exact source line number cannot be calculated for a validation error.
57 /// </summary>
58 private SourceLineNumber OutputSourceLineNumber { get; set; }
59
60 private Dictionary<string, SourceLineNumber> SourceLineNumbersByTablePrimaryKey { get; set; }
61
62 public void Execute()
63 {
64 var stopwatch = Stopwatch.StartNew();
65
66 this.Messaging.Write(VerboseMessages.ValidatingDatabase());
67
68 // Ensure the temporary files can be created the working folder.
69 var workingFolder = Path.Combine(this.IntermediateFolder, "_validate");
70 Directory.CreateDirectory(workingFolder);
71
72 // Copy the database to a temporary location so it can be manipulated.
73 // Ensure it is not read-only.
74 var workingDatabasePath = Path.Combine(workingFolder, Path.GetFileName(this.OutputPath));
75 FileSystem.CopyFile(this.OutputPath, workingDatabasePath, allowHardlink: false);
76
77 var attributes = File.GetAttributes(workingDatabasePath);
78 File.SetAttributes(workingDatabasePath, attributes & ~FileAttributes.ReadOnly);
79
80 var validator = new WindowsInstallerValidator(this, workingDatabasePath, this.CubeFiles, this.Ices, this.SuppressedIces);
81 validator.Execute();
82
83 stopwatch.Stop();
84 this.Messaging.Write(VerboseMessages.ValidatedDatabase(stopwatch.ElapsedMilliseconds));
85 }
86
87 private void LogValidationMessage(ValidationMessage message)
88 {
89 var messageSourceLineNumbers = this.OutputSourceLineNumber;
90 if (!String.IsNullOrEmpty(message.Table) && !String.IsNullOrEmpty(message.Column) && message.PrimaryKeys != null)
91 {
92 messageSourceLineNumbers = this.GetSourceLineNumbers(message.Table, message.PrimaryKeys);
93 }
94
95 switch (message.Type)
96 {
97 case ValidationMessageType.InternalFailure:
98 case ValidationMessageType.Error:
99 this.Messaging.Write(ErrorMessages.ValidationError(messageSourceLineNumbers, message.IceName, message.Description));
100 break;
101 case ValidationMessageType.Warning:
102 this.Messaging.Write(WarningMessages.ValidationWarning(messageSourceLineNumbers, message.IceName, message.Description));
103 break;
104 case ValidationMessageType.Info:
105 this.Messaging.Write(VerboseMessages.ValidationInfo(message.IceName, message.Description));
106 break;
107 default:
108 throw new WixException(ErrorMessages.InvalidValidatorMessageType(message.Type.ToString()));
109 }
110 }
111
112 /// <summary>
113 /// Validation blocked by other installation operation for <see cref="IWindowsInstallerValidatorCallback"/>.
114 /// </summary>
115 public void ValidationBlocked()
116 {
117 this.Messaging.Write(VerboseMessages.ValidationSerialized());
118 }
119
120 /// <summary>
121 /// Validation message implementation for <see cref="IWindowsInstallerValidatorCallback"/>.
122 /// </summary>
123 public bool ValidationMessage(ValidationMessage message)
124 {
125 this.LogValidationMessage(message);
126 return true;
127 }
128
129 /// <summary>
130 /// Gets the source line information (if available) for a row by its table name and primary key.
131 /// </summary>
132 /// <param name="tableName">The table name of the row.</param>
133 /// <param name="primaryKeys">The primary keys of the row.</param>
134 /// <returns>The source line number information if found; null otherwise.</returns>
135 private SourceLineNumber GetSourceLineNumbers(string tableName, IEnumerable<string> primaryKeys)
136 {
137 // Source line information only exists if an output file was supplied
138 if (this.Data == null)
139 {
140 // Use the file name as the source line information.
141 return this.OutputSourceLineNumber;
142 }
143
144 // Index the source line information if it hasn't been indexed already.
145 if (this.SourceLineNumbersByTablePrimaryKey == null)
146 {
147 this.SourceLineNumbersByTablePrimaryKey = new Dictionary<string, SourceLineNumber>();
148
149 // Index each real table
150 foreach (var table in this.Data.Tables.Where(t => !t.Definition.Unreal))
151 {
152 // Index each row that contain source line information
153 foreach (var row in table.Rows.Where(r => r.SourceLineNumbers != null))
154 {
155 // Index the row using its table name and primary key
156 var primaryKey = row.GetPrimaryKey(';');
157
158 if (!String.IsNullOrEmpty(primaryKey))
159 {
160 try
161 {
162 var key = String.Concat(table.Name, ":", primaryKey);
163 this.SourceLineNumbersByTablePrimaryKey.Add(key, row.SourceLineNumbers);
164 }
165 catch (ArgumentException)
166 {
167 this.Messaging.Write(WarningMessages.DuplicatePrimaryKey(row.SourceLineNumbers, primaryKey, table.Name));
168 }
169 }
170 }
171 }
172 }
173
174 return this.SourceLineNumbersByTablePrimaryKey.TryGetValue(String.Concat(tableName, ":", String.Join(";", primaryKeys)), out var sourceLineNumbers) ? sourceLineNumbers : null;
175 }
176 }
177}