1 // Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
2 
3 using System.Collections;
4 using System.Collections.Specialized;
5 using System.Diagnostics.CodeAnalysis;
6 using System.Diagnostics.Contracts;
7 using System.Net.Http.Formatting;
8 using System.Runtime.Serialization;
9 using System.Text;
10 using System.Web.Http.Properties;
11 
12 namespace System.Web.Http.Internal
13 {
14     // TODO: Once IVT relationship from Formatting DLL has been removed we flip to a link
15     // so that we don't have two copies of this code.
16 
17     /// <summary>
18     /// Helpers for encoding, decoding, and parsing URI query components.
19     /// </summary>
20     internal static class UriQueryUtility
21     {
ParseQueryString(string query)22         public static NameValueCollection ParseQueryString(string query)
23         {
24             if (query == null)
25             {
26                 throw new ArgumentNullException("query");
27             }
28 
29             if (query.Length > 0 && query[0] == '?')
30             {
31                 query = query.Substring(1);
32             }
33 
34             return new HttpValueCollection(query);
35         }
36 
37         [Serializable]
38         internal class HttpValueCollection : NameValueCollection
39         {
40             [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "Ported from WCF")]
HttpValueCollection(string str)41             internal HttpValueCollection(string str)
42                 : base(StringComparer.OrdinalIgnoreCase)
43             {
44                 if (!string.IsNullOrEmpty(str))
45                 {
46                     FillFromString(str, true);
47                 }
48 
49                 IsReadOnly = false;
50             }
51 
HttpValueCollection(SerializationInfo info, StreamingContext context)52             protected HttpValueCollection(SerializationInfo info, StreamingContext context)
53                 : base(info, context)
54             {
55             }
56 
ToString()57             public override string ToString()
58             {
59                 return ToString(true, null);
60             }
61 
FillFromString(string s, bool urlencoded)62             internal void FillFromString(string s, bool urlencoded)
63             {
64                 int l = (s != null) ? s.Length : 0;
65                 int i = 0;
66 
67                 while (i < l)
68                 {
69                     ThrowIfMaxHttpCollectionKeysExceeded();
70 
71                     // find next & while noting first = on the way (and if there are more)
72                     int si = i;
73                     int ti = -1;
74 
75                     while (i < l)
76                     {
77                         char ch = s[i];
78 
79                         if (ch == '=')
80                         {
81                             if (ti < 0)
82                             {
83                                 ti = i;
84                             }
85                         }
86                         else if (ch == '&')
87                         {
88                             break;
89                         }
90 
91                         i++;
92                     }
93 
94                     // extract the name / value pair
95                     string name = string.Empty;
96                     string value = string.Empty;
97 
98                     if (ti >= 0)
99                     {
100                         name = s.Substring(si, ti - si);
101                         value = s.Substring(ti + 1, i - ti - 1);
102                     }
103                     else
104                     {
105                         value = s.Substring(si, i - si);
106                     }
107 
108                     // add name / value pair to the collection
109                     if (urlencoded)
110                     {
111                         Add(UriQueryUtility.UrlDecode(name), UriQueryUtility.UrlDecode(value));
112                     }
113                     else
114                     {
115                         Add(name, value);
116                     }
117 
118                     // trailing '&'
119                     if (i == l - 1 && s[i] == '&')
120                     {
121                         Add(string.Empty, string.Empty);
122                     }
123 
124                     i++;
125                 }
126             }
127 
ToString(bool urlencoded, IDictionary excludeKeys)128             string ToString(bool urlencoded, IDictionary excludeKeys)
129             {
130                 int n = Count;
131                 if (n == 0)
132                 {
133                     return string.Empty;
134                 }
135 
136                 StringBuilder s = new StringBuilder();
137                 string key, keyPrefix, item;
138 
139                 for (int i = 0; i < n; i++)
140                 {
141                     key = GetKey(i);
142 
143                     if (excludeKeys != null && key != null && excludeKeys[key] != null)
144                     {
145                         continue;
146                     }
147 
148                     if (urlencoded)
149                     {
150                         key = UriQueryUtility.UrlEncode(key);
151                     }
152 
153                     keyPrefix = (!string.IsNullOrEmpty(key)) ? (key + "=") : string.Empty;
154 
155                     ArrayList values = (ArrayList)BaseGet(i);
156                     int numValues = (values != null) ? values.Count : 0;
157 
158                     if (s.Length > 0)
159                     {
160                         s.Append('&');
161                     }
162 
163                     if (numValues == 1)
164                     {
165                         s.Append(keyPrefix);
166                         item = (string)values[0];
167                         if (urlencoded)
168                         {
169                             item = UriQueryUtility.UrlEncode(item);
170                         }
171 
172                         s.Append(item);
173                     }
174                     else if (numValues == 0)
175                     {
176                         s.Append(keyPrefix);
177                     }
178                     else
179                     {
180                         for (int j = 0; j < numValues; j++)
181                         {
182                             if (j > 0)
183                             {
184                                 s.Append('&');
185                             }
186 
187                             s.Append(keyPrefix);
188                             item = (string)values[j];
189                             if (urlencoded)
190                             {
191                                 item = UriQueryUtility.UrlEncode(item);
192                             }
193 
194                             s.Append(item);
195                         }
196                     }
197                 }
198 
199                 return s.ToString();
200             }
201 
ThrowIfMaxHttpCollectionKeysExceeded()202             private void ThrowIfMaxHttpCollectionKeysExceeded()
203             {
204                 if (Count >= MediaTypeFormatter.MaxHttpCollectionKeys)
205                 {
206                     throw Error.InvalidOperation(SRResources.MaxHttpCollectionKeyLimitReached, MediaTypeFormatter.MaxHttpCollectionKeys, typeof(MediaTypeFormatter));
207                 }
208             }
209         }
210 
211         // The implementation below is ported from WebUtility for use in .Net 4
212 
213         #region UrlEncode implementation
214 
UrlEncode(byte[] bytes, int offset, int count, bool alwaysCreateNewReturnValue)215         private static byte[] UrlEncode(byte[] bytes, int offset, int count, bool alwaysCreateNewReturnValue)
216         {
217             byte[] encoded = UrlEncode(bytes, offset, count);
218 
219             return (alwaysCreateNewReturnValue && (encoded != null) && (encoded == bytes))
220                 ? (byte[])encoded.Clone()
221                 : encoded;
222         }
223 
UrlEncode(byte[] bytes, int offset, int count)224         private static byte[] UrlEncode(byte[] bytes, int offset, int count)
225         {
226             if (!ValidateUrlEncodingParameters(bytes, offset, count))
227             {
228                 return null;
229             }
230 
231             int cSpaces = 0;
232             int cUnsafe = 0;
233 
234             // count them first
235             for (int i = 0; i < count; i++)
236             {
237                 char ch = (char)bytes[offset + i];
238 
239                 if (ch == ' ')
240                     cSpaces++;
241                 else if (!IsUrlSafeChar(ch))
242                     cUnsafe++;
243             }
244 
245             // nothing to expand?
246             if (cSpaces == 0 && cUnsafe == 0)
247                 return bytes;
248 
249             // expand not 'safe' characters into %XX, spaces to +s
250             byte[] expandedBytes = new byte[count + cUnsafe * 2];
251             int pos = 0;
252 
253             for (int i = 0; i < count; i++)
254             {
255                 byte b = bytes[offset + i];
256                 char ch = (char)b;
257 
258                 if (IsUrlSafeChar(ch))
259                 {
260                     expandedBytes[pos++] = b;
261                 }
262                 else if (ch == ' ')
263                 {
264                     expandedBytes[pos++] = (byte)'+';
265                 }
266                 else
267                 {
268                     expandedBytes[pos++] = (byte)'%';
269                     expandedBytes[pos++] = (byte)IntToHex((b >> 4) & 0xf);
270                     expandedBytes[pos++] = (byte)IntToHex(b & 0x0f);
271                 }
272             }
273 
274             return expandedBytes;
275         }
276 
277         #endregion
278 
279         #region UrlEncode public methods
280 
UrlEncode(string str)281         public static string UrlEncode(string str)
282         {
283             if (str == null)
284                 return null;
285 
286             byte[] bytes = Encoding.UTF8.GetBytes(str);
287             return Encoding.ASCII.GetString(UrlEncode(bytes, 0, bytes.Length, false /* alwaysCreateNewReturnValue */));
288         }
289 
UrlEncodeToBytes(byte[] bytes, int offset, int count)290         public static byte[] UrlEncodeToBytes(byte[] bytes, int offset, int count)
291         {
292             return UrlEncode(bytes, offset, count, true /* alwaysCreateNewReturnValue */);
293         }
294 
295         #endregion
296 
297         #region UrlDecode implementation
298 
UrlDecodeInternal(string value, Encoding encoding)299         private static string UrlDecodeInternal(string value, Encoding encoding)
300         {
301             if (value == null)
302             {
303                 return null;
304             }
305 
306             int count = value.Length;
307             UrlDecoder helper = new UrlDecoder(count, encoding);
308 
309             // go through the string's chars collapsing %XX and %uXXXX and
310             // appending each char as char, with exception of %XX constructs
311             // that are appended as bytes
312 
313             for (int pos = 0; pos < count; pos++)
314             {
315                 char ch = value[pos];
316 
317                 if (ch == '+')
318                 {
319                     ch = ' ';
320                 }
321                 else if (ch == '%' && pos < count - 2)
322                 {
323                     if (value[pos + 1] == 'u' && pos < count - 5)
324                     {
325                         int h1 = HexToInt(value[pos + 2]);
326                         int h2 = HexToInt(value[pos + 3]);
327                         int h3 = HexToInt(value[pos + 4]);
328                         int h4 = HexToInt(value[pos + 5]);
329 
330                         if (h1 >= 0 && h2 >= 0 && h3 >= 0 && h4 >= 0)
331                         {   // valid 4 hex chars
332                             ch = (char)((h1 << 12) | (h2 << 8) | (h3 << 4) | h4);
333                             pos += 5;
334 
335                             // only add as char
336                             helper.AddChar(ch);
337                             continue;
338                         }
339                     }
340                     else
341                     {
342                         int h1 = HexToInt(value[pos + 1]);
343                         int h2 = HexToInt(value[pos + 2]);
344 
345                         if (h1 >= 0 && h2 >= 0)
346                         {     // valid 2 hex chars
347                             byte b = (byte)((h1 << 4) | h2);
348                             pos += 2;
349 
350                             // don't add as char
351                             helper.AddByte(b);
352                             continue;
353                         }
354                     }
355                 }
356 
357                 if ((ch & 0xFF80) == 0)
358                     helper.AddByte((byte)ch); // 7 bit have to go as bytes because of Unicode
359                 else
360                     helper.AddChar(ch);
361             }
362 
363             return helper.GetString();
364         }
365 
UrlDecodeInternal(byte[] bytes, int offset, int count)366         private static byte[] UrlDecodeInternal(byte[] bytes, int offset, int count)
367         {
368             if (!ValidateUrlEncodingParameters(bytes, offset, count))
369             {
370                 return null;
371             }
372 
373             int decodedBytesCount = 0;
374             byte[] decodedBytes = new byte[count];
375 
376             for (int i = 0; i < count; i++)
377             {
378                 int pos = offset + i;
379                 byte b = bytes[pos];
380 
381                 if (b == '+')
382                 {
383                     b = (byte)' ';
384                 }
385                 else if (b == '%' && i < count - 2)
386                 {
387                     int h1 = HexToInt((char)bytes[pos + 1]);
388                     int h2 = HexToInt((char)bytes[pos + 2]);
389 
390                     if (h1 >= 0 && h2 >= 0)
391                     {     // valid 2 hex chars
392                         b = (byte)((h1 << 4) | h2);
393                         i += 2;
394                     }
395                 }
396 
397                 decodedBytes[decodedBytesCount++] = b;
398             }
399 
400             if (decodedBytesCount < decodedBytes.Length)
401             {
402                 byte[] newDecodedBytes = new byte[decodedBytesCount];
403                 Array.Copy(decodedBytes, newDecodedBytes, decodedBytesCount);
404                 decodedBytes = newDecodedBytes;
405             }
406 
407             return decodedBytes;
408         }
409 
410         #endregion
411 
412         #region UrlDecode public methods
413 
UrlDecode(string str)414         public static string UrlDecode(string str)
415         {
416             if (str == null)
417                 return null;
418 
419             return UrlDecodeInternal(str, Encoding.UTF8);
420         }
421 
UrlDecodeToBytes(byte[] bytes, int offset, int count)422         public static byte[] UrlDecodeToBytes(byte[] bytes, int offset, int count)
423         {
424             return UrlDecodeInternal(bytes, offset, count);
425         }
426 
427         #endregion
428 
429         #region Helper methods
430 
HexToInt(char h)431         private static int HexToInt(char h)
432         {
433             return (h >= '0' && h <= '9') ? h - '0' :
434             (h >= 'a' && h <= 'f') ? h - 'a' + 10 :
435             (h >= 'A' && h <= 'F') ? h - 'A' + 10 :
436             -1;
437         }
438 
IntToHex(int n)439         private static char IntToHex(int n)
440         {
441             Contract.Assert(n < 0x10);
442 
443             if (n <= 9)
444                 return (char)(n + (int)'0');
445             else
446                 return (char)(n - 10 + (int)'a');
447         }
448 
449         // Set of safe chars, from RFC 1738.4 minus '+'
IsUrlSafeChar(char ch)450         private static bool IsUrlSafeChar(char ch)
451         {
452             if (ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' || ch >= '0' && ch <= '9')
453                 return true;
454 
455             switch (ch)
456             {
457                 case '-':
458                 case '_':
459                 case '.':
460                 case '!':
461                 case '*':
462                 case '(':
463                 case ')':
464                     return true;
465             }
466 
467             return false;
468         }
469 
ValidateUrlEncodingParameters(byte[] bytes, int offset, int count)470         private static bool ValidateUrlEncodingParameters(byte[] bytes, int offset, int count)
471         {
472             if (bytes == null && count == 0)
473                 return false;
474             if (bytes == null)
475             {
476                 throw new ArgumentNullException("bytes");
477             }
478             if (offset < 0 || offset > bytes.Length)
479             {
480                 throw new ArgumentOutOfRangeException("offset");
481             }
482             if (count < 0 || offset + count > bytes.Length)
483             {
484                 throw new ArgumentOutOfRangeException("count");
485             }
486 
487             return true;
488         }
489 
490         #endregion
491 
492         #region UrlDecoder nested class
493 
494         // Internal class to facilitate URL decoding -- keeps char buffer and byte buffer, allows appending of either chars or bytes
495         private class UrlDecoder
496         {
497             private int _bufferSize;
498 
499             // Accumulate characters in a special array
500             private int _numChars;
501             private char[] _charBuffer;
502 
503             // Accumulate bytes for decoding into characters in a special array
504             private int _numBytes;
505             private byte[] _byteBuffer;
506 
507             // Encoding to convert chars to bytes
508             private Encoding _encoding;
509 
FlushBytes()510             private void FlushBytes()
511             {
512                 if (_numBytes > 0)
513                 {
514                     _numChars += _encoding.GetChars(_byteBuffer, 0, _numBytes, _charBuffer, _numChars);
515                     _numBytes = 0;
516                 }
517             }
518 
UrlDecoder(int bufferSize, Encoding encoding)519             internal UrlDecoder(int bufferSize, Encoding encoding)
520             {
521                 _bufferSize = bufferSize;
522                 _encoding = encoding;
523 
524                 _charBuffer = new char[bufferSize];
525                 // byte buffer created on demand
526             }
527 
AddChar(char ch)528             internal void AddChar(char ch)
529             {
530                 if (_numBytes > 0)
531                     FlushBytes();
532 
533                 _charBuffer[_numChars++] = ch;
534             }
535 
AddByte(byte b)536             internal void AddByte(byte b)
537             {
538                 if (_byteBuffer == null)
539                     _byteBuffer = new byte[_bufferSize];
540 
541                 _byteBuffer[_numBytes++] = b;
542             }
543 
GetString()544             internal String GetString()
545             {
546                 if (_numBytes > 0)
547                     FlushBytes();
548 
549                 if (_numChars > 0)
550                     return new String(_charBuffer, 0, _numChars);
551                 else
552                     return String.Empty;
553             }
554         }
555 
556         #endregion
557     }
558 }