| 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.Bind |
| 4 | { |
| 5 | using System; |
| 6 | using System.Collections.Generic; |
| 7 | using System.Globalization; |
| 8 | using System.Linq; |
| 9 | using System.Text.RegularExpressions; |
| 10 | using WixToolset.Core.Native.Msi; |
| 11 | using WixToolset.Data; |
| 12 | using WixToolset.Data.Symbols; |
| 13 | using WixToolset.Data.WindowsInstaller; |
| 14 | using WixToolset.Data.WindowsInstaller.Rows; |
| 15 | using WixToolset.Extensibility.Services; |
| 16 | |
| 17 | /// <summary> |
| 18 | /// Include transforms in a patch. |
| 19 | /// </summary> |
| 20 | internal class CreatePatchSubStoragesCommand |
| 21 | { |
| 22 | private static readonly string[] PatchUninstallBreakingTables = new[] |
| 23 | { |
| 24 | "AppId", |
| 25 | "BindImage", |
| 26 | "Class", |
| 27 | "Complus", |
| 28 | "CreateFolder", |
| 29 | "DuplicateFile", |
| 30 | "Environment", |
| 31 | "Extension", |
| 32 | "Font", |
| 33 | "IniFile", |
| 34 | "IsolatedComponent", |
| 35 | "LockPermissions", |
| 36 | "MIME", |
| 37 | "MoveFile", |
| 38 | "MsiLockPermissionsEx", |
| 39 | "MsiServiceConfig", |
| 40 | "MsiServiceConfigFailureActions", |
| 41 | "ODBCAttribute", |
| 42 | "ODBCDataSource", |
| 43 | "ODBCDriver", |
| 44 | "ODBCSourceAttribute", |
| 45 | "ODBCTranslator", |
| 46 | "ProgId", |
| 47 | "PublishComponent", |
| 48 | "RemoveIniFile", |
| 49 | "SelfReg", |
| 50 | "ServiceControl", |
| 51 | "ServiceInstall", |
| 52 | "TypeLib", |
| 53 | "Verb", |
| 54 | }; |
| 55 | |
| 56 | private readonly TableDefinitionCollection tableDefinitions; |
| 57 | |
| 58 | public CreatePatchSubStoragesCommand(IMessaging messaging, IBackendHelper backendHelper, Intermediate intermediate, IEnumerable<PatchTransform> transforms) |
| 59 | { |
| 60 | this.tableDefinitions = new TableDefinitionCollection(WindowsInstallerTableDefinitions.All); |
| 61 | this.Messaging = messaging; |
| 62 | this.BackendHelper = backendHelper; |
| 63 | this.Intermediate = intermediate; |
| 64 | this.Transforms = transforms; |
| 65 | } |
| 66 | |
| 67 | private IMessaging Messaging { get; } |
| 68 | |
| 69 | private IBackendHelper BackendHelper { get; } |
| 70 | |
| 71 | private Intermediate Intermediate { get; } |
| 72 | |
| 73 | private IEnumerable<PatchTransform> Transforms { get; } |
| 74 | |
| 75 | public IEnumerable<SubStorage> SubStorages { get; private set; } |
| 76 | |
| 77 | public IEnumerable<SubStorage> Execute() |
| 78 | { |
| 79 | var subStorages = new List<SubStorage>(); |
| 80 | |
| 81 | if (this.Transforms == null || !this.Transforms.Any()) |
| 82 | { |
| 83 | this.Messaging.Write(ErrorMessages.PatchWithoutTransforms()); |
| 84 | return subStorages; |
| 85 | } |
| 86 | |
| 87 | var summaryInfo = this.ExtractPatchSummaryInfo(); |
| 88 | |
| 89 | var section = this.Intermediate.Sections.First(); |
| 90 | |
| 91 | var symbols = this.Intermediate.Sections.SelectMany(s => s.Symbols).ToList(); |
| 92 | |
| 93 | // Get the patch id from the WixPatchId symbol. |
| 94 | var patchSymbol = symbols.OfType<WixPatchSymbol>().FirstOrDefault(); |
| 95 | |
| 96 | if (String.IsNullOrEmpty(patchSymbol.Id?.Id)) |
| 97 | { |
| 98 | this.Messaging.Write(ErrorMessages.ExpectedPatchIdInWixMsp()); |
| 99 | return subStorages; |
| 100 | } |
| 101 | |
| 102 | if (String.IsNullOrEmpty(patchSymbol.ClientPatchId)) |
| 103 | { |
| 104 | this.Messaging.Write(ErrorMessages.ExpectedClientPatchIdInWixMsp()); |
| 105 | return subStorages; |
| 106 | } |
| 107 | |
| 108 | // enumerate patch.Media to map diskId to Media row |
| 109 | var patchMediaByDiskId = symbols.OfType<MediaSymbol>().ToDictionary(t => t.DiskId); |
| 110 | |
| 111 | if (patchMediaByDiskId.Count == 0) |
| 112 | { |
| 113 | this.Messaging.Write(ErrorMessages.ExpectedMediaRowsInWixMsp()); |
| 114 | return subStorages; |
| 115 | } |
| 116 | |
| 117 | // populate MSP summary information |
| 118 | var patchMetadata = this.PopulateSummaryInformation(summaryInfo, symbols, patchSymbol); |
| 119 | |
| 120 | // enumerate transforms |
| 121 | var productCodes = new SortedSet<string>(); |
| 122 | var transformNames = new List<string>(); |
| 123 | var validTransform = new List<Tuple<string, WindowsInstallerData>>(); |
| 124 | |
| 125 | var baselineSymbolsById = symbols.OfType<WixPatchBaselineSymbol>().ToDictionary(t => t.Id.Id); |
| 126 | |
| 127 | foreach (var mainTransform in this.Transforms) |
| 128 | { |
| 129 | // Validate the transform doesn't break any patch specific rules. |
| 130 | this.Validate(mainTransform); |
| 131 | |
| 132 | // ensure consistent File.Sequence within each Media |
| 133 | var baselineSymbol = baselineSymbolsById[mainTransform.Baseline]; |
| 134 | var mediaSymbol = patchMediaByDiskId[baselineSymbol.DiskId]; |
| 135 | |
| 136 | // Ensure that files are sequenced after the last file in any transform. |
| 137 | if (mainTransform.Transform.Tables.TryGetTable("Media", out var transformMediaTable)) |
| 138 | { |
| 139 | foreach (MediaRow transformMediaRow in transformMediaTable.Rows) |
| 140 | { |
| 141 | if (!mediaSymbol.LastSequence.HasValue || mediaSymbol.LastSequence < transformMediaRow.LastSequence) |
| 142 | { |
| 143 | // The Binder will pre-increment the sequence. |
| 144 | mediaSymbol.LastSequence = transformMediaRow.LastSequence; |
| 145 | } |
| 146 | } |
| 147 | } |
| 148 | |
| 149 | // Use the Media/@DiskId if greater than the last sequence for backward compatibility. |
| 150 | if (!mediaSymbol.LastSequence.HasValue || mediaSymbol.LastSequence < mediaSymbol.DiskId) |
| 151 | { |
| 152 | mediaSymbol.LastSequence = mediaSymbol.DiskId; |
| 153 | } |
| 154 | |
| 155 | // Ignore media table in the transform. |
| 156 | mainTransform.Transform.Tables.Remove("Media"); |
| 157 | mainTransform.Transform.Tables.Remove("MsiDigitalSignature"); |
| 158 | |
| 159 | var pairedTransform = this.BuildPairedTransform(summaryInfo, patchMetadata, patchSymbol, mainTransform.Transform, mediaSymbol, baselineSymbol, out var productCode); |
| 160 | |
| 161 | productCode = productCode.ToUpperInvariant(); |
| 162 | productCodes.Add(productCode); |
| 163 | validTransform.Add(Tuple.Create(productCode, mainTransform.Transform)); |
| 164 | |
| 165 | // Attach the main and paired transforms to the patch object. |
| 166 | var baseTransformName = mainTransform.Baseline; |
| 167 | var countSuffix = "." + validTransform.Count.ToString(CultureInfo.InvariantCulture); |
| 168 | |
| 169 | if (PatchConstants.PairedPatchTransformPrefix.Length + baseTransformName.Length + countSuffix.Length > PatchConstants.MaxPatchTransformName) |
| 170 | { |
| 171 | var trimmedTransformName = baseTransformName.Substring(0, PatchConstants.MaxPatchTransformName - PatchConstants.PairedPatchTransformPrefix.Length - countSuffix.Length); |
| 172 | |
| 173 | this.Messaging.Write(WindowsInstallerBackendWarnings.LongPatchBaselineIdTrimmed(baselineSymbol.SourceLineNumbers, baseTransformName, trimmedTransformName)); |
| 174 | |
| 175 | baseTransformName = trimmedTransformName; |
| 176 | } |
| 177 | |
| 178 | var transformName = baseTransformName + countSuffix; |
| 179 | subStorages.Add(new SubStorage(transformName, mainTransform.Transform)); |
| 180 | transformNames.Add(":" + transformName); |
| 181 | |
| 182 | var pairedTransformName = PatchConstants.PairedPatchTransformPrefix + transformName; |
| 183 | subStorages.Add(new SubStorage(pairedTransformName, pairedTransform)); |
| 184 | transformNames.Add(":" + pairedTransformName); |
| 185 | } |
| 186 | |
| 187 | if (validTransform.Count == 0) |
| 188 | { |
| 189 | this.Messaging.Write(ErrorMessages.PatchWithoutValidTransforms()); |
| 190 | return subStorages; |
| 191 | } |
| 192 | |
| 193 | // Validate that a patch authored as removable is actually removable |
| 194 | if (patchMetadata.TryGetValue("AllowRemoval", out var allowRemoval) && allowRemoval.Value == "1") |
| 195 | { |
| 196 | var uninstallable = true; |
| 197 | |
| 198 | foreach (var entry in validTransform) |
| 199 | { |
| 200 | uninstallable &= this.CheckUninstallableTransform(entry.Item1, entry.Item2); |
| 201 | } |
| 202 | |
| 203 | if (!uninstallable) |
| 204 | { |
| 205 | this.Messaging.Write(ErrorMessages.PatchNotRemovable()); |
| 206 | return subStorages; |
| 207 | } |
| 208 | } |
| 209 | |
| 210 | // Finish filling tables with transform-dependent data. |
| 211 | productCodes = FinalizePatchProductCodes(symbols, productCodes); |
| 212 | |
| 213 | // Semicolon delimited list of the product codes that can accept the patch. |
| 214 | summaryInfo.Add(SummaryInformationType.PatchProductCodes, new SummaryInformationSymbol(patchSymbol.SourceLineNumbers) |
| 215 | { |
| 216 | PropertyId = SummaryInformationType.PatchProductCodes, |
| 217 | Value = String.Join(";", productCodes) |
| 218 | }); |
| 219 | |
| 220 | // Semicolon delimited list of transform substorage names in the order they are applied. |
| 221 | summaryInfo.Add(SummaryInformationType.TransformNames, new SummaryInformationSymbol(patchSymbol.SourceLineNumbers) |
| 222 | { |
| 223 | PropertyId = SummaryInformationType.TransformNames, |
| 224 | Value = String.Join(";", transformNames) |
| 225 | }); |
| 226 | |
| 227 | // Put the summary information that was extracted back in now that it is updated. |
| 228 | foreach (var readSummaryInfo in summaryInfo.Values.OrderBy(s => s.PropertyId)) |
| 229 | { |
| 230 | section.AddSymbol(readSummaryInfo); |
| 231 | } |
| 232 | |
| 233 | this.SubStorages = subStorages; |
| 234 | |
| 235 | return subStorages; |
| 236 | } |
| 237 | |
| 238 | private Dictionary<SummaryInformationType, SummaryInformationSymbol> ExtractPatchSummaryInfo() |
| 239 | { |
| 240 | var result = new Dictionary<SummaryInformationType, SummaryInformationSymbol>(); |
| 241 | |
| 242 | foreach (var section in this.Intermediate.Sections) |
| 243 | { |
| 244 | // Remove all summary information from the symbols and remember those that |
| 245 | // are not calculated or reserved. |
| 246 | foreach (var patchSummaryInfo in section.Symbols.OfType<SummaryInformationSymbol>().ToList()) |
| 247 | { |
| 248 | section.RemoveSymbol(patchSummaryInfo); |
| 249 | |
| 250 | if (patchSummaryInfo.PropertyId != SummaryInformationType.PatchProductCodes && |
| 251 | patchSummaryInfo.PropertyId != SummaryInformationType.PatchCode && |
| 252 | patchSummaryInfo.PropertyId != SummaryInformationType.PatchInstallerRequirement && |
| 253 | patchSummaryInfo.PropertyId != SummaryInformationType.Reserved11 && |
| 254 | patchSummaryInfo.PropertyId != SummaryInformationType.Reserved14 && |
| 255 | patchSummaryInfo.PropertyId != SummaryInformationType.Reserved16) |
| 256 | { |
| 257 | result.Add(patchSummaryInfo.PropertyId, patchSummaryInfo); |
| 258 | } |
| 259 | } |
| 260 | } |
| 261 | |
| 262 | return result; |
| 263 | } |
| 264 | |
| 265 | private Dictionary<string, MsiPatchMetadataSymbol> PopulateSummaryInformation(Dictionary<SummaryInformationType, SummaryInformationSymbol> summaryInfo, List<IntermediateSymbol> symbols, WixPatchSymbol patchSymbol) |
| 266 | { |
| 267 | // PID_CODEPAGE |
| 268 | if (!summaryInfo.ContainsKey(SummaryInformationType.Codepage)) |
| 269 | { |
| 270 | // Set the code page by default to the same code page for the |
| 271 | // string pool in the database. |
| 272 | AddSummaryInformation(SummaryInformationType.Codepage, patchSymbol.Codepage?.ToString(CultureInfo.InvariantCulture) ?? "0", patchSymbol.SourceLineNumbers); |
| 273 | } |
| 274 | |
| 275 | // GUID patch code for the patch. |
| 276 | AddSummaryInformation(SummaryInformationType.PatchCode, patchSymbol.Id.Id, patchSymbol.SourceLineNumbers); |
| 277 | |
| 278 | // Indicates the minimum Windows Installer version that is required to install the patch. |
| 279 | AddSummaryInformation(SummaryInformationType.PatchInstallerRequirement, ((int)SummaryInformation.InstallerRequirement.Version31).ToString(CultureInfo.InvariantCulture), patchSymbol.SourceLineNumbers); |
| 280 | |
| 281 | if (!summaryInfo.ContainsKey(SummaryInformationType.Security)) |
| 282 | { |
| 283 | AddSummaryInformation(SummaryInformationType.Security, "4", patchSymbol.SourceLineNumbers); // Read-only enforced; |
| 284 | } |
| 285 | |
| 286 | // Use authored comments or default to display name. |
| 287 | MsiPatchMetadataSymbol commentsSymbol = null; |
| 288 | |
| 289 | var metadataSymbols = symbols.OfType<MsiPatchMetadataSymbol>().Where(t => String.IsNullOrEmpty(t.Company)).ToDictionary(t => t.Property); |
| 290 | |
| 291 | if (!summaryInfo.ContainsKey(SummaryInformationType.Title) && |
| 292 | metadataSymbols.TryGetValue("DisplayName", out var displayName)) |
| 293 | { |
| 294 | AddSummaryInformation(SummaryInformationType.Title, displayName.Value, displayName.SourceLineNumbers); |
| 295 | |
| 296 | // Default comments to use display name as-is. |
| 297 | commentsSymbol = displayName; |
| 298 | } |
| 299 | |
| 300 | // TODO: This code below seems unnecessary given the codepage is set at the top of this method. |
| 301 | //if (!summaryInfo.ContainsKey(SummaryInformationType.Codepage) && |
| 302 | // metadataValues.TryGetValue("CodePage", out var codepage)) |
| 303 | //{ |
| 304 | // AddSummaryInformation(SummaryInformationType.Codepage, codepage); |
| 305 | //} |
| 306 | |
| 307 | if (!summaryInfo.ContainsKey(SummaryInformationType.PatchPackageName) && |
| 308 | metadataSymbols.TryGetValue("Description", out var description)) |
| 309 | { |
| 310 | AddSummaryInformation(SummaryInformationType.PatchPackageName, description.Value, description.SourceLineNumbers); |
| 311 | } |
| 312 | |
| 313 | if (!summaryInfo.ContainsKey(SummaryInformationType.Author) && |
| 314 | metadataSymbols.TryGetValue("ManufacturerName", out var manufacturer)) |
| 315 | { |
| 316 | AddSummaryInformation(SummaryInformationType.Author, manufacturer.Value, manufacturer.SourceLineNumbers); |
| 317 | } |
| 318 | |
| 319 | // Special metadata marshalled through the build. |
| 320 | //var wixMetadataValues = symbols.OfType<WixPatchMetadataSymbol>().ToDictionary(t => t.Id.Id, t => t.Value); |
| 321 | |
| 322 | //if (wixMetadataValues.TryGetValue("Comments", out var wixComments)) |
| 323 | if (metadataSymbols.TryGetValue("Comments", out var wixComments)) |
| 324 | { |
| 325 | commentsSymbol = wixComments; |
| 326 | } |
| 327 | |
| 328 | // Write the package comments to summary info. |
| 329 | if (!summaryInfo.ContainsKey(SummaryInformationType.Comments) && |
| 330 | commentsSymbol != null) |
| 331 | { |
| 332 | AddSummaryInformation(SummaryInformationType.Comments, commentsSymbol.Value, commentsSymbol.SourceLineNumbers); |
| 333 | } |
| 334 | |
| 335 | return metadataSymbols; |
| 336 | |
| 337 | void AddSummaryInformation(SummaryInformationType type, string value, SourceLineNumber sourceLineNumber) |
| 338 | { |
| 339 | summaryInfo.Add(type, new SummaryInformationSymbol(sourceLineNumber) |
| 340 | { |
| 341 | PropertyId = type, |
| 342 | Value = value |
| 343 | }); |
| 344 | } |
| 345 | } |
| 346 | |
| 347 | /// <summary> |
| 348 | /// Ensure transform is uninstallable. |
| 349 | /// </summary> |
| 350 | /// <param name="productCode">Product code in transform.</param> |
| 351 | /// <param name="transform">Transform generated by torch.</param> |
| 352 | /// <returns>True if the transform is uninstallable</returns> |
| 353 | private bool CheckUninstallableTransform(string productCode, WindowsInstallerData transform) |
| 354 | { |
| 355 | var success = true; |
| 356 | |
| 357 | foreach (var tableName in PatchUninstallBreakingTables) |
| 358 | { |
| 359 | if (transform.TryGetTable(tableName, out var table)) |
| 360 | { |
| 361 | foreach (var row in table.Rows.Where(r => r.Operation == RowOperation.Add)) |
| 362 | { |
| 363 | success = false; |
| 364 | |
| 365 | var primaryKey = row.GetPrimaryKey('/') ?? String.Empty; |
| 366 | |
| 367 | this.Messaging.Write(ErrorMessages.NewRowAddedInTable(row.SourceLineNumbers, productCode, table.Name, primaryKey)); |
| 368 | } |
| 369 | } |
| 370 | } |
| 371 | |
| 372 | return success; |
| 373 | } |
| 374 | |
| 375 | private void Validate(PatchTransform patchTransform) |
| 376 | { |
| 377 | var transformPath = patchTransform.Baseline; |
| 378 | var transform = patchTransform.Transform; |
| 379 | |
| 380 | // Changing the ProdocutCode in a patch transform is not recommended. |
| 381 | if (transform.TryGetTable("Property", out var propertyTable)) |
| 382 | { |
| 383 | foreach (var row in propertyTable.Rows) |
| 384 | { |
| 385 | // Only interested in modified rows; fast check. |
| 386 | if (RowOperation.Modify == row.Operation && |
| 387 | "ProductCode".Equals(row.FieldAsString(0), StringComparison.Ordinal)) |
| 388 | { |
| 389 | this.Messaging.Write(WarningMessages.MajorUpgradePatchNotRecommended()); |
| 390 | } |
| 391 | } |
| 392 | } |
| 393 | |
| 394 | // If there is nothing in the component table we can return early because the remaining checks are component based. |
| 395 | if (!transform.TryGetTable("Component", out var componentTable)) |
| 396 | { |
| 397 | return; |
| 398 | } |
| 399 | |
| 400 | // Index Feature table row operations |
| 401 | var featureOps = new Dictionary<string, RowOperation>(); |
| 402 | if (transform.TryGetTable("Feature", out var featureTable)) |
| 403 | { |
| 404 | foreach (var row in featureTable.Rows) |
| 405 | { |
| 406 | featureOps[row.FieldAsString(0)] = row.Operation; |
| 407 | } |
| 408 | } |
| 409 | |
| 410 | // Index Component table and check for keypath modifications |
| 411 | var componentKeyPath = new Dictionary<string, string>(); |
| 412 | var deletedComponent = new Dictionary<string, Row>(); |
| 413 | foreach (var row in componentTable.Rows) |
| 414 | { |
| 415 | var id = row.FieldAsString(0); |
| 416 | var keypath = row.FieldAsString(5) ?? String.Empty; |
| 417 | |
| 418 | componentKeyPath.Add(id, keypath); |
| 419 | |
| 420 | if (RowOperation.Delete == row.Operation) |
| 421 | { |
| 422 | deletedComponent.Add(id, row); |
| 423 | } |
| 424 | else if (RowOperation.Modify == row.Operation) |
| 425 | { |
| 426 | if (row.Fields[1].Modified) |
| 427 | { |
| 428 | // Changing the guid of a component is equal to deleting the old one and adding a new one. |
| 429 | deletedComponent.Add(id, row); |
| 430 | } |
| 431 | |
| 432 | // If the keypath is modified its an error |
| 433 | if (row.Fields[5].Modified) |
| 434 | { |
| 435 | this.Messaging.Write(ErrorMessages.InvalidKeypathChange(row.SourceLineNumbers, id, transformPath)); |
| 436 | } |
| 437 | } |
| 438 | } |
| 439 | |
| 440 | // Verify changes in the file table |
| 441 | if (transform.TryGetTable("File", out var fileTable)) |
| 442 | { |
| 443 | var componentWithChangedKeyPath = new Dictionary<string, string>(); |
| 444 | foreach (FileRow row in fileTable.Rows) |
| 445 | { |
| 446 | if (RowOperation.None == row.Operation) |
| 447 | { |
| 448 | continue; |
| 449 | } |
| 450 | |
| 451 | var fileId = row.File; |
| 452 | var componentId = row.Component; |
| 453 | |
| 454 | // If this file is the keypath of a component |
| 455 | if (componentKeyPath.TryGetValue(componentId, out var keyPath) && keyPath.Equals(fileId, StringComparison.Ordinal)) |
| 456 | { |
| 457 | if (row.Fields[2].Modified) |
| 458 | { |
| 459 | // You can't change the filename of a file that is the keypath of a component. |
| 460 | this.Messaging.Write(ErrorMessages.InvalidKeypathChange(row.SourceLineNumbers, componentId, transformPath)); |
| 461 | } |
| 462 | |
| 463 | if (!componentWithChangedKeyPath.ContainsKey(componentId)) |
| 464 | { |
| 465 | componentWithChangedKeyPath.Add(componentId, fileId); |
| 466 | } |
| 467 | } |
| 468 | |
| 469 | if (RowOperation.Delete == row.Operation) |
| 470 | { |
| 471 | // If the file is removed from a component that is not deleted. |
| 472 | if (!deletedComponent.ContainsKey(componentId)) |
| 473 | { |
| 474 | var foundRemoveFileEntry = false; |
| 475 | var filename = this.BackendHelper.GetMsiFileName(row.FieldAsString(2), false, true); |
| 476 | |
| 477 | if (transform.TryGetTable("RemoveFile", out var removeFileTable)) |
| 478 | { |
| 479 | foreach (var removeFileRow in removeFileTable.Rows) |
| 480 | { |
| 481 | if (RowOperation.Delete == removeFileRow.Operation) |
| 482 | { |
| 483 | continue; |
| 484 | } |
| 485 | |
| 486 | if (componentId == removeFileRow.FieldAsString(1)) |
| 487 | { |
| 488 | // Check if there is a RemoveFile entry for this file |
| 489 | if (null != removeFileRow[2]) |
| 490 | { |
| 491 | var removeFileName = this.BackendHelper.GetMsiFileName(removeFileRow.FieldAsString(2), false, true); |
| 492 | |
| 493 | // Convert the MSI format for a wildcard string to Regex format. |
| 494 | removeFileName = removeFileName.Replace('.', '|').Replace('?', '.').Replace("*", ".*").Replace("|", "\\."); |
| 495 | |
| 496 | var regex = new Regex(removeFileName, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); |
| 497 | if (regex.IsMatch(filename)) |
| 498 | { |
| 499 | foundRemoveFileEntry = true; |
| 500 | break; |
| 501 | } |
| 502 | } |
| 503 | } |
| 504 | } |
| 505 | } |
| 506 | |
| 507 | if (!foundRemoveFileEntry) |
| 508 | { |
| 509 | this.Messaging.Write(WarningMessages.InvalidRemoveFile(row.SourceLineNumbers, fileId, componentId)); |
| 510 | } |
| 511 | } |
| 512 | } |
| 513 | } |
| 514 | } |
| 515 | |
| 516 | var featureComponentsTable = transform.Tables["FeatureComponents"]; |
| 517 | |
| 518 | if (0 < deletedComponent.Count) |
| 519 | { |
| 520 | // Index FeatureComponents table. |
| 521 | var featureComponents = new Dictionary<string, List<string>>(); |
| 522 | |
| 523 | if (null != featureComponentsTable) |
| 524 | { |
| 525 | foreach (var row in featureComponentsTable.Rows) |
| 526 | { |
| 527 | var componentId = row.FieldAsString(1); |
| 528 | |
| 529 | if (!featureComponents.TryGetValue(componentId, out var features)) |
| 530 | { |
| 531 | features = new List<string>(); |
| 532 | featureComponents.Add(componentId, features); |
| 533 | } |
| 534 | |
| 535 | features.Add(row.FieldAsString(0)); |
| 536 | } |
| 537 | } |
| 538 | |
| 539 | // Check to make sure if a component was deleted, the feature was too. |
| 540 | foreach (var entry in deletedComponent) |
| 541 | { |
| 542 | if (featureComponents.TryGetValue(entry.Key, out var features)) |
| 543 | { |
| 544 | foreach (var featureId in features) |
| 545 | { |
| 546 | if (!featureOps.TryGetValue(featureId, out var op) || op != RowOperation.Delete) |
| 547 | { |
| 548 | // The feature was not deleted. |
| 549 | this.Messaging.Write(ErrorMessages.InvalidRemoveComponent(((Row)entry.Value).SourceLineNumbers, entry.Key.ToString(), featureId, transformPath)); |
| 550 | } |
| 551 | } |
| 552 | } |
| 553 | } |
| 554 | } |
| 555 | |
| 556 | // Warn if new components are added to existing features |
| 557 | if (null != featureComponentsTable) |
| 558 | { |
| 559 | foreach (var row in featureComponentsTable.Rows) |
| 560 | { |
| 561 | if (RowOperation.Add == row.Operation) |
| 562 | { |
| 563 | // Check if the feature is in the Feature table |
| 564 | var feature_ = row.FieldAsString(0); |
| 565 | var component_ = row.FieldAsString(1); |
| 566 | |
| 567 | // Features may not be present if not referenced |
| 568 | if (!featureOps.ContainsKey(feature_) || RowOperation.Add != (RowOperation)featureOps[feature_]) |
| 569 | { |
| 570 | this.Messaging.Write(WarningMessages.NewComponentAddedToExistingFeature(row.SourceLineNumbers, component_, feature_, transformPath)); |
| 571 | } |
| 572 | } |
| 573 | } |
| 574 | } |
| 575 | } |
| 576 | |
| 577 | /// <summary> |
| 578 | /// Create the #transform for the given main transform. |
| 579 | /// </summary> |
| 580 | private WindowsInstallerData BuildPairedTransform(Dictionary<SummaryInformationType, SummaryInformationSymbol> summaryInfo, Dictionary<string, MsiPatchMetadataSymbol> patchMetadata, WixPatchSymbol patchIdSymbol, WindowsInstallerData mainTransform, MediaSymbol mediaSymbol, WixPatchBaselineSymbol baselineSymbol, out string productCode) |
| 581 | { |
| 582 | productCode = null; |
| 583 | |
| 584 | var pairedTransform = new WindowsInstallerData(null) |
| 585 | { |
| 586 | Type = OutputType.Transform, |
| 587 | Codepage = mainTransform.Codepage |
| 588 | }; |
| 589 | |
| 590 | // lookup productVersion property to correct summaryInformation |
| 591 | var newProductVersion = mainTransform.Tables["Property"]?.Rows.FirstOrDefault(r => r.FieldAsString(0) == "ProductVersion")?.FieldAsString(1); |
| 592 | |
| 593 | var mainSummaryTable = mainTransform.Tables["_SummaryInformation"]; |
| 594 | var mainSummaryRows = mainSummaryTable.Rows.ToDictionary(r => r.FieldAsInteger(0)); |
| 595 | |
| 596 | var baselineValidationFlags = ((int)baselineSymbol.ValidationFlags).ToString(CultureInfo.InvariantCulture); |
| 597 | |
| 598 | if (!mainSummaryRows.ContainsKey((int)SummaryInformationType.TransformValidationFlags)) |
| 599 | { |
| 600 | var mainSummaryRow = mainSummaryTable.CreateRow(baselineSymbol.SourceLineNumbers); |
| 601 | mainSummaryRow[0] = (int)SummaryInformationType.TransformValidationFlags; |
| 602 | mainSummaryRow[1] = baselineValidationFlags; |
| 603 | } |
| 604 | |
| 605 | // copy summary information from core transform |
| 606 | var pairedSummaryTable = pairedTransform.EnsureTable(this.tableDefinitions["_SummaryInformation"]); |
| 607 | |
| 608 | foreach (var mainSummaryRow in mainSummaryTable.Rows) |
| 609 | { |
| 610 | var type = (SummaryInformationType)mainSummaryRow.FieldAsInteger(0); |
| 611 | var value = mainSummaryRow.FieldAsString(1); |
| 612 | switch (type) |
| 613 | { |
| 614 | case SummaryInformationType.TransformProductCodes: |
| 615 | var propertyData = value.Split(';'); |
| 616 | var oldProductVersion = propertyData[0].Substring(38); |
| 617 | var upgradeCode = propertyData[2]; |
| 618 | productCode = propertyData[0].Substring(0, 38); |
| 619 | |
| 620 | if (newProductVersion == null) |
| 621 | { |
| 622 | newProductVersion = oldProductVersion; |
| 623 | } |
| 624 | |
| 625 | // Force mainTranform to 'old;new;upgrade' and pairedTransform to 'new;new;upgrade' |
| 626 | mainSummaryRow[1] = String.Concat(productCode, oldProductVersion, ';', productCode, newProductVersion, ';', upgradeCode); |
| 627 | value = String.Concat(productCode, newProductVersion, ';', productCode, newProductVersion, ';', upgradeCode); |
| 628 | break; |
| 629 | case SummaryInformationType.TransformValidationFlags: // use validation flags authored into the patch XML. |
| 630 | value = baselineValidationFlags; |
| 631 | mainSummaryRow[1] = value; |
| 632 | break; |
| 633 | } |
| 634 | |
| 635 | var pairedSummaryRow = pairedSummaryTable.CreateRow(mainSummaryRow.SourceLineNumbers); |
| 636 | pairedSummaryRow[0] = mainSummaryRow[0]; |
| 637 | pairedSummaryRow[1] = value; |
| 638 | } |
| 639 | |
| 640 | if (productCode == null) |
| 641 | { |
| 642 | this.Messaging.Write(ErrorMessages.CouldNotDetermineProductCodeFromTransformSummaryInfo()); |
| 643 | return null; |
| 644 | } |
| 645 | |
| 646 | // Copy File table |
| 647 | if (mainTransform.Tables.TryGetTable("File", out var mainFileTable) && 0 < mainFileTable.Rows.Count) |
| 648 | { |
| 649 | var pairedFileTable = pairedTransform.EnsureTable(mainFileTable.Definition); |
| 650 | |
| 651 | foreach (var mainFileRow in mainFileTable.Rows.Cast<FileRow>()) |
| 652 | { |
| 653 | // Set File.Sequence to non null to satisfy transform bind and suppress any |
| 654 | // change to File.Sequence to avoid bloat. |
| 655 | mainFileRow.Sequence = 1; |
| 656 | mainFileRow.Fields[7].Modified = false; |
| 657 | |
| 658 | // Override authored media to the media provided in the patch. |
| 659 | mainFileRow.DiskId = mediaSymbol.DiskId; |
| 660 | |
| 661 | // Delete's don't need rows in the paired transform. |
| 662 | if (mainFileRow.Operation == RowOperation.Delete) |
| 663 | { |
| 664 | continue; |
| 665 | } |
| 666 | |
| 667 | var pairedFileRow = (FileRow)pairedFileTable.CreateRow(mainFileRow.SourceLineNumbers); |
| 668 | pairedFileRow.Operation = RowOperation.Modify; |
| 669 | mainFileRow.CopyTo(pairedFileRow); |
| 670 | |
| 671 | // Force modified File rows to appear in the transform. |
| 672 | switch (mainFileRow.Operation) |
| 673 | { |
| 674 | case RowOperation.Modify: |
| 675 | case RowOperation.Add: |
| 676 | pairedFileRow.Attributes |= WindowsInstallerConstants.MsidbFileAttributesPatchAdded; |
| 677 | pairedFileRow.Fields[6].Modified = true; |
| 678 | pairedFileRow.Operation = mainFileRow.Operation; |
| 679 | break; |
| 680 | default: |
| 681 | pairedFileRow.Fields[6].Modified = false; |
| 682 | break; |
| 683 | } |
| 684 | } |
| 685 | } |
| 686 | |
| 687 | // Add Media row to pairedTransform |
| 688 | var pairedMediaTable = pairedTransform.EnsureTable(this.tableDefinitions["Media"]); |
| 689 | var pairedMediaRow = (MediaRow)pairedMediaTable.CreateRow(mediaSymbol.SourceLineNumbers); |
| 690 | pairedMediaRow.Operation = RowOperation.Add; |
| 691 | pairedMediaRow.DiskId = mediaSymbol.DiskId; |
| 692 | pairedMediaRow.LastSequence = mediaSymbol.LastSequence ?? 0; |
| 693 | pairedMediaRow.DiskPrompt = mediaSymbol.DiskPrompt; |
| 694 | pairedMediaRow.Cabinet = mediaSymbol.Cabinet; |
| 695 | pairedMediaRow.VolumeLabel = mediaSymbol.VolumeLabel; |
| 696 | pairedMediaRow.Source = mediaSymbol.Source; |
| 697 | |
| 698 | // Add PatchPackage for this Media |
| 699 | var pairedPackageTable = pairedTransform.EnsureTable(this.tableDefinitions["PatchPackage"]); |
| 700 | pairedPackageTable.Operation = TableOperation.Add; |
| 701 | var pairedPackageRow = pairedPackageTable.CreateRow(mediaSymbol.SourceLineNumbers); |
| 702 | pairedPackageRow.Operation = RowOperation.Add; |
| 703 | pairedPackageRow[0] = patchIdSymbol.Id.Id; |
| 704 | pairedPackageRow[1] = mediaSymbol.DiskId; |
| 705 | |
| 706 | // Add the property to the patch transform's Property table. |
| 707 | var pairedPropertyTable = pairedTransform.EnsureTable(this.tableDefinitions["Property"]); |
| 708 | pairedPropertyTable.Operation = TableOperation.Add; |
| 709 | |
| 710 | // Add property to both identify client patches and whether those patches are removable or not |
| 711 | patchMetadata.TryGetValue("AllowRemoval", out var allowRemovalSymbol); |
| 712 | |
| 713 | var pairedPropertyRow = pairedPropertyTable.CreateRow(allowRemovalSymbol?.SourceLineNumbers); |
| 714 | pairedPropertyRow.Operation = RowOperation.Add; |
| 715 | pairedPropertyRow[0] = String.Concat(patchIdSymbol.ClientPatchId, ".AllowRemoval"); |
| 716 | pairedPropertyRow[1] = allowRemovalSymbol?.Value ?? "0"; |
| 717 | |
| 718 | // Add this patch code GUID to the patch transform to identify |
| 719 | // which patches are installed, including in multi-patch |
| 720 | // installations. |
| 721 | pairedPropertyRow = pairedPropertyTable.CreateRow(patchIdSymbol.SourceLineNumbers); |
| 722 | pairedPropertyRow.Operation = RowOperation.Add; |
| 723 | pairedPropertyRow[0] = String.Concat(patchIdSymbol.ClientPatchId, ".PatchCode"); |
| 724 | pairedPropertyRow[1] = patchIdSymbol.Id.Id; |
| 725 | |
| 726 | // Add PATCHNEWPACKAGECODE to apply to admin layouts. |
| 727 | pairedPropertyRow = pairedPropertyTable.CreateRow(patchIdSymbol.SourceLineNumbers); |
| 728 | pairedPropertyRow.Operation = RowOperation.Add; |
| 729 | pairedPropertyRow[0] = "PATCHNEWPACKAGECODE"; |
| 730 | pairedPropertyRow[1] = patchIdSymbol.Id.Id; |
| 731 | |
| 732 | // Add PATCHNEWSUMMARYCOMMENTS and PATCHNEWSUMMARYSUBJECT to apply to admin layouts. |
| 733 | if (summaryInfo.TryGetValue(SummaryInformationType.Subject, out var subjectSymbol)) |
| 734 | { |
| 735 | pairedPropertyRow = pairedPropertyTable.CreateRow(subjectSymbol.SourceLineNumbers); |
| 736 | pairedPropertyRow.Operation = RowOperation.Add; |
| 737 | pairedPropertyRow[0] = "PATCHNEWSUMMARYSUBJECT"; |
| 738 | pairedPropertyRow[1] = subjectSymbol.Value; |
| 739 | } |
| 740 | |
| 741 | if (summaryInfo.TryGetValue(SummaryInformationType.Comments, out var commentsSymbol)) |
| 742 | { |
| 743 | pairedPropertyRow = pairedPropertyTable.CreateRow(commentsSymbol.SourceLineNumbers); |
| 744 | pairedPropertyRow.Operation = RowOperation.Add; |
| 745 | pairedPropertyRow[0] = "PATCHNEWSUMMARYCOMMENTS"; |
| 746 | pairedPropertyRow[1] = commentsSymbol.Value; |
| 747 | } |
| 748 | |
| 749 | return pairedTransform; |
| 750 | } |
| 751 | |
| 752 | private static SortedSet<string> FinalizePatchProductCodes(List<IntermediateSymbol> symbols, SortedSet<string> productCodes) |
| 753 | { |
| 754 | var patchTargetSymbols = symbols.OfType<WixPatchTargetSymbol>().ToList(); |
| 755 | |
| 756 | if (patchTargetSymbols.Any()) |
| 757 | { |
| 758 | var targets = new SortedSet<string>(); |
| 759 | var replace = true; |
| 760 | foreach (var wixPatchTargetRow in patchTargetSymbols) |
| 761 | { |
| 762 | var target = wixPatchTargetRow.ProductCode.ToUpperInvariant(); |
| 763 | if (target == "*") |
| 764 | { |
| 765 | replace = false; |
| 766 | } |
| 767 | else |
| 768 | { |
| 769 | targets.Add(target); |
| 770 | } |
| 771 | } |
| 772 | |
| 773 | // Replace the target ProductCodes with the authored list. |
| 774 | if (replace) |
| 775 | { |
| 776 | productCodes = targets; |
| 777 | } |
| 778 | else |
| 779 | { |
| 780 | // Copy the authored target ProductCodes into the list. |
| 781 | foreach (var target in targets) |
| 782 | { |
| 783 | productCodes.Add(target); |
| 784 | } |
| 785 | } |
| 786 | } |
| 787 | |
| 788 | return productCodes; |
| 789 | } |
| 790 | } |
| 791 | } |