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 }