File: System\Data\Services\HttpProcessUtility.cs
Project: ndp\fx\src\DataWeb\Server\System.Data.Services.csproj (System.Data.Services)
//---------------------------------------------------------------------
// <copyright file="HttpProcessUtility.cs" company="Microsoft">
//      Copyright (c) Microsoft Corporation.  All rights reserved.
// </copyright>
// <summary>
//      Provides a utility class to help in processing HTTP requests.
// </summary>
//
// @owner  Microsoft
//---------------------------------------------------------------------
 
#if ASTORIA_CLIENT
namespace System.Data.Services.Client
#else
namespace System.Data.Services
#endif
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Text;
 
    /// <summary>Provides helper methods for processing HTTP requests.</summary>
    internal static class HttpProcessUtility
    {
        /// <summary>UTF-8 encoding, without the BOM preamble.</summary>
        /// <remarks>
        /// While a BOM preamble on UTF8 is generally benign, it seems that some MIME handlers under IE6 will not 
        /// process the payload correctly when included.
        /// 
        /// Because the data service should include the encoding as part of the Content-Type in the response,
        /// there should be no ambiguity as to what encoding is being used.
        /// 
        /// For further information, see http://www.unicode.org/faq/utf_bom.html#BOM.
        /// </remarks>
        internal static readonly UTF8Encoding EncodingUtf8NoPreamble = new UTF8Encoding(false, true);
 
        /// <summary>Encoding to fall back to an appropriate encoding is not available.</summary>
        internal static Encoding FallbackEncoding
        {
            get
            {
                return EncodingUtf8NoPreamble;
            }
        }
 
        /// <summary>Encoding implied by an unspecified encoding value.</summary>
        /// <remarks>See http://tools.ietf.org/html/rfc2616#section-3.4.1 for details.</remarks>
        private static Encoding MissingEncoding
        {
            get
            {
#if ASTORIA_LIGHT   // ISO-8859-1 not available
                return Encoding.UTF8;
#else
                return Encoding.GetEncoding("ISO-8859-1", new EncoderExceptionFallback(), new DecoderExceptionFallback());
#endif
            }
        }
 
#if !ASTORIA_CLIENT
        
        /// <summary>Builds a Content-Type header which includes MIME type and encoding information.</summary>
        /// <param name="mime">MIME type to be used.</param>
        /// <param name="encoding">Encoding to be used in response, possibly null.</param>
        /// <returns>The value for the Content-Type header.</returns>
        internal static string BuildContentType(string mime, Encoding encoding)
        {
            Debug.Assert(mime != null, "mime != null");
            if (encoding == null)
            {
                return mime;
            }
            else
            {
                return mime + ";charset=" + encoding.WebName;
            }
        }
 
        /// <summary>Selects an acceptable MIME type that satisfies the Accepts header.</summary>
        /// <param name="acceptTypesText">Text for Accepts header.</param>
        /// <param name="availableTypes">
        /// Types that the server is willing to return, in descending order 
        /// of preference.
        /// </param>
        /// <returns>The best MIME type for the client</returns>
        internal static string SelectMimeType(string acceptTypesText, string[] availableTypes)
        {
            Debug.Assert(availableTypes != null, "acceptableTypes != null");
            string selectedContentType = null;
            int selectedMatchingParts = -1;
            int selectedQualityValue = 0;
            int selectedPreferenceIndex = Int32.MaxValue;
            bool acceptable = false;
            bool acceptTypesEmpty = true;
            if (!String.IsNullOrEmpty(acceptTypesText))
            {
                IEnumerable<MediaType> acceptTypes = MimeTypesFromAcceptHeader(acceptTypesText);
                foreach (MediaType acceptType in acceptTypes)
                {
                    acceptTypesEmpty = false;
                    for (int i = 0; i < availableTypes.Length; i++)
                    {
                        string availableType = availableTypes[i];
                        int matchingParts = acceptType.GetMatchingParts(availableType);
                        if (matchingParts < 0)
                        {
                            continue;
                        }
 
                        if (matchingParts > selectedMatchingParts)
                        {
                            // A more specific type wins.
                            selectedContentType = availableType;
                            selectedMatchingParts = matchingParts;
                            selectedQualityValue = acceptType.SelectQualityValue();
                            selectedPreferenceIndex = i;
                            acceptable = selectedQualityValue != 0;
                        }
                        else if (matchingParts == selectedMatchingParts)
                        {
                            // A type with a higher q-value wins.
                            int candidateQualityValue = acceptType.SelectQualityValue();
                            if (candidateQualityValue > selectedQualityValue)
                            {
                                selectedContentType = availableType;
                                selectedQualityValue = candidateQualityValue;
                                selectedPreferenceIndex = i;
                                acceptable = selectedQualityValue != 0;
                            }
                            else if (candidateQualityValue == selectedQualityValue)
                            {
                                // A type that is earlier in the availableTypes array wins.
                                if (i < selectedPreferenceIndex)
                                {
                                    selectedContentType = availableType;
                                    selectedPreferenceIndex = i;
                                }
                            }
                        }
                    }
                }
            }
 
            if (acceptTypesEmpty)
            {
                selectedContentType = availableTypes[0];
            }
            else if (!acceptable)
            {
                selectedContentType = null;
            }
 
            return selectedContentType;
        }
 
        /// <summary>Gets the appropriate MIME type for the request, throwing if there is none.</summary>
        /// <param name='acceptTypesText'>Text as it appears in an HTTP Accepts header.</param>
        /// <param name='exactContentType'>Preferred content type to match if an exact media type is given - this is in descending order of preference.</param>
        /// <param name='inexactContentType'>Preferred fallback content type for inexact matches.</param>
        /// <returns>One of exactContentType or inexactContentType.</returns>
        internal static string SelectRequiredMimeType(
            string acceptTypesText,
            string[] exactContentType,
            string inexactContentType)
        {
            Debug.Assert(exactContentType != null && exactContentType.Length != 0, "exactContentType != null && exactContentType.Length != 0");
            Debug.Assert(inexactContentType != null, "inexactContentType != null");
 
            string selectedContentType = null;
            int selectedMatchingParts = -1;
            int selectedQualityValue = 0;
            bool acceptable = false;
            bool acceptTypesEmpty = true;
            bool foundExactMatch = false;
 
            if (!String.IsNullOrEmpty(acceptTypesText))
            {
                IEnumerable<MediaType> acceptTypes = MimeTypesFromAcceptHeader(acceptTypesText);
                foreach (MediaType acceptType in acceptTypes)
                {
                    acceptTypesEmpty = false;
                    for (int i = 0; i < exactContentType.Length; i++)
                    {
                        if (WebUtil.CompareMimeType(acceptType.MimeType, exactContentType[i]))
                        {
                            selectedContentType = exactContentType[i];
                            selectedQualityValue = acceptType.SelectQualityValue();
                            acceptable = selectedQualityValue != 0;
                            foundExactMatch = true;
                            break;
                        }
                    }
 
                    if (foundExactMatch)
                    {
                        break;
                    }
 
                    int matchingParts = acceptType.GetMatchingParts(inexactContentType);
                    if (matchingParts < 0)
                    {
                        continue;
                    }
 
                    if (matchingParts > selectedMatchingParts)
                    {
                        // A more specific type wins.
                        selectedContentType = inexactContentType;
                        selectedMatchingParts = matchingParts;
                        selectedQualityValue = acceptType.SelectQualityValue();
                        acceptable = selectedQualityValue != 0;
                    }
                    else if (matchingParts == selectedMatchingParts)
                    {
                        // A type with a higher q-value wins.
                        int candidateQualityValue = acceptType.SelectQualityValue();
                        if (candidateQualityValue > selectedQualityValue)
                        {
                            selectedContentType = inexactContentType;
                            selectedQualityValue = candidateQualityValue;
                            acceptable = selectedQualityValue != 0;
                        }
                    }
                }
            }
 
            if (!acceptable && !acceptTypesEmpty)
            {
                throw Error.HttpHeaderFailure(415, Strings.DataServiceException_UnsupportedMediaType);
            }
 
            if (acceptTypesEmpty)
            {
                Debug.Assert(selectedContentType == null, "selectedContentType == null - otherwise accept types were not empty");
                selectedContentType = inexactContentType;
            }
 
            Debug.Assert(selectedContentType != null, "selectedContentType != null - otherwise no selection was made");
            return selectedContentType;
        }
 
        /// <summary>Gets the best encoding available for the specified charset request.</summary>
        /// <param name="acceptCharset">
        /// The Accept-Charset header value (eg: "iso-8859-5, unicode-1-1;q=0.8").
        /// </param>
        /// <returns>An Encoding object appropriate to the specifed charset request.</returns>
        internal static Encoding EncodingFromAcceptCharset(string acceptCharset)
        {
            // Determines the appropriate encoding mapping according to
            // RFC 2616.14.2 (http://tools.ietf.org/html/rfc2616#section-14.2).
            Encoding result = null;
            if (!string.IsNullOrEmpty(acceptCharset))
            {
                // PERF: keep a cache of original strings to resolved Encoding.
                List<CharsetPart> parts = new List<CharsetPart>(AcceptCharsetParts(acceptCharset));
                parts.Sort(delegate(CharsetPart x, CharsetPart y)
                {
                    return y.Quality - x.Quality;
                });
 
                var encoderFallback = new EncoderExceptionFallback();
                var decoderFallback = new DecoderExceptionFallback();
                foreach (CharsetPart part in parts)
                {
                    if (part.Quality > 0)
                    {
                        // When UTF-8 is specified, select the version that doesn't use the BOM.
                        if (String.Compare("utf-8", part.Charset, StringComparison.OrdinalIgnoreCase) == 0)
                        {
                            result = FallbackEncoding;
                            break;
                        }
                        else
                        {
                            try
                            {
                                result = Encoding.GetEncoding(part.Charset, encoderFallback, decoderFallback);
                                break;
                            }
                            catch (ArgumentException)
                            {
                                // This exception is thrown when the character
                                // set isn't supported - it is ignored so
                                // other possible charsets are evaluated.
                            }
                        }
                    }
                }
            }
 
            // No Charset was specifed, or if charsets were specified, no valid charset was found.
            // Returning a different charset is also valid.
            if (result == null)
            {
                result = FallbackEncoding;
            }
 
            return result;
        }
#endif
 
        /// <summary>Reads a Content-Type header and extracts the MIME type/subtype and encoding.</summary>
        /// <param name="contentType">The Content-Type header.</param>
        /// <param name="mime">The MIME type in standard type/subtype form, without parameters.</param>
        /// <param name="encoding">Encoding (possibly null).</param>
        /// <returns>parameters of content type</returns>
        internal static KeyValuePair<string, string>[] ReadContentType(string contentType, out string mime, out Encoding encoding)
        {
            if (String.IsNullOrEmpty(contentType))
            {
                throw Error.HttpHeaderFailure(400, Strings.HttpProcessUtility_ContentTypeMissing);
            }
 
            MediaType mediaType = ReadMediaType(contentType);
            mime = mediaType.MimeType;
            encoding = mediaType.SelectEncoding();
            return mediaType.Parameters;
        }
 
#if !ASTORIA_CLIENT
        /// <summary>
        /// Given the parameters, search for the parameter with the given name and returns its value.
        /// </summary>
        /// <param name="parameters">list of parameters specified.</param>
        /// <param name="parameterName">name of the parameter whose value needs to be returned.</param>
        /// <returns>returns the value of the parameter with the given name. Returns null, if the parameter is not found.</returns>
        internal static string GetParameterValue(KeyValuePair<string, string>[] parameters, string parameterName)
        {
            if (parameters == null)
            {
                return null;
            }
 
            foreach (KeyValuePair<string, string> parameterInfo in parameters)
            {
                if (parameterInfo.Key == parameterName)
                {
                    return parameterInfo.Value;
                }
            }
 
            return null;
        }
 
#endif
 
        /// <summary>Tries to read a WCF Data Service version string.</summary>
        /// <param name="text">Text to read.</param>
        /// <param name="result">Parsed version and trailing text.</param>
        /// <returns>true if the version was read successfully; false otherwise.</returns>
        internal static bool TryReadVersion(string text, out KeyValuePair<Version, string> result)
        {
            Debug.Assert(text != null, "text != null");
 
            // Separate version number and extra string.
            int separator = text.IndexOf(';');
            string versionText, libraryName;
            if (separator >= 0)
            {
                versionText = text.Substring(0, separator);
                libraryName = text.Substring(separator + 1).Trim();
            }
            else
            {
                versionText = text;
                libraryName = null;
            }
 
            result = default(KeyValuePair<Version, string>);
            versionText = versionText.Trim();
 
            // The Version constructor allows for a more complex syntax, including
            // build, revisions, and major/minor for revisions. We only take two
            // number parts separated by a single dot.
            bool dotFound = false;
            for (int i = 0; i < versionText.Length; i++)
            {
                if (versionText[i] == '.')
                {
                    if (dotFound)
                    {
                        return false;
                    }
 
                    dotFound = true;
                }
                else if (versionText[i] < '0' || versionText[i] > '9')
                {
                    return false;
                }
            }
 
            try
            {
                result = new KeyValuePair<Version, string>(new Version(versionText), libraryName);
                return true;
            }
            catch (Exception e)
            {
                if (e is FormatException || e is OverflowException || e is ArgumentException)
                {
                    return false;
                }
 
                throw;
            }
        }
 
        /// <summary>Gets the named encoding if specified.</summary>
        /// <param name="name">Name (possibly null or empty).</param>
        /// <returns>
        /// The named encoding if specified; the encoding for HTTP missing 
        /// charset specification otherwise.
        /// </returns>
        /// <remarks>
        /// See http://tools.ietf.org/html/rfc2616#section-3.4.1 for details.
        /// </remarks>
        private static Encoding EncodingFromName(string name)
        {
            if (name == null)
            {
                return MissingEncoding;
            }
 
            name = name.Trim();
            if (name.Length == 0)
            {
                return MissingEncoding;
            }
            else
            {
                try
                {
#if ASTORIA_LIGHT
                    // SQLBU 641147: Silverlight client failing with FireFox 3.0
                    // we know we can do this with Silverlight because our http stack
                    // lets the browser convert from "ISO-8859-1" to string and then the stack
                    // converts the string into UTF8 for streaming into the client.
                    // it works for both nested payloads in batch and direct requests.
                    // Coordinate any change to this with the HttpWebRequest.ReadResponse()
                    return Encoding.UTF8;
#else
                    return Encoding.GetEncoding(name);
#endif
                }
                catch (ArgumentException)
                {
                    // 400 - Bad Request
                    throw Error.HttpHeaderFailure(400, Strings.HttpProcessUtility_EncodingNotSupported(name));
                }
            }
        }
 
#if !ASTORIA_CLIENT
        /// <summary>Creates a new exception for parsing errors.</summary>
        /// <param name="message">Message for error.</param>
        /// <returns>A new exception that can be thrown for a parsing error.</returns>
        private static DataServiceException CreateParsingException(string message)
        {
            // Status code "400"  ; Section 10.4.1: Bad Request
            return Error.HttpHeaderFailure(400, message);
        }
#endif
 
        /// <summary>Reads the type and subtype specifications for a MIME type.</summary>
        /// <param name='text'>Text in which specification exists.</param>
        /// <param name='textIndex'>Pointer into text.</param>
        /// <param name='type'>Type of media found.</param>
        /// <param name='subType'>Subtype of media found.</param>
        private static void ReadMediaTypeAndSubtype(string text, ref int textIndex, out string type, out string subType)
        {
            Debug.Assert(text != null, "text != null");
            int textStart = textIndex;
            if (ReadToken(text, ref textIndex))
            {
                throw Error.HttpHeaderFailure(400, Strings.HttpProcessUtility_MediaTypeUnspecified);
            }
 
            if (text[textIndex] != '/')
            {
                throw Error.HttpHeaderFailure(400, Strings.HttpProcessUtility_MediaTypeRequiresSlash);
            }
 
            type = text.Substring(textStart, textIndex - textStart);
            textIndex++;
 
            int subTypeStart = textIndex;
            ReadToken(text, ref textIndex);
 
            if (textIndex == subTypeStart)
            {
                throw Error.HttpHeaderFailure(400, Strings.HttpProcessUtility_MediaTypeRequiresSubType);
            }
 
            subType = text.Substring(subTypeStart, textIndex - subTypeStart);
        }
 
        /// <summary>Reads a media type definition as used in a Content-Type header.</summary>
        /// <param name="text">Text to read.</param>
        /// <returns>The <see cref="MediaType"/> defined by the specified <paramref name="text"/></returns>
        /// <remarks>All syntactic errors will produce a 400 - Bad Request status code.</remarks>
        private static MediaType ReadMediaType(string text)
        {
            Debug.Assert(text != null, "text != null");
 
            string type;
            string subType;
            int textIndex = 0;
            ReadMediaTypeAndSubtype(text, ref textIndex, out type, out subType);
 
            KeyValuePair<string, string>[] parameters = null;
            while (!SkipWhitespace(text, ref textIndex))
            {
                if (text[textIndex] != ';')
                {
                    throw Error.HttpHeaderFailure(400, Strings.HttpProcessUtility_MediaTypeRequiresSemicolonBeforeParameter);
                }
 
                textIndex++;
                if (SkipWhitespace(text, ref textIndex))
                {
                    // ';' should be a leading separator, but we choose to be a 
                    // bit permissive and allow it as a final delimiter as well.
                    break;
                }
 
                ReadMediaTypeParameter(text, ref textIndex, ref parameters);
            }
 
            return new MediaType(type, subType, parameters);
        }
 
        /// <summary>
        /// Reads a token on the specified text by advancing an index on it.
        /// </summary>
        /// <param name="text">Text to read token from.</param>
        /// <param name="textIndex">Index for the position being scanned on text.</param>
        /// <returns>true if the end of the text was reached; false otherwise.</returns>
        private static bool ReadToken(string text, ref int textIndex)
        {
            while (textIndex < text.Length && IsHttpToken(text[textIndex]))
            {
                textIndex++;
            }
 
            return (textIndex == text.Length);
        }
 
        /// <summary>
        /// Skips whitespace in the specified text by advancing an index to
        /// the next non-whitespace character.
        /// </summary>
        /// <param name="text">Text to scan.</param>
        /// <param name="textIndex">Index to begin scanning from.</param>
        /// <returns>true if the end of the string was reached, false otherwise.</returns>
        private static bool SkipWhitespace(string text, ref int textIndex)
        {
            Debug.Assert(text != null, "text != null");
            Debug.Assert(text.Length >= 0, "text >= 0");
            Debug.Assert(textIndex <= text.Length, "text <= text.Length");
 
            while (textIndex < text.Length && Char.IsWhiteSpace(text, textIndex))
            {
                textIndex++;
            }
 
            return (textIndex == text.Length);
        }
 
#if !ASTORIA_CLIENT
        /// <summary>
        /// Verfies whether the specified character is a valid separator in
        /// an HTTP header list of element.
        /// </summary>
        /// <param name="c">Character to verify.</param>
        /// <returns>true if c is a valid character for separating elements; false otherwise.</returns>
        private static bool IsHttpElementSeparator(char c)
        {
            return c == ',' || c == ' ' || c == '\t';
        }
 
        /// <summary>
        /// "Reads" a literal from the specified string by verifying that
        /// the exact text can be found at the specified position.
        /// </summary>
        /// <param name="text">Text within which a literal should be checked.</param>
        /// <param name="textIndex">Index in text where the literal should be found.</param>
        /// <param name="literal">Literal to check at the specified position.</param>
        /// <returns>true if the end of string is found; false otherwise.</returns>
        private static bool ReadLiteral(string text, int textIndex, string literal)
        {
            if (String.Compare(text, textIndex, literal, 0, literal.Length, StringComparison.Ordinal) != 0)
            {
                // Failed to find expected literal.
                throw CreateParsingException(Strings.HttpContextServiceHost_MalformedHeaderValue);
            }
 
            return textIndex + literal.Length == text.Length;
        }
 
        /// <summary>
        /// Converts the specified character from the ASCII range to a digit.
        /// </summary>
        /// <param name="c">Character to convert.</param>
        /// <returns>
        /// The Int32 value for c, or -1 if it is an element separator.
        /// </returns>
        private static int DigitToInt32(char c)
        {
            if (c >= '0' && c <= '9')
            {
                return (int)(c - '0');
            }
            else
            {
                if (IsHttpElementSeparator(c))
                {
                    return -1;
                }
                else
                {
                    throw CreateParsingException(Strings.HttpContextServiceHost_MalformedHeaderValue);
                }
            }
        }
 
        /// <summary>Returns all MIME types from the specified (non-blank) <paramref name='text' />.</summary>
        /// <param name='text'>Non-blank text, as it appears on an HTTP Accepts header.</param>
        /// <returns>An enumerable object with media type descriptions.</returns>
        private static IEnumerable<MediaType> MimeTypesFromAcceptHeader(string text)
        {
            Debug.Assert(!String.IsNullOrEmpty(text), "!String.IsNullOrEmpty(text)");
            List<MediaType> mediaTypes = new List<MediaType>();
            int textIndex = 0;
            while (!SkipWhitespace(text, ref textIndex))
            {
                string type;
                string subType;
                ReadMediaTypeAndSubtype(text, ref textIndex, out type, out subType);
 
                KeyValuePair<string, string>[] parameters = null;
                while (!SkipWhitespace(text, ref textIndex))
                {
                    if (text[textIndex] == ',')
                    {
                        textIndex++;
                        break;
                    }
 
                    if (text[textIndex] != ';')
                    {
                        throw Error.HttpHeaderFailure(400, Strings.HttpProcessUtility_MediaTypeRequiresSemicolonBeforeParameter);
                    }
 
                    textIndex++;
                    if (SkipWhitespace(text, ref textIndex))
                    {
                        // ';' should be a leading separator, but we choose to be a 
                        // bit permissive and allow it as a final delimiter as well.
                        break;
                    }
 
                    ReadMediaTypeParameter(text, ref textIndex, ref parameters);
                }
 
                mediaTypes.Add(new MediaType(type, subType, parameters));
            }
 
            return mediaTypes;
        }
#endif
 
        /// <summary>Read a parameter for a media type/range.</summary>
        /// <param name="text">Text to read from.</param>
        /// <param name="textIndex">Pointer in text.</param>
        /// <param name="parameters">Array with parameters to grow as necessary.</param>
        private static void ReadMediaTypeParameter(string text, ref int textIndex, ref KeyValuePair<string, string>[] parameters)
        {
            int startIndex = textIndex;
            if (ReadToken(text, ref textIndex))
            {
                throw Error.HttpHeaderFailure(400, Strings.HttpProcessUtility_MediaTypeMissingValue);
            }
 
            string parameterName = text.Substring(startIndex, textIndex - startIndex);
            if (text[textIndex] != '=')
            {
                throw Error.HttpHeaderFailure(400, Strings.HttpProcessUtility_MediaTypeMissingValue);
            }
 
            textIndex++;
 
            string parameterValue = ReadQuotedParameterValue(parameterName, text, ref textIndex);
 
            // Add the parameter name/value pair to the list.
            if (parameters == null)
            {
                parameters = new KeyValuePair<string, string>[1];
            }
            else
            {
                KeyValuePair<string, string>[] grow = new KeyValuePair<string, string>[parameters.Length + 1];
                Array.Copy(parameters, grow, parameters.Length);
                parameters = grow;
            }
 
            parameters[parameters.Length - 1] = new KeyValuePair<string, string>(parameterName, parameterValue);
        }
 
        /// <summary>
        /// Reads Mime type parameter value for a particular parameter in the Content-Type/Accept headers.
        /// </summary>
        /// <param name="parameterName">Name of parameter.</param>
        /// <param name="headerText">Header text.</param>
        /// <param name="textIndex">Parsing index in <paramref name="headerText"/>.</param>
        /// <returns>String representing the value of the <paramref name="parameterName"/> parameter.</returns>
        private static string ReadQuotedParameterValue(string parameterName, string headerText, ref int textIndex)
        {
            StringBuilder parameterValue = new StringBuilder();
            
            // Check if the value is quoted.
            bool valueIsQuoted = false;
            if (textIndex < headerText.Length)
            {
                if (headerText[textIndex] == '\"')
                {
                    textIndex++;
                    valueIsQuoted = true;
                }
            }
 
            while (textIndex < headerText.Length)
            {
                char currentChar = headerText[textIndex];
 
                if (currentChar == '\\' || currentChar == '\"')
                {
                    if (!valueIsQuoted)
                    {
                        throw Error.HttpHeaderFailure(400, Strings.HttpProcessUtility_EscapeCharWithoutQuotes(parameterName));
                    }
 
                    textIndex++;
 
                    // End of quoted parameter value.
                    if (currentChar == '\"')
                    {
                        valueIsQuoted = false;
                        break;
                    }
 
                    if (textIndex >= headerText.Length)
                    {
                        throw Error.HttpHeaderFailure(400, Strings.HttpProcessUtility_EscapeCharAtEnd(parameterName));
                    }
 
                    currentChar = headerText[textIndex];        
                }
                else
                if (!IsHttpToken(currentChar))
                {
                    // If the given character is special, we stop processing.
                    break;
                }
 
                parameterValue.Append(currentChar);
                textIndex++;
            }
 
            if (valueIsQuoted)
            {
                throw Error.HttpHeaderFailure(400, Strings.HttpProcessUtility_ClosingQuoteNotFound(parameterName));
            }
 
            return parameterValue.ToString();
        }
 
#if !ASTORIA_CLIENT
        /// <summary>
        /// Reads the numeric part of a quality value substring, normalizing it to 0-1000
        /// rather than the standard 0.000-1.000 ranges.
        /// </summary>
        /// <param name="text">Text to read qvalue from.</param>
        /// <param name="textIndex">Index into text where the qvalue starts.</param>
        /// <param name="qualityValue">After the method executes, the normalized qvalue.</param>
        /// <remarks>
        /// For more information, see RFC 2616.3.8.
        /// </remarks>
        private static void ReadQualityValue(string text, ref int textIndex, out int qualityValue)
        {
            char digit = text[textIndex++];
            if (digit == '0')
            {
                qualityValue = 0;
            }
            else if (digit == '1')
            {
                qualityValue = 1;
            }
            else
            {
                throw CreateParsingException(Strings.HttpContextServiceHost_MalformedHeaderValue);
            }
 
            if (textIndex < text.Length && text[textIndex] == '.')
            {
                textIndex++;
 
                int adjustFactor = 1000;
                while (adjustFactor > 1 && textIndex < text.Length)
                {
                    char c = text[textIndex];
                    int charValue = DigitToInt32(c);
                    if (charValue >= 0)
                    {
                        textIndex++;
                        adjustFactor /= 10;
                        qualityValue *= 10;
                        qualityValue += charValue;
                    }
                    else
                    {
                        break;
                    }
                }
 
                qualityValue = qualityValue *= adjustFactor;
                if (qualityValue > 1000)
                {
                    // Too high of a value in qvalue.
                    throw CreateParsingException(Strings.HttpContextServiceHost_MalformedHeaderValue);
                }
            }
            else
            {
                qualityValue *= 1000;
            }
        }
 
        /// <summary>
        /// Enumerates each charset part in the specified Accept-Charset header.
        /// </summary>
        /// <param name="headerValue">Non-null and non-empty header value for Accept-Charset.</param>
        /// <returns>
        /// A (non-sorted) enumeration of CharsetPart elements, which include
        /// a charset name and a quality (preference) value, normalized to 0-1000.
        /// </returns>
        private static IEnumerable<CharsetPart> AcceptCharsetParts(string headerValue)
        {
            Debug.Assert(!String.IsNullOrEmpty(headerValue), "!String.IsNullOrEmpty(headerValuer)");
 
            // PERF: optimize for common patterns.
            bool commaRequired = false; // Whether a comma should be found
            int headerIndex = 0;        // Index of character being procesed on headerValue.
            int headerStart;            // Index into headerValue for the start of the charset name.
            int headerNameEnd;          // Index into headerValue for the end of the charset name (+1).
            int headerEnd;              // Index into headerValue for this charset part (+1).
            int qualityValue;           // Normalized qvalue for this charset.
 
            while (headerIndex < headerValue.Length)
            {
                if (SkipWhitespace(headerValue, ref headerIndex))
                {
                    yield break;
                }
 
                if (headerValue[headerIndex] == ',')
                {
                    commaRequired = false;
                    headerIndex++;
                    continue;
                }
 
                if (commaRequired)
                {
                    // Comma missing between charset elements.
                    throw CreateParsingException(Strings.HttpContextServiceHost_MalformedHeaderValue);
                }
 
                headerStart = headerIndex;
                headerNameEnd = headerStart;
 
                bool endReached = ReadToken(headerValue, ref headerNameEnd);
                if (headerNameEnd == headerIndex)
                {
                    // Invalid charset name.
                    throw CreateParsingException(Strings.HttpContextServiceHost_MalformedHeaderValue);
                }
 
                if (endReached)
                {
                    qualityValue = 1000;
                    headerEnd = headerNameEnd;
                }
                else
                {
                    char afterNameChar = headerValue[headerNameEnd];
                    if (IsHttpSeparator(afterNameChar))
                    {
                        if (afterNameChar == ';')
                        {
                            if (ReadLiteral(headerValue, headerNameEnd, ";q="))
                            {
                                // Unexpected end of qvalue.
                                throw CreateParsingException(Strings.HttpContextServiceHost_MalformedHeaderValue);
                            }
 
                            headerEnd = headerNameEnd + 3;
                            ReadQualityValue(headerValue, ref headerEnd, out qualityValue);
                        }
                        else
                        {
                            qualityValue = 1000;
                            headerEnd = headerNameEnd;
                        }
                    }
                    else
                    {
                        // Invalid separator character.
                        throw CreateParsingException(Strings.HttpContextServiceHost_MalformedHeaderValue);
                    }
                }
 
                yield return new CharsetPart(headerValue.Substring(headerStart, headerNameEnd - headerStart), qualityValue);
 
                // Prepare for next charset; we require at least one comma before we process it.
                commaRequired = true;
                headerIndex = headerEnd;
            }
        }
#endif
 
        /// <summary>
        /// Determines whether the specified character is a valid HTTP separator.
        /// </summary>
        /// <param name="c">Character to verify.</param>
        /// <returns>true if c is a separator; false otherwise.</returns>
        /// <remarks>
        /// See RFC 2616 2.2 for further information.
        /// </remarks>
        private static bool IsHttpSeparator(char c)
        {
            return
                c == '(' || c == ')' || c == '<' || c == '>' || c == '@' ||
                c == ',' || c == ';' || c == ':' || c == '\\' || c == '"' ||
                c == '/' || c == '[' || c == ']' || c == '?' || c == '=' ||
                c == '{' || c == '}' || c == ' ' || c == '\x9';
        }
 
        /// <summary>
        /// Determines whether the specified character is a valid HTTP header token character.
        /// </summary>
        /// <param name="c">Character to verify.</param>
        /// <returns>true if c is a valid HTTP header token character; false otherwise.</returns>
        private static bool IsHttpToken(char c)
        {
            // A token character is any character (0-127) except control (0-31) or
            // separators. 127 is DEL, a control character.
            return c < '\x7F' && c > '\x1F' && !IsHttpSeparator(c);
        }
 
#if !ASTORIA_CLIENT
        /// <summary>Provides a struct to encapsulate a charset name and its relative desirability.</summary>
        private struct CharsetPart
        {
            /// <summary>Name of the charset.</summary>
            internal readonly string Charset;
 
            /// <summary>Charset quality (desirability), normalized to 0-1000.</summary>
            internal readonly int Quality;
 
            /// <summary>
            /// Initializes a new CharsetPart with the specified values.
            /// </summary>
            /// <param name="charset">Name of charset.</param>
            /// <param name="quality">Charset quality (desirability), normalized to 0-1000.</param>
            internal CharsetPart(string charset, int quality)
            {
                Debug.Assert(charset != null, "charset != null");
                Debug.Assert(charset.Length > 0, "charset.Length > 0");
                Debug.Assert(0 <= quality && quality <= 1000, "0 <= quality && quality <= 1000");
 
                this.Charset = charset;
                this.Quality = quality;
            }
        }
#endif
 
        /// <summary>Use this class to represent a media type definition.</summary>
        [DebuggerDisplay("MediaType [{type}/{subType}]")]
        private sealed class MediaType
        {
            /// <summary>Parameters specified on the media type.</summary>
            private readonly KeyValuePair<string, string>[] parameters;
 
            /// <summary>Sub-type specification (for example, 'plain').</summary>
            private readonly string subType;
 
            /// <summary>Type specification (for example, 'text').</summary>
            private readonly string type;
 
            /// <summary>
            /// Initializes a new <see cref="MediaType"/> read-only instance.
            /// </summary>
            /// <param name="type">Type specification (for example, 'text').</param>
            /// <param name="subType">Sub-type specification (for example, 'plain').</param>
            /// <param name="parameters">Parameters specified on the media type.</param>
            internal MediaType(string type, string subType, KeyValuePair<string, string>[] parameters)
            {
                Debug.Assert(type != null, "type != null");
                Debug.Assert(subType != null, "subType != null");
 
                this.type = type;
                this.subType = subType;
                this.parameters = parameters;
            }
 
            /// <summary>Returns the MIME type in standard type/subtype form, without parameters.</summary>
            internal string MimeType
            {
                get { return this.type + "/" + this.subType; }
            }
 
            /// <summary>media type parameters</summary>
            internal KeyValuePair<string, string>[] Parameters
            {
                get { return this.parameters; }
            }
 
#if !ASTORIA_CLIENT
            /// <summary>Gets a number of non-* matching types, or -1 if not matching at all.</summary>
            /// <param name="candidate">Candidate MIME type to match.</param>
            /// <returns>The number of non-* matching types, or -1 if not matching at all.</returns>
            internal int GetMatchingParts(string candidate)
            {
                Debug.Assert(candidate != null, "candidate must not be null.");
 
                int result = -1;
                if (candidate.Length > 0)
                {
                    if (this.type == "*")
                    {
                        result = 0;
                    }
                    else
                    {
                        int separatorIdx = candidate.IndexOf('/');
                        if (separatorIdx >= 0) 
                        {
                            string candidateType = candidate.Substring(0, separatorIdx);
                            if (WebUtil.CompareMimeType(this.type, candidateType))
                            {
                                if (this.subType == "*")
                                {
                                    result = 1;
                                }
                                else
                                {
                                    string candidateSubType = candidate.Substring(candidateType.Length + 1);
                                    if (WebUtil.CompareMimeType(this.subType, candidateSubType))
                                    {
                                        result = 2;
                                    }
                                }
                            }
                        }
                    }
                }
 
                return result;
            }
 
            /// <summary>Selects a quality value for the specified type.</summary>
            /// <returns>The quality value, in range from 0 through 1000.</returns>
            /// <remarks>See http://tools.ietf.org/html/rfc2616#section-14.1 for further details.</remarks>
            internal int SelectQualityValue()
            {
                if (this.parameters != null)
                {
                    foreach (KeyValuePair<string, string> parameter in this.parameters)
                    {
                        if (String.Equals(parameter.Key, XmlConstants.HttpQValueParameter, StringComparison.OrdinalIgnoreCase))
                        {
                            string qvalueText = parameter.Value.Trim();
                            if (qvalueText.Length > 0)
                            {
                                int result;
                                int textIndex = 0;
                                ReadQualityValue(qvalueText, ref textIndex, out result);
                                return result;
                            }
                        }
                    }
                }
 
                return 1000;
            }
#endif
 
            /// <summary>
            /// Selects the encoding appropriate for this media type specification
            /// (possibly null).
            /// </summary>
            /// <returns>
            /// The encoding explicitly defined on the media type specification, or
            /// the default encoding for well-known media types.
            /// </returns>
            /// <remarks>
            /// As per http://tools.ietf.org/html/rfc2616#section-3.7, the type, 
            /// subtype and parameter name attributes are case-insensitive.
            /// </remarks>
            internal Encoding SelectEncoding()
            {
                if (this.parameters != null)
                {
                    foreach (KeyValuePair<string, string> parameter in this.parameters)
                    {
                        if (String.Equals(parameter.Key, XmlConstants.HttpCharsetParameter, StringComparison.OrdinalIgnoreCase))
                        {
                            string encodingName = parameter.Value.Trim();
                            if (encodingName.Length > 0)
                            {
                                return EncodingFromName(parameter.Value);
                            }
                        }
                    }
                }
 
                // Select the default encoding for this media type.
                if (String.Equals(this.type, XmlConstants.MimeTextType, StringComparison.OrdinalIgnoreCase))
                {
                    // HTTP 3.7.1 Canonicalization and Text Defaults
                    // "text" subtypes default to ISO-8859-1
                    //
                    // Unless the subtype is XML, in which case we should default
                    // to us-ascii. Instead we return null, to let the encoding
                    // in the <?xml ...?> PI win (http://tools.ietf.org/html/rfc3023#section-3.1)
                    if (String.Equals(this.subType, XmlConstants.MimeXmlSubType, StringComparison.OrdinalIgnoreCase))
                    {
                        return null;
                    }
                    else
                    {
                        return MissingEncoding;
                    }
                }
                else if (String.Equals(this.type, XmlConstants.MimeApplicationType, StringComparison.OrdinalIgnoreCase) &&
                    String.Equals(this.subType, XmlConstants.MimeJsonSubType, StringComparison.OrdinalIgnoreCase))
                {
                    // http://tools.ietf.org/html/rfc4627#section-3
                    // The default encoding is UTF-8.
                    return FallbackEncoding;
                }
                else
                {
                    return null;
                }
            }
        }
    }
}