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