1 module hunt.net.util.HttpURI;
2 
3 import hunt.collection.MultiMap;
4 
5 import hunt.Exceptions;
6 import hunt.text.Charset;
7 import hunt.text.Common;
8 import hunt.util.StringBuilder;
9 import hunt.util.ConverterUtils;
10 import hunt.net.util.UrlEncoded;
11 
12 import std.array;
13 import std.conv;
14 import std.string;
15 
16 import hunt.logging;
17 
18 
19 /**
20  * Http URI. Parse a HTTP URI from a string or byte array. Given a URI
21  * <code>http://user@host:port/path/info;param?query#fragment</code> this class
22  * will split it into the following undecoded optional elements:
23  * <ul>
24  * <li>{@link #getScheme()} - http:</li>
25  * <li>{@link #getAuthority()} - //name@host:port</li>
26  * <li>{@link #getHost()} - host</li>
27  * <li>{@link #getPort()} - port</li>
28  * <li>{@link #getPath()} - /path/info</li>
29  * <li>{@link #getParam()} - param</li>
30  * <li>{@link #getQuery()} - query</li>
31  * <li>{@link #getFragment()} - fragment</li>
32  * </ul>
33  * 
34 	https://bob:bobby@www.lunatech.com:8080/file;p=1?q=2#third
35 	\___/   \_/ \___/ \______________/ \__/\_______/ \_/ \___/
36 	|      |    |          |          |      | \_/  |    |
37 	Scheme User Password    Host       Port  Path |   | Fragment
38 			\_____________________________/       | Query
39 						|               Path parameter
40 					Authority 
41  * <p>
42  * Any parameters will be returned from {@link #getPath()}, but are excluded
43  * from the return value of {@link #getDecodedPath()}. If there are multiple
44  * parameters, the {@link #getParam()} method returns only the last one.
45  * 
46  * See_Also:
47  *	 https://stackoverflow.com/questions/1634271/url-encoding-the-space-character-or-20
48  */
49 class HttpURI {
50 	private enum State {
51 		START, HOST_OR_PATH, SCHEME_OR_PATH, HOST, IPV6, PORT, PATH, PARAM, QUERY, FRAGMENT, ASTERISK
52 	}
53 
54 	private string _scheme;
55 	private string _userInfo;
56 	private string _user;
57 	private string _password;
58 	private string _host;
59 	private int _port;
60 	private string _path;
61 	private string _param;
62 	private string _query;
63 	private string _fragment;
64 	private MultiMap!string _parameters;
65 
66 	string _uri;
67 	string _decodedPath;
68 
69 	/**
70 	 * Construct a normalized URI. Port is not set if it is the default port.
71 	 * 
72 	 * @param scheme
73 	 *            the URI scheme
74 	 * @param host
75 	 *            the URI hose
76 	 * @param port
77 	 *            the URI port
78 	 * @param path
79 	 *            the URI path
80 	 * @param param
81 	 *            the URI param
82 	 * @param query
83 	 *            the URI query
84 	 * @param fragment
85 	 *            the URI fragment
86 	 * @return the normalized URI
87 	 */
88 	static HttpURI createHttpURI(string scheme, string host, int port, string path, string param, string query,
89 			string fragment) {
90 		if (port == 80 && (scheme == "http"))
91 			port = 0;
92 		if (port == 443 && (scheme == "https"))
93 			port = 0;
94 		return new HttpURI(scheme, host, port, path, param, query, fragment);
95 	}
96 
97 	this() {
98 	}
99 
100 	this(string scheme, string host, int port, string path, string param, string query, string fragment) {
101 		_scheme = scheme;
102 		_host = host;
103 		_port = port;
104 		_path = path;
105 		_param = param;
106 		_query = query;
107 		_fragment = fragment;
108 	}
109 
110 	this(HttpURI uri) {
111 		this(uri._scheme, uri._host, uri._port, uri._path, uri._param, uri._query, uri._fragment);
112 		_uri = uri._uri;
113 	}
114 
115 	this(string uri) {
116 		_port = -1;
117 		parse(State.START, uri);
118 	}
119 
120 	this(string scheme, string host, int port, string pathQuery) {
121 		_uri = null;
122 
123 		_scheme = scheme;
124 		_host = host;
125 		_port = port;
126 
127 		parse(State.PATH, pathQuery);
128 
129 	}
130 
131 	void parse(string uri) {
132 		clear();
133 		_uri = uri;
134 		parse(State.START, uri);
135 	}
136 
137 	/**
138 	 * Parse according to https://tools.ietf.org/html/rfc7230#section-5.3
139 	 * 
140 	 * @param method
141 	 *            the request method
142 	 * @param uri
143 	 *            the request uri
144 	 */
145 	void parseRequestTarget(string method, string uri) {
146 		clear();
147 		_uri = uri;
148 
149 		if (method == "CONNECT")
150 			_path = uri;
151 		else
152 			parse(uri.startsWith("/") ? State.PATH : State.START, uri);
153 	}
154 
155 	void parse(string uri, int offset, int length) {
156 		clear();
157 		int end = offset + length;
158 		_uri = uri[offset .. end];
159 		parse(State.START, uri);
160 	}
161 
162 	private void parse(State state, string uri) {
163 		bool encoded = false;
164 		int end = cast(int)uri.length;
165 		int mark = 0;
166 		int path_mark = 0;
167 		char last = '/';
168 		for (int i = 0; i < end; i++) {
169 			char c = uri[i];
170 
171 			final switch (state) {
172 			case State.START: {
173 				switch (c) {
174 				case '/':
175 					mark = i;
176 					state = State.HOST_OR_PATH;
177 					break;
178 				case ';':
179 					mark = i + 1;
180 					state = State.PARAM;
181 					break;
182 				case '?':
183 					// assume empty path (if seen at start)
184 					_path = "";
185 					mark = i + 1;
186 					state = State.QUERY;
187 					break;
188 				case '#':
189 					mark = i + 1;
190 					state = State.FRAGMENT;
191 					break;
192 				case '*':
193 					_path = "*";
194 					state = State.ASTERISK;
195 					break;
196 
197 				case '.':
198 					path_mark = i;
199 					state = State.PATH;
200 					encoded = true;
201 					break;
202 
203 				default:
204 					mark = i;
205 					if (_scheme is null)
206 						state = State.SCHEME_OR_PATH;
207 					else {
208 						path_mark = i;
209 						state = State.PATH;
210 					}
211 					break;
212 				}
213 
214 				continue;
215 			}
216 
217 			case State.SCHEME_OR_PATH: {
218 				switch (c) {
219 				case ':':
220 					// must have been a scheme
221 					_scheme = uri[mark .. i];
222 					// Start again with scheme set
223 					state = State.START;
224 					break;
225 
226 				case '/':
227 					// must have been in a path and still are
228 					state = State.PATH;
229 					break;
230 
231 				case ';':
232 					// must have been in a path
233 					mark = i + 1;
234 					state = State.PARAM;
235 					break;
236 
237 				case '?':
238 					// must have been in a path
239 					_path = uri[mark .. i];
240 					mark = i + 1;
241 					state = State.QUERY;
242 					break;
243 
244 				case '%':
245 					// must have be in an encoded path
246 					encoded = true;
247 					state = State.PATH;
248 					break;
249 
250 				case '#':
251 					// must have been in a path
252 					_path = uri[mark .. i];
253 					state = State.FRAGMENT;
254 					break;
255 
256 				default:
257 					break;
258 				}
259 				continue;
260 			}
261 
262 			case State.HOST_OR_PATH: {
263 				switch (c) {
264 				case '/':
265 					_host = "";
266 					mark = i + 1;
267 					state = State.HOST;
268 					break;
269 
270 				case '@':
271 				case ';':
272 				case '?':
273 				case '#':
274 					// was a path, look again
275 					i--;
276 					path_mark = mark;
277 					state = State.PATH;
278 					break;
279 
280 				case '.':
281 					// it is a path
282 					encoded = true;
283 					path_mark = mark;
284 					state = State.PATH;
285 					break;
286 
287 				default:
288 					// it is a path
289 					path_mark = mark;
290 					state = State.PATH;
291 				}
292 				continue;
293 			}
294 
295 			case State.HOST: {
296 				switch (c) {
297 				case '/':
298 					_host = uri[mark .. i];
299 					path_mark = mark = i;
300 					state = State.PATH;
301 					break;
302 				case ':':
303 					if (i > mark)
304 						_host = uri[mark .. i];
305 					mark = i + 1;
306 					state = State.PORT;
307 					break;
308 				case '@':
309 					if (!_userInfo.empty())
310 						throw new IllegalArgumentException("Bad authority");
311 					_userInfo = uri[mark .. i];
312 					string[] parts = _userInfo.split(":");
313 					if(parts.length>0)
314 						_user = parts[0];
315 					if(parts.length>1)
316 						_password = parts[1];
317 					mark = i + 1;
318 					break;
319 
320 				case '[':
321 					state = State.IPV6;
322 					break;
323 					
324 				default:
325 					break;
326 				}
327 				break;
328 			}
329 
330 			case State.IPV6: {
331 				switch (c) {
332 				case '/':
333 					throw new IllegalArgumentException("No closing ']' for ipv6 in " ~ uri);
334 				case ']':
335 					c = uri.charAt(++i);
336 					_host = uri[mark .. i];
337 					if (c == ':') {
338 						mark = i + 1;
339 						state = State.PORT;
340 					} else {
341 						path_mark = mark = i;
342 						state = State.PATH;
343 					}
344 					break;
345 					
346 				default:
347 					break;
348 				}
349 
350 				break;
351 			}
352 
353 			case State.PORT: {
354 				if (c == '@') {
355 					if (_userInfo !is null)
356 						throw new IllegalArgumentException("Bad authority");
357 					// It wasn't a port, but a password!
358 					_userInfo = _host ~ ":" ~ uri[mark .. i];
359 					string[] parts = _userInfo.split(":");
360 					if(parts.length>0)
361 						_user = parts[0];
362 					if(parts.length>1)
363 						_password = parts[1];
364 
365 					mark = i + 1;
366 					state = State.HOST;
367 				} else if (c == '/') {
368 					// _port = ConverterUtils.parseInt(uri, mark, i - mark, 10);
369 					_port = to!int(uri[mark .. i], 10);
370 					path_mark = mark = i;
371 					state = State.PATH;
372 				}
373 				break;
374 			}
375 
376 			case State.PATH: {
377 				switch (c) {
378 				case ';':
379 					mark = i + 1;
380 					state = State.PARAM;
381 					break;
382 				case '?':
383 					_path = uri[path_mark .. i];
384 					mark = i + 1;
385 					state = State.QUERY;
386 					break;
387 				case '#':
388 					_path = uri[path_mark .. i];
389 					mark = i + 1;
390 					state = State.FRAGMENT;
391 					break;
392 				case '%':
393 					encoded = true;
394 					break;
395 				case '.':
396 					if ('/' == last)
397 						encoded = true;
398 					break;
399 					
400 				default:
401 					break;
402 				}
403 				break;
404 			}
405 
406 			case State.PARAM: {
407 				switch (c) {
408 				case '?':
409 					_path = uri[path_mark .. i];
410 					_param = uri[mark .. i];
411 					mark = i + 1;
412 					state = State.QUERY;
413 					break;
414 				case '#':
415 					_path = uri[path_mark .. i];
416 					_param = uri[mark .. i];
417 					mark = i + 1;
418 					state = State.FRAGMENT;
419 					break;
420 				case '/':
421 					encoded = true;
422 					// ignore internal params
423 					state = State.PATH;
424 					break;
425 				case ';':
426 					// multiple parameters
427 					mark = i + 1;
428 					break;
429 					
430 				default:
431 					break;
432 				}
433 				break;
434 			}
435 
436 			case State.QUERY: {
437 				if (c == '#') {
438 					_query = uri[mark .. i];
439 					mark = i + 1;
440 					state = State.FRAGMENT;
441 				}
442 				break;
443 			}
444 
445 			case State.ASTERISK: {
446 				throw new IllegalArgumentException("Bad character '*'");
447 			}
448 
449 			case State.FRAGMENT: {
450 				_fragment = uri[mark .. end];
451 				i = end;
452 				break;
453 			}
454 			}
455 			last = c;
456 		}
457 
458 		final switch (state) {
459 		case State.START:
460 			break;
461 		case State.SCHEME_OR_PATH:
462 			_path = uri[mark .. end];
463 			break;
464 
465 		case State.HOST_OR_PATH:
466 			_path = uri[mark .. end];
467 			break;
468 
469 		case State.HOST:
470 			if (end > mark)
471 				_host = uri[mark .. end];
472 			break;
473 
474 		case State.IPV6:
475 			throw new IllegalArgumentException("No closing ']' for ipv6 in " ~ uri);
476 
477 		case State.PORT:
478 			// _port = ConverterUtils.parseInt(uri, mark, end - mark, 10);
479 			_port = to!int(uri[mark .. end], 10);
480 			break;
481 
482 		case State.ASTERISK:
483 			break;
484 
485 		case State.FRAGMENT:
486 			_fragment = uri[mark .. end];
487 			break;
488 
489 		case State.PARAM:
490 			_path = uri[path_mark .. end];
491 			_param = uri[mark .. end];
492 			break;
493 
494 		case State.PATH:
495 			_path = uri[path_mark .. end];
496 			break;
497 
498 		case State.QUERY:
499 			_query = uri[mark .. end];
500 			break;
501 		}
502 
503 		if (!encoded) {
504 			if (_param is null)
505 				_decodedPath = _path;
506 			else
507 				_decodedPath = _path[0 .. _path.length - _param.length - 1];
508 		}
509 	}
510 
511 	string getScheme() {
512 		return _scheme;
513 	}
514 
515 	string getHost() {
516 		// Return null for empty host to retain compatibility with java.net.URI
517 		if (_host !is null && _host.length == 0)
518 			return null;
519 		return _host;
520 	}
521 
522 	int getPort() {
523 		return _port;
524 	}
525 
526 	/**
527 	 * The parsed Path.
528 	 * 
529 	 * @return the path as parsed on valid URI. null for invalid URI.
530 	 */
531 	string getPath() {
532 		return _path;
533 	}
534 
535 	string getDecodedPath() {
536 		if (_decodedPath.empty && !_path.empty)
537 			_decodedPath = URIUtils.canonicalPath(URIUtils.decodePath(_path));
538 		return _decodedPath;
539 	}
540 
541 	string getParam() {
542 		return _param;
543 	}
544 
545 	string getQuery() {
546 		return _query;
547 	}
548 
549 	bool hasQuery() {
550 		return _query !is null && _query.length > 0;
551 	}
552 
553 	string getFragment() {
554 		return _fragment;
555 	}
556 
557 	// void decodeQueryTo(MultiMap!string parameters, string encoding = StandardCharsets.UTF_8) {
558 	// 	if (_query == _fragment)
559 	// 		return;
560 
561 	// 	decodeTo(_query, parameters, encoding);
562 	// }
563 
564 	MultiMap!string decodeQuery() {
565 		
566 		if(_parameters is null) {
567 			UrlEncoded urlEncoded = new UrlEncoded();
568 			if (_query != _fragment) {
569 				urlEncoded.decode(_query);
570 			}
571 			_parameters = urlEncoded;
572 		}
573 
574 		return _parameters;
575 	}
576 
577 	void clear() {
578 		_uri = null;
579 
580 		_scheme = null;
581 		_host = null;
582 		_port = -1;
583 		_path = null;
584 		_param = null;
585 		_query = null;
586 		_fragment = null;
587 
588 		_decodedPath = null;
589 	}
590 
591 	bool isAbsolute() {
592 		return _scheme !is null && _scheme.length > 0;
593 	}
594 
595 	override
596 	string toString() {
597 		if (_uri is null) {
598 			StringBuilder ot = new StringBuilder();
599 
600 			if (_scheme !is null)
601 				ot.append(_scheme).append(':');
602 
603 			if (_host !is null) {
604 				ot.append("//");
605 				if (_userInfo !is null)
606 					ot.append(_userInfo).append('@');
607 				ot.append(_host);
608 			}
609 
610 			if (_port > 0)
611 				ot.append(':').append(_port);
612 
613 			if (_path !is null)
614 				ot.append(_path);
615 
616 			if (_query !is null)
617 				ot.append('?').append(_query);
618 
619 			if (_fragment !is null)
620 				ot.append('#').append(_fragment);
621 
622 			if (ot.length > 0)
623 				_uri = ot.toString();
624 			else
625 				_uri = "";
626 		}
627 		return _uri;
628 	}
629 
630 	bool equals(Object o) {
631 		if (o is this)
632 			return true;
633 		if (!(typeid(o) == typeid(HttpURI)))
634 			return false;
635 		return toString().equals(o.toString());
636 	}
637 
638 	void setScheme(string scheme) {
639 		_scheme = scheme;
640 		_uri = null;
641 	}
642 
643 	/**
644 	 * @param host
645 	 *            the host
646 	 * @param port
647 	 *            the port
648 	 */
649 	void setAuthority(string host, int port) {
650 		_host = host;
651 		_port = port;
652 		_uri = null;
653 	}
654 
655 	/**
656 	 * @param path
657 	 *            the path
658 	 */
659 	void setPath(string path) {
660 		_uri = null;
661 		_path = path;
662 		_decodedPath = null;
663 	}
664 
665 	/**
666 	 * @param path
667 	 *            the decoded path
668 	 */
669 	// void setDecodedPath(string path) {
670 	// 	_uri = null;
671 	// 	_path = URIUtils.encodePath(path);
672 	// 	_decodedPath = path;
673 	// }
674 
675 	void setPathQuery(string path) {
676 		_uri = null;
677 		_path = null;
678 		_decodedPath = null;
679 		_param = null;
680 		_fragment = null;
681 		if (!path.empty)
682 			parse(State.PATH, path);
683 	}
684 
685 	void setQuery(string query) {
686 		_query = query;
687 		_uri = null;
688 	}
689 
690 	// URI toURI() {
691 	// 	return new URI(_scheme, null, _host, _port, _path, _query is null ? null : UrlEncoded.decodestring(_query),
692 	// 			_fragment);
693 	// }
694 
695 	string getPathQuery() {
696 		if (_query is null)
697 			return _path;
698 		return _path ~ "?" ~ _query;
699 	}
700 
701 	bool hasAuthority() {
702 		return _host !is null;
703 	}
704 
705 	string getAuthority() {
706 		if (_port > 0)
707 			return _host ~ ":" ~ to!string(_port);
708 		return _host;
709 	}
710 
711 	string getUserInfo() {
712 		return _userInfo;
713 	}
714 
715 	string getUser() {
716 		return _user;
717 	}
718 
719 	string getPassword() {
720 		return _password;
721 	}
722 
723 }
724 
725 
726 /**
727  * Parse an authority string into Host and Port
728  * <p>Parse a string in the form "host:port", handling IPv4 an IPv6 hosts</p>
729  *
730  */
731 class URIUtils
732 {
733 	/* ------------------------------------------------------------ */
734     /* Decode a URI path and strip parameters
735      */
736     static string decodePath(string path) {
737         return decodePath(path, 0, cast(int)path.length);
738     }
739 
740     /* ------------------------------------------------------------ */
741     /* Decode a URI path and strip parameters of UTF-8 path
742      */
743     static string decodePath(string path, int offset, int length) {
744         try {
745             StringBuilder builder = null;
746 
747             int end = offset + length;
748             for (int i = offset; i < end; i++) {
749                 char c = path[i];
750                 switch (c) {
751                     case '%':
752                         if (builder is null) {
753                             builder = new StringBuilder(path.length);
754                             builder.append(path, offset, i - offset);
755                         }
756                         if ((i + 2) < end) {
757                             char u = path.charAt(i + 1);
758                             if (u == 'u') {
759                                 // TODO this is wrong. This is a codepoint not a char
760                                 builder.append(cast(char) (0xffff & ConverterUtils.parseInt(path, i + 2, 4, 16)));
761                                 i += 5;
762                             } else {
763                                 builder.append(cast(byte) (0xff & (ConverterUtils.convertHexDigit(u) * 16 + 
764 									ConverterUtils.convertHexDigit(path.charAt(i + 2)))));
765                                 i += 2;
766                             }
767                         } else {
768                             throw new IllegalArgumentException("Bad URI % encoding");
769                         }
770 
771                         break;
772 
773                     case ';':
774                         if (builder is null) {
775                             builder = new StringBuilder(path.length);
776                             builder.append(path, offset, i - offset);
777                         }
778 
779                         while (++i < end) {
780                             if (path[i] == '/') {
781                                 builder.append('/');
782                                 break;
783                             }
784                         }
785 
786                         break;
787 
788                     default:
789                         if (builder !is null)
790                             builder.append(c);
791                         break;
792                 }
793             }
794 
795             if (builder !is null)
796                 return builder.toString();
797             if (offset == 0 && length == path.length)
798                 return path;
799             return path[offset .. end];
800         } catch (Exception e) {
801             // System.err.println(path.substring(offset, offset + length) ~ " " ~ e);
802 			error(e.toString);
803             return decodeISO88591Path(path, offset, length);
804         }
805     }
806 
807 
808     /* ------------------------------------------------------------ */
809     /* Decode a URI path and strip parameters of ISO-8859-1 path
810      */
811     private static string decodeISO88591Path(string path, int offset, int length) {
812         StringBuilder builder = null;
813         int end = offset + length;
814         for (int i = offset; i < end; i++) {
815             char c = path[i];
816             switch (c) {
817                 case '%':
818                     if (builder is null) {
819                         builder = new StringBuilder(path.length);
820                         builder.append(path, offset, i - offset);
821                     }
822                     if ((i + 2) < end) {
823                         char u = path.charAt(i + 1);
824                         if (u == 'u') {
825                             // TODO this is wrong. This is a codepoint not a char
826                             builder.append(cast(char) (0xffff & ConverterUtils.parseInt(path, i + 2, 4, 16)));
827                             i += 5;
828                         } else {
829                             builder.append(cast(byte) (0xff & (ConverterUtils.convertHexDigit(u) * 16 + ConverterUtils.convertHexDigit(path.charAt(i + 2)))));
830                             i += 2;
831                         }
832                     } else {
833                         throw new IllegalArgumentException("");
834                     }
835 
836                     break;
837 
838                 case ';':
839                     if (builder is null) {
840                         builder = new StringBuilder(path.length);
841                         builder.append(path, offset, i - offset);
842                     }
843                     while (++i < end) {
844                         if (path[i] == '/') {
845                             builder.append('/');
846                             break;
847                         }
848                     }
849                     break;
850 
851 
852                 default:
853                     if (builder !is null)
854                         builder.append(c);
855                     break;
856             }
857         }
858 
859         if (builder !is null)
860             return builder.toString();
861         if (offset == 0 && length == path.length)
862             return path;
863         return path[offset .. end];
864     }
865 
866 	/* ------------------------------------------------------------ */
867 
868     /**
869      * Convert a decoded path to a canonical form.
870      * <p>
871      * All instances of "." and ".." are factored out.
872      * </p>
873      * <p>
874      * Null is returned if the path tries to .. above its root.
875      * </p>
876      *
877      * @param path the path to convert, decoded, with path separators '/' and no queries.
878      * @return the canonical path, or null if path traversal above root.
879      */
880     static string canonicalPath(string path) {
881         if (path.empty)
882             return path;
883 
884         bool slash = true;
885         int end = cast(int)path.length;
886         int i = 0;
887 
888         loop:
889         while (i < end) {
890             char c = path[i];
891             switch (c) {
892                 case '/':
893                     slash = true;
894                     break;
895 
896                 case '.':
897                     if (slash)
898                         break loop;
899                     slash = false;
900                     break;
901 
902                 default:
903                     slash = false;
904             }
905 
906             i++;
907         }
908 
909         if (i == end)
910             return path;
911 
912         StringBuilder canonical = new StringBuilder(path.length);
913         canonical.append(path, 0, i);
914 
915         int dots = 1;
916         i++;
917         while (i <= end) {
918             char c = i < end ? path[i] : '\0';
919             switch (c) {
920                 case '\0':
921                 case '/':
922                     switch (dots) {
923                         case 0:
924                             if (c != '\0')
925                                 canonical.append(c);
926                             break;
927 
928                         case 1:
929                             break;
930 
931                         case 2:
932                             if (canonical.length < 2)
933                                 return null;
934                             canonical.setLength(canonical.length - 1);
935                             canonical.setLength(canonical.lastIndexOf("/") + 1);
936                             break;
937 
938                         default:
939                             while (dots-- > 0)
940                                 canonical.append('.');
941                             if (c != '\0')
942                                 canonical.append(c);
943                     }
944 
945                     slash = true;
946                     dots = 0;
947                     break;
948 
949                 case '.':
950                     if (dots > 0)
951                         dots++;
952                     else if (slash)
953                         dots = 1;
954                     else
955                         canonical.append('.');
956                     slash = false;
957                     break;
958 
959                 default:
960                     while (dots-- > 0)
961                         canonical.append('.');
962                     canonical.append(c);
963                     dots = 0;
964                     slash = false;
965             }
966 
967             i++;
968         }
969         return canonical.toString();
970     }
971 
972 
973     /* ------------------------------------------------------------ */
974 
975     /**
976      * Convert a path to a cananonical form.
977      * <p>
978      * All instances of "." and ".." are factored out.
979      * </p>
980      * <p>
981      * Null is returned if the path tries to .. above its root.
982      * </p>
983      *
984      * @param path the path to convert (expects URI/URL form, encoded, and with path separators '/')
985      * @return the canonical path, or null if path traversal above root.
986      */
987     static string canonicalEncodedPath(string path) {
988         if (path.empty)
989             return path;
990 
991         bool slash = true;
992         int end = cast(int)path.length;
993         int i = 0;
994 
995         loop:
996         while (i < end) {
997             char c = path[i];
998             switch (c) {
999                 case '/':
1000                     slash = true;
1001                     break;
1002 
1003                 case '.':
1004                     if (slash)
1005                         break loop;
1006                     slash = false;
1007                     break;
1008 
1009                 case '?':
1010                     return path;
1011 
1012                 default:
1013                     slash = false;
1014             }
1015 
1016             i++;
1017         }
1018 
1019         if (i == end)
1020             return path;
1021 
1022         StringBuilder canonical = new StringBuilder(path.length);
1023         canonical.append(path, 0, i);
1024 
1025         int dots = 1;
1026         i++;
1027         while (i <= end) {
1028             char c = i < end ? path[i] : '\0';
1029             switch (c) {
1030                 case '\0':
1031                 case '/':
1032                 case '?':
1033                     switch (dots) {
1034                         case 0:
1035                             if (c != '\0')
1036                                 canonical.append(c);
1037                             break;
1038 
1039                         case 1:
1040                             if (c == '?')
1041                                 canonical.append(c);
1042                             break;
1043 
1044                         case 2:
1045                             if (canonical.length < 2)
1046                                 return null;
1047                             canonical.setLength(canonical.length - 1);
1048                             canonical.setLength(canonical.lastIndexOf("/") + 1);
1049                             if (c == '?')
1050                                 canonical.append(c);
1051                             break;
1052                         default:
1053                             while (dots-- > 0)
1054                                 canonical.append('.');
1055                             if (c != '\0')
1056                                 canonical.append(c);
1057                     }
1058 
1059                     slash = true;
1060                     dots = 0;
1061                     break;
1062 
1063                 case '.':
1064                     if (dots > 0)
1065                         dots++;
1066                     else if (slash)
1067                         dots = 1;
1068                     else
1069                         canonical.append('.');
1070                     slash = false;
1071                     break;
1072 
1073                 default:
1074                     while (dots-- > 0)
1075                         canonical.append('.');
1076                     canonical.append(c);
1077                     dots = 0;
1078                     slash = false;
1079             }
1080 
1081             i++;
1082         }
1083         return canonical.toString();
1084     }
1085 
1086 
1087 
1088     /* ------------------------------------------------------------ */
1089 
1090     /**
1091      * Convert a path to a compact form.
1092      * All instances of "//" and "///" etc. are factored out to single "/"
1093      *
1094      * @param path the path to compact
1095      * @return the compacted path
1096      */
1097     static string compactPath(string path) {
1098         if (path is null || path.length == 0)
1099             return path;
1100 
1101         int state = 0;
1102         int end = cast(int)path.length;
1103         int i = 0;
1104 
1105         loop:
1106         while (i < end) {
1107             char c = path[i];
1108             switch (c) {
1109                 case '?':
1110                     return path;
1111                 case '/':
1112                     state++;
1113                     if (state == 2)
1114                         break loop;
1115                     break;
1116                 default:
1117                     state = 0;
1118             }
1119             i++;
1120         }
1121 
1122         if (state < 2)
1123             return path;
1124 
1125         StringBuilder buf = new StringBuilder(path.length);
1126         buf.append(path, 0, i);
1127 
1128         loop2:
1129         while (i < end) {
1130             char c = path[i];
1131             switch (c) {
1132                 case '?':
1133                     buf.append(path, i, end);
1134                     break loop2;
1135                 case '/':
1136                     if (state++ == 0)
1137                         buf.append(c);
1138                     break;
1139                 default:
1140                     state = 0;
1141                     buf.append(c);
1142             }
1143             i++;
1144         }
1145 
1146         return buf.toString();
1147     }
1148 
1149     /* ------------------------------------------------------------ */
1150 
1151     /**
1152      * @param uri URI
1153      * @return True if the uri has a scheme
1154      */
1155     static bool hasScheme(string uri) {
1156         for (int i = 0; i < uri.length; i++) {
1157             char c = uri[i];
1158             if (c == ':')
1159                 return true;
1160             if (!(c >= 'a' && c <= 'z' ||
1161                     c >= 'A' && c <= 'Z' ||
1162                     (i > 0 && (c >= '0' && c <= '9' ||
1163                             c == '.' ||
1164                             c == '+' ||
1165                             c == '-'))
1166             ))
1167                 break;
1168         }
1169         return false;
1170     }
1171 }
1172 
1173 
1174 
1175 
1176 /**
1177  * A mapping from schemes to their default ports.
1178  *
1179  * This is not exhaustive. Not all schemes use ports. Not all schemes uniquely identify a port to
1180  * use even if they use ports. Entries here should be treated as best guesses.
1181  */
1182 enum ushort[string] SchemePortMap = [
1183     "aaa": 3868,
1184     "aaas": 5658,
1185     "acap": 674,
1186     "amqp": 5672,
1187     "cap": 1026,
1188     "coap": 5683,
1189     "coaps": 5684,
1190     "dav": 443,
1191     "dict": 2628,
1192     "ftp": 21,
1193     "git": 9418,
1194     "go": 1096,
1195     "gopher": 70,
1196     "http": 80,
1197     "https": 443,
1198     "ws": 80,
1199     "wss": 443,
1200     "iac": 4569,
1201     "icap": 1344,
1202     "imap": 143,
1203     "ipp": 631,
1204     "ipps": 631,  // yes, they're both mapped to port 631
1205     "irc": 6667,  // De facto default port, not the IANA reserved port.
1206     "ircs": 6697,
1207     "iris": 702,  // defaults to iris.beep
1208     "iris.beep": 702,
1209     "iris.lwz": 715,
1210     "iris.xpc": 713,
1211     "iris.xpcs": 714,
1212     "jabber": 5222,  // client-to-server
1213     "ldap": 389,
1214     "ldaps": 636,
1215     "msrp": 2855,
1216     "msrps": 2855,
1217     "mtqp": 1038,
1218     "mupdate": 3905,
1219     "news": 119,
1220     "nfs": 2049,
1221     "pop": 110,
1222     "redis": 6379,
1223     "reload": 6084,
1224     "rsync": 873,
1225     "rtmfp": 1935,
1226     "rtsp": 554,
1227     "shttp": 80,
1228     "sieve": 4190,
1229     "sip": 5060,
1230     "sips": 5061,
1231     "smb": 445,
1232     "smtp": 25,
1233     "snews": 563,
1234     "snmp": 161,
1235     "soap.beep": 605,
1236     "ssh": 22,
1237     "stun": 3478,
1238     "stuns": 5349,
1239     "svn": 3690,
1240     "teamspeak": 9987,
1241     "telnet": 23,
1242     "tftp": 69,
1243     "tip": 3372,
1244     "mysql": 3306,
1245     "postgresql": 5432,
1246 ];