- /**
- URL parsing routines.
-
- Copyright: © 2012 rejectedsoftware e.K.
- License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
- Authors: Sönke Ludwig
- */
- module dub.internal.vibecompat.inet.url;
-
- public import dub.internal.vibecompat.inet.path;
-
- version (Have_vibe_d_inet) public import vibe.inet.url;
- else:
-
- import std.algorithm;
- import std.array;
- import std.conv;
- import std.exception;
- import std.string;
- import std.uri;
- import std.meta : AliasSeq;
-
-
- /**
- Represents a URL decomposed into its components.
- */
- struct URL {
- private {
- string m_schema;
- string m_pathString;
- NativePath m_path;
- string m_host;
- ushort m_port;
- string m_username;
- string m_password;
- string m_queryString;
- string m_anchor;
- alias m_schemes = AliasSeq!("http", "https", "ftp", "spdy", "file", "sftp");
- }
-
- /// Constructs a new URL object from its components.
- this(string schema, string host, ushort port, NativePath path)
- {
- m_schema = schema;
- m_host = host;
- m_port = port;
- m_path = path;
- m_pathString = path.toString();
- }
- /// ditto
- this(string schema, NativePath path)
- {
- this(schema, null, 0, path);
- }
-
- /** Constructs a URL from its string representation.
-
- TODO: additional validation required (e.g. valid host and user names and port)
- */
- this(string url_string)
- {
- auto str = url_string;
- enforce(str.length > 0, "Empty URL.");
- if( str[0] != '/' ){
- auto idx = str.countUntil(':');
- enforce(idx > 0, "No schema in URL:"~str);
- m_schema = str[0 .. idx];
- str = str[idx+1 .. $];
- bool requires_host = false;
-
- auto schema_parts = m_schema.split("+");
- if (!schema_parts.empty && schema_parts.back.canFind(m_schemes))
- {
- // proto://server/path style
- enforce(str.startsWith("//"), "URL must start with proto://...");
- requires_host = true;
- str = str[2 .. $];
- }
-
- auto si = str.countUntil('/');
- if( si < 0 ) si = str.length;
- auto ai = str[0 .. si].countUntil('@');
- ptrdiff_t hs = 0;
- if( ai >= 0 ){
- hs = ai+1;
- auto ci = str[0 .. ai].countUntil(':');
- if( ci >= 0 ){
- m_username = str[0 .. ci];
- m_password = str[ci+1 .. ai];
- } else m_username = str[0 .. ai];
- enforce(m_username.length > 0, "Empty user name in URL.");
- }
-
- m_host = str[hs .. si];
- auto pi = m_host.countUntil(':');
- if(pi > 0) {
- enforce(pi < m_host.length-1, "Empty port in URL.");
- m_port = to!ushort(m_host[pi+1..$]);
- m_host = m_host[0 .. pi];
- }
-
- enforce(!requires_host || m_schema == "file" || m_host.length > 0,
- "Empty server name in URL.");
- str = str[si .. $];
- }
-
- this.localURI = (str == "") ? "/" : str;
- }
- /// ditto
- static URL parse(string url_string)
- {
- return URL(url_string);
- }
-
- /// The schema/protocol part of the URL
- @property string schema() const { return m_schema; }
- /// ditto
- @property void schema(string v) { m_schema = v; }
-
- /// The path part of the URL in the original string form
- @property string pathString() const { return m_pathString; }
-
- /// The path part of the URL
- @property NativePath path() const { return m_path; }
- /// ditto
- @property void path(NativePath p)
- {
- m_path = p;
- auto pstr = p.toString();
- m_pathString = pstr;
- }
-
- /// The host part of the URL (depends on the schema)
- @property string host() const { return m_host; }
- /// ditto
- @property void host(string v) { m_host = v; }
-
- /// The port part of the URL (optional)
- @property ushort port() const { return m_port; }
- /// ditto
- @property port(ushort v) { m_port = v; }
-
- /// The user name part of the URL (optional)
- @property string username() const { return m_username; }
- /// ditto
- @property void username(string v) { m_username = v; }
-
- /// The password part of the URL (optional)
- @property string password() const { return m_password; }
- /// ditto
- @property void password(string v) { m_password = v; }
-
- /// The query string part of the URL (optional)
- @property string queryString() const { return m_queryString; }
- /// ditto
- @property void queryString(string v) { m_queryString = v; }
-
- /// The anchor part of the URL (optional)
- @property string anchor() const { return m_anchor; }
-
- /// The path part plus query string and anchor
- @property string localURI()
- const {
- auto str = appender!string();
- str.reserve(m_pathString.length + 2 + queryString.length + anchor.length);
- str.put(encode(path.toString()));
- if( queryString.length ) {
- str.put("?");
- str.put(queryString);
- }
- if( anchor.length ) {
- str.put("#");
- str.put(anchor);
- }
- return str.data;
- }
- /// ditto
- @property void localURI(string str)
- {
- auto ai = str.countUntil('#');
- if( ai >= 0 ){
- m_anchor = str[ai+1 .. $];
- str = str[0 .. ai];
- }
-
- auto qi = str.countUntil('?');
- if( qi >= 0 ){
- m_queryString = str[qi+1 .. $];
- str = str[0 .. qi];
- }
-
- m_pathString = str;
- m_path = NativePath(decode(str));
- }
-
- /// The URL to the parent path with query string and anchor stripped.
- @property URL parentURL() const {
- URL ret;
- ret.schema = schema;
- ret.host = host;
- ret.port = port;
- ret.username = username;
- ret.password = password;
- ret.path = path.parentPath;
- return ret;
- }
-
- /// Converts this URL object to its string representation.
- string toString()
- const {
- import std.format;
- auto dst = appender!string();
- dst.put(schema);
- dst.put(":");
- auto schema_parts = schema.split("+");
- if (!schema_parts.empty && schema_parts.back.canFind(m_schemes))
- {
- dst.put("//");
- }
- dst.put(host);
- if( m_port > 0 ) formattedWrite(dst, ":%d", m_port);
- dst.put(localURI);
- return dst.data;
- }
-
- bool startsWith(const URL rhs) const {
- if( m_schema != rhs.m_schema ) return false;
- if( m_host != rhs.m_host ) return false;
- // FIXME: also consider user, port, querystring, anchor etc
- return path.startsWith(rhs.m_path);
- }
-
- URL opBinary(string OP)(NativePath rhs) const if( OP == "~" ) { return URL(m_schema, m_host, m_port, m_path ~ rhs); }
- URL opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { return URL(m_schema, m_host, m_port, m_path ~ rhs); }
- void opOpAssign(string OP)(NativePath rhs) if( OP == "~" ) { m_path ~= rhs; }
- void opOpAssign(string OP)(PathEntry rhs) if( OP == "~" ) { m_path ~= rhs; }
-
- /// Tests two URLs for equality using '=='.
- bool opEquals(ref const URL rhs) const {
- if( m_schema != rhs.m_schema ) return false;
- if( m_host != rhs.m_host ) return false;
- if( m_path != rhs.m_path ) return false;
- return true;
- }
- /// ditto
- bool opEquals(const URL other) const { return opEquals(other); }
-
- int opCmp(ref const URL rhs) const {
- if( m_schema != rhs.m_schema ) return m_schema.cmp(rhs.m_schema);
- if( m_host != rhs.m_host ) return m_host.cmp(rhs.m_host);
- if( m_path != rhs.m_path ) return m_path.opCmp(rhs.m_path);
- return true;
- }
- }
-
- unittest {
- auto url = URL.parse("https://www.example.net/index.html");
- assert(url.schema == "https", url.schema);
- assert(url.host == "www.example.net", url.host);
- assert(url.path == NativePath("/index.html"), url.path.toString());
-
- url = URL.parse("http://jo.doe:password@sub.www.example.net:4711/sub2/index.html?query#anchor");
- assert(url.schema == "http", url.schema);
- assert(url.username == "jo.doe", url.username);
- assert(url.password == "password", url.password);
- assert(url.port == 4711, to!string(url.port));
- assert(url.host == "sub.www.example.net", url.host);
- assert(url.path.toString() == "/sub2/index.html", url.path.toString());
- assert(url.queryString == "query", url.queryString);
- assert(url.anchor == "anchor", url.anchor);
-
- url = URL("http://localhost")~NativePath("packages");
- assert(url.toString() == "http://localhost/packages", url.toString());
-
- url = URL("http://localhost/")~NativePath("packages");
- assert(url.toString() == "http://localhost/packages", url.toString());
-
- url = URL.parse("dub+https://code.dlang.org/");
- assert(url.host == "code.dlang.org");
- assert(url.toString() == "dub+https://code.dlang.org/");
- assert(url.schema == "dub+https");
- }