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