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 }