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