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