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