1 // Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
2 
3 using System.Diagnostics.Contracts;
4 using System.Text;
5 
6 namespace System.Net.Http.Internal
7 {
8     /// <summary>
9     /// Helpers for encoding, decoding, and parsing URI query components.
10     /// </summary>
11     internal static class UriQueryUtility
12     {
13         // The implementation below is ported from WebUtility for use in .Net 4
14 
15         #region UrlEncode implementation
16 
UrlEncode(byte[] bytes, int offset, int count, bool alwaysCreateNewReturnValue)17         private static byte[] UrlEncode(byte[] bytes, int offset, int count, bool alwaysCreateNewReturnValue)
18         {
19             byte[] encoded = UrlEncode(bytes, offset, count);
20 
21             return (alwaysCreateNewReturnValue && (encoded != null) && (encoded == bytes))
22                 ? (byte[])encoded.Clone()
23                 : encoded;
24         }
25 
UrlEncode(byte[] bytes, int offset, int count)26         private static byte[] UrlEncode(byte[] bytes, int offset, int count)
27         {
28             if (!ValidateUrlEncodingParameters(bytes, offset, count))
29             {
30                 return null;
31             }
32 
33             int cSpaces = 0;
34             int cUnsafe = 0;
35 
36             // count them first
37             for (int i = 0; i < count; i++)
38             {
39                 char ch = (char)bytes[offset + i];
40 
41                 if (ch == ' ')
42                     cSpaces++;
43                 else if (!IsUrlSafeChar(ch))
44                     cUnsafe++;
45             }
46 
47             // nothing to expand?
48             if (cSpaces == 0 && cUnsafe == 0)
49                 return bytes;
50 
51             // expand not 'safe' characters into %XX, spaces to +s
52             byte[] expandedBytes = new byte[count + cUnsafe * 2];
53             int pos = 0;
54 
55             for (int i = 0; i < count; i++)
56             {
57                 byte b = bytes[offset + i];
58                 char ch = (char)b;
59 
60                 if (IsUrlSafeChar(ch))
61                 {
62                     expandedBytes[pos++] = b;
63                 }
64                 else if (ch == ' ')
65                 {
66                     expandedBytes[pos++] = (byte)'+';
67                 }
68                 else
69                 {
70                     expandedBytes[pos++] = (byte)'%';
71                     expandedBytes[pos++] = (byte)IntToHex((b >> 4) & 0xf);
72                     expandedBytes[pos++] = (byte)IntToHex(b & 0x0f);
73                 }
74             }
75 
76             return expandedBytes;
77         }
78 
79         #endregion
80 
81         #region UrlEncode public methods
82 
UrlEncode(string str)83         public static string UrlEncode(string str)
84         {
85             if (str == null)
86                 return null;
87 
88             byte[] bytes = Encoding.UTF8.GetBytes(str);
89             return Encoding.ASCII.GetString(UrlEncode(bytes, 0, bytes.Length, false /* alwaysCreateNewReturnValue */));
90         }
91 
UrlEncodeToBytes(byte[] bytes, int offset, int count)92         public static byte[] UrlEncodeToBytes(byte[] bytes, int offset, int count)
93         {
94             return UrlEncode(bytes, offset, count, true /* alwaysCreateNewReturnValue */);
95         }
96 
97         #endregion
98 
99         #region UrlDecode implementation
100 
UrlDecodeInternal(string value, Encoding encoding)101         private static string UrlDecodeInternal(string value, Encoding encoding)
102         {
103             if (value == null)
104             {
105                 return null;
106             }
107 
108             int count = value.Length;
109             UrlDecoder helper = new UrlDecoder(count, encoding);
110 
111             // go through the string's chars collapsing %XX and %uXXXX and
112             // appending each char as char, with exception of %XX constructs
113             // that are appended as bytes
114 
115             for (int pos = 0; pos < count; pos++)
116             {
117                 char ch = value[pos];
118 
119                 if (ch == '+')
120                 {
121                     ch = ' ';
122                 }
123                 else if (ch == '%' && pos < count - 2)
124                 {
125                     if (value[pos + 1] == 'u' && pos < count - 5)
126                     {
127                         int h1 = HexToInt(value[pos + 2]);
128                         int h2 = HexToInt(value[pos + 3]);
129                         int h3 = HexToInt(value[pos + 4]);
130                         int h4 = HexToInt(value[pos + 5]);
131 
132                         if (h1 >= 0 && h2 >= 0 && h3 >= 0 && h4 >= 0)
133                         {   // valid 4 hex chars
134                             ch = (char)((h1 << 12) | (h2 << 8) | (h3 << 4) | h4);
135                             pos += 5;
136 
137                             // only add as char
138                             helper.AddChar(ch);
139                             continue;
140                         }
141                     }
142                     else
143                     {
144                         int h1 = HexToInt(value[pos + 1]);
145                         int h2 = HexToInt(value[pos + 2]);
146 
147                         if (h1 >= 0 && h2 >= 0)
148                         {     // valid 2 hex chars
149                             byte b = (byte)((h1 << 4) | h2);
150                             pos += 2;
151 
152                             // don't add as char
153                             helper.AddByte(b);
154                             continue;
155                         }
156                     }
157                 }
158 
159                 if ((ch & 0xFF80) == 0)
160                     helper.AddByte((byte)ch); // 7 bit have to go as bytes because of Unicode
161                 else
162                     helper.AddChar(ch);
163             }
164 
165             return helper.GetString();
166         }
167 
UrlDecodeInternal(byte[] bytes, int offset, int count)168         private static byte[] UrlDecodeInternal(byte[] bytes, int offset, int count)
169         {
170             if (!ValidateUrlEncodingParameters(bytes, offset, count))
171             {
172                 return null;
173             }
174 
175             int decodedBytesCount = 0;
176             byte[] decodedBytes = new byte[count];
177 
178             for (int i = 0; i < count; i++)
179             {
180                 int pos = offset + i;
181                 byte b = bytes[pos];
182 
183                 if (b == '+')
184                 {
185                     b = (byte)' ';
186                 }
187                 else if (b == '%' && i < count - 2)
188                 {
189                     int h1 = HexToInt((char)bytes[pos + 1]);
190                     int h2 = HexToInt((char)bytes[pos + 2]);
191 
192                     if (h1 >= 0 && h2 >= 0)
193                     {     // valid 2 hex chars
194                         b = (byte)((h1 << 4) | h2);
195                         i += 2;
196                     }
197                 }
198 
199                 decodedBytes[decodedBytesCount++] = b;
200             }
201 
202             if (decodedBytesCount < decodedBytes.Length)
203             {
204                 byte[] newDecodedBytes = new byte[decodedBytesCount];
205                 Array.Copy(decodedBytes, newDecodedBytes, decodedBytesCount);
206                 decodedBytes = newDecodedBytes;
207             }
208 
209             return decodedBytes;
210         }
211 
212         #endregion
213 
214         #region UrlDecode public methods
215 
UrlDecode(string str)216         public static string UrlDecode(string str)
217         {
218             if (str == null)
219                 return null;
220 
221             return UrlDecodeInternal(str, Encoding.UTF8);
222         }
223 
UrlDecodeToBytes(byte[] bytes, int offset, int count)224         public static byte[] UrlDecodeToBytes(byte[] bytes, int offset, int count)
225         {
226             return UrlDecodeInternal(bytes, offset, count);
227         }
228 
229         #endregion
230 
231         #region Helper methods
232 
HexToInt(char h)233         private static int HexToInt(char h)
234         {
235             return (h >= '0' && h <= '9') ? h - '0' :
236             (h >= 'a' && h <= 'f') ? h - 'a' + 10 :
237             (h >= 'A' && h <= 'F') ? h - 'A' + 10 :
238             -1;
239         }
240 
IntToHex(int n)241         private static char IntToHex(int n)
242         {
243             Contract.Assert(n < 0x10);
244 
245             if (n <= 9)
246                 return (char)(n + (int)'0');
247             else
248                 return (char)(n - 10 + (int)'a');
249         }
250 
251         // Set of safe chars, from RFC 1738.4 minus '+'
IsUrlSafeChar(char ch)252         private static bool IsUrlSafeChar(char ch)
253         {
254             if (ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' || ch >= '0' && ch <= '9')
255                 return true;
256 
257             switch (ch)
258             {
259                 case '-':
260                 case '_':
261                 case '.':
262                 case '!':
263                 case '*':
264                 case '(':
265                 case ')':
266                     return true;
267             }
268 
269             return false;
270         }
271 
ValidateUrlEncodingParameters(byte[] bytes, int offset, int count)272         private static bool ValidateUrlEncodingParameters(byte[] bytes, int offset, int count)
273         {
274             if (bytes == null && count == 0)
275                 return false;
276             if (bytes == null)
277             {
278                 throw new ArgumentNullException("bytes");
279             }
280             if (offset < 0 || offset > bytes.Length)
281             {
282                 throw new ArgumentOutOfRangeException("offset");
283             }
284             if (count < 0 || offset + count > bytes.Length)
285             {
286                 throw new ArgumentOutOfRangeException("count");
287             }
288 
289             return true;
290         }
291 
292         #endregion
293 
294         #region UrlDecoder nested class
295 
296         // Internal class to facilitate URL decoding -- keeps char buffer and byte buffer, allows appending of either chars or bytes
297         private class UrlDecoder
298         {
299             private int _bufferSize;
300 
301             // Accumulate characters in a special array
302             private int _numChars;
303             private char[] _charBuffer;
304 
305             // Accumulate bytes for decoding into characters in a special array
306             private int _numBytes;
307             private byte[] _byteBuffer;
308 
309             // Encoding to convert chars to bytes
310             private Encoding _encoding;
311 
FlushBytes()312             private void FlushBytes()
313             {
314                 if (_numBytes > 0)
315                 {
316                     _numChars += _encoding.GetChars(_byteBuffer, 0, _numBytes, _charBuffer, _numChars);
317                     _numBytes = 0;
318                 }
319             }
320 
UrlDecoder(int bufferSize, Encoding encoding)321             internal UrlDecoder(int bufferSize, Encoding encoding)
322             {
323                 _bufferSize = bufferSize;
324                 _encoding = encoding;
325 
326                 _charBuffer = new char[bufferSize];
327                 // byte buffer created on demand
328             }
329 
AddChar(char ch)330             internal void AddChar(char ch)
331             {
332                 if (_numBytes > 0)
333                     FlushBytes();
334 
335                 _charBuffer[_numChars++] = ch;
336             }
337 
AddByte(byte b)338             internal void AddByte(byte b)
339             {
340                 if (_byteBuffer == null)
341                     _byteBuffer = new byte[_bufferSize];
342 
343                 _byteBuffer[_numBytes++] = b;
344             }
345 
GetString()346             internal String GetString()
347             {
348                 if (_numBytes > 0)
349                     FlushBytes();
350 
351                 if (_numChars > 0)
352                     return new String(_charBuffer, 0, _numChars);
353                 else
354                     return String.Empty;
355             }
356         }
357 
358         #endregion
359     }
360 }