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 }