Class to parse and manipulate URLs
Change-Id: I4b1f730c917935814ebf7b2443eca31c5eb98da4
diff --git a/speedtest/url.cc b/speedtest/url.cc
new file mode 100644
index 0000000..afbdab7
--- /dev/null
+++ b/speedtest/url.cc
@@ -0,0 +1,505 @@
+#include "url.h"
+
+#include <iostream>
+
+namespace http {
+namespace {
+
+enum class SchemeState {
+ ALPHA,
+ ALPHA_NUM,
+ COLON,
+ FIRST_SLASH,
+ SECOND_SLASH,
+};
+
+enum class IPv6State {
+ OPEN_SQUARE,
+ ADDRESS,
+ CLOSE_SQUARE,
+};
+
+enum class PortState {
+ COLON,
+ FIRST_DIGIT,
+ DIGIT,
+};
+
+enum class PathState {
+ LEADING_SLASH,
+ SLASH,
+ PCHAR,
+};
+
+enum class QueryStringState {
+ QUESTION_MARK,
+ FIRST_CHAR,
+ QUERY,
+};
+
+enum class FragmentState {
+ HASH,
+ FIRST_CHAR,
+ FRAGMENT,
+};
+
+const char *kSchemeHttp = "http";
+const char *kSchemeHttps = "https";
+const char *kDefaultScheme = kSchemeHttp;
+const int kDefaultHttpPort = 80;
+const int kDefaultHttpsPort = 443;
+const int kDefaultUrlSpace = 2000;
+
+// RFC 3986 character sets
+
+bool IsUnreserved(int ch) {
+ return std::isalnum(ch) || ch == '-' || ch == '.' || ch == '-';
+}
+
+bool IsGenDelim(int ch) {
+ switch (ch) {
+ case ':':
+ case '/':
+ case '?':
+ case '#':
+ case '[':
+ case ']':
+ case '@':
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool IsSubDelim(int ch) {
+ switch (ch) {
+ case '!':
+ case '$':
+ case '&':
+ case '`':
+ case '(':
+ case ')':
+ case '*':
+ case '+':
+ case ',':
+ case ';':
+ case '=':
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool IsReserved(int ch) {
+ return IsGenDelim(ch) || IsSubDelim(ch);
+}
+
+bool IsPchar(int ch) {
+ return IsUnreserved(ch) || IsSubDelim(ch) || ch == ':' || ch == '@';
+}
+
+bool IsQuery(int ch) {
+ return IsPchar(ch) || ch == '?' || ch == '/';
+}
+
+inline bool IsFragment(int ch) {
+ return IsQuery(ch);
+}
+
+} // namespace
+
+bool operator==(const Url &url1, const Url &url2) {
+ if (!url1.parsed_ || !url2.parsed_) {
+ return false;
+ }
+ return url1.url() == url2.url();
+}
+
+Url::Url(): parsed_(false), absolute_(false), port_(0) {
+}
+
+Url::Url(const Url &other) {
+ if (!other.parsed_) {
+ parsed_ = false;
+ return;
+ }
+ parsed_ = true;
+ absolute_ = other.absolute_;
+ scheme_ = other.scheme_;
+ host_ = other.host_;
+ port_ = other.port_;
+ path_ = other.path_;
+ query_string_ = other.query_string_;
+ fragment_ = other.fragment_;
+}
+
+Url::Url(const char *url): parsed_(false), absolute_(false), port_(0) {
+ Parse(url);
+}
+
+Url & Url::operator=(const Url &other) {
+ if (!other.parsed_) {
+ parsed_ = false;
+ }
+ parsed_ = true;
+ absolute_ = other.absolute_;
+ scheme_ = other.scheme_;
+ host_ = other.host_;
+ port_ = other.port_;
+ path_ = other.path_;
+ query_string_ = other.query_string_;
+ fragment_ = other.fragment_;
+ return *this;
+}
+
+bool Url::Parse(const std::string &url) {
+ current_ = url.begin();
+ end_ = url.end();
+
+ bool has_scheme = Scheme();
+
+ absolute_ = IPv6() || Host();
+
+ if (has_scheme) {
+ // Having a scheme means the URL must be absolute
+ if (!absolute_) {
+ return false;
+ }
+ } else if (absolute_) {
+ scheme_ = kDefaultScheme;
+ }
+
+ bool has_port = false;
+ if (absolute_) {
+ has_port = Port();
+ }
+
+ AbsolutePath();
+ QueryString();
+ Fragment();
+
+ if (current_ != end_) {
+ return false;
+ }
+
+ if (absolute_) {
+ if (has_port) {
+ // do nothing
+ } else if (scheme_ == kSchemeHttp) {
+ port_ = kDefaultHttpPort;
+ } else if (scheme_ == kSchemeHttps) {
+ port_ = kDefaultHttpsPort;
+ }
+
+ if (path_.empty()) {
+ path_ = "/";
+ }
+ }
+
+ parsed_ = true;
+ return true;
+}
+
+void Url::set_scheme(const std::string &scheme) {
+ // TODO(wshields): validate
+ scheme_ = scheme;
+ UpdateAbsolute();
+}
+
+void Url::set_host(const std::string &host) {
+ // TODO(wshields): validate
+ host_ = host;
+ UpdateAbsolute();
+}
+
+void Url::set_port(int port) {
+ // TODO(wshields): validate
+ port_ = port;
+}
+
+void Url::set_path(const std::string &path) {
+ // TODO(wshields): validate
+ path_ = path;
+}
+
+void Url::clear_path() {
+ // TODO(wshields): validate
+ path_ = absolute_ ? "/" : "";
+}
+
+void Url::set_query_string(const std::string &query_string) {
+ // TODO(wshields): validate
+ query_string_ = query_string;
+}
+
+void Url::clear_query_string() {
+ query_string_.clear();
+}
+
+void Url::set_fragment(const std::string &fragment) {
+ // TODO(wshields): validate
+ fragment_ = fragment;
+}
+
+void Url::clear_fragment() {
+ fragment_.clear();
+}
+
+std::string Url::url() const {
+ std::string url;
+ if (!parsed_) {
+ return url;
+ }
+ url.reserve(kDefaultUrlSpace);
+ if (absolute_) {
+ url.append(scheme_);
+ url.append("://");
+ url.append(host_);
+ bool is_default_port =
+ port_ == 0 ||
+ (scheme_ == kSchemeHttp && port_ == kDefaultHttpPort) ||
+ (scheme_ == kSchemeHttps && port_ == kDefaultHttpsPort);
+ if (!is_default_port) {
+ url.append(":");
+ url.append(std::to_string(port_));
+ }
+ }
+ url.append(path_);
+ if (!query_string_.empty()) {
+ url.append("?");
+ url.append(query_string_);
+ }
+ if (!fragment_.empty()) {
+ url.append("#");
+ url.append(fragment_);
+ }
+ return url;
+}
+
+bool Url::Scheme() {
+ Iter end;
+ SchemeState state = SchemeState::ALPHA;
+ for (Iter iter = current_; iter != end_; ++iter) {
+ switch (state) {
+ case SchemeState::ALPHA:
+ if (!std::isalpha(*iter)) {
+ return false;
+ }
+ state = SchemeState::ALPHA_NUM;
+ break;
+ case SchemeState::ALPHA_NUM:
+ if (*iter == ':') {
+ end = iter;
+ state = SchemeState::COLON;
+ break;
+ }
+ if (!std::isalnum(*iter) && *iter != '-' && *iter != '-' && *iter != '.') {
+ return false;
+ }
+ break;
+ case SchemeState::COLON:
+ if (*iter != '/') {
+ return false;
+ }
+ state = SchemeState::FIRST_SLASH;
+ break;
+ case SchemeState::FIRST_SLASH:
+ if (*iter != '/') {
+ return false;
+ }
+ state = SchemeState::SECOND_SLASH;
+ break;
+ case SchemeState::SECOND_SLASH:
+ scheme_.assign(current_, end);
+ current_ = iter;
+ return true;
+ }
+ }
+ return false;
+}
+
+bool Url::IPv6() {
+ IPv6State state = IPv6State::OPEN_SQUARE;
+ Iter iter = current_;
+ for (; iter != end_; ++iter) {
+ switch (state) {
+ case IPv6State::OPEN_SQUARE:
+ if (*iter != '[') {
+ return false;
+ }
+ state = IPv6State::ADDRESS;
+ break;
+ case IPv6State::ADDRESS:
+ if (*iter == ']') {
+ state = IPv6State::CLOSE_SQUARE;
+ } else if (!std::isxdigit(*iter) && *iter != ':') {
+ return false;
+ }
+ break;
+ case IPv6State::CLOSE_SQUARE:
+ goto exit_loop;
+ }
+ }
+ exit_loop: ;
+ if (state != IPv6State::CLOSE_SQUARE) {
+ return false;
+ }
+ host_.assign(current_, iter);
+ current_ = iter;
+ return true;
+}
+
+bool Url::Host() {
+ Iter iter = current_;
+ while (iter != end_ && !IsReserved(*iter)) {
+ ++iter;
+ }
+ if (iter == current_) {
+ // zero-length host names aren't allowed
+ return false;
+ }
+ host_.assign(current_, iter);
+ current_ = iter;
+ return true;
+}
+
+bool Url::Port() {
+ PortState state = PortState::COLON;
+ Iter start;
+ Iter iter = current_;
+ for (; iter != end_; ++iter) {
+ switch (state) {
+ case PortState::COLON:
+ if (*iter != ':') {
+ return false;
+ }
+ state = PortState::FIRST_DIGIT;
+ break;
+ case PortState::FIRST_DIGIT:
+ if (!std::isdigit(*iter)) {
+ return false;
+ }
+ start = iter;
+ state = PortState::DIGIT;
+ break;
+ case PortState::DIGIT:
+ if (!std::isdigit(*iter)) {
+ goto exit_loop;
+ }
+ break;
+ }
+ }
+ exit_loop: ;
+ if (state != PortState::DIGIT) {
+ return false;
+ }
+ std::string port(start, iter);
+ int portnum = std::stoi(port);
+ if (portnum < 1 || portnum > 65535) {
+ return false;
+ }
+ current_ = iter;
+ port_ = portnum;
+ return true;
+}
+
+bool Url::AbsolutePath() {
+ PathState state = PathState::LEADING_SLASH;
+ Iter iter = current_;
+ for (; iter != end_; ++iter) {
+ switch (state) {
+ case PathState::LEADING_SLASH:
+ if (*iter != '/') {
+ return false;
+ }
+ state = PathState::SLASH;
+ break;
+ case PathState::SLASH:
+ // Two consecutive slashes are invalid
+ if (!IsPchar(*iter)) {
+ goto exit_loop;
+ }
+ state = PathState::PCHAR;
+ break;
+ case PathState::PCHAR:
+ if (*iter == '/') {
+ state = PathState::SLASH;
+ } else if (!IsPchar(*iter)) {
+ goto exit_loop;
+ }
+ break;
+ }
+ }
+ exit_loop: ;
+ path_.assign(current_, iter);
+ current_ = iter;
+ return true;
+}
+
+bool Url::QueryString() {
+ QueryStringState state = QueryStringState::QUESTION_MARK;
+ Iter start;
+ Iter iter = current_;
+ for (; iter != end_; ++iter) {
+ switch (state) {
+ case QueryStringState::QUESTION_MARK:
+ if (*iter != '?') {
+ return false;
+ }
+ state = QueryStringState::FIRST_CHAR;
+ break;
+ case QueryStringState::FIRST_CHAR:
+ start = iter;
+ state = QueryStringState::QUERY;
+ // fall through
+ case QueryStringState::QUERY:
+ if (!IsQuery(*iter)) {
+ goto exit_loop;
+ }
+ break;
+ }
+ }
+ exit_loop: ;
+ if (state == QueryStringState::QUERY) {
+ query_string_.assign(start, iter);
+ }
+ current_ = iter;
+ return true;
+}
+
+bool Url::Fragment() {
+ FragmentState state = FragmentState::HASH;
+ Iter start;
+ Iter iter = current_;
+ for (; iter != end_; ++iter) {
+ switch (state) {
+ case FragmentState::HASH:
+ if (*iter != '#') {
+ return false;
+ }
+ state = FragmentState::FIRST_CHAR;
+ break;
+ case FragmentState::FIRST_CHAR:
+ start = iter;
+ state = FragmentState::FRAGMENT;
+ // fall through
+ case FragmentState::FRAGMENT:
+ if (!IsFragment(*iter)) {
+ goto exit_loop;
+ }
+ break;
+ }
+ }
+ exit_loop: ;
+ if (state == FragmentState::FRAGMENT) {
+ fragment_.assign(start, iter);
+ }
+ current_ = iter;
+ return true;
+}
+
+void Url::UpdateAbsolute() {
+ absolute_ = !scheme_.empty() || !host_.empty();
+}
+
+} // namespace http
diff --git a/speedtest/url.h b/speedtest/url.h
new file mode 100644
index 0000000..a31017b
--- /dev/null
+++ b/speedtest/url.h
@@ -0,0 +1,90 @@
+#ifndef HTTP_URL_H
+#define HTTP_URL_H
+
+#include <string>
+
+namespace http {
+
+class Url;
+
+bool operator==(const Url &url1, const Url &url2);
+
+// Partial implementation of a URL parser. This is needed because URLs need
+// to be manipulated for creating URLs for Speedtest, which is otherwise
+// awkward.
+//
+// This is implemented as a series of state machines rather than being
+// regex based as the C++11 regex implementation, at least in GCC 4.8.x,
+// is incomplete and buggy. Plus regular expressions to properly parse URLs
+// are complex.
+//
+// TODO(wshields): authority
+// TODO(wshields): pct-encoding
+// TODO(wshields): strict IPV6 parsing
+// TODO(wshields): validate setters
+// TODO(wshields): move query string param handling here
+class Url {
+ public:
+ Url();
+ Url(const Url &other);
+ explicit Url(const char *url);
+ Url &operator=(const Url &other);
+
+ bool Parse(const std::string &url);
+
+ bool ok() const { return parsed_; }
+ bool absolute() const { return absolute_; }
+
+ const std::string &scheme() const { return scheme_; }
+ void set_scheme(const std::string &scheme);
+
+ const std::string &host() const { return host_; }
+ void set_host(const std::string &host);
+
+ int port() const { return port_; }
+ void set_port(int port);
+
+ const std::string &path() const { return path_; }
+ void set_path(const std::string &path);
+ void clear_path();
+
+ const std::string &query_string() const { return query_string_; }
+ void set_query_string(const std::string &query_string);
+ void clear_query_string();
+
+ const std::string &fragment() const { return fragment_; }
+ void set_fragment(const std::string &fragment);
+ void clear_fragment();
+
+ std::string url() const;
+
+ friend bool operator==(const Url &url1, const Url &url2);
+
+ private:
+ using Iter = std::string::const_iterator;
+
+ bool Scheme();
+ bool IPv6();
+ bool Host();
+ bool Port();
+ bool AbsolutePath();
+ bool QueryString();
+ bool Fragment();
+
+ void UpdateAbsolute();
+
+ bool parsed_;
+ bool absolute_;
+ Iter current_;
+ Iter end_;
+ std::string scheme_;
+ std::string host_;
+ int port_;
+ std::string path_;
+ std::string query_string_;
+ std::string fragment_;
+};
+
+} // namespace http
+
+#endif // HTTP_URL_H
diff --git a/speedtest/url_test.cc b/speedtest/url_test.cc
new file mode 100644
index 0000000..5837ffe
--- /dev/null
+++ b/speedtest/url_test.cc
@@ -0,0 +1,426 @@
+#include <gtest/gtest.h>
+
+#include "url.h"
+
+namespace http {
+namespace {
+
+class InvalidUrlTest : public testing::TestWithParam<const char *> {
+};
+
+TEST_P(InvalidUrlTest, Invalid) {
+ Url url;
+ EXPECT_FALSE(url.Parse(GetParam()));
+ EXPECT_FALSE(url.ok());
+}
+
+INSTANTIATE_TEST_CASE_P(
+ InvalidUrls,
+ InvalidUrlTest,
+ testing::Values(
+ "http://",
+ "//",
+ "http://foo//",
+ "https://example.com:/",
+ "http://example.com:234567",
+ "2600:55::00ad:d001",
+ "2600:55::00ad:d001]",
+ "[2600:55::00ad:d001",
+ "[2600:55::00ad:d001]:"
+ ));
+
+void VerifyOk(const std::string &raw_url,
+ bool absolute,
+ const std::string &scheme,
+ const std::string &host,
+ int port,
+ const std::string &path,
+ const std::string &query_string,
+ const std::string &fragment,
+ const std::string &normal_url) {
+ Url url;
+ url.Parse(raw_url);
+ EXPECT_TRUE(url.ok());
+ EXPECT_EQ(absolute, url.absolute());
+ EXPECT_EQ(scheme, url.scheme());
+ EXPECT_EQ(host, url.host());
+ EXPECT_EQ(port, url.port());
+ EXPECT_EQ(path, url.path());
+ EXPECT_EQ(query_string, url.query_string());
+ EXPECT_EQ(fragment, url.fragment());
+ EXPECT_EQ(normal_url, url.url());
+}
+
+TEST(UrlTest, Empty_NotOk) {
+ Url url;
+ EXPECT_FALSE(url.ok());
+ EXPECT_EQ("", url.scheme());
+ EXPECT_EQ("", url.host());
+ EXPECT_EQ(0, url.port());
+ EXPECT_EQ("", url.path());
+ EXPECT_EQ("", url.query_string());
+ EXPECT_EQ("", url.fragment());
+}
+
+TEST(UrlTest, HostOnly_Ok) {
+ VerifyOk("www.example.com",
+ true,
+ "http",
+ "www.example.com",
+ 80,
+ "/",
+ "",
+ "",
+ "http://www.example.com/");
+}
+
+TEST(UrlTest, HostOnlyTrailingSlash_Ok) {
+ VerifyOk("www.example.com/",
+ true,
+ "http",
+ "www.example.com",
+ 80,
+ "/",
+ "",
+ "",
+ "http://www.example.com/");
+}
+
+TEST(UrlTest, HostPort_Ok) {
+ VerifyOk("www.example.com:3111",
+ true,
+ "http",
+ "www.example.com",
+ 3111,
+ "/",
+ "",
+ "",
+ "http://www.example.com:3111/");
+}
+
+TEST(UrlTest, HostPortTrailingSlash_Ok) {
+ VerifyOk("www.example.com:3111/",
+ true,
+ "http",
+ "www.example.com",
+ 3111,
+ "/",
+ "",
+ "",
+ "http://www.example.com:3111/");
+}
+
+TEST(UrlTest, SchemeAndHost_Ok) {
+ VerifyOk("https://www.example.com",
+ true,
+ "https",
+ "www.example.com",
+ 443,
+ "/",
+ "",
+ "",
+ "https://www.example.com/");
+}
+
+TEST(UrlTest, SchemeAndHostTrailingSlash_Ok) {
+ VerifyOk("https://www.example.com/",
+ true,
+ "https",
+ "www.example.com",
+ 443,
+ "/",
+ "",
+ "",
+ "https://www.example.com/");
+}
+
+TEST(UrlTest, SchemeHostPort_Ok) {
+ VerifyOk("http://www.example.com:7001",
+ true,
+ "http",
+ "www.example.com",
+ 7001,
+ "/",
+ "",
+ "",
+ "http://www.example.com:7001/");
+}
+
+TEST(UrlTest, SchemeHostPortTrailingSlash_Ok) {
+ VerifyOk("http://www.example.com:7001/",
+ true,
+ "http",
+ "www.example.com",
+ 7001,
+ "/",
+ "",
+ "",
+ "http://www.example.com:7001/");
+}
+
+TEST(UrlTest, AbsolutePathOnly_Ok) {
+ VerifyOk("/path/to/resource",
+ false,
+ "",
+ "",
+ 0,
+ "/path/to/resource",
+ "",
+ "",
+ "/path/to/resource");
+}
+
+TEST(UrlTest, HostPath_Ok) {
+ VerifyOk("foo/bar/path",
+ true,
+ "http",
+ "foo",
+ 80,
+ "/bar/path",
+ "",
+ "",
+ "http://foo/bar/path");
+}
+
+TEST(UrlTest, SchemaHostQueryString_Ok) {
+ VerifyOk("http://localhost?foo=bar&a=b",
+ true,
+ "http",
+ "localhost",
+ 80,
+ "/",
+ "foo=bar&a=b",
+ "",
+ "http://localhost/?foo=bar&a=b");
+}
+
+TEST(UrlTest, SchemaHostSlashQueryString_Ok) {
+ VerifyOk("http://localhost/?foo=bar&abc=def",
+ true,
+ "http",
+ "localhost",
+ 80,
+ "/",
+ "foo=bar&abc=def",
+ "",
+ "http://localhost/?foo=bar&abc=def");
+}
+
+TEST(UrlTest, SchemaHostPathQueryString_Ok) {
+ VerifyOk("http://localhost/cgi-bin/download?foo=bar",
+ true,
+ "http",
+ "localhost",
+ 80,
+ "/cgi-bin/download",
+ "foo=bar",
+ "",
+ "http://localhost/cgi-bin/download?foo=bar");
+}
+
+TEST(UrlTest, Fragment_Ok) {
+ VerifyOk("#foo",
+ false,
+ "",
+ "",
+ 0,
+ "",
+ "",
+ "foo",
+ "#foo");
+}
+
+TEST(UrlTest, HostFragment_Ok) {
+ VerifyOk("www.example.com#foo",
+ true,
+ "http",
+ "www.example.com",
+ 80,
+ "/",
+ "",
+ "foo",
+ "http://www.example.com/#foo");
+}
+
+TEST(UrlTest, HostPortSlashFragment_Ok) {
+ VerifyOk("https://www.example.com:3011/#foo",
+ true,
+ "https",
+ "www.example.com",
+ 3011,
+ "/",
+ "",
+ "foo",
+ "https://www.example.com:3011/#foo");
+}
+
+TEST(UrlTest, IPv6_Ok) {
+ VerifyOk("[e712:ff00:3::ad]",
+ true,
+ "http",
+ "[e712:ff00:3::ad]",
+ 80,
+ "/",
+ "",
+ "",
+ "http://[e712:ff00:3::ad]/");
+}
+
+TEST(UrlTest, IPv6Slash_Ok) {
+ VerifyOk("[e712:ff00:3::ad]/",
+ true,
+ "http",
+ "[e712:ff00:3::ad]",
+ 80,
+ "/",
+ "",
+ "",
+ "http://[e712:ff00:3::ad]/");
+}
+
+TEST(UrlTest, IPv6Path_Ok) {
+ VerifyOk("[e712:ff00:3::ad]/foo/bar",
+ true,
+ "http",
+ "[e712:ff00:3::ad]",
+ 80,
+ "/foo/bar",
+ "",
+ "",
+ "http://[e712:ff00:3::ad]/foo/bar");
+}
+
+TEST(UrlTest, IPv6Port_Ok) {
+ VerifyOk("[e712:ff00:3::ad]:3303",
+ true,
+ "http",
+ "[e712:ff00:3::ad]",
+ 3303,
+ "/",
+ "",
+ "",
+ "http://[e712:ff00:3::ad]:3303/");
+}
+
+TEST(UrlTest, IPv6PortSlash_Ok) {
+ VerifyOk("[e712:ff00:3::ad]:3303/",
+ true,
+ "http",
+ "[e712:ff00:3::ad]",
+ 3303,
+ "/",
+ "",
+ "",
+ "http://[e712:ff00:3::ad]:3303/");
+}
+
+TEST(UrlTest, IPv6PortPath_Ok) {
+ VerifyOk("[e712:ff00:3::ad]:3303/abc/def",
+ true,
+ "http",
+ "[e712:ff00:3::ad]",
+ 3303,
+ "/abc/def",
+ "",
+ "",
+ "http://[e712:ff00:3::ad]:3303/abc/def");
+}
+
+TEST(UrlTest, SchemeIPv6_Ok) {
+ VerifyOk("https://[e712:ff00:3::ad]",
+ true,
+ "https",
+ "[e712:ff00:3::ad]",
+ 443,
+ "/",
+ "",
+ "",
+ "https://[e712:ff00:3::ad]/");
+}
+
+TEST(UrlTest, SchemeIPv6Slash_Ok) {
+ VerifyOk("https://[e712:ff00:3::ad]/",
+ true,
+ "https",
+ "[e712:ff00:3::ad]",
+ 443,
+ "/",
+ "",
+ "",
+ "https://[e712:ff00:3::ad]/");
+}
+
+TEST(UrlTest, SchemeIPv6Path_Ok) {
+ VerifyOk("https://[e712:ff00:3::ad]/def/ghi/",
+ true,
+ "https",
+ "[e712:ff00:3::ad]",
+ 443,
+ "/def/ghi/",
+ "",
+ "",
+ "https://[e712:ff00:3::ad]/def/ghi/");
+}
+
+TEST(UrlTest, SchemeIPv6Port_Ok) {
+ VerifyOk("https://[e712:ff00:3::ad]:3303",
+ true,
+ "https",
+ "[e712:ff00:3::ad]",
+ 3303,
+ "/",
+ "",
+ "",
+ "https://[e712:ff00:3::ad]:3303/");
+}
+
+TEST(UrlTest, SchemeIPv6PortSlash_Ok) {
+ VerifyOk("https://[e712:ff00:3::ad]:3303/",
+ true,
+ "https",
+ "[e712:ff00:3::ad]",
+ 3303,
+ "/",
+ "",
+ "",
+ "https://[e712:ff00:3::ad]:3303/");
+}
+
+TEST(UrlTest, SchemeIPv6PortPath_Ok) {
+ VerifyOk("https://[e712:ff00:3::ad]:3303/dir",
+ true,
+ "https",
+ "[e712:ff00:3::ad]",
+ 3303,
+ "/dir",
+ "",
+ "",
+ "https://[e712:ff00:3::ad]:3303/dir");
+}
+
+TEST(UrlTest, FullHost_Ok) {
+ VerifyOk("http://www.example.com:7889/path/to/foo?a=b&c=d#foo",
+ true,
+ "http",
+ "www.example.com",
+ 7889,
+ "/path/to/foo",
+ "a=b&c=d",
+ "foo",
+ "http://www.example.com:7889/path/to/foo?a=b&c=d#foo");
+}
+
+TEST(UrlTest, FullIPv6_Ok) {
+ VerifyOk("http://[26e5:0030:2:4::efad:0001:200e]:2345/path?a=b&c=d#foo",
+ true,
+ "http",
+ "[26e5:0030:2:4::efad:0001:200e]",
+ 2345,
+ "/path",
+ "a=b&c=d",
+ "foo",
+ "http://[26e5:0030:2:4::efad:0001:200e]:2345/path?a=b&c=d#foo");
+}
+
+} // namespace
+} // namespace http