From edd3ec4b32d24265a1a5a4370ae3d941e29758c8 Mon Sep 17 00:00:00 2001 From: Eugen Wissner Date: Thu, 1 Dec 2016 20:02:49 +0100 Subject: [PATCH] Add URL parser --- source/tanya/network/url.d | 1191 ++++++++++++++++++++++++++++++++++++ 1 file changed, 1191 insertions(+) create mode 100644 source/tanya/network/url.d diff --git a/source/tanya/network/url.d b/source/tanya/network/url.d new file mode 100644 index 0000000..ee58e7e --- /dev/null +++ b/source/tanya/network/url.d @@ -0,0 +1,1191 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Copyright: Eugene Wissner 2016. + * License: $(LINK2 https://www.mozilla.org/en-US/MPL/2.0/, + * Mozilla Public License, v. 2.0). + * Authors: $(LINK2 mailto:belka@caraus.de, Eugene Wissner) + */ +module tanya.network.url; + +import std.ascii : isAlphaNum, isDigit; +import std.traits : isSomeString; +import std.uni : isAlpha, isNumber; +import std.uri; +import tanya.memory; + +version (unittest) private +{ + import std.typecons; + static Tuple!(string, string[string], ushort)[] URLTests; +} + +static this() +{ + version (unittest) + { + URLTests = [ + tuple(`127.0.0.1`, [ + "path": "127.0.0.1", + ], ushort(0)), + + tuple(`http://127.0.0.1`, [ + "scheme": "http", + "host": "127.0.0.1", + ], ushort(0)), + + tuple(`http://127.0.0.1/`, [ + "scheme": "http", + "host": "127.0.0.1", + "path": "/", + ], ushort(0)), + + tuple(`127.0.0.1/`, [ + "path": "127.0.0.1/", + ], ushort(0)), + + tuple(`127.0.0.1:60000/`, [ + "host": "127.0.0.1", + "path": "/", + ], ushort(60000)), + + tuple(`example.org`, [ + "path": "example.org", + ], ushort(0)), + + tuple(`example.org/`, [ + "path": "example.org/", + ], ushort(0)), + + tuple(`http://example.org`, [ + "scheme": "http", + "host": "example.org", + ], ushort(0)), + + tuple(`http://example.org/`, [ + "scheme": "http", + "host": "example.org", + "path": "/", + ], ushort(0)), + + tuple(`www.example.org`, [ + "path": "www.example.org", + ], ushort(0)), + + tuple(`www.example.org/`, [ + "path": "www.example.org/", + ], ushort(0)), + + tuple(`http://www.example.org`, [ + "scheme": "http", + "host": "www.example.org", + ], ushort(0)), + + tuple(`http://www.example.org/`, [ + "scheme": "http", + "host": "www.example.org", + "path": "/", + ], ushort(0)), + + tuple(`www.example.org:2`, [ + "host": "www.example.org", + ], ushort(2)), + + tuple(`http://www.example.org:80`, [ + "scheme": "http", + "host": "www.example.org", + ], ushort(80)), + + tuple(`http://www.example.org:80/`, [ + "scheme": "http", + "host": "www.example.org", + "path": "/", + ], ushort(80)), + + tuple(`http://www.example.org/index.html`, [ + "scheme": "http", + "host": "www.example.org", + "path": "/index.html", + ], ushort(0)), + + tuple(`www.example.org/?`, [ + "path": "www.example.org/", + "query": "", + ], ushort(0)), + + tuple(`www.example.org:80/?`, [ + "host": "www.example.org", + "path": "/", + "query": "", + ], ushort(80)), + + tuple(`http://www.example.org/?`, [ + "scheme": "http", + "host": "www.example.org", + "path": "/", + "query": "", + ], ushort(0)), + + tuple(`http://www.example.org:80/?`, [ + "scheme": "http", + "host": "www.example.org", + "path": "/", + "query": "", + ], ushort(80)), + + tuple(`http://www.example.org:80/index.html`, [ + "scheme": "http", + "host": "www.example.org", + "path": "/index.html", + ], ushort(80)), + + tuple(`http://www.example.org:80/foo/bar/index.html`, [ + "scheme": "http", + "host": "www.example.org", + "path": "/foo/bar/index.html", + ], ushort(80)), + + tuple(`http://www.example.org:80/this/is/a/very/deep/directory/structure/and/file.png`, [ + "scheme": "http", + "host": "www.example.org", + "path": "/this/is/a/very/deep/directory/structure/and/file.png", + ], ushort(80)), + + tuple(`http://www.example.org:80/deep/directory/structure/and/file.png?lots=1&of=2¶meters=3&too=4`, [ + "scheme": "http", + "host": "www.example.org", + "path": "/deep/directory/structure/and/file.png", + "query": "lots=1&of=2¶meters=3&too=4", + ], ushort(80)), + + tuple(`http://www.example.org:80/this/is/a/very/deep/directory/structure/and/`, [ + "scheme": "http", + "host": "www.example.org", + "path": "/this/is/a/very/deep/directory/structure/and/", + ], ushort(80)), + + tuple(`http://www.example.org:80/this/is/a/very/deep/directory/structure/and/file.php`, [ + "scheme": "http", + "host": "www.example.org", + "path": "/this/is/a/very/deep/directory/structure/and/file.php", + ], ushort(80)), + + tuple(`http://www.example.org:80/this/../a/../deep/directory`, [ + "scheme": "http", + "host": "www.example.org", + "path": "/this/../a/../deep/directory", + ], ushort(80)), + + tuple(`http://www.example.org:80/this/../a/../deep/directory/`, [ + "scheme": "http", + "host": "www.example.org", + "path": "/this/../a/../deep/directory/", + ], ushort(80)), + + tuple(`http://www.example.org:80/this/is/a/very/deep/directory/../image.png`, [ + "scheme": "http", + "host": "www.example.org", + "path": "/this/is/a/very/deep/directory/../image.png", + ], ushort(80)), + + tuple(`http://www.example.org:80/index.html`, [ + "scheme": "http", + "host": "www.example.org", + "path": "/index.html", + ], ushort(80)), + + tuple(`http://www.example.org:80/index.html?`, [ + "scheme": "http", + "host": "www.example.org", + "path": "/index.html", + "query": "", + ], ushort(80)), + + tuple(`http://www.example.org:80/#foo`, [ + "scheme": "http", + "host": "www.example.org", + "path": "/", + "fragment": "foo", + ], ushort(80)), + + tuple(`http://www.example.org:80/?#`, [ + "scheme": "http", + "host": "www.example.org", + "path": "/", + "query": "", + "fragment": "", + ], ushort(80)), + + tuple(`http://www.example.org:80/?test=1`, [ + "scheme": "http", + "host": "www.example.org", + "path": "/", + "query": "test=1", + ], ushort(80)), + + tuple(`http://www.example.org/?test=1&`, [ + "scheme": "http", + "host": "www.example.org", + "path": "/", + "query": "test=1&", + ], ushort(0)), + + tuple(`http://www.example.org:80/?&`, [ + "scheme": "http", + "host": "www.example.org", + "path": "/", + "query": "&", + ], ushort(80)), + + tuple(`http://www.example.org:80/index.html?test=1&`, [ + "scheme": "http", + "host": "www.example.org", + "path": "/index.html", + "query": "test=1&", + ], ushort(80)), + + tuple(`http://www.example.org/index.html?&`, [ + "scheme": "http", + "host": "www.example.org", + "path": "/index.html", + "query": "&", + ], ushort(0)), + + tuple(`http://www.example.org:80/index.html?foo&`, [ + "scheme": "http", + "host": "www.example.org", + "path": "/index.html", + "query": "foo&", + ], ushort(80)), + + tuple(`http://www.example.org/index.html?&foo`, [ + "scheme": "http", + "host": "www.example.org", + "path": "/index.html", + "query": "&foo", + ], ushort(0)), + + tuple(`http://www.example.org:80/index.html?test=1&test2=char`, [ + "scheme": "http", + "host": "www.example.org", + "path": "/index.html", + "query": "test=1&test2=char", + ], ushort(80)), + + tuple(`www.example.org:80/index.html?test=1&test2=char#some_ref123`, [ + "host": "www.example.org", + "path": "/index.html", + "query": "test=1&test2=char", + "fragment": "some_ref123", + ], ushort(80)), + + tuple(`http://secret@www.example.org:80/index.html?test=1&test2=char#some_ref123`, [ + "scheme": "http", + "host": "www.example.org", + "user": "secret", + "path": "/index.html", + "query": "test=1&test2=char", + "fragment": "some_ref123", + ], ushort(80)), + + tuple(`http://secret:@www.example.org/index.html?test=1&test2=char#some_ref123`, [ + "scheme": "http", + "host": "www.example.org", + "user": "secret", + "pass": "", + "path": "/index.html", + "query": "test=1&test2=char", + "fragment": "some_ref123", + ], ushort(0)), + + tuple(`http://:hideout@www.example.org:80/index.html?test=1&test2=char#some_ref123`, [ + "scheme": "http", + "host": "www.example.org", + "user": "", + "pass": "hideout", + "path": "/index.html", + "query": "test=1&test2=char", + "fragment": "some_ref123", + ], ushort(80)), + + tuple(`http://secret:hideout@www.example.org/index.html?test=1&test2=char#some_ref123`, [ + "scheme": "http", + "host": "www.example.org", + "user": "secret", + "pass": "hideout", + "path": "/index.html", + "query": "test=1&test2=char", + "fragment": "some_ref123", + ], ushort(0)), + + tuple(`http://secret:hid:out@www.example.org:80/index.html?test=1&test2=int#some_ref123`, [ + "scheme": "http", + "host": "www.example.org", + "user": "secret", + "pass": "hid:out", + "path": "/index.html", + "query": "test=1&test2=int", + "fragment": "some_ref123", + ], ushort(80)), + + tuple(`nntp://news.example.org`, [ + "scheme": "nntp", + "host": "news.example.org", + ], ushort(0)), + + tuple(`ftp://ftp.gnu.org/gnu/glic/glibc.tar.gz`, [ + "scheme": "ftp", + "host": "ftp.gnu.org", + "path": "/gnu/glic/glibc.tar.gz", + ], ushort(0)), + + tuple(`zlib:http://foo@bar`, [ + "scheme": "zlib", + "path": "http://foo@bar", + ], ushort(0)), + + tuple(`zlib:filename.txt`, [ + "scheme": "zlib", + "path": "filename.txt", + ], ushort(0)), + + tuple(`zlib:/path/to/my/file/file.txt`, [ + "scheme": "zlib", + "path": "/path/to/my/file/file.txt", + ], ushort(0)), + + tuple(`foo://foo@bar`, [ + "scheme": "foo", + "host": "bar", + "user": "foo", + ], ushort(0)), + + tuple(`mailto:me@mydomain.com`, [ + "scheme": "mailto", + "path": "me@mydomain.com", + ], ushort(0)), + + tuple(`/foo.php?a=b&c=d`, [ + "path": "/foo.php", + "query": "a=b&c=d", + ], ushort(0)), + + tuple(`foo.php?a=b&c=d`, [ + "path": "foo.php", + "query": "a=b&c=d", + ], ushort(0)), + + tuple(`http://user:passwd@www.example.com:8080?bar=1&boom=0`, [ + "scheme": "http", + "host": "www.example.com", + "user": "user", + "pass": "passwd", + "query": "bar=1&boom=0", + ], ushort(8080)), + + tuple(`file:///path/to/file`, [ + "scheme": "file", + "path": "/path/to/file", + ], ushort(0)), + + tuple(`file://path/to/file`, [ + "scheme": "file", + "host": "path", + "path": "/to/file", + ], ushort(0)), + + tuple(`file:/path/to/file`, [ + "scheme": "file", + "path": "/path/to/file", + ], ushort(0)), + + tuple(`http://1.2.3.4:/abc.asp?a=1&b=2`, [ + "scheme": "http", + "host": "1.2.3.4", + "path": "/abc.asp", + "query": "a=1&b=2", + ], ushort(0)), + + tuple(`http://foo.com#bar`, [ + "scheme": "http", + "host": "foo.com", + "fragment": "bar", + ], ushort(0)), + + tuple(`scheme:`, [ + "scheme": "scheme", + ], ushort(0)), + + tuple(`foo+bar://baz@bang/bla`, [ + "scheme": "foo+bar", + "host": "bang", + "user": "baz", + "path": "/bla", + ], ushort(0)), + + tuple(`gg:9130731`, [ + "scheme": "gg", + "path": "9130731", + ], ushort(0)), + + tuple(`http://10.10.10.10/:80`, [ + "scheme": "http", + "host": "10.10.10.10", + "path": "/:80", + ], ushort(0)), + + tuple(`http://x:?`, [ + "scheme": "http", + "host": "x", + "query": "", + ], ushort(0)), + + tuple(`x:blah.com`, [ + "scheme": "x", + "path": "blah.com", + ], ushort(0)), + + tuple(`x:/blah.com`, [ + "scheme": "x", + "path": "/blah.com", + ], ushort(0)), + + tuple(`http://::?`, [ + "scheme": "http", + "host": ":", + "query": "", + ], ushort(0)), + + tuple(`http://::#`, [ + "scheme": "http", + "host": ":", + "fragment": "", + ], ushort(0)), + + tuple(`http://?:/`, [ + "scheme": "http", + "host": "?", + "path": "/", + ], ushort(0)), + + tuple(`http://@?:/`, [ + "scheme": "http", + "host": "?", + "user": "", + "path": "/", + ], ushort(0)), + + tuple(`file:///:`, [ + "scheme": "file", + "path": "/:", + ], ushort(0)), + + tuple(`file:///a:/`, [ + "scheme": "file", + "path": "a:/", + ], ushort(0)), + + tuple(`file:///ab:/`, [ + "scheme": "file", + "path": "/ab:/", + ], ushort(0)), + + tuple(`file:///a:/`, [ + "scheme": "file", + "path": "a:/", + ], ushort(0)), + + tuple(`file:///@:/`, [ + "scheme": "file", + "path": "@:/", + ], ushort(0)), + + tuple(`file:///:80/`, [ + "scheme": "file", + "path": "/:80/", + ], ushort(0)), + + tuple(`[]`, [ + "path": "[]", + ], ushort(0)), + + tuple(`http://[x:80]/`, [ + "scheme": "http", + "host": "[x:80]", + "path": "/", + ], ushort(0)), + + tuple(``, [ + "path": "", + ], ushort(0)), + + tuple(`/`, [ + "path": "/", + ], ushort(0)), + + tuple(`/rest/Users?filter={"id":"789"}`, [ + "path": "/rest/Users", + "query": `filter={"id":"789"}`, + ], ushort(0)), + + tuple(`//example.org`, [ + "host": "example.org", + ], ushort(0)), + + tuple(`/standard/?fq=B:20001`, [ + "path": "/standard/", + "query": "fq=B:20001", + ], ushort(0)), + + tuple(`/standard/?fq=B:200013`, [ + "path": "/standard/", + "query": "fq=B:200013", + ], ushort(0)), + + tuple(`/standard/?fq=home:012345`, [ + "path": "/standard/", + "query": "fq=home:012345", + ], ushort(0)), + + tuple(`/standard/?fq=home:01234`, [ + "path": "/standard/", + "query": "fq=home:01234", + ], ushort(0)), + + tuple(`http://user:pass@host`, [ + "scheme": "http", + "host": "host", + "user": "user", + "pass": "pass", + ], ushort(0)), + + tuple(`//user:pass@host`, [ + "host": "host", + "user": "user", + "pass": "pass", + ], ushort(0)), + + tuple(`//user@host`, [ + "host": "host", + "user": "user", + ], ushort(0)), + + tuple(`//example.org:99/hey?a=b#c=d`, [ + "host": "example.org", + "path": "/hey", + "query": "a=b", + "fragment": "c=d", + ], ushort(99)), + + tuple(`//example.org/hey?a=b#c=d`, [ + "host": "example.org", + "path": "/hey", + "query": "a=b", + "fragment": "c=d", + ], ushort(0)), + + tuple(`http://example.org/some/path.cgi?t=1#fragment?data`, [ + "scheme": "http", + "host": "example.org", + "path": "/some/path.cgi", + "query": "t=1", + "fragment": "fragment?data", + ], ushort(0)), + + tuple(`http://example.org/some/path.cgi#fragment?data`, [ + "scheme": "http", + "host": "example.org", + "path": "/some/path.cgi", + "fragment": "fragment?data", + ], ushort(0)), + + tuple(`x://::abc/?`, string[string].init, ushort(0)), + tuple(`http:///blah.com`, string[string].init, ushort(0)), + tuple(`http://:80`, string[string].init, ushort(0)), + tuple(`http://user@:80`, string[string].init, ushort(0)), + tuple(`http://user:pass@:80`, string[string].init, ushort(0)), + tuple(`http://:`, string[string].init, ushort(0)), + tuple(`http://@/`, string[string].init, ushort(0)), + tuple(`http://@:/`, string[string].init, ushort(0)), + tuple(`http://:/`, string[string].init, ushort(0)), + tuple(`http://?`, string[string].init, ushort(0)), + tuple(`http://#`, string[string].init, ushort(0)), + tuple(`http://:?`, string[string].init, ushort(0)), + tuple(`http://blah.com:123456`, string[string].init, ushort(0)), + tuple(`http://blah.com:70000`, string[string].init, ushort(0)), + tuple(`http://blah.com:abcdef`, string[string].init, ushort(0)), + tuple(`http://secret@hideout@www.example.org:80/index.html?test=1&test2=char#some_ref123`, + string[string].init, + ushort(0)), + tuple(`http://user:@pass@host/path?argument?value#etc`, string[string].init, ushort(0)), + tuple(`http://foo.com\@bar.com`, string[string].init, ushort(0)), + tuple(`http://email@address.com:pass@example.org`, string[string].init, ushort(0)), + tuple(`:`, string[string].init, ushort(0)), + ]; + } +} + +/** + * A Unique Resource Locator. + */ +struct URL(U = string) + if (isSomeString!U) +{ + /** The URL scheme. */ + U scheme; + + /** The username. */ + U user; + + /** The password. */ + U pass; + + /** The hostname. */ + U host; + + /** The port number. */ + ushort port; + + /** The path. */ + U path; + + /** The query string. */ + U query; + + /** The anchor. */ + U fragment; + + /** + * Attempts to parse an URL from a string. + * Output string data (scheme, user, etc.) are just slices of input string (e.g., no memory allocation and copying). + * + * Params: + * source = The string containing the URL. + * + * Throws: $(D_PSYMBOL URIException) if the URL is malformed. + */ + this(U source) + { + auto value = source; + ptrdiff_t pos = -1, endPos = value.length, start; + + foreach (i, ref c; source) + { + if (pos == -1 && c == ':') + { + pos = i; + } + if (endPos == value.length && (c == '?' || c == '#')) + { + endPos = i; + } + } + + // Check if the colon is a part of the scheme or the port and parse + // the appropriate part + if (value.length > 1 && value[0] == '/' && value[1] == '/') + { + // Relative scheme + start = 2; + } + else if (pos > 0) + { + // Validate scheme + // [ toLower(alpha) | digit | "+" | "-" | "." ] + foreach (ref c; value[0..pos]) + { + if (!c.isAlphaNum && c != '+' && c != '-' && c != '.') + { + if (endPos > pos) + { + if (!parsePort(value[pos..$])) + { + throw theAllocator.make!URIException("Failed to parse port"); + } + } + goto ParsePath; + } + } + + if (value.length == pos + 1) // only scheme is available + { + scheme = value[0 .. $ - 1]; + return; + } + else if (value.length > pos + 1 && value[pos + 1] == '/') + { + scheme = value[0..pos]; + + if (value.length > pos + 2 && value[pos + 2] == '/') + { + start = pos + 3; + if (scheme == "file" && value.length > start && value[start] == '/') + { + // Windows drive letters + if (value.length - start > 2 && value[start + 2] == ':') + { + ++start; + } + goto ParsePath; + } + } + else + { + start = pos + 1; + goto ParsePath; + } + } + else // certain schemas like mailto: and zlib: may not have any / after them + { + + if (!parsePort(value[pos..$])) + { + scheme = value[0..pos]; + start = pos + 1; + goto ParsePath; + } + } + } + else if (pos == 0 && parsePort(value[pos..$])) + { + // An URL shouldn't begin with a port number + throw theAllocator.make!URIException("URL begins with port"); + } + else + { + goto ParsePath; + } + + // Parse host + pos = -1; + for (ptrdiff_t i = start; i < value.length; ++i) + { + if (value[i] == '@') + { + pos = i; + } + else if (value[i] == '/') + { + endPos = i; + break; + } + } + + // Check for login and password + if (pos != -1) + { + // *( unreserved / pct-encoded / sub-delims / ":" ) + foreach (i, c; value[start..pos]) + { + if (c == ':') + { + if (user is null) + { + user = value[start .. start + i]; + pass = value[start + i + 1 .. pos]; + } + } + else if (!c.isAlpha && + !c.isNumber && + c != '!' && + c != ';' && + c != '=' && + c != '_' && + c != '~' && + !(c >= '$' && c <= '.')) + { + if (scheme !is null) + { + scheme = null; + } + if (user !is null) + { + user = null; + } + if (pass !is null) + { + pass = null; + } + throw make!URIException(theAllocator, + "Restricted characters in user information"); + } + } + if (user is null) + { + user = value[start..pos]; + } + + start = ++pos; + } + + pos = endPos; + if (endPos <= 1 || value[start] != '[' || value[endPos - 1] != ']') + { + // Short circuit portscan + // IPv6 embedded address + for (ptrdiff_t i = endPos - 1; i >= start; --i) + { + if (value[i] == ':') + { + pos = i; + if (port == 0 && !parsePort(value[i..endPos])) + { + if (scheme !is null) + { + scheme = null; + } + if (user !is null) + { + user = null; + } + if (pass !is null) + { + pass = null; + } + throw theAllocator.make!URIException("Invalid port"); + } + break; + } + } + } + + // Check if we have a valid host, if we don't reject the string as url + if (pos <= start) + { + if (scheme !is null) + { + scheme = null; + } + if (user !is null) + { + user = null; + } + if (pass !is null) + { + pass = null; + } + throw theAllocator.make!URIException("Invalid host"); + } + + host = value[start..pos]; + + if (endPos == value.length) + { + return; + } + + start = endPos; + + ParsePath: + endPos = value.length; + pos = -1; + foreach (i, ref c; value[start..$]) + { + if (c == '?' && pos == -1) + { + pos = start + i; + } + else if (c == '#') + { + endPos = start + i; + break; + } + } + if (pos == -1) + { + pos = endPos; + } + + if (pos > start) + { + path = value[start..pos]; + } + if (endPos >= ++pos) + { + query = value[pos..endPos]; + } + if (++endPos <= value.length) + { + fragment = value[endPos..$]; + } + } + + ~this() + { + if (scheme !is null) + { + scheme = null; + } + if (user !is null) + { + user = null; + } + if (pass !is null) + { + pass = null; + } + if (host !is null) + { + host = null; + } + if (path !is null) + { + path = null; + } + if (query !is null) + { + query = null; + } + if (fragment !is null) + { + fragment = null; + } + } + + /** + * Attempts to parse and set the port. + * + * Params: + * port = String beginning with a colon followed by the port number and + * an optional path (query string and/or fragment), like: + * `:12345/some_path` or `:12345`. + * + * Returns: Whether the port could be found. + */ + private bool parsePort(U port) pure nothrow @safe @nogc + { + ptrdiff_t i = 1; + float lPort = 0; + + for (; i < port.length && port[i].isDigit() && i <= 6; ++i) + { + lPort += (port[i] - '0') / cast(float)(10 ^^ (i - 1)); + } + if (i == 1 && (i == port.length || port[i] == '/')) + { + return true; + } + else if (i == port.length || port[i] == '/') + { + lPort *= 10 ^^ (i - 2); + if (lPort > ushort.max) + { + return false; + } + this.port = cast(ushort)lPort; + return true; + } + return false; + } +} + +/// +unittest +{ + auto u = URL!()("example.org"); + assert(u.path == "example.org"); + + u = URL!()("relative/path"); + assert(u.path == "relative/path"); + + // Host and scheme + u = URL!()("https://example.org"); + assert(u.scheme == "https"); + assert(u.host == "example.org"); + assert(u.path is null); + assert(u.port == 0); + assert(u.fragment is null); + + // With user and port and path + u = URL!()("https://hilary:putnam@example.org:443/foo/bar"); + assert(u.scheme == "https"); + assert(u.host == "example.org"); + assert(u.path == "/foo/bar"); + assert(u.port == 443); + assert(u.user == "hilary"); + assert(u.pass == "putnam"); + assert(u.fragment is null); + + // With query string + u = URL!()("https://example.org/?login=true"); + assert(u.scheme == "https"); + assert(u.host == "example.org"); + assert(u.path == "/"); + assert(u.query == "login=true"); + assert(u.fragment is null); + + // With query string and fragment + u = URL!()("https://example.org/?login=false#label"); + assert(u.scheme == "https"); + assert(u.host == "example.org"); + assert(u.path == "/"); + assert(u.query == "login=false"); + assert(u.fragment == "label"); + + u = URL!()("redis://root:password@localhost:2201/path?query=value#fragment"); + assert(u.scheme == "redis"); + assert(u.user == "root"); + assert(u.pass == "password"); + assert(u.host == "localhost"); + assert(u.port == 2201); + assert(u.path == "/path"); + assert(u.query == "query=value"); + assert(u.fragment == "fragment"); +} + +private unittest +{ + foreach(t; URLTests) + { + if (t[1].length == 0 && t[2] == 0) + { + try + { + URL!()(t[0]); + assert(0); + } + catch (URIException e) + { + assert(1); + } + } + else + { + auto u = URL!()(t[0]); + assert("scheme" in t[1] ? u.scheme == t[1]["scheme"] : u.scheme is null, + t[0]); + assert("user" in t[1] ? u.user == t[1]["user"] : u.user is null, t[0]); + assert("pass" in t[1] ? u.pass == t[1]["pass"] : u.pass is null, t[0]); + assert("host" in t[1] ? u.host == t[1]["host"] : u.host is null, t[0]); + assert(u.port == t[2], t[0]); + assert("path" in t[1] ? u.path == t[1]["path"] : u.path is null, t[0]); + assert("query" in t[1] ? u.query == t[1]["query"] : u.query is null, t[0]); + if ("fragment" in t[1]) + { + assert(u.fragment == t[1]["fragment"], t[0]); + } + else + { + assert(u.fragment is null, t[0]); + } + } + } +} + +/** + * Contains possible URL components that can be returned from + * $(D_PSYMBOL parseURL). + */ +enum Component : string +{ + scheme = "scheme", + host = "host", + port = "port", + user = "user", + pass = "pass", + path = "path", + query = "query", + fragment = "fragment", +} + +/** + * Attempts to parse an URL from a string. + * + * Params: + * T = $(D_SYMBOL Component) member or $(D_KEYWORD null) for a + * struct with all components. + * source = The string containing the URL. + * + * Returns: Requested URL components. + */ +URL parseURL(U)(in U source) + if (isSomeString!U) +{ + return URL!U(source); +} + +/** ditto */ +string parseURL(string T, U)(in U source) + if ((T == "scheme" + || T =="host" + || T == "user" + || T == "pass" + || T == "path" + || T == "query" + || T == "fragment") && isSomeString!U) +{ + auto ret = URL!U(source); + return mixin("ret." ~ T); +} + +/** ditto */ +ushort parseURL(string T, U)(in U source) + if (T == "port" && isSomeString!U) +{ + auto ret = URL!U(source); + return ret.port; +} + +unittest +{ + assert(parseURL!(Component.port)("http://example.org:5326") == 5326); +} + +private unittest +{ + foreach(t; URLTests) + { + if (t[1].length == 0 && t[2] == 0) + { + try + { + parseURL!(Component.port)(t[0]); + parseURL!(Component.user)(t[0]); + parseURL!(Component.pass)(t[0]); + parseURL!(Component.host)(t[0]); + parseURL!(Component.path)(t[0]); + parseURL!(Component.query)(t[0]); + parseURL!(Component.fragment)(t[0]); + assert(0); + } + catch (URIException e) + { + assert(1); + } + } + else + { + ushort port = parseURL!(Component.port)(t[0]); + string component = parseURL!(Component.scheme)(t[0]); + assert("scheme" in t[1] ? component == t[1]["scheme"] : component is null, + t[0]); + component = parseURL!(Component.user)(t[0]); + assert("user" in t[1] ? component == t[1]["user"] : component is null, + t[0]); + component = parseURL!(Component.pass)(t[0]); + assert("pass" in t[1] ? component == t[1]["pass"] : component is null, + t[0]); + component = parseURL!(Component.host)(t[0]); + assert("host" in t[1] ? component == t[1]["host"] : component is null, + t[0]); + assert(port == t[2], t[0]); + component = parseURL!(Component.path)(t[0]); + assert("path" in t[1] ? component == t[1]["path"] : component is null, + t[0]); + component = parseURL!(Component.query)(t[0]); + assert("query" in t[1] ? component == t[1]["query"] : component is null, + t[0]); + component = parseURL!(Component.fragment)(t[0]); + if ("fragment" in t[1]) + { + assert(component == t[1]["fragment"], t[0]); + } + else + { + assert(component is null, t[0]); + } + } + } +}