File: System\Data\Services\Parsing\WebConvert.cs
Project: ndp\fx\src\DataWeb\Server\System.Data.Services.csproj (System.Data.Services)
//---------------------------------------------------------------------
// <copyright file="WebConvert.cs" company="Microsoft">
//      Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
// <summary>
//      Provides methods to convert URI and payload values.
// </summary>
//
// @owner  Microsoft
//---------------------------------------------------------------------
 
namespace System.Data.Services.Parsing
{
    using System;
    using System.Diagnostics;
    using System.Text;
    using System.Xml;
#if ASTORIA_CLIENT
    using System.Data.Services.Client;
#else
    using System.Globalization;
#endif
 
    /// <summary>Use this class to convert URI and payload values.</summary>
    internal static class WebConvert
    {
        /// <summary>Constant table of nibble-to-hex convertion values.</summary>
        private const string HexValues = "0123456789ABCDEF";
 
        /// <summary>Prefix to hex-encoded values.</summary>
        private const string XmlHexEncodePrefix = "0x";
 
#if ASTORIA_SERVER
        /// <summary>XML whitespace characters to trim around literals.</summary>
        private static char[] XmlWhitespaceChars = new char[] { ' ', '\t', '\n', '\r' };
 
        /// <summary>Determines whether the specified character is a valid hexadecimal digit.</summary>
        /// <param name="c">Character to check.</param>
        /// <returns>true if <paramref name="c"/> is a valid hex digit; false otherwise.</returns>
        internal static bool IsCharHexDigit(char c)
        {
            return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
        }
 
#endif
 
        /// <summary>Converts the given byte[] into string.</summary>
        /// <param name="byteArray">byte[] that needs to be converted.</param>
        /// <returns>String containing hex values representing the byte[].</returns>
        internal static string ConvertByteArrayToKeyString(byte[] byteArray)
        {
            StringBuilder hexBuilder = new StringBuilder(3 + byteArray.Length * 2);
            hexBuilder.Append(XmlConstants.XmlBinaryPrefix);
            hexBuilder.Append("'");
            for (int i = 0; i < byteArray.Length; i++)
            {
                hexBuilder.Append(HexValues[byteArray[i] >> 4]);
                hexBuilder.Append(HexValues[byteArray[i] & 0x0F]);
            }
 
            hexBuilder.Append("'");
#if DEBUG
            string escapee = hexBuilder.ToString(2, hexBuilder.Length-3);
            Debug.Assert(escapee == Uri.EscapeDataString(escapee), "binary representation is not expected to require escaping as a data string");
#endif
            return hexBuilder.ToString();
        }
 
#if ASTORIA_SERVER
 
        /// <summary>Checks whether the specified text is a correctly formatted quoted value.</summary>
        /// <param name='text'>Text to check.</param>
        /// <returns>true if the text is correctly formatted, false otherwise.</returns>
        internal static bool IsKeyValueQuoted(string text)
        {
            Debug.Assert(text != null, "text != null");
            if (text.Length < 2 || text[0] != '\'' || text[text.Length - 1] != '\'')
            {
                return false;
            }
            else
            {
                int startIndex = 1;
                while (startIndex < text.Length - 1)
                {
                    int match = text.IndexOf('\'', startIndex, text.Length - startIndex - 1);
                    if (match == -1)
                    {
                        break;
                    }
                    else if (match == text.Length - 2 || text[match + 1] != '\'')
                    {
                        return false;
                    }
                    else
                    {
                        startIndex = match + 2;
                    }
                }
 
                return true;
            }
        }
 
#endif
 
        /// <summary>
        /// Determines whether the values for the specified types should be 
        /// quoted in URI keys.
        /// </summary>
        /// <param name='type'>Type to check.</param>
        /// <returns>
        /// true if values of <paramref name='type' /> require quotes; false otherwise.
        /// </returns>
        internal static bool IsKeyTypeQuoted(Type type)
        {
            Debug.Assert(type != null, "type != null");
            return type == typeof(System.Xml.Linq.XElement) || type == typeof(string);
        }
 
        /// <summary>Converts the specified value to a encoded, serializable string for URI key.</summary>
        /// <param name="value">Non-null value to convert.</param>
        /// <param name="result">out parameter for value converted to a serializable string for URI key.</param>
        /// <returns>true/ false indicating success</returns>
        internal static bool TryKeyPrimitiveToString(object value, out string result)
        {
            Debug.Assert(value != null, "value != null");
            if (value.GetType() == typeof(byte[]))
            {
                result = ConvertByteArrayToKeyString((byte[])value);
            }
#if !ASTORIA_CLIENT
            else if (value.GetType() == typeof(System.Data.Linq.Binary))
            {
                return TryKeyPrimitiveToString(((System.Data.Linq.Binary)value).ToArray(), out result);
            }
#endif
            else
            {
                if (!TryXmlPrimitiveToString(value, out result))
                {
                    return false;
                }
 
                Debug.Assert(result != null, "result != null");
                if (IsKeyTypeQuoted(value.GetType()))
                {
                    result = result.Replace("'", "''");
                }
 
                // required for strings as data, DateTime for ':', numbers for '+'
                // we specifically do not want to encode leading and trailing "'" wrapping strings/datetime/guid
                result = Uri.EscapeDataString(result);
 
                if (value.GetType() == typeof(DateTime))
                {
                    result = XmlConstants.LiteralPrefixDateTime + "'" + result + "'";
                }
                else if (value.GetType() == typeof(Decimal))
                {
                    result = result + XmlConstants.XmlDecimalLiteralSuffix;
                }
                else if (value.GetType() == typeof(Guid))
                {
                    result = XmlConstants.LiteralPrefixGuid + "'" + result + "'";
                }
                else if (value.GetType() == typeof(Int64))
                {
                    result = result + XmlConstants.XmlInt64LiteralSuffix;
                }
                else if (value.GetType() == typeof(Single))
                {
                    result = result + XmlConstants.XmlSingleLiteralSuffix;
                }
#if ASTORIA_SERVER
                else if (value.GetType() == typeof(double))
                {
                    double d = (double)value;
                    if (!Double.IsInfinity(d) && !Double.IsNaN(d))
                    {
                        result = result + XmlConstants.XmlDoubleLiteralSuffix;
                    }
                }
#else
                else if (value.GetType() == typeof(double))
                {
                    result = AppendDecimalMarkerToDouble(result);
                }
#endif
                else if (IsKeyTypeQuoted(value.GetType()))
                {
                    result = "'" + result + "'";
                }
            }
 
            return true;
        }
 
#if ASTORIA_SERVER
        /// <summary>Removes quotes from the single-quotes text.</summary>
        /// <param name="text">Text to remove quotes from.</param>
        /// <returns>The specified <paramref name="text"/> with single quotes removed.</returns>
        internal static string RemoveQuotes(string text)
        {
            Debug.Assert(!String.IsNullOrEmpty(text), "!String.IsNullOrEmpty(text)");
 
            char quote = text[0];
            Debug.Assert(quote == '\'', "quote == '\''");
            Debug.Assert(text[text.Length - 1] == '\'', "text should end with '\''.");
 
            string s = text.Substring(1, text.Length - 2);
            int start = 0;
            while (true)
            {
                int i = s.IndexOf(quote, start);
                if (i < 0)
                {
                    break;
                }
 
                Debug.Assert(i + 1 < s.Length && s[i + 1] == '\'', @"Each single quote should be propertly escaped with double single quotes.");
                s = s.Remove(i, 1);
                start = i + 1;
            }
 
            return s;
        }
 
        /// <summary>Converts a string to a byte[] value.</summary>
        /// <param name="text">String text to convert.</param>
        /// <param name="targetValue">After invocation, converted value.</param>
        /// <returns>true if the value was converted; false otherwise.</returns>
        internal static bool TryKeyStringToByteArray(string text, out byte[] targetValue)
        {
            Debug.Assert(text != null, "text != null");
 
            if (!TryRemoveLiteralPrefix(XmlConstants.LiteralPrefixBinary, ref text) &&
                !TryRemoveLiteralPrefix(XmlConstants.XmlBinaryPrefix, ref text))
            {
                targetValue = null;
                return false;
            }
 
            if (!TryRemoveQuotes(ref text))
            {
                targetValue = null;
                return false;
            }
 
            if ((text.Length % 2) != 0)
            {
                targetValue = null;
                return false;
            }
 
            byte[] result = new byte[text.Length / 2];
            int resultIndex = 0;
            int textIndex = 0;
            while (resultIndex < result.Length)
            {
                char ch0 = text[textIndex];
                char ch1 = text[textIndex + 1];
                if (!IsCharHexDigit(ch0) || !IsCharHexDigit(ch1))
                {
                    targetValue = null;
                    return false;
                }
 
                result[resultIndex] = (byte)((byte)(HexCharToNibble(ch0) << 4) + HexCharToNibble(ch1));
                textIndex += 2;
                resultIndex++;
            }
 
            targetValue = result;
            return true;
        }
 
        /// <summary>Converts a string to a DateTime value.</summary>
        /// <param name="text">String text to convert.</param>
        /// <param name="targetValue">After invocation, converted value.</param>
        /// <returns>true if the value was converted; false otherwise.</returns>
        internal static bool TryKeyStringToDateTime(string text, out DateTime targetValue)
        {
            if (!TryRemoveLiteralPrefix(XmlConstants.LiteralPrefixDateTime, ref text))
            {
                targetValue = default(DateTime);
                return false;
            }
 
            if (!TryRemoveQuotes(ref text))
            {
                targetValue = default(DateTime);
                return false;
            }
 
            try
            {
                targetValue = XmlConvert.ToDateTime(text, XmlDateTimeSerializationMode.RoundtripKind);
                return true;
            }
            catch (FormatException)
            {
                targetValue = default(DateTime);
                return false;
            }
        }
 
        /// <summary>Converts a string to a GUID value.</summary>
        /// <param name="text">String text to convert.</param>
        /// <param name="targetValue">After invocation, converted value.</param>
        /// <returns>true if the value was converted; false otherwise.</returns>
        internal static bool TryKeyStringToGuid(string text, out Guid targetValue)
        {
            if (!TryRemoveLiteralPrefix(XmlConstants.LiteralPrefixGuid, ref text))
            {
                targetValue = default(Guid);
                return false;
            }
 
            if (!TryRemoveQuotes(ref text))
            {
                targetValue = default(Guid);
                return false;
            }
 
            try
            {
                targetValue = XmlConvert.ToGuid(text);
                return true;
            }
            catch (FormatException)
            {
                targetValue = default(Guid);
                return false;
            }
        }
 
        /// <summary>Converts a string to a primitive value.</summary>
        /// <param name="text">String text to convert.</param>
        /// <param name="targetType">Type to convert string to.</param>
        /// <param name="targetValue">After invocation, converted value.</param>
        /// <returns>true if the value was converted; false otherwise.</returns>
        internal static bool TryKeyStringToPrimitive(string text, Type targetType, out object targetValue)
        {
            Debug.Assert(text != null, "text != null");
            Debug.Assert(targetType != null, "targetType != null");
 
            targetType = Nullable.GetUnderlyingType(targetType) ?? targetType;
 
            byte[] byteArrayValue;
            bool binaryResult = TryKeyStringToByteArray(text, out byteArrayValue);
            if (targetType == typeof(byte[]) || targetType == typeof(System.Data.Linq.Binary))
            {
                // The object cast is required because otherwise the compiler uses the implicit byte[]
                // to Binary conversion and always returns Binary.
                targetValue =
                    (byteArrayValue != null && targetType == typeof(System.Data.Linq.Binary)) ?
                    (object)new System.Data.Linq.Binary(byteArrayValue) : (object)byteArrayValue;
                return binaryResult;
            }
            else if (binaryResult)
            {
                string keyValue = Encoding.UTF8.GetString(byteArrayValue);
                return TryKeyStringToPrimitive(keyValue, targetType, out targetValue);
            }
            // These have separate handlers for convenience - reuse them.
            else if (targetType == typeof(Guid))
            {
                Guid guidValue;
                bool result = TryKeyStringToGuid(text, out guidValue);
                targetValue = guidValue;
                return result;
            }
            else if (targetType == typeof(DateTime))
            {
                DateTime dateTimeValue;
                bool result = TryKeyStringToDateTime(text, out dateTimeValue);
                targetValue = dateTimeValue;
                return result;
            }
 
            bool quoted = WebConvert.IsKeyTypeQuoted(targetType);
            if (quoted != WebConvert.IsKeyValueQuoted(text))
            {
                targetValue = null;
                return false;
            }
 
            if (quoted)
            {
                Debug.Assert(IsKeyValueQuoted(text), "IsKeyValueQuoted(text) - otherwise caller didn't check this before");
                text = RemoveQuotes(text);
            }
 
            try
            {
                if (typeof(String) == targetType)
                {
                    targetValue = text;
                }
                else if (typeof(Boolean) == targetType)
                {
                    targetValue = XmlConvert.ToBoolean(text);
                }
                else if (typeof(Byte) == targetType)
                {
                    targetValue = XmlConvert.ToByte(text);
                }
                else if (typeof(SByte) == targetType)
                {
                    targetValue = XmlConvert.ToSByte(text);
                }
                else if (typeof(Int16) == targetType)
                {
                    targetValue = XmlConvert.ToInt16(text);
                }
                else if (typeof(Int32) == targetType)
                {
                    targetValue = XmlConvert.ToInt32(text);
                }
                else if (typeof(Int64) == targetType)
                {
                    if (TryRemoveLiteralSuffix(XmlConstants.XmlInt64LiteralSuffix, ref text))
                    {
                        targetValue = XmlConvert.ToInt64(text);
                    }
                    else
                    {
                        targetValue = default(Int64);
                        return false;
                    }
                }
                else if (typeof(Single) == targetType)
                {
                    if (TryRemoveLiteralSuffix(XmlConstants.XmlSingleLiteralSuffix, ref text))
                    {
                        targetValue = XmlConvert.ToSingle(text);
                    }
                    else
                    {
                        targetValue = default(Single);
                        return false;
                    }
                }
                else if (typeof(Double) == targetType)
                {
                    TryRemoveLiteralSuffix(XmlConstants.XmlDoubleLiteralSuffix, ref text);
                    targetValue = XmlConvert.ToDouble(text);
                }
                else if (typeof(Decimal) == targetType)
                {
                    if (TryRemoveLiteralSuffix(XmlConstants.XmlDecimalLiteralSuffix, ref text))
                    {
                        try
                        {
                            targetValue = XmlConvert.ToDecimal(text);
                        }
                        catch (FormatException)
                        {
                            // we need to support exponential format for decimals since we used to support them in V1
                            decimal result;
                            if (Decimal.TryParse(text, NumberStyles.Float, NumberFormatInfo.InvariantInfo, out result))
                            {
                                targetValue = result;
                            }
                            else
                            {
                                targetValue = default(Decimal);
                                return false;
                            }
                        }
                    }
                    else
                    {
                        targetValue = default(Decimal);
                        return false;
                    }
                }
                else
                {
                    Debug.Assert(typeof(System.Xml.Linq.XElement) == targetType, "XElement == " + targetType);
                    targetValue = System.Xml.Linq.XElement.Parse(text, System.Xml.Linq.LoadOptions.PreserveWhitespace);
                }
 
                return true;
            }
            catch (FormatException)
            {
                targetValue = null;
                return false;
            }
        }
 
        /// <summary>Converts a string to a primitive value.</summary>
        /// <param name="text">String text to convert.</param>
        /// <param name="targetType">Type to convert string to.</param>
        /// <returns>value converted to the target type.</returns>
        internal static object StringToPrimitive(string text, Type targetType)
        {
            Debug.Assert(text != null, "text != null");
            Debug.Assert(targetType != null, "targetType != null");
 
            object targetValue = null;
            targetType = Nullable.GetUnderlyingType(targetType) ?? targetType;
 
            if (typeof(String) == targetType)
            {
                targetValue = text;
            }
            else if (typeof(Boolean) == targetType)
            {
                targetValue = XmlConvert.ToBoolean(text);
            }
            else if (typeof(Byte) == targetType)
            {
                targetValue = XmlConvert.ToByte(text);
            }
            else if (typeof(byte[]) == targetType)
            {
                targetValue = Convert.FromBase64String(text);
            }
            else if (typeof(System.Data.Linq.Binary) == targetType)
            {
                targetValue = new System.Data.Linq.Binary(Convert.FromBase64String(text));
            }
            else if (typeof(SByte) == targetType)
            {
                targetValue = XmlConvert.ToSByte(text);
            }
            else if (typeof(DateTime) == targetType)
            {
                targetValue = XmlConvert.ToDateTime(text, XmlDateTimeSerializationMode.RoundtripKind);
            }
            else if (typeof(Decimal) == targetType)
            {
                targetValue = XmlConvert.ToDecimal(text);
            }
            else if (typeof(Double) == targetType)
            {
                targetValue = XmlConvert.ToDouble(text);
            }
            else if (typeof(Guid) == targetType)
            {
                targetValue = new Guid(text);
            }
            else if (typeof(Int16) == targetType)
            {
                targetValue = XmlConvert.ToInt16(text);
            }
            else if (typeof(Int32) == targetType)
            {
                targetValue = XmlConvert.ToInt32(text);
            }
            else if (typeof(Int64) == targetType)
            {
                targetValue = XmlConvert.ToInt64(text);
            }
            else if (typeof(System.Xml.Linq.XElement) == targetType)
            {
                targetValue = System.Xml.Linq.XElement.Parse(text, System.Xml.Linq.LoadOptions.PreserveWhitespace);
            }
            else
            {
                Debug.Assert(typeof(Single) == targetType, "typeof(Single) == targetType(" + targetType + ")");
                targetValue = XmlConvert.ToSingle(text);
            }
 
            return targetValue;
        }
 
        /// <summary>Removes quotes from the single-quotes text.</summary>
        /// <param name="text">Text to remove quotes from.</param>
        /// <returns>Whether quotes were successfully removed.</returns>
        internal static bool TryRemoveQuotes(ref string text)
        {
            if (text.Length < 2)
            {
                return false;
            }
 
            char quote = text[0];
            if (quote != '\'' || text[text.Length - 1] != quote)
            {
                return false;
            }
 
            string s = text.Substring(1, text.Length - 2);
            int start = 0;
            while (true)
            {
                int i = s.IndexOf(quote, start);
                if (i < 0)
                {
                    break;
                }
 
                s = s.Remove(i, 1);
                if (s.Length < i + 1 || s[i] != quote)
                {
                    return false;
                }
 
                start = i + 1;
            }
 
            text = s;
            return true;
        }
#endif
 
        /// <summary>Converts the specified value to a serializable string for XML content.</summary>
        /// <param name="value">Non-null value to convert.</param>
        /// <param name="result">The specified value converted to a serializable string for XML content. </param>
        /// <returns>boolean value indicating conversion successful conversion</returns>
        internal static bool TryXmlPrimitiveToString(object value, out string result)
        {
            Debug.Assert(value != null, "value != null");
            result = null;
 
            Type valueType = value.GetType();
            valueType = Nullable.GetUnderlyingType(valueType) ?? valueType;
 
            if (typeof(String) == valueType)
            {
                result = (string)value;
            }
            else if (typeof(Boolean) == valueType)
            {
                result = XmlConvert.ToString((bool)value);
            }
            else if (typeof(Byte) == valueType)
            {
                result = XmlConvert.ToString((byte)value);
            }
            else if (typeof(DateTime) == valueType)
            {
                result = XmlConvert.ToString((DateTime)value, XmlDateTimeSerializationMode.RoundtripKind);
            }
            else if (typeof(Decimal) == valueType)
            {
                result = XmlConvert.ToString((decimal)value);
            }
            else if (typeof(Double) == valueType)
            {
                result = XmlConvert.ToString((double)value);
            }
            else if (typeof(Guid) == valueType)
            {
                result = value.ToString();
            }
            else if (typeof(Int16) == valueType)
            {
                result = XmlConvert.ToString((Int16)value);
            }
            else if (typeof(Int32) == valueType)
            {
                result = XmlConvert.ToString((Int32)value);
            }
            else if (typeof(Int64) == valueType)
            {
                result = XmlConvert.ToString((Int64)value);
            }
            else if (typeof(SByte) == valueType)
            {
                result = XmlConvert.ToString((SByte)value);
            }
            else if (typeof(Single) == valueType)
            {
                result = XmlConvert.ToString((Single)value);
            }
            else if (typeof(byte[]) == valueType)
            {
                byte[] byteArray = (byte[])value;
                result = Convert.ToBase64String(byteArray);
            }
#if ASTORIA_SERVER
            else if (typeof(System.Data.Linq.Binary) == valueType)
            {
                return TryXmlPrimitiveToString(((System.Data.Linq.Binary)value).ToArray(), out result);
            }
#else
#if !ASTORIA_LIGHT
            else if (ClientConvert.IsBinaryValue(value))
            {
                return ClientConvert.TryKeyBinaryToString(value, out result);
            }
#endif
#endif
            else if (typeof(System.Xml.Linq.XElement) == valueType)
            {
                result = ((System.Xml.Linq.XElement)value).ToString(System.Xml.Linq.SaveOptions.None);
            }
            else
            {
                result = null;
                return false;
            }
 
            Debug.Assert(result != null, "result != null");
            return true;
        }
 
#if ASTORIA_SERVER
        /// <summary>Returns the 4 bits that correspond to the specified character.</summary>
        /// <param name="c">Character in the 0-F range to be converted.</param>
        /// <returns>The 4 bits that correspond to the specified character.</returns>
        /// <exception cref="FormatException">Thrown when 'c' is not in the '0'-'9','a'-'f' range.</exception>
        private static byte HexCharToNibble(char c)
        {
            Debug.Assert(IsCharHexDigit(c));
            switch (c)
            {
                case '0':
                    return 0;
                case '1':
                    return 1;
                case '2':
                    return 2;
                case '3':
                    return 3;
                case '4':
                    return 4;
                case '5':
                    return 5;
                case '6':
                    return 6;
                case '7':
                    return 7;
                case '8':
                    return 8;
                case '9':
                    return 9;
                case 'a':
                case 'A':
                    return 10;
                case 'b':
                case 'B':
                    return 11;
                case 'c':
                case 'C':
                    return 12;
                case 'd':
                case 'D':
                    return 13;
                case 'e':
                case 'E':
                    return 14;
                case 'f':
                case 'F':
                    return 15;
                default:
                    throw new InvalidOperationException();
            }
        }
 
        /// <summary>
        /// Check and strip the input <paramref name="text"/> for literal <paramref name="suffix"/>
        /// </summary>
        /// <param name="text">The string to check</param>
        /// <param name="suffix">The suffix value</param>
        /// <returns>A string that has been striped of the suffix</returns>
        private static bool TryRemoveLiteralSuffix(string suffix, ref string text)
        {
            Debug.Assert(text != null, "text != null");
            Debug.Assert(suffix != null, "suffix != null");
 
            text = text.Trim(XmlWhitespaceChars);
            if (text.Length <= suffix.Length || !text.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }
            else
            {
                text = text.Substring(0, text.Length - suffix.Length);
                return true;
            }
        }
 
        /// <summary>
        /// Tries to remove a literal <paramref name="prefix"/> from the specified <paramref name="text"/>.
        /// </summary>
        /// <param name="prefix">Prefix to remove; one-letter prefixes are case-sensitive, others insensitive.</param>
        /// <param name="text">Text to attempt to remove prefix from.</param>
        /// <returns>true if the prefix was found and removed; false otherwise.</returns>
        private static bool TryRemoveLiteralPrefix(string prefix, ref string text)
        {
            Debug.Assert(prefix != null, "prefix != null");
            if (text.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
            {
                text = text.Remove(0, prefix.Length);
                return true;
            }
            else
            {
                return false;
            }
        }
#else
        /// <summary>Appends the decimal marker to string form of double value if necessary.</summary>
        /// <param name="input">Input string.</param>
        /// <returns>String with decimal marker optionally added.</returns>
        private static string AppendDecimalMarkerToDouble(string input)
        {
            foreach (char c in input)
            {
                if (!Char.IsDigit(c))
                {
                    return input;
                }
            }
 
            return input + ".0";
        }
#endif
    }
}