File: System\Data\EntityClient\DbConnectionOptions.cs
Project: ndp\fx\src\DataEntity\System.Data.Entity.csproj (System.Data.Entity)
//---------------------------------------------------------------------
// <copyright file="DbConnectionOptions.cs" company="Microsoft">
//      Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
//
// @owner  Microsoft
// @backupOwner  Microsoft
//---------------------------------------------------------------------
 
namespace System.Data.EntityClient
{
    using System.Collections;
    using System.Diagnostics;
    using System.Runtime.Versioning;
    using System.Text;
    using System.Text.RegularExpressions;
 
    /// <summary>
    /// Copied from System.Data.dll
    /// </summary>
    internal class DbConnectionOptions
    {
        // instances of this class are intended to be immutable, i.e readonly
        // used by pooling classes so it is much easier to verify correctness
        // when not worried about the class being modified during execution
 
#if DEBUG
        private const string ConnectionStringPattern =                  // may not contain embedded null except trailing last value
            "([\\s;]*"                                                  // leading whitespace and extra semicolons
            + "(?![\\s;])"                                              // key does not start with space or semicolon
            + "(?<key>([^=\\s\\p{Cc}]|\\s+[^=\\s\\p{Cc}]|\\s+==|==)+)"  // allow any visible character for keyname except '=' which must quoted as '=='
            + "\\s*=(?!=)\\s*"                                          // the equal sign divides the key and value parts
            + "(?<value>"
            + "(\"([^\"\u0000]|\"\")*\")"                              // double quoted string, " must be quoted as ""
            + "|"
            + "('([^'\u0000]|'')*')"                                   // single quoted string, ' must be quoted as ''
            + "|"
            + "((?![\"'\\s])"                                          // unquoted value must not start with " or ' or space, would also like = but too late to change
            + "([^;\\s\\p{Cc}]|\\s+[^;\\s\\p{Cc}])*"                  // control characters must be quoted
            + "(?<![\"']))"                                            // unquoted value must not stop with " or '
            + ")(\\s*)(;|\u0000|$)"                                     // whitespace after value up to semicolon or end-of-line
            + ")*"                                                      // repeat the key-value pair
            + "[\\s;\u0000]*"                                           // traling whitespace/semicolons and embedded nulls (DataSourceLocator)
        ;
 
        private static readonly Regex ConnectionStringRegex = new Regex(ConnectionStringPattern, RegexOptions.ExplicitCapture | RegexOptions.Compiled);
#endif
        internal const string DataDirectory = "|datadirectory|";
 
#if DEBUG 
        private const string ConnectionStringValidKeyPattern = "^(?![;\\s])[^\\p{Cc}]+(?<!\\s)$"; // key not allowed to start with semi-colon or space or contain non-visible characters or end with space
        private const string ConnectionStringValidValuePattern = "^[^\u0000]*$";                    // value not allowed to contain embedded null   
        private static readonly Regex ConnectionStringValidKeyRegex = new Regex(ConnectionStringValidKeyPattern, RegexOptions.Compiled);
        private static readonly Regex ConnectionStringValidValueRegex = new Regex(ConnectionStringValidValuePattern, RegexOptions.Compiled);
#endif
 
        private readonly string _usersConnectionString;
        private readonly Hashtable _parsetable;
        internal readonly NameValuePair KeyChain;
 
        // synonyms hashtable is meant to be read-only translation of parsed string
        // keywords/synonyms to a known keyword string
        internal DbConnectionOptions(string connectionString, Hashtable synonyms)
        {
            _parsetable = new Hashtable();
            _usersConnectionString = ((null != connectionString) ? connectionString : "");
 
            // first pass on parsing, initial syntax check
            if (0 < _usersConnectionString.Length)
            {
                KeyChain = ParseInternal(_parsetable, _usersConnectionString, synonyms);
            }
        }
 
        internal string UsersConnectionString
        {
            get
            {
                return _usersConnectionString ?? string.Empty;
            }
        }
 
        internal bool IsEmpty
        {
            get { return (null == KeyChain); }
        }
 
        internal Hashtable Parsetable
        {
            get { return _parsetable; }
        }
 
        internal string this[string keyword]
        {
            get { return (string)_parsetable[keyword]; }
        }
 
        // SxS notes:
        // * this method queries "DataDirectory" value from the current AppDomain.
        //   This string is used for to replace "!DataDirectory!" values in the connection string, it is not considered as an "exposed resource".
        // * This method uses GetFullPath to validate that root path is valid, the result is not exposed out.
        [ResourceExposure(ResourceScope.None)]
        [ResourceConsumption(ResourceScope.Machine, ResourceScope.Machine)]
        internal static string ExpandDataDirectory(string keyword, string value)
        {
            string fullPath = null;
            if ((null != value) && value.StartsWith(DataDirectory, StringComparison.OrdinalIgnoreCase))
            {
                // find the replacement path
                object rootFolderObject = AppDomain.CurrentDomain.GetData("DataDirectory");
                string rootFolderPath = (rootFolderObject as string);
                if ((null != rootFolderObject) && (null == rootFolderPath))
                {
                    throw EntityUtil.InvalidOperation(System.Data.Entity.Strings.ADP_InvalidDataDirectory);
                }
                else if (rootFolderPath == string.Empty)
                {
                    rootFolderPath = AppDomain.CurrentDomain.BaseDirectory;
                }
                if (null == rootFolderPath)
                {
                    rootFolderPath = "";                    
                }
 
                // We don't know if rootFolderpath ends with '\', and we don't know if the given name starts with onw
                int fileNamePosition = DataDirectory.Length;    // filename starts right after the '|datadirectory|' keyword
                bool rootFolderEndsWith = (0 < rootFolderPath.Length) && rootFolderPath[rootFolderPath.Length - 1] == '\\';
                bool fileNameStartsWith = (fileNamePosition < value.Length) && value[fileNamePosition] == '\\';
 
                // replace |datadirectory| with root folder path
                if (!rootFolderEndsWith && !fileNameStartsWith)
                {
                    // need to insert '\'
                    fullPath = rootFolderPath + '\\' + value.Substring(fileNamePosition);
                }
                else if (rootFolderEndsWith && fileNameStartsWith)
                {
                    // need to strip one out
                    fullPath = rootFolderPath + value.Substring(fileNamePosition + 1);
                }
                else
                {
                    // simply concatenate the strings
                    fullPath = rootFolderPath + value.Substring(fileNamePosition);
                }
 
                // verify root folder path is a real path without unexpected "..\"
                if (!EntityUtil.GetFullPath(fullPath).StartsWith(rootFolderPath, StringComparison.Ordinal))
                {
                    throw EntityUtil.InvalidConnectionOptionValue(keyword);
                }
            }
            return fullPath;
        }
 
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")]
        static private string GetKeyName(StringBuilder buffer)
        {
            int count = buffer.Length;
            while ((0 < count) && Char.IsWhiteSpace(buffer[count - 1]))
            {
                count--; // trailing whitespace
            }
            return buffer.ToString(0, count).ToLowerInvariant();
        }
 
        static private string GetKeyValue(StringBuilder buffer, bool trimWhitespace)
        {
            int count = buffer.Length;
            int index = 0;
            if (trimWhitespace)
            {
                while ((index < count) && Char.IsWhiteSpace(buffer[index]))
                {
                    index++; // leading whitespace
                }
                while ((0 < count) && Char.IsWhiteSpace(buffer[count - 1]))
                {
                    count--; // trailing whitespace
                }
            }
            return buffer.ToString(index, count - index);
        }
 
        // transistion states used for parsing
        private enum ParserState
        {
            NothingYet = 1,   //start point
            Key,
            KeyEqual,
            KeyEnd,
            UnquotedValue,
            DoubleQuoteValue,
            DoubleQuoteValueQuote,
            SingleQuoteValue,
            SingleQuoteValueQuote,
            QuotedValueEnd,
            NullTermination,
        };
        
        static private int GetKeyValuePair(string connectionString, int currentPosition, StringBuilder buffer, out string keyname, out string keyvalue)
        {
            int startposition = currentPosition;
 
            buffer.Length = 0;
            keyname = null;
            keyvalue = null;
 
            char currentChar = '\0';
 
            ParserState parserState = ParserState.NothingYet;
            int length = connectionString.Length;
            for (; currentPosition < length; ++currentPosition)
            {
                currentChar = connectionString[currentPosition];
 
                switch (parserState)
                {
                    case ParserState.NothingYet: // [\\s;]*
                        if ((';' == currentChar) || Char.IsWhiteSpace(currentChar))
                        {
                            continue;
                        }
                        if ('\0' == currentChar) { parserState = ParserState.NullTermination; continue; } // MDAC 83540
                        if (Char.IsControl(currentChar)) { throw EntityUtil.ConnectionStringSyntax(startposition); }
                        startposition = currentPosition;
                        if ('=' != currentChar)
                        { // MDAC 86902
                            parserState = ParserState.Key;
                            break;
                        }
                        else
                        {
                            parserState = ParserState.KeyEqual;
                            continue;
                        }
 
                    case ParserState.Key: // (?<key>([^=\\s\\p{Cc}]|\\s+[^=\\s\\p{Cc}]|\\s+==|==)+)
                        if ('=' == currentChar) { parserState = ParserState.KeyEqual; continue; }
                        if (Char.IsWhiteSpace(currentChar)) { break; }
                        if (Char.IsControl(currentChar)) { throw EntityUtil.ConnectionStringSyntax(startposition); }
                        break;
 
                    case ParserState.KeyEqual: // \\s*=(?!=)\\s*
                        if ('=' == currentChar) { parserState = ParserState.Key; break; }
                        keyname = GetKeyName(buffer);
                        if (string.IsNullOrEmpty(keyname)) { throw EntityUtil.ConnectionStringSyntax(startposition); }
                        buffer.Length = 0;
                        parserState = ParserState.KeyEnd;
                        goto case ParserState.KeyEnd;
 
                    case ParserState.KeyEnd:
                        if (Char.IsWhiteSpace(currentChar)) { continue; }
                        if ('\'' == currentChar) { parserState = ParserState.SingleQuoteValue; continue; }
                        if ('"' == currentChar) { parserState = ParserState.DoubleQuoteValue; continue; }
 
                        if (';' == currentChar) { goto ParserExit; }
                        if ('\0' == currentChar) { goto ParserExit; }
                        if (Char.IsControl(currentChar)) { throw EntityUtil.ConnectionStringSyntax(startposition); }
                        parserState = ParserState.UnquotedValue;
                        break;
 
                    case ParserState.UnquotedValue: // "((?![\"'\\s])" + "([^;\\s\\p{Cc}]|\\s+[^;\\s\\p{Cc}])*" + "(?<![\"']))"
                        if (Char.IsWhiteSpace(currentChar)) { break; }
                        if (Char.IsControl(currentChar) || ';' == currentChar) { goto ParserExit; }
                        break;
 
                    case ParserState.DoubleQuoteValue: // "(\"([^\"\u0000]|\"\")*\")"
                        if ('"' == currentChar) { parserState = ParserState.DoubleQuoteValueQuote; continue; }
                        if ('\0' == currentChar) { throw EntityUtil.ConnectionStringSyntax(startposition); }
                        break;
 
                    case ParserState.DoubleQuoteValueQuote:
                        if ('"' == currentChar) { parserState = ParserState.DoubleQuoteValue; break; }
                        keyvalue = GetKeyValue(buffer, false);
                        parserState = ParserState.QuotedValueEnd;
                        goto case ParserState.QuotedValueEnd;
 
                    case ParserState.SingleQuoteValue: // "('([^'\u0000]|'')*')"
                        if ('\'' == currentChar) { parserState = ParserState.SingleQuoteValueQuote; continue; }
                        if ('\0' == currentChar) { throw EntityUtil.ConnectionStringSyntax(startposition); }
                        break;
 
                    case ParserState.SingleQuoteValueQuote:
                        if ('\'' == currentChar) { parserState = ParserState.SingleQuoteValue; break; }
                        keyvalue = GetKeyValue(buffer, false);
                        parserState = ParserState.QuotedValueEnd;
                        goto case ParserState.QuotedValueEnd;
 
                    case ParserState.QuotedValueEnd:
                        if (Char.IsWhiteSpace(currentChar)) { continue; }
                        if (';' == currentChar) { goto ParserExit; }
                        if ('\0' == currentChar) { parserState = ParserState.NullTermination; continue; } // MDAC 83540
                        throw EntityUtil.ConnectionStringSyntax(startposition);  // unbalanced single quote
 
                    case ParserState.NullTermination: // [\\s;\u0000]*
                        if ('\0' == currentChar) { continue; }
                        if (Char.IsWhiteSpace(currentChar)) { continue; } // MDAC 83540
                        throw EntityUtil.ConnectionStringSyntax(currentPosition);
 
                    default:
                        throw EntityUtil.InternalError(EntityUtil.InternalErrorCode.InvalidParserState1);
                }
                buffer.Append(currentChar);
            }
        ParserExit:
            switch (parserState)
            {
                case ParserState.Key:
                case ParserState.DoubleQuoteValue:
                case ParserState.SingleQuoteValue:
                    // keyword not found/unbalanced double/single quote
                    throw EntityUtil.ConnectionStringSyntax(startposition);
 
                case ParserState.KeyEqual:
                    // equal sign at end of line
                    keyname = GetKeyName(buffer);
                    if (string.IsNullOrEmpty(keyname)) { throw EntityUtil.ConnectionStringSyntax(startposition); }
                    break;
 
                case ParserState.UnquotedValue:
                    // unquoted value at end of line
                    keyvalue = GetKeyValue(buffer, true);
 
                    char tmpChar = keyvalue[keyvalue.Length - 1];
                    if (('\'' == tmpChar) || ('"' == tmpChar))
                    {
                        throw EntityUtil.ConnectionStringSyntax(startposition);    // unquoted value must not end in quote
                    }
                    break;
 
                case ParserState.DoubleQuoteValueQuote:
                case ParserState.SingleQuoteValueQuote:
                case ParserState.QuotedValueEnd:
                    // quoted value at end of line
                    keyvalue = GetKeyValue(buffer, false);
                    break;
 
                case ParserState.NothingYet:
                case ParserState.KeyEnd:
                case ParserState.NullTermination:
                    // do nothing
                    break;
 
                default:
                    throw EntityUtil.InternalError(EntityUtil.InternalErrorCode.InvalidParserState2);
            }
            if ((';' == currentChar) && (currentPosition < connectionString.Length))
            {
                currentPosition++;
            }
            return currentPosition;
        }
 
#if DEBUG
        static private bool IsValueValidInternal(string keyvalue)
        {
            if (null != keyvalue)
            {
 
                bool compValue = ConnectionStringValidValueRegex.IsMatch(keyvalue);
                Debug.Assert((-1 == keyvalue.IndexOf('\u0000')) == compValue, "IsValueValid mismatch with regex");
                return (-1 == keyvalue.IndexOf('\u0000'));
            }
            return true;
        }
#endif
 
        static private bool IsKeyNameValid(string keyname)
        {
            if (null != keyname)
            {
#if DEBUG
                bool compValue = ConnectionStringValidKeyRegex.IsMatch(keyname);
                Debug.Assert(((0 < keyname.Length) && (';' != keyname[0]) && !Char.IsWhiteSpace(keyname[0]) && (-1 == keyname.IndexOf('\u0000'))) == compValue, "IsValueValid mismatch with regex");
#endif
                return ((0 < keyname.Length) && (';' != keyname[0]) && !Char.IsWhiteSpace(keyname[0]) && (-1 == keyname.IndexOf('\u0000')));
            }
            return false;
        }
 
#if DEBUG
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")]
        private static Hashtable SplitConnectionString(string connectionString, Hashtable synonyms)
        {
            Hashtable parsetable = new Hashtable();
            Regex parser = ConnectionStringRegex;
 
            const int KeyIndex = 1, ValueIndex = 2;
            Debug.Assert(KeyIndex == parser.GroupNumberFromName("key"), "wrong key index");
            Debug.Assert(ValueIndex == parser.GroupNumberFromName("value"), "wrong value index");
 
            if (null != connectionString)
            {
                Match match = parser.Match(connectionString);
                if (!match.Success || (match.Length != connectionString.Length))
                {
                    throw EntityUtil.ConnectionStringSyntax(match.Length);
                }
                int indexValue = 0;
                CaptureCollection keyvalues = match.Groups[ValueIndex].Captures;
                foreach (Capture keypair in match.Groups[KeyIndex].Captures)
                {
                    string keyname = keypair.Value.Replace("==", "=").ToLowerInvariant();
                    string keyvalue = keyvalues[indexValue++].Value;
                    if (0 < keyvalue.Length)
                    {
                        switch (keyvalue[0])
                        {
                            case '\"':
                                keyvalue = keyvalue.Substring(1, keyvalue.Length - 2).Replace("\"\"", "\"");
                                break;
                            case '\'':
                                keyvalue = keyvalue.Substring(1, keyvalue.Length - 2).Replace("\'\'", "\'");
                                break;
                            default:
                                break;
                        }
                    }
                    else
                    {
                        keyvalue = null;
                    }
 
                    string realkeyname = ((null != synonyms) ? (string)synonyms[keyname] : keyname);
                    if (!IsKeyNameValid(realkeyname))
                    {
                        throw EntityUtil.ADP_KeywordNotSupported(keyname);
                    }
                    parsetable[realkeyname] = keyvalue; // last key-value pair wins (or first)
                }
            }
            return parsetable;
        }
 
        private static void ParseComparision(Hashtable parsetable, string connectionString, Hashtable synonyms, Exception e)
        {
            try
            {
                Hashtable parsedvalues = SplitConnectionString(connectionString, synonyms);
                foreach (DictionaryEntry entry in parsedvalues)
                {
                    string keyname = (string)entry.Key;
                    string value1 = (string)entry.Value;
                    string value2 = (string)parsetable[keyname];
                    Debug.Assert(parsetable.Contains(keyname), "ParseInternal code vs. regex mismatch keyname <" + keyname + ">");
                    Debug.Assert(value1 == value2, "ParseInternal code vs. regex mismatch keyvalue <" + value1 + "> <" + value2 + ">");
                }
 
            }
            catch (ArgumentException f)
            {
                if (null != e)
                {
                    string msg1 = e.Message;
                    string msg2 = f.Message;
                    if (msg1.StartsWith("Keyword not supported:", StringComparison.Ordinal) && msg2.StartsWith("Format of the initialization string", StringComparison.Ordinal))
                    {
                    }
                    else
                    {
                        // Does not always hold.
                       Debug.Assert(msg1 == msg2, "ParseInternal code vs regex message mismatch: <" + msg1 + "> <" + msg2 + ">");
                    }
                }
                else
                {
                    Debug.Assert(false, "ParseInternal code vs regex throw mismatch " + f.Message);
                }
                e = null;
            }
            if (null != e)
            {
                Debug.Assert(false, "ParseInternal code threw exception vs regex mismatch");
            }
        }
#endif
        private static NameValuePair ParseInternal(Hashtable parsetable, string connectionString, Hashtable synonyms)
        {
            Debug.Assert(null != connectionString, "null connectionstring");
            StringBuilder buffer = new StringBuilder();
            NameValuePair localKeychain = null, keychain = null;
#if DEBUG
            try
            {
#endif
                int nextStartPosition = 0;
                int endPosition = connectionString.Length;
                while (nextStartPosition < endPosition)
                {
                    int startPosition = nextStartPosition;
 
                    string keyname, keyvalue;
                    nextStartPosition = GetKeyValuePair(connectionString, startPosition, buffer, out keyname, out keyvalue);
                    if (string.IsNullOrEmpty(keyname))
                    {
                        // if (nextStartPosition != endPosition) { throw; }
                        break;
                    }
 
#if DEBUG
                    Debug.Assert(IsKeyNameValid(keyname), "ParseFailure, invalid keyname");
                    Debug.Assert(IsValueValidInternal(keyvalue), "parse failure, invalid keyvalue");
#endif
                    string realkeyname = ((null != synonyms) ? (string)synonyms[keyname] : keyname);
                    if (!IsKeyNameValid(realkeyname))
                    {
                        throw EntityUtil.ADP_KeywordNotSupported(keyname);
                    }
                    parsetable[realkeyname] = keyvalue; // last key-value pair wins (or first)
 
                    if (null != localKeychain)
                    {
                        localKeychain = localKeychain.Next = new NameValuePair(realkeyname, keyvalue, nextStartPosition - startPosition);
                    }
                    else 
                    { // first time only - don't contain modified chain from UDL file
                        keychain = localKeychain = new NameValuePair(realkeyname, keyvalue, nextStartPosition - startPosition);
                    }
                }
#if DEBUG
            }
            catch (ArgumentException e)
            {
                ParseComparision(parsetable, connectionString, synonyms, e);
                throw;
            }
            ParseComparision(parsetable, connectionString, synonyms, null);
#endif
            return keychain;
        } 
    }
}