// 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.Text; using System.Text.RegularExpressions; using System.Xml; /// /// Row containing data for a table. /// public class Row { private static long rowCount; private Field[] fields; /// /// Creates a row that belongs to a table. /// /// Original source lines for this row. /// Table this row belongs to and should get its column definitions from. /// The compiler should use this constructor exclusively. public Row(SourceLineNumber sourceLineNumbers, Table table) : this(sourceLineNumbers, table.Definition) { this.Table = table; } /// /// Creates a row that does not belong to a table. /// /// Original source lines for this row. /// TableDefinition this row should get its column definitions from. /// This constructor is used in cases where there isn't a clear owner of the row. The linker uses this constructor for the rows it generates. public Row(SourceLineNumber sourceLineNumbers, TableDefinition tableDefinition) { this.Number = rowCount++; this.SourceLineNumbers = sourceLineNumbers; this.fields = new Field[tableDefinition.Columns.Count]; this.TableDefinition = tableDefinition; for (int i = 0; i < this.fields.Length; ++i) { this.fields[i] = Field.Create(this.TableDefinition.Columns[i]); } } /// /// Creates a shallow copy of a row from another row. /// /// The row the data is copied from. protected Row(Row source) { this.Table = source.Table; this.TableDefinition = source.TableDefinition; this.Number = source.Number; this.Access = source.Access; this.Operation = source.Operation; this.Redundant = source.Redundant; this.SectionId = source.SectionId; this.SourceLineNumbers = source.SourceLineNumbers; this.fields = source.fields; } /// /// Gets or sets the access to the row's primary key. /// /// The row access modifier. public AccessModifier Access { get; set; } /// /// Gets or sets the row transform operation. /// /// The row transform operation. public RowOperation Operation { get; set; } /// /// Gets or sets wether the row is a duplicate of another row thus redundant. /// public bool Redundant { get; set; } /// /// Gets or sets the SectionId property on the row. /// /// The SectionId property on the row. public string SectionId { get; set; } /// /// Gets the source file and line number for the row. /// /// Source file and line number. public SourceLineNumber SourceLineNumbers { get; private set; } /// /// Gets the table this row belongs to. /// /// null if Row does not belong to a Table, or owner Table otherwise. public Table Table { get; private set; } /// /// Gets the table definition for this row. /// /// A Row always has a TableDefinition, even if the Row does not belong to a Table. /// TableDefinition for Row. public TableDefinition TableDefinition { get; private set; } /// /// Gets the fields contained by this row. /// /// Array of field objects [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays")] public Field[] Fields { get { return this.fields; } } /// /// Gets the unique number for the row. /// /// Number for row. public long Number { get; private set; } /// /// Gets or sets the value of a particular field in the row. /// /// field index. /// Value of a field in the row. public object this[int field] { get { return this.fields[field].Data; } set { this.fields[field].Data = value; } } /// /// Gets the field as an integer. /// /// Field's data as an integer. public int FieldAsInteger(int field) { return this.fields[field].AsInteger(); } /// /// Gets the field as an integer that could be null. /// /// Field's data as an integer that could be null. public int? FieldAsNullableInteger(int field) { return this.fields[field].AsNullableInteger(); } /// /// Gets the field as a string. /// /// Field's data as a string. public string FieldAsString(int field) { return this.fields[field].AsString(); } /// /// Sets the value of a particular field in the row without validating. /// /// field index. /// Value of a field in the row. /// True if successful, false if validation failed. public bool BestEffortSetField(int field, object value) { return this.fields[field].BestEffortSet(value); } /// /// Get the value used to represent the row in a keyed row collection. /// /// Primary key or row number if no primary key is available. public string GetKey() { return this.GetPrimaryKey() ?? Convert.ToString(this.Number, CultureInfo.InvariantCulture); } /// /// Get the primary key of this row. /// /// Delimiter character for multiple column primary keys. /// The primary key or null if the row's table has no primary key columns. public string GetPrimaryKey(char delimiter = '/') { return this.GetPrimaryKey(delimiter, String.Empty); } /// /// Get the primary key of this row. /// /// Delimiter character for multiple column primary keys. /// String to represent null values in the primary key. /// The primary key or null if the row's table has no primary key columns. public string GetPrimaryKey(char delimiter, string nullReplacement) { bool foundPrimaryKey = false; StringBuilder primaryKey = new StringBuilder(); foreach (Field field in this.fields) { if (field.Column.PrimaryKey) { if (foundPrimaryKey) { primaryKey.Append(delimiter); } primaryKey.Append((null == field.Data) ? nullReplacement : Convert.ToString(field.Data, CultureInfo.InvariantCulture)); foundPrimaryKey = true; } else // primary keys must be the first columns of a row so the first non-primary key means we can stop looking. { break; } } return foundPrimaryKey ? primaryKey.ToString() : null; } /// /// Returns true if the specified field is null or an empty string. /// /// Index of the field to check. /// true if the specified field is null or an empty string, false otherwise. public bool IsColumnEmpty(int field) { if (null == this.fields[field].Data) { return true; } string dataString = this.fields[field].Data as string; if (null != dataString && 0 == dataString.Length) { return true; } return false; } /// /// Tests if the passed in row is identical. /// /// Row to compare against. /// True if two rows are identical. public bool IsIdentical(Row row) { bool identical = (this.TableDefinition.Name == row.TableDefinition.Name && this.fields.Length == row.fields.Length); for (int i = 0; identical && i < this.fields.Length; ++i) { if (!(this.fields[i].IsIdentical(row.fields[i]))) { identical = false; } } return identical; } /// /// Returns a string representation of the Row. /// /// A string representation of the Row. public override string ToString() { return String.Join("/", (object[])this.fields); } /// /// Creates a Row from the XmlReader. /// /// Reader to get data from. /// Table for this row. /// New row object. internal static Row Read(XmlReader reader, Table table) { Debug.Assert("row" == reader.LocalName); bool empty = reader.IsEmptyElement; AccessModifier access = AccessModifier.Public; RowOperation operation = RowOperation.None; bool redundant = false; string sectionId = null; SourceLineNumber sourceLineNumbers = null; while (reader.MoveToNextAttribute()) { switch (reader.LocalName) { case "access": access = (AccessModifier)Enum.Parse(typeof(AccessModifier), reader.Value, true); break; case "op": operation = (RowOperation)Enum.Parse(typeof(RowOperation), reader.Value, true); break; case "redundant": redundant = reader.Value.Equals("yes"); break; case "sectionId": sectionId = reader.Value; break; case "sourceLineNumber": sourceLineNumbers = SourceLineNumber.CreateFromEncoded(reader.Value); break; } } Row row = table.CreateRow(sourceLineNumbers); row.Access = access; row.Operation = operation; row.Redundant = redundant; row.SectionId = sectionId; // loop through all the fields in a row if (!empty) { bool done = false; int field = 0; // loop through all the fields in a row while (!done && reader.Read()) { switch (reader.NodeType) { case XmlNodeType.Element: switch (reader.LocalName) { case "field": if (row.Fields.Length <= field) { if (!reader.IsEmptyElement) { throw new XmlException(); } } else { row.fields[field].Read(reader); } ++field; break; default: throw new XmlException(); } break; case XmlNodeType.EndElement: done = true; break; } } if (!done) { throw new XmlException(); } } return row; } /// /// Returns the row in a format usable in IDT files. /// /// Whether to keep columns added in a transform. /// String with tab delimited field values. internal string ToIdtDefinition(bool keepAddedColumns) { bool first = true; StringBuilder sb = new StringBuilder(); foreach (Field field in this.fields) { // Conditionally keep columns added in a transform; otherwise, // break because columns can only be added at the end. if (field.Column.Added && !keepAddedColumns) { break; } if (first) { first = false; } else { sb.Append('\t'); } sb.Append(field.ToIdtValue()); } sb.Append("\r\n"); return sb.ToString(); } /// /// Gets the modularized version of the field data. /// /// The field to modularize. /// String containing the GUID of the Merge Module to append the the field value, if appropriate. /// Optional collection of identifiers that should not be modularized. /// moduleGuid is expected to be null when not being used to compile a Merge Module. /// The modularized version of the field data. internal string GetModularizedValue(Field field, string modularizationGuid, ISet suppressModularizationIdentifiers) { Debug.Assert(null != field.Data && 0 < ((string)field.Data).Length); string fieldData = Convert.ToString(field.Data, CultureInfo.InvariantCulture); if (null != modularizationGuid && ColumnModularizeType.None != field.Column.ModularizeType && !(WindowsInstallerStandard.IsStandardAction(fieldData) || WindowsInstallerStandard.IsStandardProperty(fieldData))) { StringBuilder sb; int start; ColumnModularizeType modularizeType = field.Column.ModularizeType; // special logic for the ControlEvent table's Argument column // this column requires different modularization methods depending upon the value of the Event column if (ColumnModularizeType.ControlEventArgument == field.Column.ModularizeType) { switch (this[2].ToString()) { case "CheckExistingTargetPath": // redirectable property name case "CheckTargetPath": case "DoAction": // custom action name case "NewDialog": // dialog name case "SelectionBrowse": case "SetTargetPath": case "SpawnDialog": case "SpawnWaitDialog": if (Common.IsIdentifier(fieldData)) { modularizeType = ColumnModularizeType.Column; } else { modularizeType = ColumnModularizeType.Property; } break; default: // formatted modularizeType = ColumnModularizeType.Property; break; } } else if (ColumnModularizeType.ControlText == field.Column.ModularizeType) { // icons are stored in the Binary table, so they get column-type modularization if (("Bitmap" == this[2].ToString() || "Icon" == this[2].ToString()) && Common.IsIdentifier(fieldData)) { modularizeType = ColumnModularizeType.Column; } else { modularizeType = ColumnModularizeType.Property; } } switch (modularizeType) { case ColumnModularizeType.Column: // ensure the value is an identifier (otherwise it shouldn't be modularized this way) if (!Common.IsIdentifier(fieldData)) { throw new InvalidOperationException(String.Format(CultureInfo.CurrentUICulture, WixDataStrings.EXP_CannotModularizeIllegalID, fieldData)); } // if we're not supposed to suppress modularization of this identifier if (null == suppressModularizationIdentifiers || !suppressModularizationIdentifiers.Contains(fieldData)) { fieldData = String.Concat(fieldData, ".", modularizationGuid); } break; case ColumnModularizeType.Property: case ColumnModularizeType.Condition: Regex regex; if (ColumnModularizeType.Property == modularizeType) { regex = new Regex(@"\[(?[#$!]?[a-zA-Z_][a-zA-Z0-9_\.]*)]", RegexOptions.Singleline | RegexOptions.ExplicitCapture); } else { Debug.Assert(ColumnModularizeType.Condition == modularizeType); // This heinous looking regular expression is actually quite an elegant way // to shred the entire condition into the identifiers that need to be // modularized. Let's break it down piece by piece: // // 1. Look for the operators: NOT, EQV, XOR, OR, AND, IMP (plus a space). Note that the // regular expression is case insensitive so we don't have to worry about // all the permutations of these strings. // 2. Look for quoted strings. Quoted strings are just text and are ignored // outright. // 3. Look for environment variables. These look like identifiers we might // otherwise be interested in but start with a percent sign. Like quoted // strings these enviroment variable references are ignored outright. // 4. Match all identifiers that are things that need to be modularized. Note // the special characters (!, $, ?, &) that denote Component and Feature states. regex = new Regex(@"NOT\s|EQV\s|XOR\s|OR\s|AND\s|IMP\s|"".*?""|%[a-zA-Z_][a-zA-Z0-9_\.]*|(?[!$\?&]?[a-zA-Z_][a-zA-Z0-9_\.]*)", RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture); // less performant version of the above with captures showing where everything lives // regex = new Regex(@"(?NOT|EQV|XOR|OR|AND|IMP)|(?"".*?"")|(?%[a-zA-Z_][a-zA-Z0-9_\.]*)|(?[!$\?&]?[a-zA-Z_][a-zA-Z0-9_\.]*)",RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture); } MatchCollection matches = regex.Matches(fieldData); sb = new StringBuilder(fieldData); // notice how this code walks backward through the list // because it modifies the string as we through it for (int i = matches.Count - 1; 0 <= i; i--) { Group group = matches[i].Groups["identifier"]; if (group.Success) { string identifier = group.Value; if (!WindowsInstallerStandard.IsStandardProperty(identifier) && (null == suppressModularizationIdentifiers || !suppressModularizationIdentifiers.Contains(identifier))) { sb.Insert(group.Index + group.Length, '.'); sb.Insert(group.Index + group.Length + 1, modularizationGuid); } } } fieldData = sb.ToString(); break; case ColumnModularizeType.CompanionFile: // if we're not supposed to ignore this identifier and the value does not start with // a digit, we must have a companion file so modularize it if ((null == suppressModularizationIdentifiers || !suppressModularizationIdentifiers.Contains(fieldData)) && 0 < fieldData.Length && !Char.IsDigit(fieldData, 0)) { fieldData = String.Concat(fieldData, ".", modularizationGuid); } break; case ColumnModularizeType.Icon: if (null == suppressModularizationIdentifiers || !suppressModularizationIdentifiers.Contains(fieldData)) { start = fieldData.LastIndexOf(".", StringComparison.Ordinal); if (-1 == start) { fieldData = String.Concat(fieldData, ".", modularizationGuid); } else { fieldData = String.Concat(fieldData.Substring(0, start), ".", modularizationGuid, fieldData.Substring(start)); } } break; case ColumnModularizeType.SemicolonDelimited: string[] keys = fieldData.Split(';'); for (int i = 0; i < keys.Length; ++i) { keys[i] = String.Concat(keys[i], ".", modularizationGuid); } fieldData = String.Join(";", keys); break; } } return fieldData; } /// /// 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 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) { writer.WriteStartElement("row", Intermediate.XmlNamespaceUri); if (AccessModifier.Public != this.Access) { writer.WriteAttributeString("access", this.Access.ToString().ToLowerInvariant()); } if (RowOperation.None != this.Operation) { writer.WriteAttributeString("op", this.Operation.ToString().ToLowerInvariant()); } if (this.Redundant) { writer.WriteAttributeString("redundant", "yes"); } if (null != this.SectionId) { writer.WriteAttributeString("sectionId", this.SectionId); } if (null != this.SourceLineNumbers) { writer.WriteAttributeString("sourceLineNumber", this.SourceLineNumbers.GetEncoded()); } for (int i = 0; i < this.fields.Length; ++i) { this.fields[i].Write(writer); } writer.WriteEndElement(); } } }