aboutsummaryrefslogtreecommitdiff
path: root/src/WixToolset.Core.WindowsInstaller/Bind/UpdateTransformsWithFileFacades.cs
diff options
context:
space:
mode:
authorRob Mensching <rob@firegiant.com>2020-05-30 14:53:05 -0700
committerRob Mensching <rob@firegiant.com>2020-05-30 15:07:21 -0700
commitd529525a1e331f3ef9ec2707791c99bd78fdd82f (patch)
tree1d9fe1f0c0ee9850371c916802eb03ec9dc37a87 /src/WixToolset.Core.WindowsInstaller/Bind/UpdateTransformsWithFileFacades.cs
parent9c54d2fce80983bbee5f0f113b5aa30f22bc8a23 (diff)
downloadwix-d529525a1e331f3ef9ec2707791c99bd78fdd82f.tar.gz
wix-d529525a1e331f3ef9ec2707791c99bd78fdd82f.tar.bz2
wix-d529525a1e331f3ef9ec2707791c99bd78fdd82f.zip
Basic patching support
Diffstat (limited to 'src/WixToolset.Core.WindowsInstaller/Bind/UpdateTransformsWithFileFacades.cs')
-rw-r--r--src/WixToolset.Core.WindowsInstaller/Bind/UpdateTransformsWithFileFacades.cs453
1 files changed, 453 insertions, 0 deletions
diff --git a/src/WixToolset.Core.WindowsInstaller/Bind/UpdateTransformsWithFileFacades.cs b/src/WixToolset.Core.WindowsInstaller/Bind/UpdateTransformsWithFileFacades.cs
new file mode 100644
index 00000000..af2e8f85
--- /dev/null
+++ b/src/WixToolset.Core.WindowsInstaller/Bind/UpdateTransformsWithFileFacades.cs
@@ -0,0 +1,453 @@
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.Bind
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.Bind;
11 using WixToolset.Data;
12 using WixToolset.Data.Tuples;
13 using WixToolset.Data.WindowsInstaller;
14 using WixToolset.Data.WindowsInstaller.Rows;
15 using WixToolset.Extensibility.Services;
16
17 internal class UpdateTransformsWithFileFacades
18 {
19 public UpdateTransformsWithFileFacades(IMessaging messaging, WindowsInstallerData output, IEnumerable<SubStorage> subStorages, TableDefinitionCollection tableDefinitions, IEnumerable<FileFacade> fileFacades)
20 {
21 this.Messaging = messaging;
22 this.Output = output;
23 this.SubStorages = subStorages;
24 this.TableDefinitions = tableDefinitions;
25 this.FileFacades = fileFacades;
26 }
27
28 private IMessaging Messaging { get; }
29
30 private WindowsInstallerData Output { get; }
31
32 private IEnumerable<SubStorage> SubStorages { get; }
33
34 private TableDefinitionCollection TableDefinitions { get; }
35
36 private IEnumerable<FileFacade> FileFacades { get; }
37
38 public void Execute()
39 {
40 var fileFacadesByDiskId = new Dictionary<int, Dictionary<string, FileFacade>>();
41
42 // Index patch file facades by diskId+fileId.
43 foreach (var facade in this.FileFacades)
44 {
45 if (!fileFacadesByDiskId.TryGetValue(facade.DiskId, out var mediaFacades))
46 {
47 mediaFacades = new Dictionary<string, FileFacade>();
48 fileFacadesByDiskId.Add(facade.DiskId, mediaFacades);
49 }
50
51 mediaFacades.Add(facade.Id, facade);
52 }
53
54 var patchMediaRows = new RowDictionary<MediaRow>(this.Output.Tables["Media"]);
55
56 // Index paired transforms by name without the "#" prefix.
57 var pairedTransforms = this.SubStorages.Where(s => s.Name.StartsWith("#")).ToDictionary(s => s.Name, s => s.Data);
58
59 // Copy File bind data into substorages
60 foreach (var substorage in this.SubStorages.Where(s => !s.Name.StartsWith("#")))
61 {
62 var mainTransform = substorage.Data;
63
64 var mainMsiFileHashIndex = new RowDictionary<Row>(mainTransform.Tables["MsiFileHash"]);
65
66 var pairedTransform = pairedTransforms["#" + substorage.Name];
67
68 // Copy Media.LastSequence.
69 var pairedMediaTable = pairedTransform.Tables["Media"];
70 foreach (MediaRow pairedMediaRow in pairedMediaTable.Rows)
71 {
72 var patchMediaRow = patchMediaRows.Get(pairedMediaRow.DiskId);
73 pairedMediaRow.LastSequence = patchMediaRow.LastSequence;
74 }
75
76 // Validate file row changes for keypath-related issues
77 this.ValidateFileRowChanges(mainTransform);
78
79 // Index File table of pairedTransform
80 var pairedFileRows = new RowDictionary<FileRow>(pairedTransform.Tables["File"]);
81
82 var mainFileTable = mainTransform.Tables["File"];
83 if (null != mainFileTable)
84 {
85 // Remove the MsiFileHash table because it will be updated later with the final file hash for each file
86 mainTransform.Tables.Remove("MsiFileHash");
87
88 foreach (FileRow mainFileRow in mainFileTable.Rows)
89 {
90 if (RowOperation.Delete == mainFileRow.Operation)
91 {
92 continue;
93 }
94 else if (RowOperation.None == mainFileRow.Operation)
95 {
96 continue;
97 }
98
99 // Index patch files by diskId+fileId
100 if (!fileFacadesByDiskId.TryGetValue(mainFileRow.DiskId, out var mediaFacades))
101 {
102 mediaFacades = new Dictionary<string, FileFacade>();
103 fileFacadesByDiskId.Add(mainFileRow.DiskId, mediaFacades);
104 }
105
106 // copy data from the patch back to the transform
107 if (mediaFacades.TryGetValue(mainFileRow.File, out var facade))
108 {
109 var patchFileRow = facade.GetFileRow();
110 var pairedFileRow = pairedFileRows.Get(mainFileRow.File);
111
112 for (var i = 0; i < patchFileRow.Fields.Length; i++)
113 {
114 var patchValue = patchFileRow.FieldAsString(i) ?? String.Empty;
115 var mainValue = mainFileRow.FieldAsString(i) ?? String.Empty;
116
117 if (1 == i)
118 {
119 // File.Component_ changes should not come from the shared file rows
120 // that contain the file information as each individual transform might
121 // have different changes (or no changes at all).
122 }
123 else if (6 == i) // File.Attributes should not changed for binary deltas
124 {
125#if TODO_PATCHING_DELTA
126 if (null != patchFileRow.Patch)
127 {
128 // File.Attribute should not change for binary deltas
129 pairedFileRow.Attributes = mainFileRow.Attributes;
130 mainFileRow.Fields[i].Modified = false;
131 }
132#endif
133 }
134 else if (7 == i) // File.Sequence is updated in pairedTransform, not mainTransform
135 {
136 // file sequence is updated in Patch table instead of File table for delta patches
137#if TODO_PATCHING_DELTA
138 if (null != patchFileRow.Patch)
139 {
140 pairedFileRow.Fields[i].Modified = false;
141 }
142 else
143#endif
144 {
145 pairedFileRow[i] = patchFileRow[i];
146 pairedFileRow.Fields[i].Modified = true;
147 }
148 mainFileRow.Fields[i].Modified = false;
149 }
150 else if (patchValue != mainValue)
151 {
152 mainFileRow[i] = patchFileRow[i];
153 mainFileRow.Fields[i].Modified = true;
154 if (mainFileRow.Operation == RowOperation.None)
155 {
156 mainFileRow.Operation = RowOperation.Modify;
157 }
158 }
159 }
160
161 // Copy MsiFileHash row for this File.
162 if (!mainMsiFileHashIndex.TryGetValue(patchFileRow.File, out var patchHashRow))
163 {
164 //patchHashRow = patchFileRow.Hash;
165 throw new NotImplementedException();
166 }
167
168 if (null != patchHashRow)
169 {
170 var mainHashTable = mainTransform.EnsureTable(this.TableDefinitions["MsiFileHash"]);
171 var mainHashRow = mainHashTable.CreateRow(mainFileRow.SourceLineNumbers);
172 for (var i = 0; i < patchHashRow.Fields.Length; i++)
173 {
174 mainHashRow[i] = patchHashRow[i];
175 if (i > 1)
176 {
177 // assume all hash fields have been modified
178 mainHashRow.Fields[i].Modified = true;
179 }
180 }
181
182 // assume the MsiFileHash operation follows the File one
183 mainHashRow.Operation = mainFileRow.Operation;
184 }
185
186 // copy MsiAssemblyName rows for this File
187#if TODO_PATCHING
188 List<Row> patchAssemblyNameRows = patchFileRow.AssemblyNames;
189 if (null != patchAssemblyNameRows)
190 {
191 var mainAssemblyNameTable = mainTransform.EnsureTable(this.TableDefinitions["MsiAssemblyName"]);
192 foreach (var patchAssemblyNameRow in patchAssemblyNameRows)
193 {
194 // Copy if there isn't an identical modified/added row already in the transform.
195 var foundMatchingModifiedRow = false;
196 foreach (var mainAssemblyNameRow in mainAssemblyNameTable.Rows)
197 {
198 if (RowOperation.None != mainAssemblyNameRow.Operation && mainAssemblyNameRow.GetPrimaryKey('/').Equals(patchAssemblyNameRow.GetPrimaryKey('/')))
199 {
200 foundMatchingModifiedRow = true;
201 break;
202 }
203 }
204
205 if (!foundMatchingModifiedRow)
206 {
207 var mainAssemblyNameRow = mainAssemblyNameTable.CreateRow(mainFileRow.SourceLineNumbers);
208 for (var i = 0; i < patchAssemblyNameRow.Fields.Length; i++)
209 {
210 mainAssemblyNameRow[i] = patchAssemblyNameRow[i];
211 }
212
213 // assume value field has been modified
214 mainAssemblyNameRow.Fields[2].Modified = true;
215 mainAssemblyNameRow.Operation = mainFileRow.Operation;
216 }
217 }
218 }
219#endif
220
221 // Add patch header for this file
222#if TODO_PATCHING_DELTA
223 if (null != patchFileRow.Patch)
224 {
225 // Add the PatchFiles action automatically to the AdminExecuteSequence and InstallExecuteSequence tables.
226 this.AddPatchFilesActionToSequenceTable(SequenceTable.AdminExecuteSequence, mainTransform, pairedTransform, mainFileRow);
227 this.AddPatchFilesActionToSequenceTable(SequenceTable.InstallExecuteSequence, mainTransform, pairedTransform, mainFileRow);
228
229 // Add to Patch table
230 var patchTable = pairedTransform.EnsureTable(this.TableDefinitions["Patch"]);
231 if (0 == patchTable.Rows.Count)
232 {
233 patchTable.Operation = TableOperation.Add;
234 }
235
236 var patchRow = patchTable.CreateRow(mainFileRow.SourceLineNumbers);
237 patchRow[0] = patchFileRow.File;
238 patchRow[1] = patchFileRow.Sequence;
239
240 var patchFile = new FileInfo(patchFileRow.Source);
241 patchRow[2] = (int)patchFile.Length;
242 patchRow[3] = 0 == (PatchAttributeType.AllowIgnoreOnError & patchFileRow.PatchAttributes) ? 0 : 1;
243
244 var streamName = patchTable.Name + "." + patchRow[0] + "." + patchRow[1];
245 if (Msi.MsiInterop.MsiMaxStreamNameLength < streamName.Length)
246 {
247 streamName = "_" + Guid.NewGuid().ToString("D").ToUpperInvariant().Replace('-', '_');
248
249 var patchHeadersTable = pairedTransform.EnsureTable(this.TableDefinitions["MsiPatchHeaders"]);
250 if (0 == patchHeadersTable.Rows.Count)
251 {
252 patchHeadersTable.Operation = TableOperation.Add;
253 }
254
255 var patchHeadersRow = patchHeadersTable.CreateRow(mainFileRow.SourceLineNumbers);
256 patchHeadersRow[0] = streamName;
257 patchHeadersRow[1] = patchFileRow.Patch;
258 patchRow[5] = streamName;
259 patchHeadersRow.Operation = RowOperation.Add;
260 }
261 else
262 {
263 patchRow[4] = patchFileRow.Patch;
264 }
265 patchRow.Operation = RowOperation.Add;
266 }
267#endif
268 }
269 else
270 {
271 // TODO: throw because all transform rows should have made it into the patch
272 }
273 }
274 }
275
276 this.Output.Tables.Remove("Media");
277 this.Output.Tables.Remove("File");
278 this.Output.Tables.Remove("MsiFileHash");
279 this.Output.Tables.Remove("MsiAssemblyName");
280 }
281 }
282
283 /// <summary>
284 /// Adds the PatchFiles action to the sequence table if it does not already exist.
285 /// </summary>
286 /// <param name="table">The sequence table to check or modify.</param>
287 /// <param name="mainTransform">The primary authoring transform.</param>
288 /// <param name="pairedTransform">The secondary patch transform.</param>
289 /// <param name="mainFileRow">The file row that contains information about the patched file.</param>
290 private void AddPatchFilesActionToSequenceTable(SequenceTable table, WindowsInstallerData mainTransform, WindowsInstallerData pairedTransform, Row mainFileRow)
291 {
292 var tableName = table.ToString();
293
294 // Find/add PatchFiles action (also determine sequence for it).
295 // Search mainTransform first, then pairedTransform (pairedTransform overrides).
296 var hasPatchFilesAction = false;
297 var installFilesSequence = 0;
298 var duplicateFilesSequence = 0;
299
300 TestSequenceTableForPatchFilesAction(
301 mainTransform.Tables[tableName],
302 ref hasPatchFilesAction,
303 ref installFilesSequence,
304 ref duplicateFilesSequence);
305 TestSequenceTableForPatchFilesAction(
306 pairedTransform.Tables[tableName],
307 ref hasPatchFilesAction,
308 ref installFilesSequence,
309 ref duplicateFilesSequence);
310 if (!hasPatchFilesAction)
311 {
312 WindowsInstallerStandard.TryGetStandardAction(tableName, "PatchFiles", out var patchFilesActionTuple);
313
314 var sequence = patchFilesActionTuple.Sequence;
315
316 // Test for default sequence value's appropriateness
317 if (installFilesSequence >= sequence || (0 != duplicateFilesSequence && duplicateFilesSequence <= sequence))
318 {
319 if (0 != duplicateFilesSequence)
320 {
321 if (duplicateFilesSequence < installFilesSequence)
322 {
323 throw new WixException(ErrorMessages.InsertInvalidSequenceActionOrder(mainFileRow.SourceLineNumbers, tableName, "InstallFiles", "DuplicateFiles", patchFilesActionTuple.Action));
324 }
325 else
326 {
327 sequence = (duplicateFilesSequence + installFilesSequence) / 2;
328 if (installFilesSequence == sequence || duplicateFilesSequence == sequence)
329 {
330 throw new WixException(ErrorMessages.InsertSequenceNoSpace(mainFileRow.SourceLineNumbers, tableName, "InstallFiles", "DuplicateFiles", patchFilesActionTuple.Action));
331 }
332 }
333 }
334 else
335 {
336 sequence = installFilesSequence + 1;
337 }
338 }
339
340 var sequenceTable = pairedTransform.EnsureTable(this.TableDefinitions[tableName]);
341 if (0 == sequenceTable.Rows.Count)
342 {
343 sequenceTable.Operation = TableOperation.Add;
344 }
345
346 var patchAction = sequenceTable.CreateRow(null);
347 patchAction[0] = patchFilesActionTuple.Action;
348 patchAction[1] = patchFilesActionTuple.Condition;
349 patchAction[2] = sequence;
350 patchAction.Operation = RowOperation.Add;
351 }
352 }
353
354 /// <summary>
355 /// Tests sequence table for PatchFiles and associated actions
356 /// </summary>
357 /// <param name="sequenceTable">The table to test.</param>
358 /// <param name="hasPatchFilesAction">Set to true if PatchFiles action is found. Left unchanged otherwise.</param>
359 /// <param name="installFilesSequence">Set to sequence value of InstallFiles action if found. Left unchanged otherwise.</param>
360 /// <param name="duplicateFilesSequence">Set to sequence value of DuplicateFiles action if found. Left unchanged otherwise.</param>
361 private static void TestSequenceTableForPatchFilesAction(Table sequenceTable, ref bool hasPatchFilesAction, ref int installFilesSequence, ref int duplicateFilesSequence)
362 {
363 if (null != sequenceTable)
364 {
365 foreach (var row in sequenceTable.Rows)
366 {
367 var actionName = row.FieldAsString(0);
368 switch (actionName)
369 {
370 case "PatchFiles":
371 hasPatchFilesAction = true;
372 break;
373
374 case "InstallFiles":
375 installFilesSequence = row.FieldAsInteger(2);
376 break;
377
378 case "DuplicateFiles":
379 duplicateFilesSequence = row.FieldAsInteger(2);
380 break;
381 }
382 }
383 }
384 }
385
386 /// <summary>
387 /// Signal a warning if a non-keypath file was changed in a patch without also changing the keypath file of the component.
388 /// </summary>
389 /// <param name="output">The output to validate.</param>
390 private void ValidateFileRowChanges(WindowsInstallerData transform)
391 {
392 var componentTable = transform.Tables["Component"];
393 var fileTable = transform.Tables["File"];
394
395 // There's no sense validating keypaths if the transform has no component or file table
396 if (componentTable == null || fileTable == null)
397 {
398 return;
399 }
400
401 var componentKeyPath = new Dictionary<string, string>(componentTable.Rows.Count);
402
403 // Index the Component table for non-directory & non-registry key paths.
404 foreach (var row in componentTable.Rows)
405 {
406 var keyPath = row.FieldAsString(5);
407 if (keyPath != null && 0 != (row.FieldAsInteger(3) & WindowsInstallerConstants.MsidbComponentAttributesRegistryKeyPath))
408 {
409 componentKeyPath.Add(row.FieldAsString(0), keyPath);
410 }
411 }
412
413 var componentWithChangedKeyPath = new Dictionary<string, string>();
414 var componentWithNonKeyPathChanged = new Dictionary<string, string>();
415 // Verify changes in the file table, now that file diffing has occurred
416 foreach (FileRow row in fileTable.Rows)
417 {
418 if (RowOperation.Modify != row.Operation)
419 {
420 continue;
421 }
422
423 var fileId = row.FieldAsString(0);
424 var componentId = row.FieldAsString(1);
425
426 // If this file is the keypath of a component
427 if (componentKeyPath.ContainsValue(fileId))
428 {
429 if (!componentWithChangedKeyPath.ContainsKey(componentId))
430 {
431 componentWithChangedKeyPath.Add(componentId, fileId);
432 }
433 }
434 else
435 {
436 if (!componentWithNonKeyPathChanged.ContainsKey(componentId))
437 {
438 componentWithNonKeyPathChanged.Add(componentId, fileId);
439 }
440 }
441 }
442
443 foreach (var componentFile in componentWithNonKeyPathChanged)
444 {
445 // Make sure all changes to non keypath files also had a change in the keypath.
446 if (!componentWithChangedKeyPath.ContainsKey(componentFile.Key) && componentKeyPath.TryGetValue(componentFile.Key, out var keyPath))
447 {
448 this.Messaging.Write(WarningMessages.UpdateOfNonKeyPathFile(componentFile.Value, componentFile.Key, keyPath));
449 }
450 }
451 }
452 }
453}