File: System\Runtime\UrlUtility.cs
Project: ndp\cdf\src\System.ServiceModel.Internals\System.ServiceModel.Internals.csproj (System.ServiceModel.Internals)
//------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
//------------------------------------------------------------
 
namespace System.Runtime
{
    using System.Collections;
    using System.Collections.Specialized;
    using System.Runtime.Serialization;
    using System.Text;
 
    //copied from System.Web.HttpUtility code (renamed here) to remove dependency on System.Web.dll
    static class UrlUtility
    {
        //  Query string parsing support
        public static NameValueCollection ParseQueryString(string query)
        {
            return ParseQueryString(query, Encoding.UTF8);
        }
 
        public static NameValueCollection ParseQueryString(string query, Encoding encoding)
        {
            if (query == null)
            {
                throw Fx.Exception.ArgumentNull("query");
            }
 
            if (encoding == null)
            {
                throw Fx.Exception.ArgumentNull("encoding");
            }
 
            if (query.Length > 0 && query[0] == '?')
            {
                query = query.Substring(1);
            }
 
            return new HttpValueCollection(query, encoding);
        }
 
        public static string UrlEncode(string str)
        {
            if (str == null)
            {
                return null;
            }
            return UrlEncode(str, Encoding.UTF8);
        }
 
        // URL encodes a path portion of a URL string and returns the encoded string.
        public static string UrlPathEncode(string str)
        {
            if (str == null)
            {
                return null;
            }
 
            // recurse in case there is a query string
            int i = str.IndexOf('?');
            if (i >= 0)
            {
                return UrlPathEncode(str.Substring(0, i)) + str.Substring(i);
            }
 
            // encode DBCS characters and spaces only
            return UrlEncodeSpaces(UrlEncodeNonAscii(str, Encoding.UTF8));
        }
 
        public static string UrlEncode(string str, Encoding encoding)
        {
            if (str == null)
            {
                return null;
            }
            return Encoding.ASCII.GetString(UrlEncodeToBytes(str, encoding));
        }
 
        public static string UrlEncodeUnicode(string str)
        {
            if (str == null)
                return null;
            return UrlEncodeUnicodeStringToStringInternal(str, false);
 
        }
 
        private static string UrlEncodeUnicodeStringToStringInternal(string s, bool ignoreAscii)
        {
            int l = s.Length;
            StringBuilder sb = new StringBuilder(l);
 
            for (int i = 0; i < l; i++)
            {
                char ch = s[i];
 
                if ((ch & 0xff80) == 0)
                {  // 7 bit?
                    if (ignoreAscii || IsSafe(ch))
                    {
                        sb.Append(ch);
                    }
                    else if (ch == ' ')
                    {
                        sb.Append('+');
                    }
                    else
                    {
                        sb.Append('%');
                        sb.Append(IntToHex((ch >> 4) & 0xf));
                        sb.Append(IntToHex((ch) & 0xf));
                    }
                }
                else
                { // arbitrary Unicode?
                    sb.Append("%u");
                    sb.Append(IntToHex((ch >> 12) & 0xf));
                    sb.Append(IntToHex((ch >> 8) & 0xf));
                    sb.Append(IntToHex((ch >> 4) & 0xf));
                    sb.Append(IntToHex((ch) & 0xf));
                }
            }
 
            return sb.ToString();
        }
 
        //  Helper to encode the non-ASCII url characters only
        static string UrlEncodeNonAscii(string str, Encoding e)
        {
            if (string.IsNullOrEmpty(str))
            {
                return str;
            }
            if (e == null)
            {
                e = Encoding.UTF8;
            }
            byte[] bytes = e.GetBytes(str);
            bytes = UrlEncodeBytesToBytesInternalNonAscii(bytes, 0, bytes.Length, false);
            return Encoding.ASCII.GetString(bytes);
        }
 
        //  Helper to encode spaces only
        static string UrlEncodeSpaces(string str)
        {
            if (str != null && str.IndexOf(' ') >= 0)
            {
                str = str.Replace(" ", "%20");
            }
            return str;
        }
 
        public static byte[] UrlEncodeToBytes(string str, Encoding e)
        {
            if (str == null)
            {
                return null;
            }
            byte[] bytes = e.GetBytes(str);
            return UrlEncodeBytesToBytesInternal(bytes, 0, bytes.Length, false);
        }
 
        //public static string UrlDecode(string str)
        //{
        //    if (str == null)
        //        return null;
        //    return UrlDecode(str, Encoding.UTF8);
        //}
 
        public static string UrlDecode(string str, Encoding e)
        {
            if (str == null)
            {
                return null;
            }
            return UrlDecodeStringFromStringInternal(str, e);
        }
 
        //  Implementation for encoding
        static byte[] UrlEncodeBytesToBytesInternal(byte[] bytes, int offset, int count, bool alwaysCreateReturnValue)
        {
            int cSpaces = 0;
            int cUnsafe = 0;
 
            // count them first
            for (int i = 0; i < count; i++)
            {
                char ch = (char)bytes[offset + i];
 
                if (ch == ' ')
                {
                    cSpaces++;
                }
                else if (!IsSafe(ch))
                {
                    cUnsafe++;
                }
            }
 
            // nothing to expand?
            if (!alwaysCreateReturnValue && cSpaces == 0 && cUnsafe == 0)
            {
                return bytes;
            }
 
            // expand not 'safe' characters into %XX, spaces to +s
            byte[] expandedBytes = new byte[count + cUnsafe * 2];
            int pos = 0;
 
            for (int i = 0; i < count; i++)
            {
                byte b = bytes[offset + i];
                char ch = (char)b;
 
                if (IsSafe(ch))
                {
                    expandedBytes[pos++] = b;
                }
                else if (ch == ' ')
                {
                    expandedBytes[pos++] = (byte)'+';
                }
                else
                {
                    expandedBytes[pos++] = (byte)'%';
                    expandedBytes[pos++] = (byte)IntToHex((b >> 4) & 0xf);
                    expandedBytes[pos++] = (byte)IntToHex(b & 0x0f);
                }
            }
 
            return expandedBytes;
        }
 
 
        static bool IsNonAsciiByte(byte b)
        {
            return (b >= 0x7F || b < 0x20);
        }
 
        static byte[] UrlEncodeBytesToBytesInternalNonAscii(byte[] bytes, int offset, int count, bool alwaysCreateReturnValue)
        {
            int cNonAscii = 0;
 
            // count them first
            for (int i = 0; i < count; i++)
            {
                if (IsNonAsciiByte(bytes[offset + i]))
                {
                    cNonAscii++;
                }
            }
 
            // nothing to expand?
            if (!alwaysCreateReturnValue && cNonAscii == 0)
            {
                return bytes;
            }
 
            // expand not 'safe' characters into %XX, spaces to +s
            byte[] expandedBytes = new byte[count + cNonAscii * 2];
            int pos = 0;
 
            for (int i = 0; i < count; i++)
            {
                byte b = bytes[offset + i];
 
                if (IsNonAsciiByte(b))
                {
                    expandedBytes[pos++] = (byte)'%';
                    expandedBytes[pos++] = (byte)IntToHex((b >> 4) & 0xf);
                    expandedBytes[pos++] = (byte)IntToHex(b & 0x0f);
                }
                else
                {
                    expandedBytes[pos++] = b;
                }
            }
 
            return expandedBytes;
        }
 
        static string UrlDecodeStringFromStringInternal(string s, Encoding e)
        {
            int count = s.Length;
            UrlDecoder helper = new UrlDecoder(count, e);
 
            // go through the string's chars collapsing %XX and %uXXXX and
            // appending each char as char, with exception of %XX constructs
            // that are appended as bytes
 
            for (int pos = 0; pos < count; pos++)
            {
                char ch = s[pos];
 
                if (ch == '+')
                {
                    ch = ' ';
                }
                else if (ch == '%' && pos < count - 2)
                {
                    if (s[pos + 1] == 'u' && pos < count - 5)
                    {
                        int h1 = HexToInt(s[pos + 2]);
                        int h2 = HexToInt(s[pos + 3]);
                        int h3 = HexToInt(s[pos + 4]);
                        int h4 = HexToInt(s[pos + 5]);
 
                        if (h1 >= 0 && h2 >= 0 && h3 >= 0 && h4 >= 0)
                        {   // valid 4 hex chars
                            ch = (char)((h1 << 12) | (h2 << 8) | (h3 << 4) | h4);
                            pos += 5;
 
                            // only add as char
                            helper.AddChar(ch);
                            continue;
                        }
                    }
                    else
                    {
                        int h1 = HexToInt(s[pos + 1]);
                        int h2 = HexToInt(s[pos + 2]);
 
                        if (h1 >= 0 && h2 >= 0)
                        {     // valid 2 hex chars
                            byte b = (byte)((h1 << 4) | h2);
                            pos += 2;
 
                            // don't add as char
                            helper.AddByte(b);
                            continue;
                        }
                    }
                }
 
                if ((ch & 0xFF80) == 0)
                {
                    helper.AddByte((byte)ch); // 7 bit have to go as bytes because of Unicode
                }
                else
                {
                    helper.AddChar(ch);
                }
            }
 
            return helper.GetString();
        }
 
        // Private helpers for URL encoding/decoding
        static int HexToInt(char h)
        {
            return (h >= '0' && h <= '9') ? h - '0' :
            (h >= 'a' && h <= 'f') ? h - 'a' + 10 :
            (h >= 'A' && h <= 'F') ? h - 'A' + 10 :
            -1;
        }
 
        static char IntToHex(int n)
        {
            //WCF CHANGE: CHANGED FROM Debug.Assert() to Fx.Assert()
            Fx.Assert(n < 0x10, "n < 0x10");
 
            if (n <= 9)
            {
                return (char)(n + (int)'0');
            }
            else
            {
                return (char)(n - 10 + (int)'a');
            }
        }
 
        // Set of safe chars, from RFC 1738.4 minus '+'
        internal static bool IsSafe(char ch)
        {
            if (ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' || ch >= '0' && ch <= '9')
            {
                return true;
            }
 
            switch (ch)
            {
                case '-':
                case '_':
                case '.':
                case '!':
                case '*':
                case '\'':
                case '(':
                case ')':
                    return true;
            }
 
            return false;
        }
 
        // Internal class to facilitate URL decoding -- keeps char buffer and byte buffer, allows appending of either chars or bytes
        class UrlDecoder
        {
            int _bufferSize;
 
            // Accumulate characters in a special array
            int _numChars;
            char[] _charBuffer;
 
            // Accumulate bytes for decoding into characters in a special array
            int _numBytes;
            byte[] _byteBuffer;
 
            // Encoding to convert chars to bytes
            Encoding _encoding;
 
            void FlushBytes()
            {
                if (_numBytes > 0)
                {
                    _numChars += _encoding.GetChars(_byteBuffer, 0, _numBytes, _charBuffer, _numChars);
                    _numBytes = 0;
                }
            }
 
            internal UrlDecoder(int bufferSize, Encoding encoding)
            {
                _bufferSize = bufferSize;
                _encoding = encoding;
 
                _charBuffer = new char[bufferSize];
                // byte buffer created on demand
            }
 
            internal void AddChar(char ch)
            {
                if (_numBytes > 0)
                {
                    FlushBytes();
                }
 
                _charBuffer[_numChars++] = ch;
            }
 
            internal void AddByte(byte b)
            {
                // if there are no pending bytes treat 7 bit bytes as characters
                // this optimization is temp disable as it doesn't work for some encodings
 
                //if (_numBytes == 0 && ((b & 0x80) == 0)) {
                //    AddChar((char)b);
                //}
                //else
 
                {
                    if (_byteBuffer == null)
                    {
                        _byteBuffer = new byte[_bufferSize];
                    }
 
                    _byteBuffer[_numBytes++] = b;
                }
            }
 
            internal string GetString()
            {
                if (_numBytes > 0)
                {
                    FlushBytes();
                }
 
                if (_numChars > 0)
                {
                    return new String(_charBuffer, 0, _numChars);
                }
                else
                {
                    return string.Empty;
                }
            }
        }
 
        [Serializable]
        class HttpValueCollection : NameValueCollection
        {
            internal HttpValueCollection(string str, Encoding encoding)
                : base(StringComparer.OrdinalIgnoreCase)
            {
                if (!string.IsNullOrEmpty(str))
                {
                    FillFromString(str, true, encoding);
                }
 
                IsReadOnly = false;
            }
 
            protected HttpValueCollection(SerializationInfo info, StreamingContext context)
                : base(info, context)
            {
            }
 
            internal void FillFromString(string s, bool urlencoded, Encoding encoding)
            {
                int l = (s != null) ? s.Length : 0;
                int i = 0;
 
                while (i < l)
                {
                    // find next & while noting first = on the way (and if there are more)
 
                    int si = i;
                    int ti = -1;
 
                    while (i < l)
                    {
                        char ch = s[i];
 
                        if (ch == '=')
                        {
                            if (ti < 0)
                                ti = i;
                        }
                        else if (ch == '&')
                        {
                            break;
                        }
 
                        i++;
                    }
 
                    // extract the name / value pair
 
                    string name = null;
                    string value = null;
 
                    if (ti >= 0)
                    {
                        name = s.Substring(si, ti - si);
                        value = s.Substring(ti + 1, i - ti - 1);
                    }
                    else
                    {
                        value = s.Substring(si, i - si);
                    }
 
                    // add name / value pair to the collection
 
                    if (urlencoded)
                    {
                        base.Add(
                           UrlUtility.UrlDecode(name, encoding),
                           UrlUtility.UrlDecode(value, encoding));
                    }
                    else
                    {
                        base.Add(name, value);
                    }
 
                    // trailing '&'
 
                    if (i == l - 1 && s[i] == '&')
                    {
                        base.Add(null, string.Empty);
                    }
 
                    i++;
                }
            }
 
            public override string ToString()
            {
                return ToString(true, null);
            }
 
            string ToString(bool urlencoded, IDictionary excludeKeys)
            {
                int n = Count;
                if (n == 0)
                    return string.Empty;
 
                StringBuilder s = new StringBuilder();
                string key, keyPrefix, item;
 
                for (int i = 0; i < n; i++)
                {
                    key = GetKey(i);
 
                    if (excludeKeys != null && key != null && excludeKeys[key] != null)
                    {
                        continue;
                    }
                    if (urlencoded)
                    {
                        key = UrlUtility.UrlEncodeUnicode(key);
                    }
                    keyPrefix = (!string.IsNullOrEmpty(key)) ? (key + "=") : string.Empty;
 
                    ArrayList values = (ArrayList)BaseGet(i);
                    int numValues = (values != null) ? values.Count : 0;
 
                    if (s.Length > 0)
                    {
                        s.Append('&');
                    }
 
                    if (numValues == 1)
                    {
                        s.Append(keyPrefix);
                        item = (string)values[0];
                        if (urlencoded)
                            item = UrlUtility.UrlEncodeUnicode(item);
                        s.Append(item);
                    }
                    else if (numValues == 0)
                    {
                        s.Append(keyPrefix);
                    }
                    else
                    {
                        for (int j = 0; j < numValues; j++)
                        {
                            if (j > 0)
                            {
                                s.Append('&');
                            }
                            s.Append(keyPrefix);
                            item = (string)values[j];
                            if (urlencoded)
                            {
                                item = UrlUtility.UrlEncodeUnicode(item);
                            }
                            s.Append(item);
                        }
                    }
                }
 
                return s.ToString();
            }
        }
    }
}