// 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.WindowsInstaller { using System; using System.Diagnostics; using System.Globalization; using System.Xml; /// /// Field containing data for a column in a row. /// public class Field { private object data; /// /// Instantiates a new Field. /// /// Column definition for this field. protected Field(ColumnDefinition columnDefinition) { this.Column = columnDefinition; } /// /// Gets or sets the column definition for this field. /// /// Column definition. public ColumnDefinition Column { get; private set; } /// /// Gets or sets the data for this field. /// /// Data in the field. public object Data { get => this.data; set => this.data = this.ValidateValue(this.Column, value); } /// /// Gets or sets whether this field is modified. /// /// Whether this field is modified. public bool Modified { get; set; } /// /// Gets or sets the previous data. /// /// The previous data. public string PreviousData { get; set; } /// /// Instantiate a new Field object of the correct type. /// /// The column definition for the field. /// The new Field object. public static Field Create(ColumnDefinition columnDefinition) { return (ColumnType.Object == columnDefinition.Type) ? new ObjectField(columnDefinition) : new Field(columnDefinition); } /// /// 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 BestEffortSet(object value) { bool success = true; object bestEffortValue = value; try { bestEffortValue = this.ValidateValue(this.Column, value); } catch (InvalidOperationException) { success = false; } this.data = bestEffortValue; return success; } /// /// Determine if this field is identical to another field. /// /// The other field to compare to. /// true if they are equal; false otherwise. public bool IsIdentical(Field field) { return (this.Column.Name == field.Column.Name && ((null != this.data && this.data.Equals(field.data)) || (null == this.data && null == field.data))); } /// /// Overrides the built in object implementation to return the field's data as a string. /// /// Field's data as a string. public override string ToString() { return this.AsString(); } /// /// Gets the field as an integer. /// /// Field's data as an integer. public int AsInteger() { return (this.data is int) ? (int)this.data : Convert.ToInt32(this.data, CultureInfo.InvariantCulture); } /// /// Gets the field as an integer that could be null. /// /// Field's data as an integer that could be null. public int? AsNullableInteger() { return (null == this.data) ? (int?)null : (this.data is int) ? (int)this.data : Convert.ToInt32(this.data, CultureInfo.InvariantCulture); } /// /// Gets the field as a string. /// /// Field's data as a string. public string AsString() { return (null == this.data) ? null : Convert.ToString(this.data, CultureInfo.InvariantCulture); } /// /// Validate a value for this column. /// /// The value to validate. /// Validated value. internal object ValidateValue(ColumnDefinition column, object value) { if (null == value) { if (!column.Nullable) { throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Cannot set column '{0}' with a null value because this is a required field.", column.Name)); } } else // check numerical values against their specified minimum and maximum values. { if (ColumnType.Number == column.Type && !column.IsLocalizable) { // For now all enums in the tables can be represented by integers. This if statement would need to // be enhanced if that ever changes. if (value is int || value.GetType().IsEnum) { var intValue = (int)value; // validate the value against the minimum allowed value if (column.MinValue.HasValue && column.MinValue > intValue) { throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Cannot set column '{0}' with value {1} because it is less than the minimum allowed value for this column, {2}.", column.Name, intValue, column.MinValue)); } // validate the value against the maximum allowed value if (column.MaxValue.HasValue && column.MaxValue < intValue) { throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Cannot set column '{0}' with value {1} because it is greater than the maximum allowed value for this column, {2}.", column.Name, intValue, column.MaxValue)); } return intValue; } else if (value is long longValue) { // validate the value against the minimum allowed value if (column.MinValue.HasValue && column.MinValue > longValue) { throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Cannot set column '{0}' with value {1} because it is less than the minimum allowed value for this column, {2}.", column.Name, longValue, column.MinValue)); } // validate the value against the maximum allowed value if (column.MaxValue.HasValue && column.MaxValue < longValue) { throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Cannot set column '{0}' with value {1} because it is greater than the maximum allowed value for this column, {2}.", column.Name, longValue, column.MaxValue)); } return longValue; } else { throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Cannot set number column '{0}' with a value of type '{1}'.", column.Name, value.GetType().ToString())); } } else { if (!(value is string)) { //throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Cannot set string column '{0}' with a value of type '{1}'.", this.name, value.GetType().ToString())); return value.ToString(); } } } return value; } /// /// Parse a field from the xml. /// /// XmlReader where the intermediate is persisted. internal virtual void Read(XmlReader reader) { Debug.Assert("field" == reader.LocalName); bool empty = reader.IsEmptyElement; while (reader.MoveToNextAttribute()) { switch (reader.LocalName) { case "modified": this.Modified = reader.Value.Equals("yes"); break; case "previousData": this.PreviousData = reader.Value; break; } } if (!empty) { bool done = false; while (!done && reader.Read()) { switch (reader.NodeType) { case XmlNodeType.Element: throw new XmlException(); case XmlNodeType.CDATA: case XmlNodeType.Text: case XmlNodeType.SignificantWhitespace: if (0 < reader.Value.Length) { if (ColumnType.Number == this.Column.Type && !this.Column.IsLocalizable) { // older wix files could persist data as a long value (which would overflow an int) // since the Convert class always throws exceptions for overflows, read in integral // values as a long to avoid the overflow, then cast it to an int (this operation can // overflow without throwing an exception inside an unchecked block) this.data = unchecked((int)Convert.ToInt64(reader.Value, CultureInfo.InvariantCulture)); } else { this.data = reader.Value; } } break; case XmlNodeType.EndElement: done = true; break; } } if (!done) { throw new XmlException(); } } } /// /// Persists a field in an XML format. /// /// XmlWriter where the Field should persist itself as XML. internal virtual void Write(XmlWriter writer) { writer.WriteStartElement("field", WindowsInstallerData.XmlNamespaceUri); if (this.Modified) { writer.WriteAttributeString("modified", "yes"); } if (null != this.PreviousData) { writer.WriteAttributeString("previousData", this.PreviousData); } // Convert the data to a string that will persist nicely (nulls as String.Empty). string text = Convert.ToString(this.data, CultureInfo.InvariantCulture); if (this.Column.UseCData) { writer.WriteCData(text); } else { writer.WriteString(text); } writer.WriteEndElement(); } } }