// 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.Dtf.WindowsInstaller { using System; using System.IO; using System.Xml; using System.Xml.Serialization; using System.Text; using System.Collections.Generic; using System.Globalization; using System.Diagnostics.CodeAnalysis; /// /// Contains a collection of key-value pairs suitable for passing between /// immediate and deferred/rollback/commit custom actions. /// /// /// Call the method to get a string /// suitable for storing in a property and reconstructing the custom action data later. /// /// /// [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")] public sealed class CustomActionData : IDictionary { /// /// "CustomActionData" literal property name. /// public const string PropertyName = "CustomActionData"; private const char DataSeparator = ';'; private const char KeyValueSeparator = '='; private IDictionary data; /// /// Creates a new empty custom action data object. /// public CustomActionData() : this(null) { } /// /// Reconstructs a custom action data object from data that was previously /// persisted in a string. /// /// Previous output from . public CustomActionData(string keyValueList) { this.data = new Dictionary(); if (keyValueList != null) { this.Parse(keyValueList); } } /// /// Adds a key and value to the data collection. /// /// Case-sensitive data key. /// Data value (may be null). /// the key does not consist solely of letters, /// numbers, and the period, underscore, and space characters. public void Add(string key, string value) { CustomActionData.ValidateKey(key); this.data.Add(key, value); } /// /// Adds a value to the data collection, using XML serialization to persist the object as a string. /// /// Case-sensitive data key. /// Data value (may be null). /// the key does not consist solely of letters, /// numbers, and the period, underscore, and space characters. /// The value type does not support XML serialization. /// The value could not be serialized. public void AddObject(string key, T value) { if (value == null) { this.Add(key, null); } else if (typeof(T) == typeof(string) || typeof(T) == typeof(CustomActionData)) // Serialize nested CustomActionData { this.Add(key, value.ToString()); } else { string valueString = CustomActionData.Serialize(value); this.Add(key, valueString); } } /// /// Gets a value from the data collection, using XML serialization to load the object from a string. /// /// Case-sensitive data key. /// The value could not be deserialized. [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")] public T GetObject(string key) { string value = this[key]; if (value == null) { return default(T); } else if (typeof(T) == typeof(string)) { // Funny casting because the compiler doesn't know T is string here. return (T) (object) value; } else if (typeof(T) == typeof(CustomActionData)) { // Deserialize nested CustomActionData. return (T) (object) new CustomActionData(value); } else if (value.Length == 0) { return default(T); } else { return CustomActionData.Deserialize(value); } } /// /// Determines whether the data contains an item with the specified key. /// /// Case-sensitive data key. /// true if the data contains an item with the key; otherwise, false public bool ContainsKey(string key) { return this.data.ContainsKey(key); } /// /// Gets a collection object containing all the keys of the data. /// public ICollection Keys { get { return this.data.Keys; } } /// /// Removes the item with the specified key from the data. /// /// Case-sensitive data key. /// true if the item was successfully removed from the data; /// false if an item with the specified key was not found public bool Remove(string key) { return this.data.Remove(key); } /// /// Gets the value with the specified key. /// /// Case-sensitive data key. /// Value associated with the specified key, or /// null if an item with the specified key was not found /// true if the data contains an item with the specified key; otherwise, false. public bool TryGetValue(string key, out string value) { return this.data.TryGetValue(key, out value); } /// /// Gets a collection containing all the values of the data. /// public ICollection Values { get { return this.data.Values; } } /// /// Gets or sets a data value with a specified key. /// /// Case-sensitive data key. /// the key does not consist solely of letters, /// numbers, and the period, underscore, and space characters. public string this[string key] { get { return this.data[key]; } set { CustomActionData.ValidateKey(key); this.data[key] = value; } } /// /// Adds an item with key and value to the data collection. /// /// Case-sensitive data key, with a data value that may be null. /// the key does not consist solely of letters, /// numbers, and the period, underscore, and space characters. public void Add(KeyValuePair item) { CustomActionData.ValidateKey(item.Key); this.data.Add(item); } /// /// Removes all items from the data. /// public void Clear() { if (this.data.Count > 0) { this.data.Clear(); } } /// /// Determines whether the data contains a specified item. /// /// The data item to locate. /// true if the data contains the item; otherwise, false public bool Contains(KeyValuePair item) { return this.data.Contains(item); } /// /// Copies the data to an array, starting at a particular array index. /// /// Destination array. /// Index in the array at which copying begins. public void CopyTo(KeyValuePair[] array, int arrayIndex) { this.data.CopyTo(array, arrayIndex); } /// /// Gets the number of items in the data. /// public int Count { get { return this.data.Count; } } /// /// Gets a value indicating whether the data is read-only. /// public bool IsReadOnly { get { return false; } } /// /// Removes an item from the data. /// /// The item to remove. /// true if the item was successfully removed from the data; /// false if the item was not found public bool Remove(KeyValuePair item) { return this.data.Remove(item); } /// /// Returns an enumerator that iterates through the collection. /// /// An enumerator that can be used to iterate through the collection. public IEnumerator> GetEnumerator() { return this.data.GetEnumerator(); } /// /// Returns an enumerator that iterates through the collection. /// /// An enumerator that can be used to iterate through the collection. System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return ((System.Collections.IEnumerable) this.data).GetEnumerator(); } /// /// Gets a string representation of the data suitable for persisting in a property. /// /// Data string in the form "Key1=Value1;Key2=Value2" public override string ToString() { StringBuilder buf = new StringBuilder(); foreach (KeyValuePair item in this.data) { if (buf.Length > 0) { buf.Append(CustomActionData.DataSeparator); } buf.Append(item.Key); if (item.Value != null) { buf.Append(CustomActionData.KeyValueSeparator); buf.Append(CustomActionData.Escape(item.Value)); } } return buf.ToString(); } /// /// Ensures that a key contains valid characters. /// /// key to be validated /// the key does not consist solely of letters, /// numbers, and the period, underscore, and space characters. private static void ValidateKey(string key) { if (String.IsNullOrEmpty(key)) { throw new ArgumentNullException("key"); } for (int i = 0; i < key.Length; i++) { char c = key[i]; if (!Char.IsLetterOrDigit(c) && c != '_' && c != '.' && !(i > 0 && i < key.Length - 1 && c == ' ')) { throw new ArgumentOutOfRangeException("key"); } } } /// /// Serializes a value into an XML string. /// /// Type of the value. /// Value to be serialized. /// Serialized value data as a string. private static string Serialize(T value) { XmlWriterSettings xws = new XmlWriterSettings(); xws.OmitXmlDeclaration = true; StringWriter sw = new StringWriter(CultureInfo.InvariantCulture); using (XmlWriter xw = XmlWriter.Create(sw, xws)) { XmlSerializerNamespaces ns = new XmlSerializerNamespaces(); ns.Add(string.Empty, String.Empty); // Prevent output of any namespaces XmlSerializer ser = new XmlSerializer(typeof(T)); ser.Serialize(xw, value, ns); return sw.ToString(); } } /// /// Deserializes a value from an XML string. /// /// Expected type of the value. /// Serialized value data. /// Deserialized value object. private static T Deserialize(string value) { StringReader sr = new StringReader(value); using (XmlReader xr = XmlReader.Create(sr)) { XmlSerializer ser = new XmlSerializer(typeof(T)); return (T) ser.Deserialize(xr); } } /// /// Escapes a value string by doubling any data-separator (semicolon) characters. /// /// /// Escaped value string private static string Escape(string value) { value = value.Replace(String.Empty + CustomActionData.DataSeparator, String.Empty + CustomActionData.DataSeparator + CustomActionData.DataSeparator); return value; } /// /// Unescapes a value string by undoubling any doubled data-separator (semicolon) characters. /// /// /// Unescaped value string private static string Unescape(string value) { value = value.Replace(String.Empty + CustomActionData.DataSeparator + CustomActionData.DataSeparator, String.Empty + CustomActionData.DataSeparator); return value; } /// /// Loads key-value pairs from a string into the data collection. /// /// key-value pair list of the form returned by private void Parse(string keyValueList) { int itemStart = 0; while (itemStart < keyValueList.Length) { // Find the next non-escaped data separator. int semi = itemStart - 2; do { semi = keyValueList.IndexOf(CustomActionData.DataSeparator, semi + 2); } while (semi >= 0 && semi < keyValueList.Length - 1 && keyValueList[semi + 1] == CustomActionData.DataSeparator); if (semi < 0) { semi = keyValueList.Length; } // Find the next non-escaped key-value separator. int equals = itemStart - 2; do { equals = keyValueList.IndexOf(CustomActionData.KeyValueSeparator, equals + 2); } while (equals >= 0 && equals < keyValueList.Length - 1 && keyValueList[equals + 1] == CustomActionData.KeyValueSeparator); if (equals < 0 || equals > semi) { equals = semi; } string key = keyValueList.Substring(itemStart, equals - itemStart); string value = null; // If there's a key-value separator before the next data separator, then the item has a value. if (equals < semi) { value = keyValueList.Substring(equals + 1, semi - (equals + 1)); value = CustomActionData.Unescape(value); } // Add non-duplicate items to the collection. if (key.Length > 0 && !this.data.ContainsKey(key)) { this.data.Add(key, value); } // Move past the data separator to the next item. itemStart = semi + 1; } } } }