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