// 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.Data { using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Text; using System.Xml; using WixToolset.Data.Rows; /// /// Object that represents a table in a database. /// public sealed class Table { /// /// Creates a table in a section. /// /// Section to add table to. /// Definition of the table. public Table(TableDefinition tableDefinition) { this.Definition = tableDefinition; this.Rows = new List(); } /// /// Gets the table definition. /// /// Definition of the table. public TableDefinition Definition { get; private set; } /// /// Gets the name of the table. /// /// Name of the table. public string Name { get { return this.Definition.Name; } } /// /// Gets or sets the table transform operation. /// /// The table transform operation. public TableOperation Operation { get; set; } /// /// Gets the rows contained in the table. /// /// Rows contained in the table. public IList Rows { get; private set; } /// /// Creates a new row in the table. /// /// Original source lines for this row. /// Specifies whether to only create the row or add it to the table automatically. /// Row created in table. public Row CreateRow(SourceLineNumber sourceLineNumbers, bool add = true) { Row row; switch (this.Name) { case "BBControl": row = new BBControlRow(sourceLineNumbers, this); break; case "WixBundlePackage": row = new WixBundlePackageRow(sourceLineNumbers, this); break; case "WixBundleExePackage": row = new WixBundleExePackageRow(sourceLineNumbers, this); break; case "WixBundleMsiPackage": row = new WixBundleMsiPackageRow(sourceLineNumbers, this); break; case "WixBundleMspPackage": row = new WixBundleMspPackageRow(sourceLineNumbers, this); break; case "WixBundleMsuPackage": row = new WixBundleMsuPackageRow(sourceLineNumbers, this); break; case "Component": row = new ComponentRow(sourceLineNumbers, this); break; case "WixBundleContainer": row = new WixBundleContainerRow(sourceLineNumbers, this); break; case "Control": row = new ControlRow(sourceLineNumbers, this); break; case "File": row = new FileRow(sourceLineNumbers, this); break; case "WixBundleMsiFeature": row = new WixBundleMsiFeatureRow(sourceLineNumbers, this); break; case "WixBundleMsiProperty": row = new WixBundleMsiPropertyRow(sourceLineNumbers, this); break; case "Media": row = new MediaRow(sourceLineNumbers, this); break; case "WixBundlePayload": row = new WixBundlePayloadRow(sourceLineNumbers, this); break; case "Property": row = new PropertyRow(sourceLineNumbers, this); break; case "WixRelatedBundle": row = new WixRelatedBundleRow(sourceLineNumbers, this); break; case "WixBundleRelatedPackage": row = new WixBundleRelatedPackageRow(sourceLineNumbers, this); break; case "WixBundleRollbackBoundary": row = new WixBundleRollbackBoundaryRow(sourceLineNumbers, this); break; case "Upgrade": row = new UpgradeRow(sourceLineNumbers, this); break; case "WixBundleVariable": row = new WixBundleVariableRow(sourceLineNumbers, this); break; case "WixAction": row = new WixActionRow(sourceLineNumbers, this); break; case "WixApprovedExeForElevation": row = new WixApprovedExeForElevationRow(sourceLineNumbers, this); break; case "WixBundle": row = new WixBundleRow(sourceLineNumbers, this); break; case "WixBundlePackageExitCode": row = new WixBundlePackageExitCodeRow(sourceLineNumbers, this); break; case "WixBundlePatchTargetCode": row = new WixBundlePatchTargetCodeRow(sourceLineNumbers, this); break; case "WixBundleSlipstreamMsp": row = new WixBundleSlipstreamMspRow(sourceLineNumbers, this); break; case "WixBundleUpdate": row = new WixBundleUpdateRow(sourceLineNumbers, this); break; case "WixBundleCatalog": row = new WixBundleCatalogRow(sourceLineNumbers, this); break; case "WixChain": row = new WixChainRow(sourceLineNumbers, this); break; case "WixChainItem": row = new WixChainItemRow(sourceLineNumbers, this); break; case "WixBundlePackageCommandLine": row = new WixBundlePackageCommandLineRow(sourceLineNumbers, this); break; case "WixComplexReference": row = new WixComplexReferenceRow(sourceLineNumbers, this); break; case "WixDeltaPatchFile": row = new WixDeltaPatchFileRow(sourceLineNumbers, this); break; case "WixDeltaPatchSymbolPaths": row = new WixDeltaPatchSymbolPathsRow(sourceLineNumbers, this); break; case "WixFile": row = new WixFileRow(sourceLineNumbers, this); break; case "WixGroup": row = new WixGroupRow(sourceLineNumbers, this); break; case "WixMedia": row = new WixMediaRow(sourceLineNumbers, this); break; case "WixMediaTemplate": row = new WixMediaTemplateRow(sourceLineNumbers, this); break; case "WixMerge": row = new WixMergeRow(sourceLineNumbers, this); break; case "WixPayloadProperties": row = new WixPayloadPropertiesRow(sourceLineNumbers, this); break; case "WixProperty": row = new WixPropertyRow(sourceLineNumbers, this); break; case "WixSimpleReference": row = new WixSimpleReferenceRow(sourceLineNumbers, this); break; case "WixUpdateRegistration": row = new WixUpdateRegistrationRow(sourceLineNumbers, this); break; default: row = new Row(sourceLineNumbers, this); break; } if (add) { this.Rows.Add(row); } return row; } /// /// Parse a table from the xml. /// /// XmlReader where the intermediate is persisted. /// Section to populate with persisted data. /// TableDefinitions to use in the intermediate. /// The parsed table. internal static Table Read(XmlReader reader, TableDefinitionCollection tableDefinitions) { Debug.Assert("table" == reader.LocalName); bool empty = reader.IsEmptyElement; TableOperation operation = TableOperation.None; string name = null; while (reader.MoveToNextAttribute()) { switch (reader.LocalName) { case "name": name = reader.Value; break; case "op": switch (reader.Value) { case "add": operation = TableOperation.Add; break; case "drop": operation = TableOperation.Drop; break; default: throw new XmlException(); } break; } } if (null == name) { throw new XmlException(); } TableDefinition tableDefinition = tableDefinitions[name]; Table table = new Table(tableDefinition); table.Operation = operation; if (!empty) { bool done = false; // loop through all the rows in a table while (!done && reader.Read()) { switch (reader.NodeType) { case XmlNodeType.Element: switch (reader.LocalName) { case "row": Row.Read(reader, table); break; default: throw new XmlException(); } break; case XmlNodeType.EndElement: done = true; break; } } if (!done) { throw new XmlException(); } } return table; } /// /// Modularize the table. /// /// String containing the GUID of the Merge Module, if appropriate. /// Optional collection of identifiers that should not be modularized. public void Modularize(string modularizationGuid, ISet suppressModularizationIdentifiers) { List modularizedColumns = new List(); // find the modularized columns for (int i = 0; i < this.Definition.Columns.Count; i++) { if (ColumnModularizeType.None != this.Definition.Columns[i].ModularizeType) { modularizedColumns.Add(i); } } if (0 < modularizedColumns.Count) { foreach (Row row in this.Rows) { foreach (int modularizedColumn in modularizedColumns) { Field field = row.Fields[modularizedColumn]; if (null != field.Data) { field.Data = row.GetModularizedValue(field, modularizationGuid, suppressModularizationIdentifiers); } } } } } /// /// Persists a row in an XML format. /// /// XmlWriter where the Row should persist itself as XML. [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "Changing the way this string normalizes would result " + "in a change to the way the intermediate files are generated, potentially causing extra churn in patches on an MSI built from an older version of WiX. " + "Furthermore, there is no security hole here, as the strings won't need to make a round trip")] internal void Write(XmlWriter writer) { if (null == writer) { throw new ArgumentNullException("writer"); } writer.WriteStartElement("table", Intermediate.XmlNamespaceUri); writer.WriteAttributeString("name", this.Name); if (TableOperation.None != this.Operation) { writer.WriteAttributeString("op", this.Operation.ToString().ToLowerInvariant()); } foreach (Row row in this.Rows) { row.Write(writer); } writer.WriteEndElement(); } /// /// Writes the table in IDT format to the provided stream. /// /// Stream to write the table to. /// Whether to keep columns added in a transform. public void ToIdtDefinition(StreamWriter writer, bool keepAddedColumns) { if (this.Definition.Unreal) { return; } if (TableDefinition.MaxColumnsInRealTable < this.Definition.Columns.Count) { throw new WixException(WixDataErrors.TooManyColumnsInRealTable(this.Definition.Name, this.Definition.Columns.Count, TableDefinition.MaxColumnsInRealTable)); } // Tack on the table header, and flush before we start writing bytes directly to the stream. writer.Write(this.Definition.ToIdtDefinition(keepAddedColumns)); writer.Flush(); using (var binary = new BinaryWriter(writer.BaseStream, writer.Encoding, true)) { // Create an encoding that replaces characters with question marks, and doesn't throw. We'll // use this in case of errors Encoding convertEncoding = Encoding.GetEncoding(writer.Encoding.CodePage); foreach (Row row in this.Rows) { if (row.Redundant) { continue; } string rowString = row.ToIdtDefinition(keepAddedColumns); byte[] rowBytes; try { // GetBytes will throw an exception if any character doesn't match our current encoding rowBytes = writer.Encoding.GetBytes(rowString); } catch (EncoderFallbackException) { Messaging.Instance.OnMessage(WixDataErrors.InvalidStringForCodepage(row.SourceLineNumbers, Convert.ToString(writer.Encoding.WindowsCodePage, CultureInfo.InvariantCulture))); rowBytes = convertEncoding.GetBytes(rowString); } binary.Write(rowBytes, 0, rowBytes.Length); } } } /// /// Validates the rows of this OutputTable and throws if it collides on /// primary keys. /// public void ValidateRows() { Dictionary primaryKeys = new Dictionary(); foreach (Row row in this.Rows) { string primaryKey = row.GetPrimaryKey(); SourceLineNumber collisionSourceLineNumber; if (primaryKeys.TryGetValue(primaryKey, out collisionSourceLineNumber)) { throw new WixException(WixDataErrors.DuplicatePrimaryKey(collisionSourceLineNumber, primaryKey, this.Definition.Name)); } primaryKeys.Add(primaryKey, row.SourceLineNumbers); } } } }