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