From d3d3649a68cb1fa589fdd987a6690dbd5d671f0d Mon Sep 17 00:00:00 2001 From: Rob Mensching Date: Sun, 17 Sep 2017 15:35:20 -0700 Subject: Initial code commit --- src/WixToolset.Core/Link/ConnectToFeature.cs | 95 +++ .../Link/ConnectToFeatureCollection.cs | 92 +++ src/WixToolset.Core/Link/ConnectToModule.cs | 54 ++ .../Link/ConnectToModuleCollection.cs | 92 +++ .../Link/FindEntrySectionAndLoadSymbolsCommand.cs | 109 ++++ .../Link/ReportConflictingSymbolsCommand.cs | 49 ++ .../Link/ResolveReferencesCommand.cs | 178 +++++ src/WixToolset.Core/Link/WixGroupingOrdering.cs | 726 +++++++++++++++++++++ 8 files changed, 1395 insertions(+) create mode 100644 src/WixToolset.Core/Link/ConnectToFeature.cs create mode 100644 src/WixToolset.Core/Link/ConnectToFeatureCollection.cs create mode 100644 src/WixToolset.Core/Link/ConnectToModule.cs create mode 100644 src/WixToolset.Core/Link/ConnectToModuleCollection.cs create mode 100644 src/WixToolset.Core/Link/FindEntrySectionAndLoadSymbolsCommand.cs create mode 100644 src/WixToolset.Core/Link/ReportConflictingSymbolsCommand.cs create mode 100644 src/WixToolset.Core/Link/ResolveReferencesCommand.cs create mode 100644 src/WixToolset.Core/Link/WixGroupingOrdering.cs (limited to 'src/WixToolset.Core/Link') diff --git a/src/WixToolset.Core/Link/ConnectToFeature.cs b/src/WixToolset.Core/Link/ConnectToFeature.cs new file mode 100644 index 00000000..6e046b89 --- /dev/null +++ b/src/WixToolset.Core/Link/ConnectToFeature.cs @@ -0,0 +1,95 @@ +// 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.Link +{ + using System.Collections.Specialized; + using WixToolset.Data; + + /// + /// Object that connects things (components/modules) to features. + /// + public sealed class ConnectToFeature + { + private Section section; + private string childId; + + private string primaryFeature; + private bool explicitPrimaryFeature; + private StringCollection connectFeatures; + + /// + /// Creates a new connect to feature. + /// + /// Section this connect belongs to. + /// Id of the child. + public ConnectToFeature(Section section, string childId) : + this(section, childId, null, false) + { + } + + /// + /// Creates a new connect to feature. + /// + /// Section this connect belongs to. + /// Id of the child. + /// Sets the primary feature for the connection. + /// Sets if this is explicit primary. + public ConnectToFeature(Section section, string childId, string primaryFeature, bool explicitPrimaryFeature) + { + this.section = section; + this.childId = childId; + + this.primaryFeature = primaryFeature; + this.explicitPrimaryFeature = explicitPrimaryFeature; + + this.connectFeatures = new StringCollection(); + } + + /// + /// Gets the section. + /// + /// Section. + public Section Section + { + get { return this.section; } + } + + /// + /// Gets the child identifier. + /// + /// The child identifier. + public string ChildId + { + get { return this.childId; } + } + + /// + /// Gets or sets if the flag for if the primary feature was set explicitly. + /// + /// The flag for if the primary feature was set explicitly. + public bool IsExplicitPrimaryFeature + { + get { return this.explicitPrimaryFeature; } + set { this.explicitPrimaryFeature = value; } + } + + /// + /// Gets or sets the primary feature. + /// + /// The primary feature. + public string PrimaryFeature + { + get { return this.primaryFeature; } + set { this.primaryFeature = value; } + } + + /// + /// Gets the features connected to. + /// + /// Features connected to. + public StringCollection ConnectFeatures + { + get { return this.connectFeatures; } + } + } +} diff --git a/src/WixToolset.Core/Link/ConnectToFeatureCollection.cs b/src/WixToolset.Core/Link/ConnectToFeatureCollection.cs new file mode 100644 index 00000000..8dd0d22c --- /dev/null +++ b/src/WixToolset.Core/Link/ConnectToFeatureCollection.cs @@ -0,0 +1,92 @@ +// 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.Link +{ + using System; + using System.Collections; + + /// + /// Hash collection of connect to feature objects. + /// + public sealed class ConnectToFeatureCollection : ICollection + { + private Hashtable collection; + + /// + /// Instantiate a new ConnectToFeatureCollection class. + /// + public ConnectToFeatureCollection() + { + this.collection = new Hashtable(); + } + + /// + /// Gets the number of items in the collection. + /// + /// Number of items in collection. + public int Count + { + get { return this.collection.Count; } + } + + /// + /// Gets if the collection has been synchronized. + /// + /// True if the collection has been synchronized. + public bool IsSynchronized + { + get { return this.collection.IsSynchronized; } + } + + /// + /// Gets the object used to synchronize the collection. + /// + /// Oject used the synchronize the collection. + public object SyncRoot + { + get { return this.collection.SyncRoot; } + } + + /// + /// Gets a feature connection by child id. + /// + /// Identifier of child to locate. + public ConnectToFeature this[string childId] + { + get { return (ConnectToFeature)this.collection[childId]; } + } + + /// + /// Adds a feature connection to the collection. + /// + /// Feature connection to add. + public void Add(ConnectToFeature connection) + { + if (null == connection) + { + throw new ArgumentNullException("connection"); + } + + this.collection.Add(connection.ChildId, connection); + } + + /// + /// Copies the collection into an array. + /// + /// Array to copy the collection into. + /// Index to start copying from. + public void CopyTo(System.Array array, int index) + { + this.collection.CopyTo(array, index); + } + + /// + /// Gets enumerator for the collection. + /// + /// Enumerator for the collection. + public IEnumerator GetEnumerator() + { + return this.collection.Values.GetEnumerator(); + } + } +} diff --git a/src/WixToolset.Core/Link/ConnectToModule.cs b/src/WixToolset.Core/Link/ConnectToModule.cs new file mode 100644 index 00000000..d6a8338e --- /dev/null +++ b/src/WixToolset.Core/Link/ConnectToModule.cs @@ -0,0 +1,54 @@ +// 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.Link +{ + /// + /// Object that connects things to modules. + /// + public sealed class ConnectToModule + { + private string childId; + private string module; + private string moduleLanguage; + + /// + /// Creates a new connect to module. + /// + /// Id of the child. + /// Id of the module. + /// Language of the module. + public ConnectToModule(string childId, string module, string moduleLanguage) + { + this.childId = childId; + this.module = module; + this.moduleLanguage = moduleLanguage; + } + + /// + /// Gets the id of the child. + /// + /// Child identifier. + public string ChildId + { + get { return this.childId; } + } + + /// + /// Gets the id of the module. + /// + /// The id of the module. + public string Module + { + get { return this.module; } + } + + /// + /// Gets the language of the module. + /// + /// The language of the module. + public string ModuleLanguage + { + get { return this.moduleLanguage; } + } + } +} diff --git a/src/WixToolset.Core/Link/ConnectToModuleCollection.cs b/src/WixToolset.Core/Link/ConnectToModuleCollection.cs new file mode 100644 index 00000000..6595487f --- /dev/null +++ b/src/WixToolset.Core/Link/ConnectToModuleCollection.cs @@ -0,0 +1,92 @@ +// 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.Link +{ + using System; + using System.Collections; + + /// + /// Hash collection of connect to module objects. + /// + public sealed class ConnectToModuleCollection : ICollection + { + private Hashtable collection; + + /// + /// Instantiate a new ConnectToModuleCollection class. + /// + public ConnectToModuleCollection() + { + this.collection = new Hashtable(); + } + + /// + /// Gets the number of elements actually contained in the ConnectToModuleCollection. + /// + /// The number of elements actually contained in the ConnectToModuleCollection. + public int Count + { + get { return this.collection.Count; } + } + + /// + /// Gets a value indicating whether access to the ConnectToModuleCollection is synchronized (thread-safe). + /// + /// true if access to the ConnectToModuleCollection is synchronized (thread-safe); otherwise, false. The default is false. + public bool IsSynchronized + { + get { return this.collection.IsSynchronized; } + } + + /// + /// Gets an object that can be used to synchronize access to the ConnectToModuleCollection. + /// + /// An object that can be used to synchronize access to the ConnectToModuleCollection. + public object SyncRoot + { + get { return this.collection.SyncRoot; } + } + + /// + /// Gets a module connection by child id. + /// + /// Identifier of child to locate. + public ConnectToModule this[string childId] + { + get { return (ConnectToModule)this.collection[childId]; } + } + + /// + /// Adds a module connection to the collection. + /// + /// Module connection to add. + public void Add(ConnectToModule connection) + { + if (null == connection) + { + throw new ArgumentNullException("connection"); + } + + this.collection.Add(connection.ChildId, connection); + } + + /// + /// Copies the entire ConnectToModuleCollection to a compatible one-dimensional Array, starting at the specified index of the target array. + /// + /// The one-dimensional Array that is the destination of the elements copied from this ConnectToModuleCollection. The Array must have zero-based indexing. + /// The zero-based index in array at which copying begins. + public void CopyTo(System.Array array, int index) + { + this.collection.Keys.CopyTo(array, index); + } + + /// + /// Returns an enumerator for the entire ConnectToModuleCollection. + /// + /// An IEnumerator for the entire ConnectToModuleCollection. + public IEnumerator GetEnumerator() + { + return this.collection.Keys.GetEnumerator(); + } + } +} diff --git a/src/WixToolset.Core/Link/FindEntrySectionAndLoadSymbolsCommand.cs b/src/WixToolset.Core/Link/FindEntrySectionAndLoadSymbolsCommand.cs new file mode 100644 index 00000000..effb06e4 --- /dev/null +++ b/src/WixToolset.Core/Link/FindEntrySectionAndLoadSymbolsCommand.cs @@ -0,0 +1,109 @@ +// 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.Link +{ + using System; + using System.Collections.Generic; + using System.Linq; + using WixToolset.Data; + + internal class FindEntrySectionAndLoadSymbolsCommand : ICommand + { + private IEnumerable
sections; + + public FindEntrySectionAndLoadSymbolsCommand(IEnumerable
sections) + { + this.sections = sections; + } + + /// + /// Sets the expected entry output type, based on output file extension provided to the linker. + /// + public OutputType ExpectedOutputType { private get; set; } + + /// + /// Gets the located entry section after the command is executed. + /// + public Section EntrySection { get; private set; } + + /// + /// Gets the collection of loaded symbols. + /// + public IDictionary Symbols { get; private set; } + + public IEnumerable PossiblyConflictingSymbols { get; private set; } + + public void Execute() + { + Dictionary symbols = new Dictionary(); + HashSet possibleConflicts = new HashSet(); + + SectionType expectedEntrySectionType; + if (!Enum.TryParse(this.ExpectedOutputType.ToString(), out expectedEntrySectionType)) + { + expectedEntrySectionType = SectionType.Unknown; + } + + foreach (Section section in this.sections) + { + // Try to find the one and only entry section. + if (SectionType.Product == section.Type || SectionType.Module == section.Type || SectionType.PatchCreation == section.Type || SectionType.Patch == section.Type || SectionType.Bundle == section.Type) + { + if (SectionType.Unknown != expectedEntrySectionType && section.Type != expectedEntrySectionType) + { + string outputExtension = Output.GetExtension(this.ExpectedOutputType); + Messaging.Instance.OnMessage(WixWarnings.UnexpectedEntrySection(section.SourceLineNumbers, section.Type.ToString(), expectedEntrySectionType.ToString(), outputExtension)); + } + + if (null == this.EntrySection) + { + this.EntrySection = section; + } + else + { + Messaging.Instance.OnMessage(WixErrors.MultipleEntrySections(this.EntrySection.SourceLineNumbers, this.EntrySection.Id, section.Id)); + Messaging.Instance.OnMessage(WixErrors.MultipleEntrySections2(section.SourceLineNumbers)); + } + } + + // Load all the symbols from the section's tables that create symbols. + foreach (Table table in section.Tables.Where(t => t.Definition.CreateSymbols)) + { + foreach (Row row in table.Rows) + { + Symbol symbol = new Symbol(row); + + Symbol existingSymbol; + if (!symbols.TryGetValue(symbol.Name, out existingSymbol)) + { + symbols.Add(symbol.Name, symbol); + } + else // uh-oh, duplicate symbols. + { + // If the duplicate symbols are both private directories, there is a chance that they + // point to identical rows. Identical directory rows are redundant and will not cause + // conflicts. + if (AccessModifier.Private == existingSymbol.Access && AccessModifier.Private == symbol.Access && + "Directory" == existingSymbol.Row.Table.Name && existingSymbol.Row.IsIdentical(symbol.Row)) + { + // Ensure identical symbol's row is marked redundant to ensure (should the row be + // referenced into the final output) it will not add duplicate primary keys during + // the .IDT importing. + symbol.Row.Redundant = true; + existingSymbol.AddRedundant(symbol); + } + else + { + existingSymbol.AddPossibleConflict(symbol); + possibleConflicts.Add(existingSymbol); + } + } + } + } + } + + this.Symbols = symbols; + this.PossiblyConflictingSymbols = possibleConflicts; + } + } +} diff --git a/src/WixToolset.Core/Link/ReportConflictingSymbolsCommand.cs b/src/WixToolset.Core/Link/ReportConflictingSymbolsCommand.cs new file mode 100644 index 00000000..39c3a5c2 --- /dev/null +++ b/src/WixToolset.Core/Link/ReportConflictingSymbolsCommand.cs @@ -0,0 +1,49 @@ +// 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.Link +{ + using System.Collections.Generic; + using System.Linq; + using WixToolset.Data; + + public class ReportConflictingSymbolsCommand : ICommand + { + private IEnumerable possibleConflicts; + private IEnumerable
resolvedSections; + + public ReportConflictingSymbolsCommand(IEnumerable possibleConflicts, IEnumerable
resolvedSections) + { + this.possibleConflicts = possibleConflicts; + this.resolvedSections = resolvedSections; + } + + public void Execute() + { + // Do a quick check if there are any possibly conflicting symbols that don't come from tables that allow + // overriding. Hopefully the symbols with possible conflicts list is usually very short list (empty should + // be the most common). If we find any matches, we'll do a more costly check to see if the possible conflicting + // symbols are in sections we actually referenced. From the resulting set, show an error for each duplicate + // (aka: conflicting) symbol. This should catch any rows with colliding primary keys (since symbols are based + // on the primary keys of rows). + List illegalDuplicates = possibleConflicts.Where(s => "WixAction" != s.Row.Table.Name && "WixVariable" != s.Row.Table.Name).ToList(); + if (0 < illegalDuplicates.Count) + { + HashSet
referencedSections = new HashSet
(resolvedSections); + foreach (Symbol referencedDuplicateSymbol in illegalDuplicates.Where(s => referencedSections.Contains(s.Section))) + { + List actuallyReferencedDuplicateSymbols = referencedDuplicateSymbol.PossiblyConflictingSymbols.Where(s => referencedSections.Contains(s.Section)).ToList(); + + if (actuallyReferencedDuplicateSymbols.Any()) + { + Messaging.Instance.OnMessage(WixErrors.DuplicateSymbol(referencedDuplicateSymbol.Row.SourceLineNumbers, referencedDuplicateSymbol.Name)); + + foreach (Symbol duplicate in actuallyReferencedDuplicateSymbols) + { + Messaging.Instance.OnMessage(WixErrors.DuplicateSymbol2(duplicate.Row.SourceLineNumbers)); + } + } + } + } + } + } +} diff --git a/src/WixToolset.Core/Link/ResolveReferencesCommand.cs b/src/WixToolset.Core/Link/ResolveReferencesCommand.cs new file mode 100644 index 00000000..5a985f3f --- /dev/null +++ b/src/WixToolset.Core/Link/ResolveReferencesCommand.cs @@ -0,0 +1,178 @@ +// 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.Link +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Linq; + using WixToolset.Data; + using WixToolset.Data.Rows; + + /// + /// Resolves all the simple references in a section. + /// + internal class ResolveReferencesCommand : ICommand + { + private Section entrySection; + private IDictionary symbols; + private HashSet referencedSymbols; + private HashSet
resolvedSections; + + public ResolveReferencesCommand(Section entrySection, IDictionary symbols) + { + this.entrySection = entrySection; + this.symbols = symbols; + } + + public bool BuildingMergeModule { private get; set; } + + public IEnumerable ReferencedSymbols { get { return this.referencedSymbols; } } + + public IEnumerable
ResolvedSections { get { return this.resolvedSections; } } + + /// + /// Resolves all the simple references in a section. + /// + public void Execute() + { + this.resolvedSections = new HashSet
(); + this.referencedSymbols = new HashSet(); + + this.RecursivelyResolveReferences(this.entrySection); + } + + /// + /// Recursive helper function to resolve all references of passed in section. + /// + /// Section with references to resolve. + /// Note: recursive function. + private void RecursivelyResolveReferences(Section section) + { + // If we already resolved this section, move on to the next. + if (!this.resolvedSections.Add(section)) + { + return; + } + + // Process all of the references contained in this section using the collection of + // symbols provided. Then recursively call this method to process the + // located symbol's section. All in all this is a very simple depth-first + // search of the references per-section. + Table wixSimpleReferenceTable; + if (section.Tables.TryGetTable("WixSimpleReference", out wixSimpleReferenceTable)) + { + foreach (WixSimpleReferenceRow wixSimpleReferenceRow in wixSimpleReferenceTable.Rows) + { + Debug.Assert(wixSimpleReferenceRow.Section == section); + + // If we're building a Merge Module, ignore all references to the Media table + // because Merge Modules don't have Media tables. + if (this.BuildingMergeModule && "Media" == wixSimpleReferenceRow.TableName) + { + continue; + } + + Symbol symbol; + if (!this.symbols.TryGetValue(wixSimpleReferenceRow.SymbolicName, out symbol)) + { + Messaging.Instance.OnMessage(WixErrors.UnresolvedReference(wixSimpleReferenceRow.SourceLineNumbers, wixSimpleReferenceRow.SymbolicName)); + } + else // see if the symbol (and any of its duplicates) are appropriately accessible. + { + IList accessible = DetermineAccessibleSymbols(section, symbol); + if (!accessible.Any()) + { + Messaging.Instance.OnMessage(WixErrors.UnresolvedReference(wixSimpleReferenceRow.SourceLineNumbers, wixSimpleReferenceRow.SymbolicName, symbol.Access)); + } + else if (1 == accessible.Count) + { + Symbol accessibleSymbol = accessible[0]; + this.referencedSymbols.Add(accessibleSymbol); + + if (null != accessibleSymbol.Section) + { + RecursivelyResolveReferences(accessibleSymbol.Section); + } + } + else // display errors for the duplicate symbols. + { + Symbol accessibleSymbol = accessible[0]; + string referencingSourceLineNumber = wixSimpleReferenceRow.SourceLineNumbers.ToString(); + if (String.IsNullOrEmpty(referencingSourceLineNumber)) + { + Messaging.Instance.OnMessage(WixErrors.DuplicateSymbol(accessibleSymbol.Row.SourceLineNumbers, accessibleSymbol.Name)); + } + else + { + Messaging.Instance.OnMessage(WixErrors.DuplicateSymbol(accessibleSymbol.Row.SourceLineNumbers, accessibleSymbol.Name, referencingSourceLineNumber)); + } + + foreach (Symbol accessibleDuplicate in accessible.Skip(1)) + { + Messaging.Instance.OnMessage(WixErrors.DuplicateSymbol2(accessibleDuplicate.Row.SourceLineNumbers)); + } + } + } + } + } + } + + /// + /// Determine if the symbol and any of its duplicates are accessbile by referencing section. + /// + /// Section referencing the symbol. + /// Symbol being referenced. + /// List of symbols accessible by referencing section. + private IList DetermineAccessibleSymbols(Section referencingSection, Symbol symbol) + { + List symbols = new List(); + + if (AccessibleSymbol(referencingSection, symbol)) + { + symbols.Add(symbol); + } + + foreach (Symbol dupe in symbol.PossiblyConflictingSymbols) + { + if (AccessibleSymbol(referencingSection, dupe)) + { + symbols.Add(dupe); + } + } + + foreach (Symbol dupe in symbol.RedundantSymbols) + { + if (AccessibleSymbol(referencingSection, dupe)) + { + symbols.Add(dupe); + } + } + + return symbols; + } + + /// + /// Determine if a single symbol is accessible by the referencing section. + /// + /// Section referencing the symbol. + /// Symbol being referenced. + /// True if symbol is accessible. + private bool AccessibleSymbol(Section referencingSection, Symbol symbol) + { + switch (symbol.Access) + { + case AccessModifier.Public: + return true; + case AccessModifier.Internal: + return symbol.Row.Section.IntermediateId.Equals(referencingSection.IntermediateId) || (null != symbol.Row.Section.LibraryId && symbol.Row.Section.LibraryId.Equals(referencingSection.LibraryId)); + case AccessModifier.Protected: + return symbol.Row.Section.IntermediateId.Equals(referencingSection.IntermediateId); + case AccessModifier.Private: + return referencingSection == symbol.Section; + default: + throw new InvalidOperationException(); + } + } + } +} diff --git a/src/WixToolset.Core/Link/WixGroupingOrdering.cs b/src/WixToolset.Core/Link/WixGroupingOrdering.cs new file mode 100644 index 00000000..fc0ce43b --- /dev/null +++ b/src/WixToolset.Core/Link/WixGroupingOrdering.cs @@ -0,0 +1,726 @@ +// 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.Link +{ + using System; + using System.Collections; + using System.Collections.ObjectModel; + using System.Collections.Generic; + using System.Diagnostics; + using System.Globalization; + using System.Linq; + using System.Text; + using WixToolset.Extensibility; + using WixToolset.Data; + + /// + /// Grouping and Ordering class of the WiX toolset. + /// + internal sealed class WixGroupingOrdering : IMessageHandler + { + private Output output; + private IMessageHandler 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(Output output, IMessageHandler messageHandler) + { + this.output = output; + this.messageHandler = messageHandler; + + this.rowsUsed = new List(); + this.loaded = false; + this.encounteredError = false; + } + + /// + /// 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; + } + + /// + /// Sends a message to the message handler if there is one. + /// + /// Message event arguments. + public void OnMessage(MessageEventArgs e) + { + WixErrorEventArgs errorEventArgs = e as WixErrorEventArgs; + + if (null != errorEventArgs || MessageLevel.Error == e.Level) + { + this.encounteredError = true; + } + + if (null != this.messageHandler) + { + this.messageHandler.OnMessage(e); + } + else if (null != errorEventArgs) + { + throw new WixException(errorEventArgs); + } + } + + /// + /// 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)); + + List orderedItems; + this.CreateOrderedList(parentType, parentId, out 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.OnMessage(WixErrors.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() + { + List 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); + } + } + + /// + /// 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. + Table wixGroupTable = this.output.Tables["WixGroup"]; + Debug.Assert(null != wixGroupTable); + + foreach (Item item in orderedItems) + { + Row row = wixGroupTable.CreateRow(item.Row.SourceLineNumbers); + row[0] = parentId; + row[1] = parentType; + row[2] = item.Id; + row[3] = item.Type; + } + } + + // 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.OnMessage(WixErrors.MissingBundleInformation("WixGroup")); + } + + // Collect all of the groups + for (int rowIndex = 0; rowIndex < wixGroupTable.Rows.Count; ++rowIndex) + { + Row row = wixGroupTable.Rows[rowIndex]; + string rowParentName = (string)row[0]; + string rowParentType = (string)row[1]; + string rowChildName = (string)row[2]; + string rowChildType = (string)row[3]; + + // 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); + + Item parentItem; + if (!this.items.TryGetValue(rowParentType, rowParentName, out parentItem)) + { + parentItem = new Item(row, rowParentType, rowParentName); + this.items.Add(parentItem); + } + + Item childItem; + if (!this.items.TryGetValue(rowChildType, rowChildName, out 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.OnMessage(WixErrors.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 (Row row in wixOrderingTable.Rows) + { + string rowItemType = (string)row[0]; + string rowItemName = (string)row[1]; + string rowDependsOnType = (string)row[2]; + string rowDependsOnName = (string)row[3]; + + // 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; + } + + Item item = null; + Item dependsOn = null; + + if (!this.items.TryGetValue(rowItemType, rowItemName, out item)) + { + this.OnMessage(WixErrors.IdentifierNotFound(rowItemType, rowItemName)); + } + + if (!this.items.TryGetValue(rowDependsOnType, rowDependsOnName, out dependsOn)) + { + this.OnMessage(WixErrors.IdentifierNotFound(rowDependsOnType, rowDependsOnName)); + } + + if (null == item || null == dependsOn) + { + continue; + } + + item.AddAfter(dependsOn, this); + } + } + + /// + /// 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); + } + + foreach (Item item in this.items) + { + item.FlattenAfters(this); + } + } + + /// + /// 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(Row 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 Row 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, IMessageHandler 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, IMessageHandler 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.OnMessage(WixErrors.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(IMessageHandler 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(IMessageHandler 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 sealed 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); + } + } + } + } +} -- cgit v1.2.3-55-g6feb