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 }