HttpURI.d 27 KB

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