main
cs 684 lines 27.3 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.Link
4 {
5 using System;
6 using System.Collections.ObjectModel;
7 using System.Collections.Generic;
8 using System.Diagnostics;
9 using System.Globalization;
10 using System.Linq;
11 using System.Text;
12 using WixToolset.Data;
13 using WixToolset.Data.Symbols;
14 using WixToolset.Extensibility.Services;
15 using WixToolset.Data.Burn;
16
17 /// <summary>
18 /// Grouping and Ordering class of the WiX toolset.
19 /// </summary>
20 internal class WixGroupingOrdering
21 {
22 private readonly IMessaging Messaging;
23 private List<string> groupTypes;
24 private List<string> itemTypes;
25 private ItemCollection items;
26 private readonly List<IntermediateSymbol> symbolsUsed;
27 private bool loaded;
28
29 /// <summary>
30 /// Creates a WixGroupingOrdering object.
31 /// </summary>
32 /// <param name="entrySections">Output from which to read the group and order information.</param>
33 /// <param name="messageHandler">Handler for any error messages.</param>
34 public WixGroupingOrdering(IntermediateSection entrySections, IMessaging messageHandler)
35 {
36 this.EntrySection = entrySections;
37 this.Messaging = messageHandler;
38
39 this.symbolsUsed = new List<IntermediateSymbol>();
40 this.loaded = false;
41 }
42
43 private IntermediateSection EntrySection { get; }
44
45 /// <summary>
46 /// Switches a WixGroupingOrdering object to operate on a new set of groups/items.
47 /// </summary>
48 /// <param name="groupTypes">Group types to include.</param>
49 /// <param name="itemTypes">Item types to include.</param>
50 public void UseTypes(IEnumerable<ComplexReferenceParentType> groupTypes, IEnumerable<ComplexReferenceChildType> itemTypes)
51 {
52 this.groupTypes = new List<string>(groupTypes.Select(g => g.ToString()));
53 this.itemTypes = new List<string>(itemTypes.Select(i => i.ToString()));
54
55 this.items = new ItemCollection();
56 this.loaded = false;
57 }
58
59 /// <summary>
60 /// Finds all nested items under a parent group and creates new WixGroup data for them.
61 /// </summary>
62 /// <param name="parentType">The group type for the parent group to flatten.</param>
63 /// <param name="parentId">The identifier of the parent group to flatten.</param>
64 /// <param name="removeUsedRows">Whether to remove used group rows before returning.</param>
65 public void FlattenAndRewriteRows(ComplexReferenceParentType parentType, string parentId, bool removeUsedRows)
66 {
67 var parentTypeString = parentType.ToString();
68 Debug.Assert(this.groupTypes.Contains(parentTypeString));
69
70 this.CreateOrderedList(parentTypeString, parentId, out var orderedItems);
71 if (this.Messaging.EncounteredError)
72 {
73 return;
74 }
75
76 this.CreateNewGroupRows(parentTypeString, parentId, orderedItems);
77
78 if (removeUsedRows)
79 {
80 this.RemoveUsedGroupRows();
81 }
82 }
83
84 /// <summary>
85 /// Finds all items under a parent group type and creates new WixGroup data for them.
86 /// </summary>
87 /// <param name="parentType">The type of the parent group to flatten.</param>
88 /// <param name="removeUsedRows">Whether to remove used group rows before returning.</param>
89 public void FlattenAndRewriteGroups(ComplexReferenceParentType parentType, bool removeUsedRows)
90 {
91 var parentTypeString = parentType.ToString();
92 Debug.Assert(this.groupTypes.Contains(parentTypeString));
93
94 this.LoadFlattenOrderGroups();
95 if (this.Messaging.EncounteredError)
96 {
97 return;
98 }
99
100 foreach (Item item in this.items)
101 {
102 if (parentTypeString == item.Type)
103 {
104 this.CreateOrderedList(item.Type, item.Id, out var orderedItems);
105 this.CreateNewGroupRows(item.Type, item.Id, orderedItems);
106 }
107 }
108
109 if (removeUsedRows)
110 {
111 this.RemoveUsedGroupRows();
112 }
113 }
114
115
116 /// <summary>
117 /// Creates a flattened and ordered list of items for the given parent group.
118 /// </summary>
119 /// <param name="parentType">The group type for the parent group to flatten.</param>
120 /// <param name="parentId">The identifier of the parent group to flatten.</param>
121 /// <param name="orderedItems">The returned list of ordered items.</param>
122 private void CreateOrderedList(string parentType, string parentId, out List<Item> orderedItems)
123 {
124 orderedItems = null;
125
126 this.LoadFlattenOrderGroups();
127 if (this.Messaging.EncounteredError)
128 {
129 return;
130 }
131
132 if (!this.items.TryGetValue(parentType, parentId, out var parentItem))
133 {
134 this.Messaging.Write(ErrorMessages.IdentifierNotFound(parentType, parentId));
135 return;
136 }
137
138 orderedItems = new List<Item>(parentItem.ChildItems);
139 orderedItems.Sort(new Item.AfterItemComparer());
140 }
141
142 /// <summary>
143 /// Removes rows from WixGroup that have been used by this object.
144 /// </summary>
145 public void RemoveUsedGroupRows()
146 {
147 foreach (var symbol in this.symbolsUsed)
148 {
149 this.EntrySection.RemoveSymbol(symbol);
150 }
151 }
152
153 /// <summary>
154 /// Creates new WixGroup rows for a list of items.
155 /// </summary>
156 /// <param name="parentType">The group type for the parent group in the new rows.</param>
157 /// <param name="parentId">The identifier of the parent group in the new rows.</param>
158 /// <param name="orderedItems">The list of new items.</param>
159 private void CreateNewGroupRows(string parentType, string parentId, List<Item> orderedItems)
160 {
161 // TODO: MSIs don't guarantee that rows stay in the same order, and technically, neither
162 // does WiX (although they do, currently). We probably want to "upgrade" this to a new
163 // table that includes a sequence number, and then change the code that uses ordered
164 // groups to read from that table instead.
165 foreach (var item in orderedItems)
166 {
167 this.EntrySection.AddSymbol(new WixGroupSymbol(item.Row.SourceLineNumbers)
168 {
169 ParentId = parentId,
170 ParentType = (ComplexReferenceParentType)Enum.Parse(typeof(ComplexReferenceParentType), parentType),
171 ChildId = item.Id,
172 ChildType = (ComplexReferenceChildType)Enum.Parse(typeof(ComplexReferenceChildType), item.Type),
173 });
174 }
175 }
176
177 // Group/Ordering Flattening Logic
178 //
179 // What follows is potentially convoluted logic. Two somewhat orthogonal concepts are in
180 // play: grouping (parent/child relationships) and ordering (before/after relationships).
181 // Dealing with just one or the other is straghtforward. Groups can be flattened
182 // recursively. Ordering can be propagated in either direction. When the ordering also
183 // participates in the grouping constructions, however, things get trickier. For the
184 // purposes of this discussion, we're dealing with "items" and "groups", and an instance
185 // of either of them can be marked as coming "after" some other instance.
186 //
187 // For simple item-to-item ordering, the "after" values simply propagate: if A is after B,
188 // and B is after C, then we can say that A is after *both* B and C. If a group is involved,
189 // it acts as a proxy for all of its included items and any sub-groups.
190
191 /// <summary>
192 /// Internal workhorse for ensuring that group and ordering information has
193 /// been loaded and applied.
194 /// </summary>
195 private void LoadFlattenOrderGroups()
196 {
197 if (!this.loaded)
198 {
199 this.LoadGroups();
200 this.LoadOrdering();
201
202 // It would be really nice to have a "find circular after dependencies"
203 // function, but it gets much more complicated because of the way that
204 // the dependencies are propagated across group boundaries. For now, we
205 // just live with the dependency loop detection as we flatten the
206 // dependencies. Group references, however, we can check directly.
207 this.FindCircularGroupReferences();
208
209 if (!this.Messaging.EncounteredError)
210 {
211 this.FlattenGroups();
212 this.FlattenOrdering();
213 }
214
215 this.loaded = true;
216 }
217 }
218
219 /// <summary>
220 /// Loads data from the WixGroup table.
221 /// </summary>
222 private void LoadGroups()
223 {
224 //Table wixGroupTable = this.output.Tables["WixGroup"];
225 //if (null == wixGroupTable || 0 == wixGroupTable.Rows.Count)
226 //{
227 // // TODO: Change message name to make it *not* Bundle specific?
228 // this.Write(WixErrors.MissingBundleInformation("WixGroup"));
229 //}
230
231 // Collect all of the groups
232 foreach (var symbol in this.EntrySection.Symbols.OfType<WixGroupSymbol>())
233 {
234 var rowParentName = symbol.ParentId;
235 var rowParentType = symbol.ParentType.ToString();
236 var rowChildName = symbol.ChildId;
237 var rowChildType = symbol.ChildType.ToString();
238
239 // If this row specifies a parent or child type that's not in our
240 // lists, we assume it's not a row that we're concerned about.
241 if (!this.groupTypes.Contains(rowParentType) ||
242 !this.itemTypes.Contains(rowChildType))
243 {
244 continue;
245 }
246
247 this.symbolsUsed.Add(symbol);
248
249 if (!this.items.TryGetValue(rowParentType, rowParentName, out var parentItem))
250 {
251 parentItem = new Item(symbol, rowParentType, rowParentName);
252 this.items.Add(parentItem);
253 }
254
255 if (!this.items.TryGetValue(rowChildType, rowChildName, out var childItem))
256 {
257 childItem = new Item(symbol, rowChildType, rowChildName);
258 this.items.Add(childItem);
259 }
260
261 parentItem.ChildItems.Add(childItem);
262 }
263 }
264
265 /// <summary>
266 /// Flattens group/item information.
267 /// </summary>
268 private void FlattenGroups()
269 {
270 foreach (Item item in this.items)
271 {
272 item.FlattenChildItems();
273 }
274 }
275
276 /// <summary>
277 /// Finds and reports circular references in the group/item data.
278 /// </summary>
279 private void FindCircularGroupReferences()
280 {
281 ItemCollection itemsInKnownLoops = new ItemCollection();
282 foreach (Item item in this.items)
283 {
284 if (itemsInKnownLoops.Contains(item))
285 {
286 continue;
287 }
288
289 ItemCollection itemsSeen = new ItemCollection();
290 string circularReference;
291 if (this.FindCircularGroupReference(item, item, itemsSeen, out circularReference))
292 {
293 itemsInKnownLoops.Add(itemsSeen);
294 this.Messaging.Write(ErrorMessages.ReferenceLoopDetected(item.Row.SourceLineNumbers, circularReference));
295 }
296 }
297 }
298
299 /// <summary>
300 /// Recursive worker to find and report circular references in group/item data.
301 /// </summary>
302 /// <param name="checkItem">The sentinal item being checked.</param>
303 /// <param name="currentItem">The current item in the recursion.</param>
304 /// <param name="itemsSeen">A list of all items already visited (for performance).</param>
305 /// <param name="circularReference">A list of items in the current circular reference, if one was found; null otherwise.</param>
306 /// <returns>True if a circular reference was found; false otherwise.</returns>
307 private bool FindCircularGroupReference(Item checkItem, Item currentItem, ItemCollection itemsSeen, out string circularReference)
308 {
309 circularReference = null;
310 foreach (Item subitem in currentItem.ChildItems)
311 {
312 if (checkItem == subitem)
313 {
314 // TODO: Even better would be to include the source lines for each reference!
315 circularReference = String.Format(CultureInfo.InvariantCulture, "{0}:{1} -> {2}:{3}",
316 currentItem.Type, currentItem.Id, subitem.Type, subitem.Id);
317 return true;
318 }
319
320 if (!itemsSeen.Contains(subitem))
321 {
322 itemsSeen.Add(subitem);
323 if (this.FindCircularGroupReference(checkItem, subitem, itemsSeen, out circularReference))
324 {
325 // TODO: Even better would be to include the source lines for each reference!
326 circularReference = String.Format(CultureInfo.InvariantCulture, "{0}:{1} -> {2}",
327 currentItem.Type, currentItem.Id, circularReference);
328 return true;
329 }
330 }
331 }
332
333 return false;
334 }
335
336 /// <summary>
337 /// Loads ordering dependency data from the WixOrdering table.
338 /// </summary>
339 private void LoadOrdering()
340 {
341 //Table wixOrderingTable = output.Tables["WixOrdering"];
342 //if (null == wixOrderingTable || 0 == wixOrderingTable.Rows.Count)
343 //{
344 // // TODO: Do we need a message here?
345 // return;
346 //}
347
348 foreach (var row in this.EntrySection.Symbols.OfType<WixOrderingSymbol>())
349 {
350 var rowItemType = row.ItemType.ToString();
351 var rowItemName = row.ItemIdRef;
352 var rowDependsOnType = row.DependsOnType.ToString();
353 var rowDependsOnName = row.DependsOnIdRef;
354
355 // If this row specifies some other (unknown) type in either
356 // position, we assume it's not a row that we're concerned about.
357 // For ordering, we allow group and item in either position.
358 if (!(this.groupTypes.Contains(rowItemType) || this.itemTypes.Contains(rowItemType)) ||
359 !(this.groupTypes.Contains(rowDependsOnType) || this.itemTypes.Contains(rowDependsOnType)))
360 {
361 continue;
362 }
363
364 if (!this.items.TryGetValue(rowItemType, rowItemName, out var item))
365 {
366 this.Messaging.Write(ErrorMessages.IdentifierNotFound(rowItemType, rowItemName));
367 }
368
369 if (!this.items.TryGetValue(rowDependsOnType, rowDependsOnName, out var dependsOn))
370 {
371 this.Messaging.Write(ErrorMessages.IdentifierNotFound(rowDependsOnType, rowDependsOnName));
372 }
373
374 if (null == item || null == dependsOn)
375 {
376 continue;
377 }
378
379 item.AddAfter(dependsOn, this.Messaging);
380 }
381 }
382
383 /// <summary>
384 /// Flattens the ordering dependencies in the groups/items.
385 /// </summary>
386 private void FlattenOrdering()
387 {
388 // Because items don't know about their parent groups (and can, in fact, be
389 // in more than one group at a time), we need to pre-propagate the 'afters'
390 // from each parent item to its children before we attempt to flatten the
391 // ordering.
392 foreach (Item item in this.items)
393 {
394 item.PropagateAfterToChildItems(this.Messaging);
395 }
396
397 foreach (Item item in this.items)
398 {
399 item.FlattenAfters(this.Messaging);
400 }
401 }
402
403 /// <summary>
404 /// A variant of KeyedCollection that doesn't throw when an item is re-added.
405 /// </summary>
406 /// <typeparam name="TKey">Key type for the collection.</typeparam>
407 /// <typeparam name="TItem">Item type for the colelction.</typeparam>
408 internal abstract class EnhancedKeyCollection<TKey, TItem> : KeyedCollection<TKey, TItem>
409 {
410 new public void Add(TItem item)
411 {
412 if (!this.Contains(item))
413 {
414 base.Add(item);
415 }
416 }
417
418 public void Add(Collection<TItem> list)
419 {
420 foreach (TItem item in list)
421 {
422 this.Add(item);
423 }
424 }
425
426 public void Remove(Collection<TItem> list)
427 {
428 foreach (TItem item in list)
429 {
430 this.Remove(item);
431 }
432 }
433
434 public bool TryGetValue(TKey key, out TItem item)
435 {
436 // KeyedCollection doesn't implement the TryGetValue() method, but it's
437 // a useful concept. We can't just always pass this to the enclosed
438 // Dictionary, however, because it doesn't always exist! If it does, we
439 // can delegate to it as one would expect. If it doesn't, we have to
440 // implement everything ourselves in terms of Contains().
441
442 if (null != this.Dictionary)
443 {
444 return this.Dictionary.TryGetValue(key, out item);
445 }
446
447 if (this.Contains(key))
448 {
449 item = this[key];
450 return true;
451 }
452
453 item = default(TItem);
454 return false;
455 }
456
457 #if DEBUG
458 // This just makes debugging easier...
459 public override string ToString()
460 {
461 StringBuilder sb = new StringBuilder();
462 foreach (TItem item in this)
463 {
464 sb.AppendFormat("{0}, ", item);
465 }
466 sb.Length -= 2;
467 return sb.ToString();
468 }
469 #endif // DEBUG
470 }
471
472 /// <summary>
473 /// A specialized EnhancedKeyCollection, typed to Items.
474 /// </summary>
475 internal class ItemCollection : EnhancedKeyCollection<string, Item>
476 {
477 protected override string GetKeyForItem(Item item)
478 {
479 return item.Key;
480 }
481
482 public bool TryGetValue(string type, string id, out Item item)
483 {
484 return this.TryGetValue(CreateKeyFromTypeId(type, id), out item);
485 }
486
487 public static string CreateKeyFromTypeId(string type, string id)
488 {
489 return String.Format(CultureInfo.InvariantCulture, "{0}_{1}", type, id);
490 }
491 }
492
493 /// <summary>
494 /// An item (or group) in the grouping/ordering engine.
495 /// </summary>
496 /// <remarks>Encapsulates nested group membership and also before/after
497 /// ordering dependencies.</remarks>
498 internal class Item
499 {
500 private readonly ItemCollection afterItems;
501 private readonly ItemCollection beforeItems; // for checking for circular references
502 private bool flattenedAfterItems;
503
504 public Item(IntermediateSymbol row, string type, string id)
505 {
506 this.Row = row;
507 this.Type = type;
508 this.Id = id;
509
510 this.Key = ItemCollection.CreateKeyFromTypeId(type, id);
511
512 this.afterItems = new ItemCollection();
513 this.beforeItems = new ItemCollection();
514 this.flattenedAfterItems = false;
515 }
516
517 public IntermediateSymbol Row { get; private set; }
518 public string Type { get; private set; }
519 public string Id { get; private set; }
520 public string Key { get; private set; }
521
522 #if DEBUG
523 // Makes debugging easier...
524 public override string ToString()
525 {
526 return this.Key;
527 }
528 #endif // DEBUG
529
530 public ItemCollection ChildItems { get; } = new ItemCollection();
531
532 /// <summary>
533 /// Removes any nested groups under this item and replaces
534 /// them with their child items.
535 /// </summary>
536 public void FlattenChildItems()
537 {
538 ItemCollection flattenedChildItems = new ItemCollection();
539
540 foreach (Item childItem in this.ChildItems)
541 {
542 if (0 == childItem.ChildItems.Count)
543 {
544 flattenedChildItems.Add(childItem);
545 }
546 else
547 {
548 childItem.FlattenChildItems();
549 flattenedChildItems.Add(childItem.ChildItems);
550 }
551 }
552
553 this.ChildItems.Clear();
554 this.ChildItems.Add(flattenedChildItems);
555 }
556
557 /// <summary>
558 /// Adds a list of items to the 'after' ordering collection.
559 /// </summary>
560 /// <param name="items">List of items to add.</param>
561 /// <param name="messageHandler">Message handler in case a circular ordering reference is found.</param>
562 public void AddAfter(ItemCollection items, IMessaging messageHandler)
563 {
564 foreach (Item item in items)
565 {
566 this.AddAfter(item, messageHandler);
567 }
568 }
569
570 /// <summary>
571 /// Adds an item to the 'after' ordering collection.
572 /// </summary>
573 /// <param name="after">Item to add.</param>
574 /// <param name="messageHandler">Message handler in case a circular ordering reference is found.</param>
575 public void AddAfter(Item after, IMessaging messageHandler)
576 {
577 if (this.beforeItems.Contains(after))
578 {
579 // We could try to chain this up (the way that group circular dependencies
580 // are reported), but since we're in the process of flattening, we may already
581 // have lost some distinction between authored and propagated ordering.
582 string circularReference = String.Format(CultureInfo.InvariantCulture, "{0}:{1} -> {2}:{3} -> {0}:{1}",
583 this.Type, this.Id, after.Type, after.Id);
584 messageHandler.Write(ErrorMessages.OrderingReferenceLoopDetected(after.Row.SourceLineNumbers, circularReference));
585 return;
586 }
587
588 this.afterItems.Add(after);
589 after.beforeItems.Add(this);
590 }
591
592 /// <summary>
593 /// Propagates 'after' dependencies from an item to its child items.
594 /// </summary>
595 /// <param name="messageHandler">Message handler in case a circular ordering reference is found.</param>
596 /// <remarks>Because items don't know about their parent groups (and can, in fact, be in more
597 /// than one group at a time), we need to propagate the 'afters' from each parent item to its children
598 /// before we attempt to flatten the ordering.</remarks>
599 public void PropagateAfterToChildItems(IMessaging messageHandler)
600 {
601 if (this.ShouldItemPropagateChildOrdering())
602 {
603 foreach (Item childItem in this.ChildItems)
604 {
605 childItem.AddAfter(this.afterItems, messageHandler);
606 }
607 }
608 }
609
610 /// <summary>
611 /// Flattens the ordering dependency for this item.
612 /// </summary>
613 /// <param name="messageHandler">Message handler in case a circular ordering reference is found.</param>
614 public void FlattenAfters(IMessaging messageHandler)
615 {
616 if (this.flattenedAfterItems)
617 {
618 return;
619 }
620
621 this.flattenedAfterItems = true;
622
623 // Ensure that if we're after something (A), and *it's* after something (B),
624 // that we list ourselved as after both (A) *and* (B).
625 ItemCollection nestedAfterItems = new ItemCollection();
626
627 foreach (Item afterItem in this.afterItems)
628 {
629 afterItem.FlattenAfters(messageHandler);
630 nestedAfterItems.Add(afterItem.afterItems);
631
632 if (afterItem.ShouldItemPropagateChildOrdering())
633 {
634 // If we are after a group, it really means
635 // we are after all of the group's children.
636 foreach (Item childItem in afterItem.ChildItems)
637 {
638 childItem.FlattenAfters(messageHandler);
639 nestedAfterItems.Add(childItem.afterItems);
640 nestedAfterItems.Add(childItem);
641 }
642 }
643 }
644
645 this.AddAfter(nestedAfterItems, messageHandler);
646 }
647
648 // We *don't* propagate ordering information from Packages, PayloadGroups, or
649 // Containers to their children, because ordering doesn't matter
650 // for them, and a Payload in two Packages (or Containers) can
651 // cause a circular reference to occur.
652 private bool ShouldItemPropagateChildOrdering()
653 {
654 if (String.Equals(nameof(ComplexReferenceParentType.Package), this.Type, StringComparison.Ordinal) ||
655 String.Equals(nameof(ComplexReferenceParentType.PayloadGroup), this.Type, StringComparison.Ordinal) ||
656 String.Equals(nameof(ComplexReferenceParentType.Container), this.Type, StringComparison.Ordinal))
657 {
658 return false;
659 }
660 return true;
661 }
662
663 /// <summary>
664 /// Helper IComparer class to make ordering easier.
665 /// </summary>
666 internal class AfterItemComparer : IComparer<Item>
667 {
668 public int Compare(Item x, Item y)
669 {
670 if (x.afterItems.Contains(y))
671 {
672 return 1;
673 }
674 else if (y.afterItems.Contains(x))
675 {
676 return -1;
677 }
678
679 return String.CompareOrdinal(x.Id, y.Id);
680 }
681 }
682 }
683 }
684 }