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