// 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. namespace WixToolset.Core.Link { using System; using System.Collections.ObjectModel; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Text; using WixToolset.Data; using WixToolset.Data.Tuples; using WixToolset.Extensibility.Services; /// /// Grouping and Ordering class of the WiX toolset. /// internal class WixGroupingOrdering { private IMessaging messageHandler; private List groupTypes; private List itemTypes; private ItemCollection items; private List rowsUsed; private bool loaded; private bool encounteredError; /// /// Creates a WixGroupingOrdering object. /// /// Output from which to read the group and order information. /// Handler for any error messages. /// Group types to include. /// Item types to include. public WixGroupingOrdering(IntermediateSection entrySections, IMessaging messageHandler) { this.EntrySection = entrySections; this.messageHandler = messageHandler; this.rowsUsed = new List(); this.loaded = false; this.encounteredError = false; } private IntermediateSection EntrySection { get; } /// /// Switches a WixGroupingOrdering object to operate on a new set of groups/items. /// /// Group types to include. /// Item types to include. public void UseTypes(IEnumerable groupTypes, IEnumerable itemTypes) { this.groupTypes = new List(groupTypes); this.itemTypes = new List(itemTypes); this.items = new ItemCollection(); this.loaded = false; } /// /// Finds all nested items under a parent group and creates new WixGroup data for them. /// /// The group type for the parent group to flatten. /// The identifier of the parent group to flatten. /// Whether to remove used group rows before returning. public void FlattenAndRewriteRows(string parentType, string parentId, bool removeUsedRows) { Debug.Assert(this.groupTypes.Contains(parentType)); this.CreateOrderedList(parentType, parentId, out var orderedItems); if (this.encounteredError) { return; } this.CreateNewGroupRows(parentType, parentId, orderedItems); if (removeUsedRows) { this.RemoveUsedGroupRows(); } } /// /// Finds all items under a parent group type and creates new WixGroup data for them. /// /// The type of the parent group to flatten. /// Whether to remove used group rows before returning. public void FlattenAndRewriteGroups(string parentType, bool removeUsedRows) { Debug.Assert(this.groupTypes.Contains(parentType)); this.LoadFlattenOrderGroups(); if (this.encounteredError) { return; } foreach (Item item in this.items) { if (parentType == item.Type) { List orderedItems; this.CreateOrderedList(item.Type, item.Id, out orderedItems); this.CreateNewGroupRows(item.Type, item.Id, orderedItems); } } if (removeUsedRows) { this.RemoveUsedGroupRows(); } } /// /// Creates a flattened and ordered list of items for the given parent group. /// /// The group type for the parent group to flatten. /// The identifier of the parent group to flatten. /// The returned list of ordered items. private void CreateOrderedList(string parentType, string parentId, out List orderedItems) { orderedItems = null; this.LoadFlattenOrderGroups(); if (this.encounteredError) { return; } Item parentItem; if (!this.items.TryGetValue(parentType, parentId, out parentItem)) { this.messageHandler.Write(ErrorMessages.IdentifierNotFound(parentType, parentId)); return; } orderedItems = new List(parentItem.ChildItems); orderedItems.Sort(new Item.AfterItemComparer()); } /// /// Removes rows from WixGroup that have been used by this object. /// public void RemoveUsedGroupRows() { var sortedIndexes = this.rowsUsed.Distinct().OrderByDescending(i => i).ToList(); //Table wixGroupTable = this.output.Tables["WixGroup"]; //Debug.Assert(null != wixGroupTable); //Debug.Assert(sortedIndexes[0] < wixGroupTable.Rows.Count); foreach (int rowIndex in sortedIndexes) { //wixGroupTable.Rows.RemoveAt(rowIndex); this.EntrySection.Tuples.RemoveAt(rowIndex); } } /// /// Creates new WixGroup rows for a list of items. /// /// The group type for the parent group in the new rows. /// The identifier of the parent group in the new rows. /// The list of new items. private void CreateNewGroupRows(string parentType, string parentId, List orderedItems) { // TODO: MSIs don't guarantee that rows stay in the same order, and technically, neither // does WiX (although they do, currently). We probably want to "upgrade" this to a new // table that includes a sequence number, and then change the code that uses ordered // groups to read from that table instead. foreach (Item item in orderedItems) { var row = new WixGroupTuple(item.Row.SourceLineNumbers); row.ParentId = parentId; row.ParentType = (ComplexReferenceParentType)Enum.Parse(typeof(ComplexReferenceParentType), parentType); row.ChildId = item.Id; row.ChildType = (ComplexReferenceChildType)Enum.Parse(typeof(ComplexReferenceChildType), item.Type); this.EntrySection.Tuples.Add(row); } } // Group/Ordering Flattening Logic // // What follows is potentially convoluted logic. Two somewhat orthogonal concepts are in // play: grouping (parent/child relationships) and ordering (before/after relationships). // Dealing with just one or the other is straghtforward. Groups can be flattened // recursively. Ordering can be propagated in either direction. When the ordering also // participates in the grouping constructions, however, things get trickier. For the // purposes of this discussion, we're dealing with "items" and "groups", and an instance // of either of them can be marked as coming "after" some other instance. // // For simple item-to-item ordering, the "after" values simply propagate: if A is after B, // and B is after C, then we can say that A is after *both* B and C. If a group is involved, // it acts as a proxy for all of its included items and any sub-groups. /// /// Internal workhorse for ensuring that group and ordering information has /// been loaded and applied. /// private void LoadFlattenOrderGroups() { if (!this.loaded) { this.LoadGroups(); this.LoadOrdering(); // It would be really nice to have a "find circular after dependencies" // function, but it gets much more complicated because of the way that // the dependencies are propagated across group boundaries. For now, we // just live with the dependency loop detection as we flatten the // dependencies. Group references, however, we can check directly. this.FindCircularGroupReferences(); if (!this.encounteredError) { this.FlattenGroups(); this.FlattenOrdering(); } this.loaded = true; } } /// /// Loads data from the WixGroup table. /// private void LoadGroups() { //Table wixGroupTable = this.output.Tables["WixGroup"]; //if (null == wixGroupTable || 0 == wixGroupTable.Rows.Count) //{ // // TODO: Change message name to make it *not* Bundle specific? // this.Write(WixErrors.MissingBundleInformation("WixGroup")); //} // Collect all of the groups for (int rowIndex = 0; rowIndex < this.EntrySection.Tuples.Count; ++rowIndex) { if (this.EntrySection.Tuples[rowIndex] is WixGroupTuple row) { var rowParentName = row.ParentId; var rowParentType = row.ParentType.ToString(); var rowChildName = row.ChildId; var rowChildType = row.ChildType.ToString(); // If this row specifies a parent or child type that's not in our // lists, we assume it's not a row that we're concerned about. if (!this.groupTypes.Contains(rowParentType) || !this.itemTypes.Contains(rowChildType)) { continue; } this.rowsUsed.Add(rowIndex); if (!this.items.TryGetValue(rowParentType, rowParentName, out var parentItem)) { parentItem = new Item(row, rowParentType, rowParentName); this.items.Add(parentItem); } if (!this.items.TryGetValue(rowChildType, rowChildName, out var childItem)) { childItem = new Item(row, rowChildType, rowChildName); this.items.Add(childItem); } parentItem.ChildItems.Add(childItem); } } } /// /// Flattens group/item information. /// private void FlattenGroups() { foreach (Item item in this.items) { item.FlattenChildItems(); } } /// /// Finds and reports circular references in the group/item data. /// private void FindCircularGroupReferences() { ItemCollection itemsInKnownLoops = new ItemCollection(); foreach (Item item in this.items) { if (itemsInKnownLoops.Contains(item)) { continue; } ItemCollection itemsSeen = new ItemCollection(); string circularReference; if (this.FindCircularGroupReference(item, item, itemsSeen, out circularReference)) { itemsInKnownLoops.Add(itemsSeen); this.messageHandler.Write(ErrorMessages.ReferenceLoopDetected(item.Row.SourceLineNumbers, circularReference)); } } } /// /// Recursive worker to find and report circular references in group/item data. /// /// The sentinal item being checked. /// The current item in the recursion. /// A list of all items already visited (for performance). /// A list of items in the current circular reference, if one was found; null otherwise. /// True if a circular reference was found; false otherwise. private bool FindCircularGroupReference(Item checkItem, Item currentItem, ItemCollection itemsSeen, out string circularReference) { circularReference = null; foreach (Item subitem in currentItem.ChildItems) { if (checkItem == subitem) { // TODO: Even better would be to include the source lines for each reference! circularReference = String.Format(CultureInfo.InvariantCulture, "{0}:{1} -> {2}:{3}", currentItem.Type, currentItem.Id, subitem.Type, subitem.Id); return true; } if (!itemsSeen.Contains(subitem)) { itemsSeen.Add(subitem); if (this.FindCircularGroupReference(checkItem, subitem, itemsSeen, out circularReference)) { // TODO: Even better would be to include the source lines for each reference! circularReference = String.Format(CultureInfo.InvariantCulture, "{0}:{1} -> {2}", currentItem.Type, currentItem.Id, circularReference); return true; } } } return false; } /// /// Loads ordering dependency data from the WixOrdering table. /// private void LoadOrdering() { //Table wixOrderingTable = output.Tables["WixOrdering"]; //if (null == wixOrderingTable || 0 == wixOrderingTable.Rows.Count) //{ // // TODO: Do we need a message here? // return; //} foreach (var row in this.EntrySection.Tuples.OfType()) { var rowItemType = row.ItemType; var rowItemName = row.ItemId_; var rowDependsOnType = row.DependsOnType; var rowDependsOnName = row.DependsOnId_; // If this row specifies some other (unknown) type in either // position, we assume it's not a row that we're concerned about. // For ordering, we allow group and item in either position. if (!(this.groupTypes.Contains(rowItemType) || this.itemTypes.Contains(rowItemType)) || !(this.groupTypes.Contains(rowDependsOnType) || this.itemTypes.Contains(rowDependsOnType))) { continue; } if (!this.items.TryGetValue(rowItemType, rowItemName, out var item)) { this.messageHandler.Write(ErrorMessages.IdentifierNotFound(rowItemType, rowItemName)); } if (!this.items.TryGetValue(rowDependsOnType, rowDependsOnName, out var dependsOn)) { this.messageHandler.Write(ErrorMessages.IdentifierNotFound(rowDependsOnType, rowDependsOnName)); } if (null == item || null == dependsOn) { continue; } item.AddAfter(dependsOn, this.messageHandler); } } /// /// Flattens the ordering dependencies in the groups/items. /// private void FlattenOrdering() { // Because items don't know about their parent groups (and can, in fact, be // in more than one group at a time), we need to pre-propagate the 'afters' // from each parent item to its children before we attempt to flatten the // ordering. foreach (Item item in this.items) { item.PropagateAfterToChildItems(this.messageHandler); } foreach (Item item in this.items) { item.FlattenAfters(this.messageHandler); } } /// /// A variant of KeyedCollection that doesn't throw when an item is re-added. /// /// Key type for the collection. /// Item type for the colelction. internal abstract class EnhancedKeyCollection : KeyedCollection { new public void Add(TItem item) { if (!this.Contains(item)) { base.Add(item); } } public void Add(Collection list) { foreach (TItem item in list) { this.Add(item); } } public void Remove(Collection list) { foreach (TItem item in list) { this.Remove(item); } } public bool TryGetValue(TKey key, out TItem item) { // KeyedCollection doesn't implement the TryGetValue() method, but it's // a useful concept. We can't just always pass this to the enclosed // Dictionary, however, because it doesn't always exist! If it does, we // can delegate to it as one would expect. If it doesn't, we have to // implement everything ourselves in terms of Contains(). if (null != this.Dictionary) { return this.Dictionary.TryGetValue(key, out item); } if (this.Contains(key)) { item = this[key]; return true; } item = default(TItem); return false; } #if DEBUG // This just makes debugging easier... public override string ToString() { StringBuilder sb = new StringBuilder(); foreach (TItem item in this) { sb.AppendFormat("{0}, ", item); } sb.Length -= 2; return sb.ToString(); } #endif // DEBUG } /// /// A specialized EnhancedKeyCollection, typed to Items. /// internal class ItemCollection : EnhancedKeyCollection { protected override string GetKeyForItem(Item item) { return item.Key; } public bool TryGetValue(string type, string id, out Item item) { return this.TryGetValue(CreateKeyFromTypeId(type, id), out item); } public static string CreateKeyFromTypeId(string type, string id) { return String.Format(CultureInfo.InvariantCulture, "{0}_{1}", type, id); } } /// /// An item (or group) in the grouping/ordering engine. /// /// Encapsulates nested group membership and also before/after /// ordering dependencies. internal class Item { private ItemCollection afterItems; private ItemCollection beforeItems; // for checking for circular references private bool flattenedAfterItems; public Item(IntermediateTuple row, string type, string id) { this.Row = row; this.Type = type; this.Id = id; this.Key = ItemCollection.CreateKeyFromTypeId(type, id); afterItems = new ItemCollection(); beforeItems = new ItemCollection(); flattenedAfterItems = false; } public IntermediateTuple Row { get; private set; } public string Type { get; private set; } public string Id { get; private set; } public string Key { get; private set; } #if DEBUG // Makes debugging easier... public override string ToString() { return this.Key; } #endif // DEBUG private ItemCollection childItems = new ItemCollection(); public ItemCollection ChildItems { get { return childItems; } } /// /// Removes any nested groups under this item and replaces /// them with their child items. /// public void FlattenChildItems() { ItemCollection flattenedChildItems = new ItemCollection(); foreach (Item childItem in this.ChildItems) { if (0 == childItem.ChildItems.Count) { flattenedChildItems.Add(childItem); } else { childItem.FlattenChildItems(); flattenedChildItems.Add(childItem.ChildItems); } } this.ChildItems.Clear(); this.ChildItems.Add(flattenedChildItems); } /// /// Adds a list of items to the 'after' ordering collection. /// /// List of items to add. /// Message handler in case a circular ordering reference is found. public void AddAfter(ItemCollection items, IMessaging messageHandler) { foreach (Item item in items) { this.AddAfter(item, messageHandler); } } /// /// Adds an item to the 'after' ordering collection. /// /// Items to add. /// Message handler in case a circular ordering reference is found. public void AddAfter(Item after, IMessaging messageHandler) { if (this.beforeItems.Contains(after)) { // We could try to chain this up (the way that group circular dependencies // are reported), but since we're in the process of flattening, we may already // have lost some distinction between authored and propagated ordering. string circularReference = String.Format(CultureInfo.InvariantCulture, "{0}:{1} -> {2}:{3} -> {0}:{1}", this.Type, this.Id, after.Type, after.Id); messageHandler.Write(ErrorMessages.OrderingReferenceLoopDetected(after.Row.SourceLineNumbers, circularReference)); return; } this.afterItems.Add(after); after.beforeItems.Add(this); } /// /// Propagates 'after' dependencies from an item to its child items. /// /// Message handler in case a circular ordering reference is found. /// Because items don't know about their parent groups (and can, in fact, be in more /// than one group at a time), we need to propagate the 'afters' from each parent item to its children /// before we attempt to flatten the ordering. public void PropagateAfterToChildItems(IMessaging messageHandler) { if (this.ShouldItemPropagateChildOrdering()) { foreach (Item childItem in this.childItems) { childItem.AddAfter(this.afterItems, messageHandler); } } } /// /// Flattens the ordering dependency for this item. /// /// Message handler in case a circular ordering reference is found. public void FlattenAfters(IMessaging messageHandler) { if (this.flattenedAfterItems) { return; } this.flattenedAfterItems = true; // Ensure that if we're after something (A), and *it's* after something (B), // that we list ourselved as after both (A) *and* (B). ItemCollection nestedAfterItems = new ItemCollection(); foreach (Item afterItem in this.afterItems) { afterItem.FlattenAfters(messageHandler); nestedAfterItems.Add(afterItem.afterItems); if (afterItem.ShouldItemPropagateChildOrdering()) { // If we are after a group, it really means // we are after all of the group's children. foreach (Item childItem in afterItem.ChildItems) { childItem.FlattenAfters(messageHandler); nestedAfterItems.Add(childItem.afterItems); nestedAfterItems.Add(childItem); } } } this.AddAfter(nestedAfterItems, messageHandler); } // We *don't* propagate ordering information from Packages or // Containers to their children, because ordering doesn't matter // for them, and a Payload in two Packages (or Containers) can // cause a circular reference to occur. We do, however, need to // track the ordering in the UX Container, because we need the // first payload to be the entrypoint. private bool ShouldItemPropagateChildOrdering() { if (String.Equals("Package", this.Type, StringComparison.Ordinal) || (String.Equals("Container", this.Type, StringComparison.Ordinal) && !String.Equals(Compiler.BurnUXContainerId, this.Id, StringComparison.Ordinal))) { return false; } return true; } /// /// Helper IComparer class to make ordering easier. /// internal class AfterItemComparer : IComparer { public int Compare(Item x, Item y) { if (x.afterItems.Contains(y)) { return 1; } else if (y.afterItems.Contains(x)) { return -1; } return String.CompareOrdinal(x.Id, y.Id); } } } } }