1 /** 2 Contains HTML/urlencoded form parsing and construction routines. 3 4 Copyright: © 2012-2014 RejectedSoftware e.K. 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Sönke Ludwig, Jan Krüger 7 */ 8 module vibe.inet.webform; 9 10 import vibe.core.file; 11 import vibe.core.log; 12 import vibe.core.path; 13 import vibe.inet.message; 14 import vibe.stream.operations; 15 import vibe.textfilter.urlencode; 16 import vibe.utils.string; 17 import vibe.utils.dictionarylist; 18 import std.range : isOutputRange; 19 import std.traits : ValueType, KeyType; 20 21 import std.array; 22 import std.exception; 23 import std.string; 24 25 26 /** 27 Parses form data according to an HTTP Content-Type header. 28 29 Writes the form fields into a key-value of type $(D FormFields), parsed from the 30 specified $(D InputStream) and using the corresponding Content-Type header. Parsing 31 is gracefully aborted if the Content-Type header is unrelated. 32 33 Params: 34 fields = The key-value map to which form fields must be written 35 files = The $(D FilePart)s mapped to the corresponding key in which details on 36 transmitted files will be written to. 37 content_type = The value of the Content-Type HTTP header. 38 body_reader = A valid $(D InputSteram) data stream consumed by the parser. 39 max_line_length = The byte-sized maximum length of lines used as boundary delimitors in Multi-Part forms. 40 */ 41 bool parseFormData(ref FormFields fields, ref FilePartFormFields files, string content_type, InputStream body_reader, size_t max_line_length) 42 @safe { 43 auto ct_entries = content_type.split(";"); 44 if (!ct_entries.length) return false; 45 46 switch (ct_entries[0].strip()) { 47 default: 48 return false; 49 case "application/x-www-form-urlencoded": 50 parseURLEncodedForm(body_reader.readAllUTF8(), fields); 51 break; 52 case "multipart/form-data": 53 parseMultiPartForm(fields, files, content_type, body_reader, max_line_length); 54 break; 55 } 56 return false; 57 } 58 59 /** 60 Parses a URL encoded form and stores the key/value pairs. 61 62 Writes to the $(D FormFields) the key-value map associated to an 63 "application/x-www-form-urlencoded" MIME formatted string, ie. all '+' 64 characters are considered as ' ' spaces. 65 */ 66 void parseURLEncodedForm(string str, ref FormFields params) 67 @safe { 68 while (str.length > 0) { 69 // name part 70 auto idx = str.indexOf("="); 71 if (idx == -1) { 72 idx = vibe.utils..string.indexOfAny(str, "&;"); 73 if (idx == -1) { 74 params.addField(formDecode(str[0 .. $]), ""); 75 return; 76 } else { 77 params.addField(formDecode(str[0 .. idx]), ""); 78 str = str[idx+1 .. $]; 79 continue; 80 } 81 } else { 82 auto idx_amp = vibe.utils..string.indexOfAny(str, "&;"); 83 if (idx_amp > -1 && idx_amp < idx) { 84 params.addField(formDecode(str[0 .. idx_amp]), ""); 85 str = str[idx_amp+1 .. $]; 86 continue; 87 } else { 88 string name = formDecode(str[0 .. idx]); 89 str = str[idx+1 .. $]; 90 // value part 91 for( idx = 0; idx < str.length && str[idx] != '&' && str[idx] != ';'; idx++) {} 92 string value = formDecode(str[0 .. idx]); 93 params.addField(name, value); 94 str = idx < str.length ? str[idx+1 .. $] : null; 95 } 96 } 97 } 98 } 99 100 /** 101 This example demonstrates parsing using all known form separators, it builds 102 a key-value map into the destination $(D FormFields) 103 */ 104 unittest 105 { 106 FormFields dst; 107 parseURLEncodedForm("a=b;c;dee=asd&e=fgh&f=j%20l", dst); 108 assert("a" in dst && dst["a"] == "b"); 109 assert("c" in dst && dst["c"] == ""); 110 assert("dee" in dst && dst["dee"] == "asd"); 111 assert("e" in dst && dst["e"] == "fgh"); 112 assert("f" in dst && dst["f"] == "j l"); 113 } 114 115 116 /** 117 Parses a form in "multipart/form-data" format. 118 119 If any files are contained in the form, they are written to temporary files using 120 $(D vibe.core.file.createTempFile) and their details returned in the files field. 121 122 Params: 123 fields = The key-value map to which form fields must be written 124 files = The $(D FilePart)s mapped to the corresponding key in which details on 125 transmitted files will be written to. 126 content_type = The value of the Content-Type HTTP header. 127 body_reader = A valid $(D InputSteram) data stream consumed by the parser. 128 max_line_length = The byte-sized maximum length of lines used as boundary delimitors in Multi-Part forms. 129 */ 130 void parseMultiPartForm(InputStream)(ref FormFields fields, ref FilePartFormFields files, 131 string content_type, InputStream body_reader, size_t max_line_length) 132 if (isInputStream!InputStream) 133 { 134 import std.algorithm : strip; 135 136 auto pos = content_type.indexOf("boundary="); 137 enforce(pos >= 0 , "no boundary for multipart form found"); 138 auto boundary = content_type[pos+9 .. $].strip('"'); 139 auto firstBoundary = () @trusted { return cast(string)body_reader.readLine(max_line_length); } (); 140 enforce(firstBoundary == "--" ~ boundary, "Invalid multipart form data!"); 141 142 while (parseMultipartFormPart(body_reader, fields, files, cast(const(ubyte)[])("\r\n--" ~ boundary), max_line_length)) {} 143 } 144 145 alias FormFields = DictionaryList!(string, true, 16); 146 alias FilePartFormFields = DictionaryList!(FilePart, true, 0); 147 148 @safe unittest 149 { 150 import vibe.stream.memory; 151 152 auto content_type = "multipart/form-data; boundary=\"AaB03x\""; 153 154 auto input = createMemoryStream(cast(ubyte[])( 155 "--AaB03x\r\n" ~ 156 "Content-Disposition: form-data; name=\"submit-name\"\r\n" ~ 157 "\r\n" ~ 158 "Larry\r\n" ~ 159 "--AaB03x\r\n" ~ 160 "Content-Disposition: form-data; name=\"files\"; filename=\"file1.txt\"\r\n" ~ 161 "Content-Type: text/plain\r\n" ~ 162 "\r\n" ~ 163 "... contents of file1.txt ...\r\n" ~ 164 "--AaB03x--\r\n").dup, false); 165 166 FormFields fields; 167 FilePartFormFields files; 168 169 parseMultiPartForm(fields, files, content_type, input, 4096); 170 171 assert(fields["submit-name"] == "Larry"); 172 assert(files["files"].filename == "file1.txt"); 173 } 174 175 unittest { // issue #1220 - wrong handling of Content-Length 176 import vibe.stream.memory; 177 178 auto content_type = "multipart/form-data; boundary=\"AaB03x\""; 179 180 auto input = createMemoryStream(cast(ubyte[])( 181 "--AaB03x\r\n" ~ 182 "Content-Disposition: form-data; name=\"submit-name\"\r\n" ~ 183 "\r\n" ~ 184 "Larry\r\n" ~ 185 "--AaB03x\r\n" ~ 186 "Content-Disposition: form-data; name=\"files\"; filename=\"file1.txt\"\r\n" ~ 187 "Content-Type: text/plain\r\n" ~ 188 "Content-Length: 29\r\n" ~ 189 "\r\n" ~ 190 "... contents of file1.txt ...\r\n" ~ 191 "--AaB03x--\r\n" ~ 192 "Content-Disposition: form-data; name=\"files\"; filename=\"file2.txt\"\r\n" ~ 193 "Content-Type: text/plain\r\n" ~ 194 "\r\n" ~ 195 "... contents of file1.txt ...\r\n" ~ 196 "--AaB03x--\r\n").dup, false); 197 198 FormFields fields; 199 FilePartFormFields files; 200 201 parseMultiPartForm(fields, files, content_type, input, 4096); 202 203 assert(fields["submit-name"] == "Larry"); 204 assert(files["files"].filename == "file1.txt"); 205 } 206 207 unittest { // use of unquoted strings in Content-Disposition 208 import vibe.stream.memory; 209 210 auto content_type = "multipart/form-data; boundary=\"AaB03x\""; 211 212 auto input = createMemoryStream(cast(ubyte[])( 213 "--AaB03x\r\n" ~ 214 "Content-Disposition: form-data; name=submitname\r\n" ~ 215 "\r\n" ~ 216 "Larry\r\n" ~ 217 "--AaB03x\r\n" ~ 218 "Content-Disposition: form-data; name=files; filename=file1.txt\r\n" ~ 219 "Content-Type: text/plain\r\n" ~ 220 "Content-Length: 29\r\n" ~ 221 "\r\n" ~ 222 "... contents of file1.txt ...\r\n" ~ 223 "--AaB03x--\r\n").dup, false); 224 225 FormFields fields; 226 FilePartFormFields files; 227 228 parseMultiPartForm(fields, files, content_type, input, 4096); 229 230 assert(fields["submitname"] == "Larry"); 231 assert(files["files"].filename == "file1.txt"); 232 } 233 234 /** 235 Single part of a multipart form. 236 237 A FilePart is the data structure for individual "multipart/form-data" parts 238 according to RFC 1867 section 7. 239 */ 240 struct FilePart { 241 InetHeaderMap headers; 242 NativePath.Segment filename; 243 NativePath tempPath; 244 } 245 246 247 private bool parseMultipartFormPart(InputStream)(InputStream stream, ref FormFields form, ref FilePartFormFields files, const(ubyte)[] boundary, size_t max_line_length) 248 if (isInputStream!InputStream) 249 { 250 //find end of quoted string 251 auto indexOfQuote(string str) { 252 foreach (i, ch; str) { 253 if (ch == '"' && (i == 0 || str[i-1] != '\\')) return i; 254 } 255 return -1; 256 } 257 258 auto parseValue(ref string str) { 259 string res; 260 if (str[0]=='"') { 261 str = str[1..$]; 262 auto pos = indexOfQuote(str); 263 res = str[0..pos].replace(`\"`, `"`); 264 str = str[pos..$]; 265 } 266 else { 267 auto pos = str.indexOf(';'); 268 if (pos < 0) { 269 res = str; 270 str = ""; 271 } else { 272 res = str[0 .. pos]; 273 str = str[pos..$]; 274 } 275 } 276 277 return res; 278 } 279 280 InetHeaderMap headers; 281 stream.parseRFC5322Header(headers); 282 auto pv = "Content-Disposition" in headers; 283 enforce(pv, "invalid multipart"); 284 auto cd = *pv; 285 string name; 286 auto pos = cd.indexOf("name="); 287 if (pos >= 0) { 288 cd = cd[pos+5 .. $]; 289 name = parseValue(cd); 290 } 291 string filename; 292 pos = cd.indexOf("filename="); 293 if (pos >= 0) { 294 cd = cd[pos+9 .. $]; 295 filename = parseValue(cd); 296 } 297 298 if (filename.length > 0) { 299 FilePart fp; 300 fp.headers = headers; 301 version (Have_vibe_core) 302 fp.filename = NativePath.Segment(filename); 303 else 304 fp.filename = PathEntry.validateFilename(filename); 305 306 auto file = createTempFile(); 307 fp.tempPath = file.path; 308 if (auto plen = "Content-Length" in headers) { 309 import std.conv : to; 310 stream.pipe(file, (*plen).to!long); 311 enforce(stream.skipBytes(boundary), "Missing multi-part end boundary marker."); 312 } else stream.readUntil(file, boundary); 313 logDebug("file: %s", fp.tempPath.toString()); 314 file.close(); 315 316 files.addField(name, fp); 317 318 // TODO: temp files must be deleted after the request has been processed! 319 } else { 320 auto data = () @trusted { return cast(string)stream.readUntil(boundary); } (); 321 form.addField(name, data); 322 } 323 324 ubyte[2] ub; 325 stream.read(ub, IOMode.all); 326 if (ub == "--") 327 { 328 stream.pipe(nullSink()); 329 return false; 330 } 331 enforce(ub == cast(const(ubyte)[])"\r\n"); 332 return true; 333 } 334 335 /** 336 Encodes a Key-Value map into a form URL encoded string. 337 338 Writes to the $(D OutputRange) an application/x-www-form-urlencoded MIME formatted string, 339 ie. all spaces ' ' are replaced by the '+' character 340 341 Params: 342 dst = The destination $(D OutputRange) where the resulting string must be written to. 343 map = An iterable key-value map iterable with $(D foreach(string key, string value; map)). 344 sep = A valid form separator, common values are '&' or ';' 345 */ 346 void formEncode(R, T)(auto ref R dst, T map, char sep = '&') 347 if (isFormMap!T && isOutputRange!(R, char)) 348 { 349 formEncodeImpl(dst, map, sep, true); 350 } 351 352 /** 353 The following example demonstrates the use of $(D formEncode) with a json map, 354 the ordering of keys will be preserved in $(D Bson) and $(D DictionaryList) objects only. 355 */ 356 unittest { 357 import std.array : Appender; 358 string[string] map; 359 map["numbers"] = "123456789"; 360 map["spaces"] = "1 2 3 4 a b c d"; 361 362 Appender!string app; 363 app.formEncode(map); 364 assert(app.data == "spaces=1+2+3+4+a+b+c+d&numbers=123456789" || 365 app.data == "numbers=123456789&spaces=1+2+3+4+a+b+c+d"); 366 } 367 368 /** 369 Encodes a Key-Value map into a form URL encoded string. 370 371 Returns an application/x-www-form-urlencoded MIME formatted string, 372 ie. all spaces ' ' are replaced by the '+' character 373 374 Params: 375 map = An iterable key-value map iterable with $(D foreach(string key, string value; map)). 376 sep = A valid form separator, common values are '&' or ';' 377 */ 378 string formEncode(T)(T map, char sep = '&') 379 if (isFormMap!T) 380 { 381 return formEncodeImpl(map, sep, true); 382 } 383 384 /** 385 Writes to the $(D OutputRange) an URL encoded string as specified in RFC 3986 section 2 386 387 Params: 388 dst = The destination $(D OutputRange) where the resulting string must be written to. 389 map = An iterable key-value map iterable with $(D foreach(string key, string value; map)). 390 */ 391 void urlEncode(R, T)(auto ref R dst, T map) 392 if (isFormMap!T && isOutputRange!(R, char)) 393 { 394 formEncodeImpl(dst, map, "&", false); 395 } 396 397 398 /** 399 Returns an URL encoded string as specified in RFC 3986 section 2 400 401 Params: 402 map = An iterable key-value map iterable with $(D foreach(string key, string value; map)). 403 */ 404 string urlEncode(T)(T map) 405 if (isFormMap!T) 406 { 407 return formEncodeImpl(map, '&', false); 408 } 409 410 /** 411 Tests if a given type is suitable for storing a web form. 412 413 Types that define iteration support with the key typed as $(D string) and 414 the value either also typed as $(D string), or as a $(D vibe.data.json.Json) 415 like value. The latter case specifically requires a $(D .type) property that 416 is tested for equality with $(D T.Type.string), as well as a 417 $(D .get!string) method. 418 */ 419 template isFormMap(T) 420 { 421 import std.conv; 422 enum isFormMap = isStringMap!T || isJsonLike!T; 423 } 424 425 private template isStringMap(T) 426 { 427 enum isStringMap = __traits(compiles, () { 428 foreach (string key, string value; T.init) {} 429 } ()); 430 } 431 432 unittest { 433 static assert(isStringMap!(string[string])); 434 435 static struct M { 436 int opApply(int delegate(string key, string value)) { return 0; } 437 } 438 static assert(isStringMap!M); 439 } 440 441 private template isJsonLike(T) 442 { 443 enum isJsonLike = __traits(compiles, () { 444 import std.conv; 445 string r; 446 foreach (string key, value; T.init) 447 r = value.type == T.Type..string ? value.get!string : value.to!string; 448 } ()); 449 } 450 451 unittest { 452 import vibe.data.json; 453 import vibe.data.bson; 454 static assert(isJsonLike!Json); 455 static assert(isJsonLike!Bson); 456 } 457 458 private string formEncodeImpl(T)(T map, char sep, bool form_encode) 459 if (isStringMap!T) 460 { 461 import std.array : Appender; 462 Appender!string dst; 463 size_t len; 464 465 foreach (key, ref value; map) { 466 len += key.length; 467 len += value.length; 468 } 469 470 // characters will be expanded, better use more space the first time and avoid additional allocations 471 dst.reserve(len*2); 472 dst.formEncodeImpl(map, sep, form_encode); 473 return dst.data; 474 } 475 476 477 private string formEncodeImpl(T)(T map, char sep, bool form_encode) 478 if (isJsonLike!T) 479 { 480 import std.array : Appender; 481 Appender!string dst; 482 size_t len; 483 484 foreach (string key, T value; map) { 485 len += key.length; 486 len += value.length; 487 } 488 489 // characters will be expanded, better use more space the first time and avoid additional allocations 490 dst.reserve(len*2); 491 dst.formEncodeImpl(map, sep, form_encode); 492 return dst.data; 493 } 494 495 private void formEncodeImpl(R, T)(auto ref R dst, T map, char sep, bool form_encode) 496 if (isOutputRange!(R, string) && isStringMap!T) 497 { 498 bool flag; 499 500 foreach (key, value; map) { 501 if (flag) 502 dst.put(sep); 503 else 504 flag = true; 505 filterURLEncode(dst, key, null, form_encode); 506 dst.put("="); 507 filterURLEncode(dst, value, null, form_encode); 508 } 509 } 510 511 private void formEncodeImpl(R, T)(auto ref R dst, T map, char sep, bool form_encode) 512 if (isOutputRange!(R, string) && isJsonLike!T) 513 { 514 bool flag; 515 516 foreach (string key, T value; map) { 517 if (flag) 518 dst.put(sep); 519 else 520 flag = true; 521 filterURLEncode(dst, key, null, form_encode); 522 dst.put("="); 523 if (value.type == T.Type..string) 524 filterURLEncode(dst, value.get!string, null, form_encode); 525 else { 526 static if (T.stringof == "Json") 527 filterURLEncode(dst, value.to!string, null, form_encode); 528 else 529 filterURLEncode(dst, value.toString(), null, form_encode); 530 531 } 532 } 533 } 534 535 unittest 536 { 537 import vibe.utils.dictionarylist : DictionaryList; 538 import vibe.data.json : Json; 539 import vibe.data.bson : Bson; 540 541 string[string] aaMap; 542 DictionaryList!string dlMap; 543 Json jsonMap = Json.emptyObject; 544 Bson bsonMap = Bson.emptyObject; 545 546 aaMap["unicode"] = "╤╳"; 547 aaMap["numbers"] = "123456789"; 548 aaMap["spaces"] = "1 2 3 4 a b c d"; 549 aaMap["slashes"] = "1/2/3/4/5"; 550 aaMap["equals"] = "1=2=3=4=5=6=7"; 551 aaMap["complex"] = "╤╳/=$$\"'1!2()'\""; 552 aaMap["╤╳"] = "1"; 553 554 555 dlMap["unicode"] = "╤╳"; 556 dlMap["numbers"] = "123456789"; 557 dlMap["spaces"] = "1 2 3 4 a b c d"; 558 dlMap["slashes"] = "1/2/3/4/5"; 559 dlMap["equals"] = "1=2=3=4=5=6=7"; 560 dlMap["complex"] = "╤╳/=$$\"'1!2()'\""; 561 dlMap["╤╳"] = "1"; 562 563 564 jsonMap["unicode"] = "╤╳"; 565 jsonMap["numbers"] = "123456789"; 566 jsonMap["spaces"] = "1 2 3 4 a b c d"; 567 jsonMap["slashes"] = "1/2/3/4/5"; 568 jsonMap["equals"] = "1=2=3=4=5=6=7"; 569 jsonMap["complex"] = "╤╳/=$$\"'1!2()'\""; 570 jsonMap["╤╳"] = "1"; 571 572 bsonMap["unicode"] = "╤╳"; 573 bsonMap["numbers"] = "123456789"; 574 bsonMap["spaces"] = "1 2 3 4 a b c d"; 575 bsonMap["slashes"] = "1/2/3/4/5"; 576 bsonMap["equals"] = "1=2=3=4=5=6=7"; 577 bsonMap["complex"] = "╤╳/=$$\"'1!2()'\""; 578 bsonMap["╤╳"] = "1"; 579 580 assert(urlEncode(aaMap) == "complex=%E2%95%A4%E2%95%B3%2F%3D%24%24%22%271%212%28%29%27%22&unicode=%E2%95%A4%E2%95%B3&spaces=1%202%203%204%20a%20b%20c%20d&numbers=123456789&slashes=1%2F2%2F3%2F4%2F5&equals=1%3D2%3D3%3D4%3D5%3D6%3D7&%E2%95%A4%E2%95%B3=1" || 581 urlEncode(aaMap) == "slashes=1%2F2%2F3%2F4%2F5&spaces=1%202%203%204%20a%20b%20c%20d&equals=1%3D2%3D3%3D4%3D5%3D6%3D7&%E2%95%A4%E2%95%B3=1&unicode=%E2%95%A4%E2%95%B3&numbers=123456789&complex=%E2%95%A4%E2%95%B3%2F%3D%24%24%22%271%212%28%29%27%22"); 582 assert(urlEncode(dlMap) == "unicode=%E2%95%A4%E2%95%B3&numbers=123456789&spaces=1%202%203%204%20a%20b%20c%20d&slashes=1%2F2%2F3%2F4%2F5&equals=1%3D2%3D3%3D4%3D5%3D6%3D7&complex=%E2%95%A4%E2%95%B3%2F%3D%24%24%22%271%212%28%29%27%22&%E2%95%A4%E2%95%B3=1"); 583 assert(urlEncode(jsonMap) == "%E2%95%A4%E2%95%B3=1&equals=1%3D2%3D3%3D4%3D5%3D6%3D7&unicode=%E2%95%A4%E2%95%B3&spaces=1%202%203%204%20a%20b%20c%20d&numbers=123456789&complex=%E2%95%A4%E2%95%B3%2F%3D%24%24%22%271%212%28%29%27%22&slashes=1%2F2%2F3%2F4%2F5" || 584 urlEncode(jsonMap) == "slashes=1%2F2%2F3%2F4%2F5&spaces=1%202%203%204%20a%20b%20c%20d&equals=1%3D2%3D3%3D4%3D5%3D6%3D7&%E2%95%A4%E2%95%B3=1&unicode=%E2%95%A4%E2%95%B3&numbers=123456789&complex=%E2%95%A4%E2%95%B3%2F%3D%24%24%22%271%212%28%29%27%22"); 585 assert(urlEncode(bsonMap) == "unicode=%E2%95%A4%E2%95%B3&numbers=123456789&spaces=1%202%203%204%20a%20b%20c%20d&slashes=1%2F2%2F3%2F4%2F5&equals=1%3D2%3D3%3D4%3D5%3D6%3D7&complex=%E2%95%A4%E2%95%B3%2F%3D%24%24%22%271%212%28%29%27%22&%E2%95%A4%E2%95%B3=1"); 586 { 587 FormFields aaFields; 588 parseURLEncodedForm(urlEncode(aaMap), aaFields); 589 assert(urlEncode(aaMap) == urlEncode(aaFields)); 590 591 FormFields dlFields; 592 parseURLEncodedForm(urlEncode(dlMap), dlFields); 593 assert(urlEncode(dlMap) == urlEncode(dlFields)); 594 595 FormFields jsonFields; 596 parseURLEncodedForm(urlEncode(jsonMap), jsonFields); 597 assert(urlEncode(jsonMap) == urlEncode(jsonFields)); 598 599 FormFields bsonFields; 600 parseURLEncodedForm(urlEncode(bsonMap), bsonFields); 601 assert(urlEncode(bsonMap) == urlEncode(bsonFields)); 602 } 603 604 assert(formEncode(aaMap) == "complex=%E2%95%A4%E2%95%B3%2F%3D%24%24%22%271%212%28%29%27%22&unicode=%E2%95%A4%E2%95%B3&spaces=1+2+3+4+a+b+c+d&numbers=123456789&slashes=1%2F2%2F3%2F4%2F5&equals=1%3D2%3D3%3D4%3D5%3D6%3D7&%E2%95%A4%E2%95%B3=1" || 605 formEncode(aaMap) == "slashes=1%2F2%2F3%2F4%2F5&spaces=1+2+3+4+a+b+c+d&equals=1%3D2%3D3%3D4%3D5%3D6%3D7&%E2%95%A4%E2%95%B3=1&unicode=%E2%95%A4%E2%95%B3&numbers=123456789&complex=%E2%95%A4%E2%95%B3%2F%3D%24%24%22%271%212%28%29%27%22"); 606 assert(formEncode(dlMap) == "unicode=%E2%95%A4%E2%95%B3&numbers=123456789&spaces=1+2+3+4+a+b+c+d&slashes=1%2F2%2F3%2F4%2F5&equals=1%3D2%3D3%3D4%3D5%3D6%3D7&complex=%E2%95%A4%E2%95%B3%2F%3D%24%24%22%271%212%28%29%27%22&%E2%95%A4%E2%95%B3=1"); 607 assert(formEncode(jsonMap) == "%E2%95%A4%E2%95%B3=1&equals=1%3D2%3D3%3D4%3D5%3D6%3D7&unicode=%E2%95%A4%E2%95%B3&spaces=1+2+3+4+a+b+c+d&numbers=123456789&complex=%E2%95%A4%E2%95%B3%2F%3D%24%24%22%271%212%28%29%27%22&slashes=1%2F2%2F3%2F4%2F5" || 608 formEncode(jsonMap) == "slashes=1%2F2%2F3%2F4%2F5&spaces=1+2+3+4+a+b+c+d&equals=1%3D2%3D3%3D4%3D5%3D6%3D7&%E2%95%A4%E2%95%B3=1&unicode=%E2%95%A4%E2%95%B3&numbers=123456789&complex=%E2%95%A4%E2%95%B3%2F%3D%24%24%22%271%212%28%29%27%22"); 609 assert(formEncode(bsonMap) == "unicode=%E2%95%A4%E2%95%B3&numbers=123456789&spaces=1+2+3+4+a+b+c+d&slashes=1%2F2%2F3%2F4%2F5&equals=1%3D2%3D3%3D4%3D5%3D6%3D7&complex=%E2%95%A4%E2%95%B3%2F%3D%24%24%22%271%212%28%29%27%22&%E2%95%A4%E2%95%B3=1"); 610 611 { 612 FormFields aaFields; 613 parseURLEncodedForm(formEncode(aaMap), aaFields); 614 assert(formEncode(aaMap) == formEncode(aaFields)); 615 616 FormFields dlFields; 617 parseURLEncodedForm(formEncode(dlMap), dlFields); 618 assert(formEncode(dlMap) == formEncode(dlFields)); 619 620 FormFields jsonFields; 621 parseURLEncodedForm(formEncode(jsonMap), jsonFields); 622 assert(formEncode(jsonMap) == formEncode(jsonFields)); 623 624 FormFields bsonFields; 625 parseURLEncodedForm(formEncode(bsonMap), bsonFields); 626 assert(formEncode(bsonMap) == formEncode(bsonFields)); 627 } 628 629 }