| 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 | } |