Newer
Older
dub_jkp / source / dub / internal / vibecompat / inet / url.d
  1. /**
  2. URL parsing routines.
  3.  
  4. Copyright: © 2012 rejectedsoftware e.K.
  5. License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
  6. Authors: Sönke Ludwig
  7. */
  8. module dub.internal.vibecompat.inet.url;
  9.  
  10. public import dub.internal.vibecompat.inet.path;
  11.  
  12. version (Have_vibe_d_inet) public import vibe.inet.url;
  13. else:
  14.  
  15. import std.algorithm;
  16. import std.array;
  17. import std.conv;
  18. import std.exception;
  19. import std.string;
  20. import std.uri;
  21. import std.meta : AliasSeq;
  22.  
  23.  
  24. /**
  25. Represents a URL decomposed into its components.
  26. */
  27. struct URL {
  28. private {
  29. string m_schema;
  30. string m_pathString;
  31. NativePath m_path;
  32. string m_host;
  33. ushort m_port;
  34. string m_username;
  35. string m_password;
  36. string m_queryString;
  37. string m_anchor;
  38. alias m_schemes = AliasSeq!("http", "https", "ftp", "spdy", "file", "sftp");
  39. }
  40.  
  41. /// Constructs a new URL object from its components.
  42. this(string schema, string host, ushort port, NativePath path)
  43. {
  44. m_schema = schema;
  45. m_host = host;
  46. m_port = port;
  47. m_path = path;
  48. m_pathString = path.toString();
  49. }
  50. /// ditto
  51. this(string schema, NativePath path)
  52. {
  53. this(schema, null, 0, path);
  54. }
  55.  
  56. /** Constructs a URL from its string representation.
  57.  
  58. TODO: additional validation required (e.g. valid host and user names and port)
  59. */
  60. this(string url_string)
  61. {
  62. auto str = url_string;
  63. enforce(str.length > 0, "Empty URL.");
  64. if( str[0] != '/' ){
  65. auto idx = str.countUntil(':');
  66. enforce(idx > 0, "No schema in URL:"~str);
  67. m_schema = str[0 .. idx];
  68. str = str[idx+1 .. $];
  69. bool requires_host = false;
  70.  
  71. auto schema_parts = m_schema.split("+");
  72. if (!schema_parts.empty && schema_parts.back.canFind(m_schemes))
  73. {
  74. // proto://server/path style
  75. enforce(str.startsWith("//"), "URL must start with proto://...");
  76. requires_host = true;
  77. str = str[2 .. $];
  78. }
  79.  
  80. auto si = str.countUntil('/');
  81. if( si < 0 ) si = str.length;
  82. auto ai = str[0 .. si].countUntil('@');
  83. ptrdiff_t hs = 0;
  84. if( ai >= 0 ){
  85. hs = ai+1;
  86. auto ci = str[0 .. ai].countUntil(':');
  87. if( ci >= 0 ){
  88. m_username = str[0 .. ci];
  89. m_password = str[ci+1 .. ai];
  90. } else m_username = str[0 .. ai];
  91. enforce(m_username.length > 0, "Empty user name in URL.");
  92. }
  93.  
  94. m_host = str[hs .. si];
  95. auto pi = m_host.countUntil(':');
  96. if(pi > 0) {
  97. enforce(pi < m_host.length-1, "Empty port in URL.");
  98. m_port = to!ushort(m_host[pi+1..$]);
  99. m_host = m_host[0 .. pi];
  100. }
  101.  
  102. enforce(!requires_host || m_schema == "file" || m_host.length > 0,
  103. "Empty server name in URL.");
  104. str = str[si .. $];
  105. }
  106.  
  107. this.localURI = (str == "") ? "/" : str;
  108. }
  109. /// ditto
  110. static URL parse(string url_string)
  111. {
  112. return URL(url_string);
  113. }
  114.  
  115. /// The schema/protocol part of the URL
  116. @property string schema() const { return m_schema; }
  117. /// ditto
  118. @property void schema(string v) { m_schema = v; }
  119.  
  120. /// The path part of the URL in the original string form
  121. @property string pathString() const { return m_pathString; }
  122.  
  123. /// The path part of the URL
  124. @property NativePath path() const { return m_path; }
  125. /// ditto
  126. @property void path(NativePath p)
  127. {
  128. m_path = p;
  129. auto pstr = p.toString();
  130. m_pathString = pstr;
  131. }
  132.  
  133. /// The host part of the URL (depends on the schema)
  134. @property string host() const { return m_host; }
  135. /// ditto
  136. @property void host(string v) { m_host = v; }
  137.  
  138. /// The port part of the URL (optional)
  139. @property ushort port() const { return m_port; }
  140. /// ditto
  141. @property port(ushort v) { m_port = v; }
  142.  
  143. /// The user name part of the URL (optional)
  144. @property string username() const { return m_username; }
  145. /// ditto
  146. @property void username(string v) { m_username = v; }
  147.  
  148. /// The password part of the URL (optional)
  149. @property string password() const { return m_password; }
  150. /// ditto
  151. @property void password(string v) { m_password = v; }
  152.  
  153. /// The query string part of the URL (optional)
  154. @property string queryString() const { return m_queryString; }
  155. /// ditto
  156. @property void queryString(string v) { m_queryString = v; }
  157.  
  158. /// The anchor part of the URL (optional)
  159. @property string anchor() const { return m_anchor; }
  160.  
  161. /// The path part plus query string and anchor
  162. @property string localURI()
  163. const {
  164. auto str = appender!string();
  165. str.reserve(m_pathString.length + 2 + queryString.length + anchor.length);
  166. str.put(encode(path.toString()));
  167. if( queryString.length ) {
  168. str.put("?");
  169. str.put(queryString);
  170. }
  171. if( anchor.length ) {
  172. str.put("#");
  173. str.put(anchor);
  174. }
  175. return str.data;
  176. }
  177. /// ditto
  178. @property void localURI(string str)
  179. {
  180. auto ai = str.countUntil('#');
  181. if( ai >= 0 ){
  182. m_anchor = str[ai+1 .. $];
  183. str = str[0 .. ai];
  184. }
  185.  
  186. auto qi = str.countUntil('?');
  187. if( qi >= 0 ){
  188. m_queryString = str[qi+1 .. $];
  189. str = str[0 .. qi];
  190. }
  191.  
  192. m_pathString = str;
  193. m_path = NativePath(decode(str));
  194. }
  195.  
  196. /// The URL to the parent path with query string and anchor stripped.
  197. @property URL parentURL() const {
  198. URL ret;
  199. ret.schema = schema;
  200. ret.host = host;
  201. ret.port = port;
  202. ret.username = username;
  203. ret.password = password;
  204. ret.path = path.parentPath;
  205. return ret;
  206. }
  207.  
  208. /// Converts this URL object to its string representation.
  209. string toString()
  210. const {
  211. import std.format;
  212. auto dst = appender!string();
  213. dst.put(schema);
  214. dst.put(":");
  215. auto schema_parts = schema.split("+");
  216. if (!schema_parts.empty && schema_parts.back.canFind(m_schemes))
  217. {
  218. dst.put("//");
  219. }
  220. dst.put(host);
  221. if( m_port > 0 ) formattedWrite(dst, ":%d", m_port);
  222. dst.put(localURI);
  223. return dst.data;
  224. }
  225.  
  226. bool startsWith(const URL rhs) const {
  227. if( m_schema != rhs.m_schema ) return false;
  228. if( m_host != rhs.m_host ) return false;
  229. // FIXME: also consider user, port, querystring, anchor etc
  230. return path.startsWith(rhs.m_path);
  231. }
  232.  
  233. URL opBinary(string OP)(NativePath rhs) const if( OP == "~" ) { return URL(m_schema, m_host, m_port, m_path ~ rhs); }
  234. URL opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { return URL(m_schema, m_host, m_port, m_path ~ rhs); }
  235. void opOpAssign(string OP)(NativePath rhs) if( OP == "~" ) { m_path ~= rhs; }
  236. void opOpAssign(string OP)(PathEntry rhs) if( OP == "~" ) { m_path ~= rhs; }
  237.  
  238. /// Tests two URLs for equality using '=='.
  239. bool opEquals(ref const URL rhs) const {
  240. if( m_schema != rhs.m_schema ) return false;
  241. if( m_host != rhs.m_host ) return false;
  242. if( m_path != rhs.m_path ) return false;
  243. return true;
  244. }
  245. /// ditto
  246. bool opEquals(const URL other) const { return opEquals(other); }
  247.  
  248. int opCmp(ref const URL rhs) const {
  249. if( m_schema != rhs.m_schema ) return m_schema.cmp(rhs.m_schema);
  250. if( m_host != rhs.m_host ) return m_host.cmp(rhs.m_host);
  251. if( m_path != rhs.m_path ) return m_path.opCmp(rhs.m_path);
  252. return true;
  253. }
  254. }
  255.  
  256. unittest {
  257. auto url = URL.parse("https://www.example.net/index.html");
  258. assert(url.schema == "https", url.schema);
  259. assert(url.host == "www.example.net", url.host);
  260. assert(url.path == NativePath("/index.html"), url.path.toString());
  261.  
  262. url = URL.parse("http://jo.doe:password@sub.www.example.net:4711/sub2/index.html?query#anchor");
  263. assert(url.schema == "http", url.schema);
  264. assert(url.username == "jo.doe", url.username);
  265. assert(url.password == "password", url.password);
  266. assert(url.port == 4711, to!string(url.port));
  267. assert(url.host == "sub.www.example.net", url.host);
  268. assert(url.path.toString() == "/sub2/index.html", url.path.toString());
  269. assert(url.queryString == "query", url.queryString);
  270. assert(url.anchor == "anchor", url.anchor);
  271.  
  272. url = URL("http://localhost")~NativePath("packages");
  273. assert(url.toString() == "http://localhost/packages", url.toString());
  274.  
  275. url = URL("http://localhost/")~NativePath("packages");
  276. assert(url.toString() == "http://localhost/packages", url.toString());
  277.  
  278. url = URL.parse("dub+https://code.dlang.org/");
  279. assert(url.host == "code.dlang.org");
  280. assert(url.toString() == "dub+https://code.dlang.org/");
  281. assert(url.schema == "dub+https");
  282. }