aboutsummaryrefslogtreecommitdiff
path: root/src/WixToolset.Core.WindowsInstaller/Bind/GenerateTransformCommand.cs
diff options
context:
space:
mode:
authorRob Mensching <rob@firegiant.com>2020-01-24 15:27:20 -0800
committerRob Mensching <rob@firegiant.com>2020-02-05 16:15:47 -0800
commit6ff680e386b1543ad1a58d1b1d465ce8aa20bc7d (patch)
treec717333cd10d5592e59dfb898b391275bba1f389 /src/WixToolset.Core.WindowsInstaller/Bind/GenerateTransformCommand.cs
parent6e2e67ab55c75f4655397588c0dcc64f50d22f92 (diff)
downloadwix-6ff680e386b1543ad1a58d1b1d465ce8aa20bc7d.tar.gz
wix-6ff680e386b1543ad1a58d1b1d465ce8aa20bc7d.tar.bz2
wix-6ff680e386b1543ad1a58d1b1d465ce8aa20bc7d.zip
Start on new patch infrastructure
Diffstat (limited to 'src/WixToolset.Core.WindowsInstaller/Bind/GenerateTransformCommand.cs')
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/GenerateTransformCommand.cs588
1 files changed, 588 insertions, 0 deletions
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/GenerateTransformCommand.cs b/src/WixToolset.Core.WindowsInstaller/Bind/GenerateTransformCommand.cs
new file mode 100644
index 00000000..8a7dd702
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/GenerateTransformCommand.cs
@@ -0,0 +1,588 @@
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
4{
5 using System;
6 using System.Collections.Generic;
7 using System.Globalization;
8 using WixToolset.Core.WindowsInstaller.Msi;
9 using WixToolset.Data;
10 using WixToolset.Data.Tuples;
11 using WixToolset.Data.WindowsInstaller;
12 using WixToolset.Data.WindowsInstaller.Rows;
13 using WixToolset.Extensibility;
14 using WixToolset.Extensibility.Services;
15
16 /// <summary>
17 /// Creates a transform by diffing two outputs.
18 /// </summary>
19 public sealed class GenerateTransformCommand
20 {
21 private const char sectionDelimiter = '/';
22 private readonly IMessaging messaging;
23 private SummaryInformationStreams transformSummaryInfo;
24
25 /// <summary>
26 /// Instantiates a new Differ class.
27 /// </summary>
28 public GenerateTransformCommand(IMessaging messaging, WindowsInstallerData targetOutput, WindowsInstallerData updatedOutput, bool showPedanticMessages)
29 {
30 this.messaging = messaging;
31 this.TargetOutput = targetOutput;
32 this.UpdatedOutput = updatedOutput;
33 this.ShowPedanticMessages = showPedanticMessages;
34 }
35
36 private WindowsInstallerData TargetOutput { get; }
37
38 private WindowsInstallerData UpdatedOutput { get; }
39
40 private TransformFlags ValidationFlags { get; }
41
42 /// <summary>
43 /// Gets or sets the option to show pedantic messages.
44 /// </summary>
45 /// <value>The option to show pedantic messages.</value>
46 private bool ShowPedanticMessages { get; }
47
48 /// <summary>
49 /// Gets or sets the option to suppress keeping special rows.
50 /// </summary>
51 /// <value>The option to suppress keeping special rows.</value>
52 private bool SuppressKeepingSpecialRows { get; }
53
54 /// <summary>
55 /// Gets or sets the flag to determine if all rows, even unchanged ones will be persisted in the output.
56 /// </summary>
57 /// <value>The option to keep all rows including unchanged rows.</value>
58 private bool PreserveUnchangedRows { get; }
59
60 public WindowsInstallerData Transform { get; private set; }
61
62 /// <summary>
63 /// Creates a transform by diffing two outputs.
64 /// </summary>
65 /// <param name="targetOutput">The target output.</param>
66 /// <param name="updatedOutput">The updated output.</param>
67 /// <param name="validationFlags"></param>
68 /// <returns>The transform.</returns>
69 public WindowsInstallerData Execute()
70 {
71 var targetOutput = this.TargetOutput;
72 var updatedOutput = this.UpdatedOutput;
73 var validationFlags = this.ValidationFlags;
74
75 var transform = new WindowsInstallerData(null)
76 {
77 Type = OutputType.Transform,
78 Codepage = updatedOutput.Codepage
79 };
80
81 this.transformSummaryInfo = new SummaryInformationStreams();
82
83 // compare the codepages
84 if (targetOutput.Codepage != updatedOutput.Codepage && 0 == (TransformFlags.ErrorChangeCodePage & validationFlags))
85 {
86 this.messaging.Write(ErrorMessages.OutputCodepageMismatch(targetOutput.SourceLineNumbers, targetOutput.Codepage, updatedOutput.Codepage));
87 if (null != updatedOutput.SourceLineNumbers)
88 {
89 this.messaging.Write(ErrorMessages.OutputCodepageMismatch2(updatedOutput.SourceLineNumbers));
90 }
91 }
92
93 // compare the output types
94 if (targetOutput.Type != updatedOutput.Type)
95 {
96 throw new WixException(ErrorMessages.OutputTypeMismatch(targetOutput.SourceLineNumbers, targetOutput.Type.ToString(), updatedOutput.Type.ToString()));
97 }
98
99 // compare the contents of the tables
100 foreach (var targetTable in targetOutput.Tables)
101 {
102 var updatedTable = updatedOutput.Tables[targetTable.Name];
103 var operation = TableOperation.None;
104
105 var rows = this.CompareTables(targetOutput, targetTable, updatedTable, out operation);
106
107 if (TableOperation.Drop == operation)
108 {
109 var droppedTable = transform.EnsureTable(targetTable.Definition);
110 droppedTable.Operation = TableOperation.Drop;
111 }
112 else if (TableOperation.None == operation)
113 {
114 var modified = transform.EnsureTable(updatedTable.Definition);
115 foreach (var row in rows)
116 {
117 modified.Rows.Add(row);
118 }
119 }
120 }
121
122 // added tables
123 foreach (var updatedTable in updatedOutput.Tables)
124 {
125 if (null == targetOutput.Tables[updatedTable.Name])
126 {
127 var addedTable = transform.EnsureTable(updatedTable.Definition);
128 addedTable.Operation = TableOperation.Add;
129
130 foreach (var updatedRow in updatedTable.Rows)
131 {
132 updatedRow.Operation = RowOperation.Add;
133 updatedRow.SectionId = sectionDelimiter + updatedRow.SectionId;
134 addedTable.Rows.Add(updatedRow);
135 }
136 }
137 }
138
139 // set summary information properties
140 if (!this.SuppressKeepingSpecialRows)
141 {
142 var summaryInfoTable = transform.Tables["_SummaryInformation"];
143 this.UpdateTransformSummaryInformationTable(summaryInfoTable, validationFlags);
144 }
145
146 this.Transform = transform;
147 return this.Transform;
148 }
149
150 /// <summary>
151 /// Add a row to the <paramref name="index"/> using the primary key.
152 /// </summary>
153 /// <param name="index">The indexed rows.</param>
154 /// <param name="row">The row to index.</param>
155 private void AddIndexedRow(Dictionary<string, Row> index, Row row)
156 {
157 var primaryKey = row.GetPrimaryKey();
158
159 if (null != primaryKey)
160 {
161 if (index.TryGetValue(primaryKey, out var collisionRow))
162 {
163 // Overriding WixActionRows have a primary key defined and take precedence in the index.
164 if (row is WixActionRow actionRow)
165 {
166 // If the current row is not overridable, see if the indexed row is.
167 if (!actionRow.Overridable)
168 {
169 if (collisionRow is WixActionRow indexedRow && indexedRow.Overridable)
170 {
171 // The indexed key is overridable and should be replaced.
172 index[primaryKey] = actionRow;
173 }
174 }
175
176 // If we got this far, the row does not need to be indexed.
177 return;
178 }
179
180 if (this.ShowPedanticMessages)
181 {
182 this.messaging.Write(ErrorMessages.DuplicatePrimaryKey(row.SourceLineNumbers, primaryKey, row.Table.Name));
183 }
184 }
185 else
186 {
187 index.Add(primaryKey, row);
188 }
189 }
190 else // use the string representation of the row as its primary key (it may not be unique)
191 {
192 // this is provided for compatibility with unreal tables with no primary key
193 // all real tables must specify at least one column as the primary key
194 primaryKey = row.ToString();
195 index[primaryKey] = row;
196 }
197 }
198
199 private bool CompareRows(Table targetTable, Row targetRow, Row updatedRow, out Row comparedRow)
200 {
201 comparedRow = null;
202
203 var keepRow = false;
204
205 if (null == targetRow ^ null == updatedRow)
206 {
207 if (null == targetRow)
208 {
209 updatedRow.Operation = RowOperation.Add;
210 comparedRow = updatedRow;
211 }
212 else if (null == updatedRow)
213 {
214 targetRow.Operation = RowOperation.Delete;
215 targetRow.SectionId += sectionDelimiter;
216
217 comparedRow = targetRow;
218 keepRow = true;
219 }
220 }
221 else // possibly modified
222 {
223 updatedRow.Operation = RowOperation.None;
224 if (!this.SuppressKeepingSpecialRows && "_SummaryInformation" == targetTable.Name)
225 {
226 // ignore rows that shouldn't be in a transform
227 if (Enum.IsDefined(typeof(SummaryInformation.Transform), (int)updatedRow[0]))
228 {
229 updatedRow.SectionId = targetRow.SectionId + sectionDelimiter + updatedRow.SectionId;
230 comparedRow = updatedRow;
231 keepRow = true;
232 }
233 }
234 else
235 {
236 if (this.PreserveUnchangedRows)
237 {
238 keepRow = true;
239 }
240
241 for (var i = 0; i < updatedRow.Fields.Length; i++)
242 {
243 var columnDefinition = updatedRow.Fields[i].Column;
244
245 if (columnDefinition.Unreal)
246 {
247 }
248 else if (!columnDefinition.PrimaryKey)
249 {
250 var modified = false;
251
252 if (i >= targetRow.Fields.Length)
253 {
254 columnDefinition.Added = true;
255 modified = true;
256 }
257 else if (ColumnType.Number == columnDefinition.Type && !columnDefinition.IsLocalizable)
258 {
259 if (null == targetRow[i] ^ null == updatedRow[i])
260 {
261 modified = true;
262 }
263 else if (null != targetRow[i] && null != updatedRow[i])
264 {
265 modified = (targetRow.FieldAsInteger(i) != updatedRow.FieldAsInteger(i));
266 }
267 }
268 else if (ColumnType.Preserved == columnDefinition.Type)
269 {
270 updatedRow.Fields[i].PreviousData = targetRow.FieldAsString(i);
271
272 // keep rows containing preserved fields so the historical data is available to the binder
273 keepRow = !this.SuppressKeepingSpecialRows;
274 }
275 else if (ColumnType.Object == columnDefinition.Type)
276 {
277 var targetObjectField = (ObjectField)targetRow.Fields[i];
278 var updatedObjectField = (ObjectField)updatedRow.Fields[i];
279
280 updatedObjectField.PreviousEmbeddedFileIndex = targetObjectField.EmbeddedFileIndex;
281 updatedObjectField.PreviousBaseUri = targetObjectField.BaseUri;
282
283 // always keep a copy of the previous data even if they are identical
284 // This makes diff.wixmst clean and easier to control patch logic
285 updatedObjectField.PreviousData = (string)targetObjectField.Data;
286
287 // always remember the unresolved data for target build
288 updatedObjectField.UnresolvedPreviousData = targetObjectField.UnresolvedData;
289
290 // keep rows containing object fields so the files can be compared in the binder
291 keepRow = !this.SuppressKeepingSpecialRows;
292 }
293 else
294 {
295 modified = (targetRow.FieldAsString(i) != updatedRow.FieldAsString(i));
296 }
297
298 if (modified)
299 {
300 if (null != updatedRow.Fields[i].PreviousData)
301 {
302 updatedRow.Fields[i].PreviousData = targetRow.FieldAsString(i);
303 }
304
305 updatedRow.Fields[i].Modified = true;
306 updatedRow.Operation = RowOperation.Modify;
307 keepRow = true;
308 }
309 }
310 }
311
312 if (keepRow)
313 {
314 comparedRow = updatedRow;
315 comparedRow.SectionId = targetRow.SectionId + sectionDelimiter + updatedRow.SectionId;
316 }
317 }
318 }
319
320 return keepRow;
321 }
322
323 private List<Row> CompareTables(WindowsInstallerData targetOutput, Table targetTable, Table updatedTable, out TableOperation operation)
324 {
325 var rows = new List<Row>();
326 operation = TableOperation.None;
327
328 // dropped tables
329 if (null == updatedTable ^ null == targetTable)
330 {
331 if (null == targetTable)
332 {
333 operation = TableOperation.Add;
334 rows.AddRange(updatedTable.Rows);
335 }
336 else if (null == updatedTable)
337 {
338 operation = TableOperation.Drop;
339 }
340 }
341 else // possibly modified tables
342 {
343 var updatedPrimaryKeys = new Dictionary<string, Row>();
344 var targetPrimaryKeys = new Dictionary<string, Row>();
345
346 // compare the table definitions
347 if (0 != targetTable.Definition.CompareTo(updatedTable.Definition))
348 {
349 // continue to the next table; may be more mismatches
350 this.messaging.Write(ErrorMessages.DatabaseSchemaMismatch(targetOutput.SourceLineNumbers, targetTable.Name));
351 }
352 else
353 {
354 this.IndexPrimaryKeys(targetTable, targetPrimaryKeys, updatedTable, updatedPrimaryKeys);
355
356 // diff the target and updated rows
357 foreach (var targetPrimaryKeyEntry in targetPrimaryKeys)
358 {
359 var targetPrimaryKey = targetPrimaryKeyEntry.Key;
360 var targetRow = targetPrimaryKeyEntry.Value;
361 updatedPrimaryKeys.TryGetValue(targetPrimaryKey, out var updatedRow);
362
363 var keepRow = this.CompareRows(targetTable, targetRow, updatedRow, out var compared);
364
365 if (keepRow)
366 {
367 rows.Add(compared);
368 }
369 }
370
371 // find the inserted rows
372 foreach (var updatedPrimaryKeyEntry in updatedPrimaryKeys)
373 {
374 var updatedPrimaryKey = updatedPrimaryKeyEntry.Key;
375
376 if (!targetPrimaryKeys.ContainsKey(updatedPrimaryKey))
377 {
378 var updatedRow = updatedPrimaryKeyEntry.Value;
379
380 updatedRow.Operation = RowOperation.Add;
381 updatedRow.SectionId = sectionDelimiter + updatedRow.SectionId;
382 rows.Add(updatedRow);
383 }
384 }
385 }
386 }
387
388 return rows;
389 }
390
391 private void IndexPrimaryKeys(Table targetTable, Dictionary<string, Row> targetPrimaryKeys, Table updatedTable, Dictionary<string, Row> updatedPrimaryKeys)
392 {
393 // index the target rows
394 foreach (var row in targetTable.Rows)
395 {
396 this.AddIndexedRow(targetPrimaryKeys, row);
397
398 if ("Property" == targetTable.Name)
399 {
400 var id = row.FieldAsString(0);
401
402 if ("ProductCode" == id)
403 {
404 this.transformSummaryInfo.TargetProductCode = row.FieldAsString(1);
405
406 if ("*" == this.transformSummaryInfo.TargetProductCode)
407 {
408 this.messaging.Write(ErrorMessages.ProductCodeInvalidForTransform(row.SourceLineNumbers));
409 }
410 }
411 else if ("ProductVersion" == id)
412 {
413 this.transformSummaryInfo.TargetProductVersion = row.FieldAsString(1);
414 }
415 else if ("UpgradeCode" == id)
416 {
417 this.transformSummaryInfo.TargetUpgradeCode = row.FieldAsString(1);
418 }
419 }
420 else if ("_SummaryInformation" == targetTable.Name)
421 {
422 var id = row.FieldAsInteger(0);
423
424 if (1 == id) // PID_CODEPAGE
425 {
426 this.transformSummaryInfo.TargetSummaryInfoCodepage = row.FieldAsString(1);
427 }
428 else if (7 == id) // PID_TEMPLATE
429 {
430 this.transformSummaryInfo.TargetPlatformAndLanguage = row.FieldAsString(1);
431 }
432 else if (14 == id) // PID_PAGECOUNT
433 {
434 this.transformSummaryInfo.TargetMinimumVersion = row.FieldAsString(1);
435 }
436 }
437 }
438
439 // index the updated rows
440 foreach (var row in updatedTable.Rows)
441 {
442 this.AddIndexedRow(updatedPrimaryKeys, row);
443
444 if ("Property" == updatedTable.Name)
445 {
446 var id = row.FieldAsString(0);
447
448 if ("ProductCode" == id)
449 {
450 this.transformSummaryInfo.UpdatedProductCode = row.FieldAsString(1);
451
452 if ("*" == this.transformSummaryInfo.UpdatedProductCode)
453 {
454 this.messaging.Write(ErrorMessages.ProductCodeInvalidForTransform(row.SourceLineNumbers));
455 }
456 }
457 else if ("ProductVersion" == id)
458 {
459 this.transformSummaryInfo.UpdatedProductVersion = row.FieldAsString(1);
460 }
461 }
462 else if ("_SummaryInformation" == updatedTable.Name)
463 {
464 var id = row.FieldAsInteger(0);
465
466 if (1 == id) // PID_CODEPAGE
467 {
468 this.transformSummaryInfo.UpdatedSummaryInfoCodepage = row.FieldAsString(1);
469 }
470 else if (7 == id) // PID_TEMPLATE
471 {
472 this.transformSummaryInfo.UpdatedPlatformAndLanguage = row.FieldAsString(1);
473 }
474 else if (14 == id) // PID_PAGECOUNT
475 {
476 this.transformSummaryInfo.UpdatedMinimumVersion = row.FieldAsString(1);
477 }
478 }
479 }
480 }
481
482 private void UpdateTransformSummaryInformationTable(Table summaryInfoTable, TransformFlags validationFlags)
483 {
484 // calculate the minimum version of MSI required to process the transform
485 var minimumVersion = 100;
486
487 if (Int32.TryParse(this.transformSummaryInfo.TargetMinimumVersion, out var targetMin) && Int32.TryParse(this.transformSummaryInfo.UpdatedMinimumVersion, out var updatedMin))
488 {
489 minimumVersion = Math.Max(targetMin, updatedMin);
490 }
491
492 var summaryRows = new Dictionary<int, Row>(summaryInfoTable.Rows.Count);
493
494 foreach (var row in summaryInfoTable.Rows)
495 {
496 var id = row.FieldAsInteger(0);
497
498 summaryRows[id] = row;
499
500 if ((int)SummaryInformation.Transform.CodePage == id)
501 {
502 row.Fields[1].Data = this.transformSummaryInfo.UpdatedSummaryInfoCodepage;
503 row.Fields[1].PreviousData = this.transformSummaryInfo.TargetSummaryInfoCodepage;
504 }
505 else if ((int)SummaryInformation.Transform.TargetPlatformAndLanguage == id)
506 {
507 row[1] = this.transformSummaryInfo.TargetPlatformAndLanguage;
508 }
509 else if ((int)SummaryInformation.Transform.UpdatedPlatformAndLanguage == id)
510 {
511 row[1] = this.transformSummaryInfo.UpdatedPlatformAndLanguage;
512 }
513 else if ((int)SummaryInformation.Transform.ProductCodes == id)
514 {
515 row[1] = String.Concat(this.transformSummaryInfo.TargetProductCode, this.transformSummaryInfo.TargetProductVersion, ';', this.transformSummaryInfo.UpdatedProductCode, this.transformSummaryInfo.UpdatedProductVersion, ';', this.transformSummaryInfo.TargetUpgradeCode);
516 }
517 else if ((int)SummaryInformation.Transform.InstallerRequirement == id)
518 {
519 row[1] = minimumVersion.ToString(CultureInfo.InvariantCulture);
520 }
521 else if ((int)SummaryInformation.Transform.Security == id)
522 {
523 row[1] = "4";
524 }
525 }
526
527 if (!summaryRows.ContainsKey((int)SummaryInformation.Transform.TargetPlatformAndLanguage))
528 {
529 var summaryRow = summaryInfoTable.CreateRow(null);
530 summaryRow[0] = (int)SummaryInformation.Transform.TargetPlatformAndLanguage;
531 summaryRow[1] = this.transformSummaryInfo.TargetPlatformAndLanguage;
532 }
533
534 if (!summaryRows.ContainsKey((int)SummaryInformation.Transform.UpdatedPlatformAndLanguage))
535 {
536 var summaryRow = summaryInfoTable.CreateRow(null);
537 summaryRow[0] = (int)SummaryInformation.Transform.UpdatedPlatformAndLanguage;
538 summaryRow[1] = this.transformSummaryInfo.UpdatedPlatformAndLanguage;
539 }
540
541 if (!summaryRows.ContainsKey((int)SummaryInformation.Transform.ValidationFlags))
542 {
543 var summaryRow = summaryInfoTable.CreateRow(null);
544 summaryRow[0] = (int)SummaryInformation.Transform.ValidationFlags;
545 summaryRow[1] = ((int)validationFlags).ToString(CultureInfo.InvariantCulture);
546 }
547
548 if (!summaryRows.ContainsKey((int)SummaryInformation.Transform.InstallerRequirement))
549 {
550 var summaryRow = summaryInfoTable.CreateRow(null);
551 summaryRow[0] = (int)SummaryInformation.Transform.InstallerRequirement;
552 summaryRow[1] = minimumVersion.ToString(CultureInfo.InvariantCulture);
553 }
554
555 if (!summaryRows.ContainsKey((int)SummaryInformation.Transform.Security))
556 {
557 var summaryRow = summaryInfoTable.CreateRow(null);
558 summaryRow[0] = (int)SummaryInformation.Transform.Security;
559 summaryRow[1] = "4";
560 }
561 }
562
563 private class SummaryInformationStreams
564 {
565 public string TargetSummaryInfoCodepage { get; set; }
566
567 public string TargetPlatformAndLanguage { get; set; }
568
569 public string TargetProductCode { get; set; }
570
571 public string TargetProductVersion { get; set; }
572
573 public string TargetUpgradeCode { get; set; }
574
575 public string TargetMinimumVersion { get; set; }
576
577 public string UpdatedSummaryInfoCodepage { get; set; }
578
579 public string UpdatedPlatformAndLanguage { get; set; }
580
581 public string UpdatedProductCode { get; set; }
582
583 public string UpdatedProductVersion { get; set; }
584
585 public string UpdatedMinimumVersion { get; set; }
586 }
587 }
588}