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