main
cs 197 lines 8.82 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.Validate
4 {
5 using System;
6 using System.Collections.Generic;
7 using System.Diagnostics;
8 using System.IO;
9 using System.Linq;
10 using WixToolset.Core.Native;
11 using WixToolset.Data;
12 using WixToolset.Data.WindowsInstaller;
13 using WixToolset.Extensibility.Data;
14 using WixToolset.Extensibility.Services;
15
16 internal class ValidateDatabaseCommand : IWindowsInstallerValidatorCallback
17 {
18 // Set of ICEs that have equivalent-or-better checks in WiX.
19 private static readonly string[] WellKnownSuppressedIces = new[] { "ICE08", "ICE33", "ICE47", "ICE66" };
20
21 public ValidateDatabaseCommand(IMessaging messaging, IFileSystem fileSystem, string intermediateFolder, string databasePath, WindowsInstallerData data, IEnumerable<string> cubeFiles, IEnumerable<string> ices, IEnumerable<string> suppressedIces)
22 {
23 this.Messaging = messaging;
24 this.FileSystem = fileSystem;
25 this.Data = data;
26 this.DatabasePath = databasePath;
27 this.CubeFiles = cubeFiles;
28 this.Ices = ices;
29 this.IntermediateFolder = intermediateFolder;
30 this.OutputSourceLineNumber = new SourceLineNumber(databasePath);
31 this.SuppressedIces = suppressedIces == null ? WellKnownSuppressedIces : suppressedIces.Union(WellKnownSuppressedIces);
32
33 // Suppress ICE103 for merge modules because the custom action DLL in mergemod.cub is borked.
34 // See https://github.com/wixtoolset/issues/issues/6567.
35 if (Path.GetExtension(this.DatabasePath) == ".msm")
36 {
37 this.SuppressedIces = this.SuppressedIces.Union(new[] { "ICE103" });
38 }
39 }
40
41 public IEnumerable<ITrackedFile> TrackedFiles { get; private set; }
42
43 public bool EncounteredError => this.Messaging.EncounteredError;
44
45 private IMessaging Messaging { get; }
46
47 private IFileSystem FileSystem { get; }
48
49 private WindowsInstallerData Data { get; }
50
51 private string DatabasePath { get; }
52
53 private IEnumerable<string> CubeFiles { get; }
54
55 private IEnumerable<string> Ices { get; }
56
57 private IEnumerable<string> SuppressedIces { get; }
58
59 private string IntermediateFolder { get; }
60
61 /// <summary>
62 /// Fallback when an exact source line number cannot be calculated for a validation error.
63 /// </summary>
64 private SourceLineNumber OutputSourceLineNumber { get; set; }
65
66 private Dictionary<string, SourceLineNumber> SourceLineNumbersByTablePrimaryKey { get; set; }
67
68 public void Execute()
69 {
70 var stopwatch = Stopwatch.StartNew();
71
72 this.Messaging.Write(VerboseMessages.ValidatingDatabase());
73
74 // Copy the database to a temporary location so it can be manipulated.
75 // Ensure it is not read-only.
76 var workingDatabaseFilename = String.Concat(Path.GetFileNameWithoutExtension(this.DatabasePath), "_validate", Path.GetExtension(this.DatabasePath));
77 var workingDatabasePath = Path.Combine(this.IntermediateFolder, workingDatabaseFilename);
78 try
79 {
80 this.FileSystem.CopyFile(null, this.DatabasePath, workingDatabasePath, allowHardlink: false);
81
82 var attributes = File.GetAttributes(workingDatabasePath);
83 File.SetAttributes(workingDatabasePath, attributes & ~FileAttributes.ReadOnly);
84
85 var validator = new WindowsInstallerValidator(this, workingDatabasePath, this.CubeFiles, this.Ices, this.SuppressedIces);
86 validator.Execute();
87 }
88 finally
89 {
90 this.FileSystem.DeleteFile(null, workingDatabasePath);
91 }
92
93 stopwatch.Stop();
94 this.Messaging.Write(VerboseMessages.ValidatedDatabase(stopwatch.ElapsedMilliseconds));
95 }
96
97 private void LogValidationMessage(ValidationMessage message)
98 {
99 var messageSourceLineNumbers = this.OutputSourceLineNumber;
100 if (!String.IsNullOrEmpty(message.Table) && !String.IsNullOrEmpty(message.Column) && message.PrimaryKeys != null)
101 {
102 messageSourceLineNumbers = this.GetSourceLineNumbers(message.Table, message.PrimaryKeys);
103 }
104
105 // Sigh. These are bad messages we get from the poorly-built mergemod.cub that supports Arm64.
106 // TODO: Re-evaluate mergemod.cub that's current for the next version of WiX.
107 if (message.IceName == "ICE03"
108 && (message.Table == "File" && message.Description.Contains("_ICEM07CAB Missing specifications"))
109 || (message.Table == "Component" && message.Description.Contains("_IceM05Mark Missing specifications"))
110 )
111 {
112 return;
113 }
114
115 switch (message.Type)
116 {
117 case ValidationMessageType.InternalFailure:
118 case ValidationMessageType.Error:
119 this.Messaging.Write(ErrorMessages.ValidationError(messageSourceLineNumbers, message.IceName, message.Description));
120 break;
121 case ValidationMessageType.Warning:
122 this.Messaging.Write(WarningMessages.ValidationWarning(messageSourceLineNumbers, message.IceName, message.Description));
123 break;
124 case ValidationMessageType.Info:
125 this.Messaging.Write(VerboseMessages.ValidationInfo(message.IceName, message.Description));
126 break;
127 default:
128 throw new WixException(ErrorMessages.InvalidValidatorMessageType(message.Type.ToString()));
129 }
130 }
131
132 /// <summary>
133 /// Validation message implementation for <see cref="IWindowsInstallerValidatorCallback"/>.
134 /// </summary>
135 public bool ValidationMessage(ValidationMessage message)
136 {
137 this.LogValidationMessage(message);
138 return true;
139 }
140
141 /// <summary>
142 /// Normal message encountered while preparing for ICE validation for <see cref="IWindowsInstallerValidatorCallback"/>.
143 /// </summary>
144 public void WriteMessage(Message message)
145 {
146 this.Messaging.Write(message);
147 }
148
149 /// <summary>
150 /// Gets the source line information (if available) for a row by its table name and primary key.
151 /// </summary>
152 /// <param name="tableName">The table name of the row.</param>
153 /// <param name="primaryKeys">The primary keys of the row.</param>
154 /// <returns>The source line number information if found; null otherwise.</returns>
155 private SourceLineNumber GetSourceLineNumbers(string tableName, IEnumerable<string> primaryKeys)
156 {
157 // Source line information only exists if an output file was supplied
158 if (this.Data == null)
159 {
160 // Use the file name as the source line information.
161 return this.OutputSourceLineNumber;
162 }
163
164 // Index the source line information if it hasn't been indexed already.
165 if (this.SourceLineNumbersByTablePrimaryKey == null)
166 {
167 this.SourceLineNumbersByTablePrimaryKey = new Dictionary<string, SourceLineNumber>();
168
169 // Index each real table
170 foreach (var table in this.Data.Tables.Where(t => !t.Definition.Unreal))
171 {
172 // Index each row that contain source line information
173 foreach (var row in table.Rows.Where(r => r.SourceLineNumbers != null))
174 {
175 // Index the row using its table name and primary key
176 var primaryKey = row.GetPrimaryKey(';');
177
178 if (!String.IsNullOrEmpty(primaryKey))
179 {
180 try
181 {
182 var key = String.Concat(table.Name, ":", primaryKey);
183 this.SourceLineNumbersByTablePrimaryKey.Add(key, row.SourceLineNumbers);
184 }
185 catch (ArgumentException)
186 {
187 this.Messaging.Write(WarningMessages.DuplicatePrimaryKey(row.SourceLineNumbers, primaryKey, table.Name));
188 }
189 }
190 }
191 }
192 }
193
194 return this.SourceLineNumbersByTablePrimaryKey.TryGetValue(String.Concat(tableName, ":", String.Join(";", primaryKeys)), out var sourceLineNumbers) ? sourceLineNumbers : null;
195 }
196 }
197 }