summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKonstantin Käfer <mail@kkaefer.com>2016-12-16 18:46:26 +0100
committerKonstantin Käfer <mail@kkaefer.com>2016-12-21 13:00:32 +0100
commit77db372123920ca6c3233c0b2c7393703db66635 (patch)
tree23342ce785de100e2ce6a451a14720c91997b1cc
parent2b38af877871e234d9afb415dab68aa0543403da (diff)
downloadqtlocation-mapboxgl-77db372123920ca6c3233c0b2c7393703db66635.tar.gz
[core] refactor URL parsing
-rw-r--r--src/mbgl/util/mapbox.cpp228
-rw-r--r--src/mbgl/util/url.cpp102
-rw-r--r--src/mbgl/util/url.hpp41
-rw-r--r--test/util/mapbox.test.cpp5
-rw-r--r--test/util/url.test.cpp182
5 files changed, 421 insertions, 137 deletions
diff --git a/src/mbgl/util/mapbox.cpp b/src/mbgl/util/mapbox.cpp
index 839924d77c..642945ba07 100644
--- a/src/mbgl/util/mapbox.cpp
+++ b/src/mbgl/util/mapbox.cpp
@@ -1,10 +1,12 @@
#include <mbgl/util/mapbox.hpp>
#include <mbgl/util/constants.hpp>
#include <mbgl/util/logging.hpp>
+#include <mbgl/util/url.hpp>
#include <stdexcept>
#include <vector>
#include <iostream>
+#include <cstring>
namespace {
@@ -21,188 +23,144 @@ bool isMapboxURL(const std::string& url) {
return url.compare(0, protocolLength, protocol) == 0;
}
-static std::vector<std::string> getMapboxURLPathname(const std::string& url) {
- std::vector<std::string> pathname;
- auto startIndex = protocolLength;
- auto end = url.find_first_of("?#");
- if (end == std::string::npos) {
- end = url.length();
- }
- while (startIndex < end) {
- auto endIndex = url.find("/", startIndex);
- if (endIndex == std::string::npos) {
- endIndex = end;
- }
- pathname.push_back(url.substr(startIndex, endIndex - startIndex));
- startIndex = endIndex + 1;
- }
- return pathname;
-}
-
-static std::pair<std::string, std::size_t> normalizeQuery(const std::string& url) {
- std::string query;
-
- auto queryIdx = url.find("?");
- if (queryIdx != std::string::npos) {
- query = url.substr(queryIdx + 1, url.length() - queryIdx + 1);
- if (!query.empty()) {
- query = "&" + query;
- }
- }
-
- return std::make_pair(query, queryIdx);
+static bool equals(const std::string& str, const URL::Segment& segment, const char* ref) {
+ return str.compare(segment.first, segment.second, ref) == 0;
}
-std::string normalizeSourceURL(const std::string& baseURL, const std::string& url, const std::string& accessToken) {
- if (!isMapboxURL(url)) {
- return url;
+std::string normalizeSourceURL(const std::string& baseURL,
+ const std::string& str,
+ const std::string& accessToken) {
+ if (!isMapboxURL(str)) {
+ return str;
}
-
if (accessToken.empty()) {
- throw std::runtime_error("You must provide a Mapbox API access token for Mapbox tile sources");
+ throw std::runtime_error(
+ "You must provide a Mapbox API access token for Mapbox tile sources");
}
- auto query = normalizeQuery(url);
- return baseURL + "/v4/" + url.substr(protocolLength, query.second - protocolLength) + ".json?access_token=" + accessToken + "&secure" + query.first;
+ const URL url(str);
+ const auto tpl = baseURL + "/v4/{domain}.json?access_token=" + accessToken + "&secure";
+ return transformURL(tpl, str, url);
}
-std::string normalizeStyleURL(const std::string& baseURL, const std::string& url, const std::string& accessToken) {
- if (!isMapboxURL(url)) {
- return url;
+std::string normalizeStyleURL(const std::string& baseURL,
+ const std::string& str,
+ const std::string& accessToken) {
+ if (!isMapboxURL(str)) {
+ return str;
}
- const auto pathname = getMapboxURLPathname(url);
- if (pathname.size() < 3) {
+ const URL url(str);
+ if (!equals(str, url.domain, "styles")) {
Log::Error(Event::ParseStyle, "Invalid style URL");
- return url;
+ return str;
}
- const auto& user = pathname[1];
- const auto& id = pathname[2];
- const bool isDraft = pathname.size() > 3;
- return baseURL + "/styles/v1/" + user + "/" + id + (isDraft ? "/draft" : "") + "?access_token=" + accessToken + normalizeQuery(url).first;
+ const auto tpl = baseURL + "/styles/v1/{path}?access_token=" + accessToken;
+ return transformURL(tpl, str, url);
}
-std::string normalizeSpriteURL(const std::string& baseURL, const std::string& url, const std::string& accessToken) {
- if (!isMapboxURL(url)) {
- return url;
- }
-
- const auto pathname = getMapboxURLPathname(url);
- if (pathname.size() < 3) {
- Log::Error(Event::ParseStyle, "Invalid sprite URL");
- return url;
+std::string normalizeSpriteURL(const std::string& baseURL,
+ const std::string& str,
+ const std::string& accessToken) {
+ if (!isMapboxURL(str)) {
+ return str;
}
- const auto& user = pathname[1];
- const bool isDraft = pathname.size() > 3;
-
- const auto& name = isDraft ? pathname[3] : pathname[2];
- const size_t index = name.find_first_of("@.");
- if (index == std::string::npos) {
+ const URL url(str);
+ if (!equals(str, url.domain, "sprites")) {
Log::Error(Event::ParseStyle, "Invalid sprite URL");
- return url;
+ return str;
}
- const auto& extension = name.substr(index);
-
- if (isDraft) {
- const auto& id = pathname[2];
- return baseURL + "/styles/v1/" + user + "/" + id + "/draft/sprite" + extension +
- "?access_token=" + accessToken;
- } else {
- const auto& id = pathname[2].substr(0, index);
- return baseURL + "/styles/v1/" + user + "/" + id + "/sprite" + extension + "?access_token=" +
- accessToken;
- }
+ const auto tpl =
+ baseURL + "/styles/v1/{directory}{filename}/sprite{extension}?access_token=" + accessToken;
+ return transformURL(tpl, str, url);
}
-std::string normalizeGlyphsURL(const std::string& baseURL, const std::string& url, const std::string& accessToken) {
- if (!isMapboxURL(url)) {
- return url;
+std::string normalizeGlyphsURL(const std::string& baseURL,
+ const std::string& str,
+ const std::string& accessToken) {
+ if (!isMapboxURL(str)) {
+ return str;
}
- const auto pathname = getMapboxURLPathname(url);
- if (pathname.size() < 4) {
- Log::Error(Event::ParseStyle, "Invalid glyph URL");
- return url;
+ const URL url(str);
+ if (!equals(str, url.domain, "fonts")) {
+ Log::Error(Event::ParseStyle, "Invalid glyph URL");
+ return str;
}
- const auto& user = pathname[1];
- const auto& fontstack = pathname[2];
- const auto& range = pathname[3];
-
- return baseURL + "/fonts/v1/" + user + "/" + fontstack + "/" + range + "?access_token=" + accessToken;
+ const auto tpl = baseURL + "/fonts/v1/{path}?access_token=" + accessToken;
+ return transformURL(tpl, str, url);
}
-std::string normalizeTileURL(const std::string& baseURL, const std::string& url, const std::string& accessToken) {
- if (!isMapboxURL(url)) {
- return url;
+std::string normalizeTileURL(const std::string& baseURL,
+ const std::string& str,
+ const std::string& accessToken) {
+ if (!isMapboxURL(str)) {
+ return str;
}
- auto query = normalizeQuery(url);
- return baseURL + "/v4/" + url.substr(sizeof("mapbox://tiles/") - 1, query.second - sizeof("mapbox://tiles/") + 1) + "?access_token=" + accessToken + normalizeQuery(url).first;
-}
-
-std::string canonicalizeTileURL(const std::string& url, SourceType type, uint16_t tileSize) {
- auto tilesetStartIdx = url.find("/v4/");
- if (tilesetStartIdx == std::string::npos) {
- return url;
+ const URL url(str);
+ if (!equals(str, url.domain, "tiles")) {
+ Log::Error(Event::ParseStyle, "Invalid tile URL");
+ return str;
}
- tilesetStartIdx += sizeof("/v4/") - 1;
+ const auto tpl = baseURL + "/v4/{path}?access_token=" + accessToken;
+ return transformURL(tpl, str, url);
+}
- const auto tilesetEndIdx = url.find("/", tilesetStartIdx);
- if (tilesetEndIdx == std::string::npos) {
- return url;
- }
+std::string
+canonicalizeTileURL(const std::string& str, const SourceType type, const uint16_t tileSize) {
+ const char* version = "v4/";
+ const size_t versionLen = strlen(version);
- auto queryIdx = url.rfind("?");
- if (queryIdx == std::string::npos) {
- queryIdx = url.length();
- }
+ const URL url(str);
+ const Path path(str, url.path.first, url.path.second);
- auto basenameIdx = url.rfind("/", queryIdx);
- if (basenameIdx == std::string::npos || basenameIdx == queryIdx - 1) {
- basenameIdx = url.length();
- } else {
- basenameIdx += 1;
+ // Make sure that we are dealing with a valid Mapbox tile URL.
+ // Has to be /v4/, with a valid filename + extension
+ if (str.compare(url.path.first, versionLen, version) != 0 || path.filename.second == 0 ||
+ path.extension.second <= 1) {
+ // Not a proper Mapbox tile URL.
+ return str;
}
- const auto extensionIdx = url.find(".", basenameIdx);
- if (extensionIdx == std::string::npos || extensionIdx == queryIdx - 1) {
- return url;
+ // Reassemble the canonical URL from the parts we've parsed before.
+ std::string result = "mapbox://tiles/";
+ result.append(str, path.directory.first + versionLen, path.directory.second - versionLen);
+ result.append(str, path.filename.first, path.filename.second);
+ if (type == SourceType::Raster) {
+ result += tileSize == util::tileSize ? "@2x" : "{ratio}";
}
- const auto tileset = url.substr(tilesetStartIdx, tilesetEndIdx - tilesetStartIdx);
- auto extension = url.substr(extensionIdx + 1, queryIdx - extensionIdx - 1);
-
#if !defined(__ANDROID__) && !defined(__APPLE__) && !defined(QT_IMAGE_DECODERS)
- // Replace PNG with WebP.
- if (extension == "png") {
- extension = "webp";
- }
+ const bool forceWebP = str.compare(path.extension.first, path.extension.second, ".png") == 0;
+#else
+ const bool forceWebP = false;
#endif // !defined(__ANDROID__) && !defined(__APPLE__) && !defined(QT_IMAGE_DECODERS)
- std::string result = "mapbox://tiles/" + tileset + "/{z}/{x}/{y}";
-
- if (type == SourceType::Raster) {
- result += tileSize == util::tileSize ? "@2x" : "{ratio}";
+ // Replace PNG with WebP if necessary.
+ if (forceWebP) {
+ result += ".webp";
+ } else {
+ result.append(str, path.extension.first, path.extension.second);
}
- result += "." + extension;
-
- // get the query and remove access_token, if more parameters exist, add them to the final result
- if (queryIdx != url.length()) {
- auto idx = queryIdx;
+ // Append the query string, minus the access token parameter.
+ if (url.query.second > 1) {
+ auto idx = url.query.first;
bool hasQuery = false;
while (idx != std::string::npos) {
idx++; // skip & or ?
- auto ampersandIdx = url.find("&", idx);
- if (url.substr(idx, 13) != "access_token=") {
- result += hasQuery ? "&" : "?";
- result += url.substr(idx, ampersandIdx != std::string::npos ? ampersandIdx - idx
- : std::string::npos);
+ auto ampersandIdx = str.find('&', idx);
+ const char* accessToken = "access_token=";
+ if (str.compare(idx, strlen(accessToken), accessToken) != 0) {
+ result.append(1, hasQuery ? '&' : '?');
+ result.append(str, idx, ampersandIdx != std::string::npos ? ampersandIdx - idx
+ : std::string::npos);
hasQuery = true;
}
idx = ampersandIdx;
diff --git a/src/mbgl/util/url.cpp b/src/mbgl/util/url.cpp
index 9831bc9038..f070135213 100644
--- a/src/mbgl/util/url.cpp
+++ b/src/mbgl/util/url.cpp
@@ -1,11 +1,12 @@
#include <mbgl/util/url.hpp>
+#include <mbgl/util/token.hpp>
#include <iomanip>
#include <sstream>
#include <string>
#include <cstdlib>
#include <algorithm>
-
+#include <cstring>
namespace {
@@ -78,5 +79,104 @@ bool isURL(const std::string& input) {
(it != input.end() && *it++ == '/');
}
+URL::URL(const std::string& str)
+ : query([&]() -> Segment {
+ const auto hashPos = str.find('#');
+ const auto queryPos = str.find('?');
+ if (queryPos == std::string::npos || hashPos < queryPos) {
+ return { hashPos != std::string::npos ? hashPos : str.size(), 0 };
+ }
+ return { queryPos, (hashPos != std::string::npos ? hashPos : str.size()) - queryPos };
+ }()),
+ scheme([&]() -> Segment {
+ auto schemeEnd = str.find(':');
+ return { 0, schemeEnd == std::string::npos || schemeEnd > query.first ? 0 : schemeEnd };
+ }()),
+ domain([&]() -> Segment {
+ auto domainPos = scheme.first + scheme.second;
+ while (domainPos < query.first && (str[domainPos] == ':' || str[domainPos] == '/')) {
+ ++domainPos;
+ }
+ const bool isData = str.compare(scheme.first, scheme.second, "data") == 0;
+ const auto endPos = str.find(isData ? ',' : '/', domainPos);
+ return { domainPos, std::min(query.first, endPos) - domainPos };
+ }()),
+ path([&]() -> Segment {
+ auto pathPos = domain.first + domain.second;
+ const bool isData = str.compare(scheme.first, scheme.second, "data") == 0;
+ if (isData) {
+ // Skip comma
+ pathPos++;
+ } else {
+ // Skip optional leading slash
+ while (pathPos < query.first && (str[pathPos] == '/')) {
+ ++pathPos;
+ }
+ }
+ return { pathPos, query.first - pathPos };
+ }()) {
+}
+
+Path::Path(const std::string& str, const size_t pos, const size_t count)
+ : directory([&]() -> Segment {
+ // Finds the string between pos and the first /, if it exists
+ const auto endPos = count == std::string::npos ? str.size() : pos + count;
+ const auto slashPos = str.rfind('/', endPos);
+ return { pos, slashPos == std::string::npos || slashPos < pos ? 0 : slashPos + 1 - pos };
+ }()),
+ extension([&]() -> Segment {
+ auto dotPos = str.rfind('.', pos + count);
+ const auto endPos = count == std::string::npos ? str.size() : pos + count;
+ // Count a preceding @2x to the file extension as well.
+ const char* factor = "@2x";
+ const size_t factorLen = strlen(factor);
+ if (dotPos >= factorLen && dotPos < endPos &&
+ str.compare(dotPos - factorLen, factorLen, factor) == 0) {
+ dotPos -= factorLen;
+ }
+ if (dotPos == std::string::npos || dotPos < directory.first + directory.second) {
+ return { endPos, 0 };
+ }
+ return { dotPos, endPos - dotPos };
+ }()),
+ filename([&]() -> Segment {
+ const auto filePos = directory.first + directory.second;
+ return { filePos, extension.first - filePos };
+ }()) {
+}
+
+std::string transformURL(const std::string& tpl, const std::string& str, const URL& url) {
+ auto result = util::replaceTokens(tpl, [&](const std::string& token) -> std::string {
+ if (token == "path") {
+ return str.substr(url.path.first, url.path.second);
+ } else if (token == "domain") {
+ return str.substr(url.domain.first, url.domain.second);
+ } else if (token == "scheme") {
+ return str.substr(url.scheme.first, url.scheme.second);
+ } else if (token == "directory") {
+ const Path path(str, url.path.first, url.path.second);
+ return str.substr(path.directory.first, path.directory.second);
+ } else if (token == "filename") {
+ const Path path(str, url.path.first, url.path.second);
+ return str.substr(path.filename.first, path.filename.second);
+ } else if (token == "extension") {
+ const Path path(str, url.path.first, url.path.second);
+ return str.substr(path.extension.first, path.extension.second);
+ }
+ return "";
+ });
+
+ // Append the query string if it exists.
+ if (url.query.second > 1) {
+ const auto amp = result.find('?') != std::string::npos ? result.size() : std::string::npos;
+ result.append(str, url.query.first, url.query.second);
+ // Transform the question mark to an ampersand if we had a query string previously.
+ if (amp < result.size()) {
+ result[amp] = '&';
+ }
+ }
+ return result;
+}
+
} // namespace util
} // namespace mbgl
diff --git a/src/mbgl/util/url.hpp b/src/mbgl/util/url.hpp
index 2d18f7476a..e361b07a7c 100644
--- a/src/mbgl/util/url.hpp
+++ b/src/mbgl/util/url.hpp
@@ -10,5 +10,46 @@ std::string percentEncode(const std::string&);
std::string percentDecode(const std::string&);
bool isURL(const std::string&);
+// Class that holds position + lenth pairs for scheme, domain, path + query string of a URL.
+class URL {
+public:
+ using Segment = std::pair<size_t, size_t>; // position, length
+
+ const Segment query;
+ const Segment scheme;
+ const Segment domain;
+ const Segment path;
+
+ URL(const std::string&);
+};
+
+// Class that holds position + lenth pairs for directory, extension, and filename of a path.
+// The extension will contain the preceding ., and optionally a preceding @2x specifier.
+// The filename will not contain the file extension.
+class Path {
+public:
+ using Segment = std::pair<size_t, size_t>; // position, length
+
+ const Segment directory;
+ const Segment extension;
+ const Segment filename;
+
+ Path(const std::string&, size_t pos = 0, size_t count = std::string::npos);
+};
+
+// Parses the given URL and replaces the tokens in template with parts of the URL.
+// When parsing "http://example.com/foo/bar/baz.png", valid tokens are:
+// * {scheme} == "http"
+// * {domain} == "example.com"
+// * {path} == "foo/bar/baz.png"
+// * {directory} == "foo/bar/"
+// * {filename} == "baz"
+// * {extension} == ".png"
+// The query string of the source URL will always be appended.
+std::string transformURL(const std::string& tpl, const std::string& url, const URL&);
+inline std::string transformURL(const std::string& tpl, const std::string& url) {
+ return transformURL(tpl, url, URL(url));
+}
+
} // namespace util
} // namespace mbgl
diff --git a/test/util/mapbox.test.cpp b/test/util/mapbox.test.cpp
index 70bb8d0700..452106d6e6 100644
--- a/test/util/mapbox.test.cpp
+++ b/test/util/mapbox.test.cpp
@@ -91,7 +91,7 @@ TEST(Mapbox, SpriteURL) {
"https://api.mapbox.com/styles/v1/mapbox/streets-v8/draft/sprite@2x.png?access_token=key",
mbgl::util::mapbox::normalizeSpriteURL(util::API_BASE_URL, "mapbox://sprites/mapbox/streets-v8/draft@2x.png", "key"));
EXPECT_EQ(
- "mapbox://sprites/mapbox/streets-v9?fresh=true.png",
+ "https://api.mapbox.com/styles/v1/mapbox/streets-v9/sprite?access_token=key&fresh=true.png",
mbgl::util::mapbox::normalizeSpriteURL(util::API_BASE_URL,
"mapbox://sprites/mapbox/streets-v9?fresh=true.png",
"key"));
@@ -139,6 +139,9 @@ TEST(Mapbox, CanonicalURL) {
"mapbox://tiles/a.b/{z}/{x}/{y}.vector.pbf",
mbgl::util::mapbox::canonicalizeTileURL("http://api.mapbox.com/v4/a.b/{z}/{x}/{y}.vector.pbf?access_token=key", SourceType::Vector, 512));
EXPECT_EQ(
+ "mapbox://tiles/a.b/{z}/{x}/{y}.vector.pbf",
+ mbgl::util::mapbox::canonicalizeTileURL("https://api.mapbox.cn/v4/a.b/{z}/{x}/{y}.vector.pbf?access_token=key", SourceType::Vector, 512));
+ EXPECT_EQ(
"mapbox://tiles/a.b,c.d/{z}/{x}/{y}.vector.pbf",
mbgl::util::mapbox::canonicalizeTileURL("http://api.mapbox.com/v4/a.b,c.d/{z}/{x}/{y}.vector.pbf?access_token=key", SourceType::Vector, 512));
EXPECT_EQ(
diff --git a/test/util/url.test.cpp b/test/util/url.test.cpp
index c0ee30efab..00b761ce2e 100644
--- a/test/util/url.test.cpp
+++ b/test/util/url.test.cpp
@@ -23,3 +23,185 @@ TEST(URL, isURL) {
EXPECT_FALSE(isURL("test/mapbox://foo"));
EXPECT_FALSE(isURL("123://foo"));
}
+
+TEST(URL, Scheme) {
+ EXPECT_EQ(URL::Segment({ 0, 4 }), URL("http://example.com/test?query=foo").scheme);
+ EXPECT_EQ(URL::Segment({ 0, 0 }), URL("htt").scheme);
+ EXPECT_EQ(URL::Segment({ 0, 6 }), URL("mapbox://").scheme);
+ EXPECT_EQ(URL::Segment({ 0, 6 }), URL("mapbox:/#").scheme);
+ EXPECT_EQ(URL::Segment({ 0, 0 }), URL("://").scheme);
+ EXPECT_EQ(URL::Segment({ 0, 0 }), URL("").scheme);
+ EXPECT_EQ(URL::Segment({ 0, 0 }), URL("http?query://baz").scheme);
+ EXPECT_EQ(URL::Segment({ 0, 0 }), URL(":::").scheme);
+ EXPECT_EQ(URL::Segment({ 0, 4 }), URL("data:,Hello%2C%20World!").scheme);
+ EXPECT_EQ(URL::Segment({ 0, 4 }), URL("data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D").scheme);
+}
+
+TEST(URL, Query) {
+ EXPECT_EQ(URL::Segment({ 23, 10 }), URL("http://example.com/test?query=foo").query);
+ EXPECT_EQ(URL::Segment({ 23, 10 }), URL("http://example.com/test?query=foo#page-2").query);
+ EXPECT_EQ(URL::Segment({ 23, 0 }), URL("http://example.com/test#query=foo?page-2").query);
+ EXPECT_EQ(URL::Segment({ 0, 10 }), URL("?query=foo").query);
+ EXPECT_EQ(URL::Segment({ 0, 10 }), URL("?query=foo#bar").query);
+ EXPECT_EQ(URL::Segment({ 0, 0 }), URL("#?query=foo#bar").query);
+ EXPECT_EQ(URL::Segment({ 4, 12 }), URL("http?query://baz").query);
+ EXPECT_EQ(URL::Segment({ 9, 4 }), URL("mapbox://?bar").query);
+ EXPECT_EQ(URL::Segment({ 12, 0 }), URL("mapbox://bar").query);
+ EXPECT_EQ(URL::Segment({ 0, 0 }), URL("").query);
+ EXPECT_EQ(URL::Segment({ 3, 0 }), URL(":::").query);
+ EXPECT_EQ(URL::Segment({ 23, 0 }), URL("data:,Hello%2C%20World!").query);
+ EXPECT_EQ(URL::Segment({ 47, 0 }), URL("data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D").query);
+}
+
+TEST(URL, Domain) {
+ EXPECT_EQ(URL::Segment({ 7, 11 }), URL("http://example.com/test?query=foo").domain);
+ EXPECT_EQ(URL::Segment({ 5, 11 }), URL("http:example.com/test?query=foo").domain);
+ EXPECT_EQ(URL::Segment({ 0, 3 }), URL("htt").domain);
+ EXPECT_EQ(URL::Segment({ 0, 4 }), URL("http?query://baz").domain);
+ EXPECT_EQ(URL::Segment({ 0, 6 }), URL("domain/foo?bar").domain);
+ EXPECT_EQ(URL::Segment({ 7, 0 }), URL("http://").domain);
+ EXPECT_EQ(URL::Segment({ 7, 6 }), URL("http://domain").domain);
+ EXPECT_EQ(URL::Segment({ 7, 6 }), URL("http://domain#bar").domain);
+ EXPECT_EQ(URL::Segment({ 7, 6 }), URL("http://domain/").domain);
+ EXPECT_EQ(URL::Segment({ 7, 6 }), URL("http://domain/foo").domain);
+ EXPECT_EQ(URL::Segment({ 7, 6 }), URL("http://domain/foo?").domain);
+ EXPECT_EQ(URL::Segment({ 0, 1 }), URL("h##p://domain/foo?bar").domain);
+ EXPECT_EQ(URL::Segment({ 7, 6 }), URL("http://domain/foo?bar").domain);
+ EXPECT_EQ(URL::Segment({ 7, 6 }), URL("http://domain/?").domain);
+ EXPECT_EQ(URL::Segment({ 7, 6 }), URL("http://domain?").domain);
+ EXPECT_EQ(URL::Segment({ 7, 6 }), URL("http://domain?foo").domain);
+ EXPECT_EQ(URL::Segment({ 3, 0 }), URL(":::").domain);
+ EXPECT_EQ(URL::Segment({ 5, 0 }), URL("data:,Hello%2C%20World!").domain);
+ EXPECT_EQ(URL::Segment({ 5, 17 }), URL("data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D").domain);
+}
+
+TEST(URL, Path) {
+ EXPECT_EQ(URL::Segment({ 19, 4 }), URL("http://example.com/test?query=foo").path);
+ EXPECT_EQ(URL::Segment({ 19, 4 }), URL("http://example.com/test?query=foo#bar").path);
+ EXPECT_EQ(URL::Segment({ 19, 4 }), URL("http://example.com/test#bar").path);
+ EXPECT_EQ(URL::Segment({ 18, 0 }), URL("http://example.com?query=foo").path);
+ EXPECT_EQ(URL::Segment({ 18, 0 }), URL("http://example.com#?query=foo").path);
+ EXPECT_EQ(URL::Segment({ 19, 0 }), URL("http://example.com/?query=foo").path);
+ EXPECT_EQ(URL::Segment({ 3, 0 }), URL(":::").path);
+ EXPECT_EQ(URL::Segment({ 13, 0 }), URL("http://domain").path);
+ EXPECT_EQ(URL::Segment({ 7, 3 }), URL("domain/foo?bar").path);
+ EXPECT_EQ(URL::Segment({ 6, 17 }), URL("data:,Hello%2C%20World!").path);
+ EXPECT_EQ(URL::Segment({ 23, 24 }), URL("data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D").path);
+}
+
+auto URLPath = [](const char* str) { const URL url(str); return Path(str, url.path.first, url.path.second); };
+
+TEST(Path, Directory) {
+ EXPECT_EQ(Path::Segment({ 0, 8 }), Path("foo/bar/baz.ext").directory);
+ EXPECT_EQ(Path::Segment({ 0, 8 }), Path("foo.bar/baz.ext").directory);
+ EXPECT_EQ(Path::Segment({ 0, 8 }), Path("foo.bar/baz").directory);
+ EXPECT_EQ(Path::Segment({ 0, 8 }), Path("foo/bar/.ext").directory);
+ EXPECT_EQ(Path::Segment({ 0, 4 }), Path("foo/bar@2x.png").directory);
+ EXPECT_EQ(Path::Segment({ 0, 4 }), Path("foo/b").directory);
+ EXPECT_EQ(Path::Segment({ 0, 4 }), Path("foo/").directory);
+ EXPECT_EQ(Path::Segment({ 0, 0 }), Path("foo").directory);
+ EXPECT_EQ(Path::Segment({ 0, 0 }), Path("foo.png").directory);
+ EXPECT_EQ(Path::Segment({ 0, 0 }), Path("").directory);
+}
+
+TEST(Path, URLDirectory) {
+ EXPECT_EQ(Path::Segment({ 19, 8 }), URLPath("http://example.com/foo/bar/baz.ext").directory);
+ EXPECT_EQ(Path::Segment({ 19, 8 }), URLPath("http://example.com/foo/bar/baz.ext?query=foo.bar").directory);
+ EXPECT_EQ(Path::Segment({ 19, 8 }), URLPath("http://example.com/foo.bar/baz.ext").directory);
+ EXPECT_EQ(Path::Segment({ 19, 8 }), URLPath("http://example.com/foo.bar/baz.ext?query=foo.bar").directory);
+ EXPECT_EQ(Path::Segment({ 19, 8 }), URLPath("http://example.com/foo.bar/baz").directory);
+ EXPECT_EQ(Path::Segment({ 19, 8 }), URLPath("http://example.com/foo.bar/baz?query=foo.bar").directory);
+ EXPECT_EQ(Path::Segment({ 19, 8 }), URLPath("http://example.com/foo/bar/.ext").directory);
+ EXPECT_EQ(Path::Segment({ 19, 8 }), URLPath("http://example.com/foo/bar/.ext?query=foo.bar").directory);
+ EXPECT_EQ(Path::Segment({ 19, 4 }), URLPath("http://example.com/foo/bar@2x.png").directory);
+ EXPECT_EQ(Path::Segment({ 19, 4 }), URLPath("http://example.com/foo/bar@2x.png?query=foo.bar").directory);
+ EXPECT_EQ(Path::Segment({ 19, 4 }), URLPath("http://example.com/foo/b").directory);
+ EXPECT_EQ(Path::Segment({ 19, 4 }), URLPath("http://example.com/foo/b?query=foo.bar").directory);
+ EXPECT_EQ(Path::Segment({ 19, 4 }), URLPath("http://example.com/foo/").directory);
+ EXPECT_EQ(Path::Segment({ 19, 4 }), URLPath("http://example.com/foo/?query=foo.bar").directory);
+ EXPECT_EQ(Path::Segment({ 19, 0 }), URLPath("http://example.com/foo").directory);
+ EXPECT_EQ(Path::Segment({ 19, 0 }), URLPath("http://example.com/foo?query=foo.bar").directory);
+ EXPECT_EQ(Path::Segment({ 19, 0 }), URLPath("http://example.com/foo.png").directory);
+ EXPECT_EQ(Path::Segment({ 19, 0 }), URLPath("http://example.com/foo.png?query=foo.bar").directory);
+ EXPECT_EQ(Path::Segment({ 19, 0 }), URLPath("http://example.com/").directory);
+ EXPECT_EQ(Path::Segment({ 19, 0 }), URLPath("http://example.com/?query=foo.bar").directory);
+ EXPECT_EQ(Path::Segment({ 18, 0 }), URLPath("http://example.com").directory);
+ EXPECT_EQ(Path::Segment({ 18, 0 }), URLPath("http://example.com?query=foo.bar").directory);
+}
+
+TEST(Path, Extension) {
+ EXPECT_EQ(Path::Segment({ 11, 4 }), Path("foo/bar/baz.ext").extension);
+ EXPECT_EQ(Path::Segment({ 11, 4 }), Path("foo.bar/baz.ext").extension);
+ EXPECT_EQ(Path::Segment({ 18, 4 }), Path("foo.bar/baz.vector.pbf").extension);
+ EXPECT_EQ(Path::Segment({ 11, 0 }), Path("foo.bar/baz").extension);
+ EXPECT_EQ(Path::Segment({ 8, 4 }), Path("foo/bar/.ext").extension);
+ EXPECT_EQ(Path::Segment({ 7, 7 }), Path("foo/bar@2x.png").extension);
+ EXPECT_EQ(Path::Segment({ 5, 0 }), Path("foo/b").extension);
+ EXPECT_EQ(Path::Segment({ 4, 0 }), Path("foo/").extension);
+ EXPECT_EQ(Path::Segment({ 3, 0 }), Path("foo").extension);
+ EXPECT_EQ(Path::Segment({ 3, 4 }), Path("foo.png").extension);
+ EXPECT_EQ(Path::Segment({ 0, 0 }), Path("").extension);
+}
+
+TEST(Path, URLExtension) {
+ EXPECT_EQ(Path::Segment({ 30, 4 }), URLPath("http://example.com/foo/bar/baz.ext").extension);
+ EXPECT_EQ(Path::Segment({ 30, 4 }), URLPath("http://example.com/foo/bar/baz.ext?query=foo.bar").extension);
+ EXPECT_EQ(Path::Segment({ 30, 4 }), URLPath("http://example.com/foo.bar/baz.ext").extension);
+ EXPECT_EQ(Path::Segment({ 30, 4 }), URLPath("http://example.com/foo.bar/baz.ext?query=foo.bar").extension);
+ EXPECT_EQ(Path::Segment({ 30, 0 }), URLPath("http://example.com/foo.bar/baz").extension);
+ EXPECT_EQ(Path::Segment({ 30, 0 }), URLPath("http://example.com/foo.bar/baz?query=foo.bar").extension);
+ EXPECT_EQ(Path::Segment({ 27, 4 }), URLPath("http://example.com/foo/bar/.ext").extension);
+ EXPECT_EQ(Path::Segment({ 27, 4 }), URLPath("http://example.com/foo/bar/.ext?query=foo.bar").extension);
+ EXPECT_EQ(Path::Segment({ 26, 7 }), URLPath("http://example.com/foo/bar@2x.png").extension);
+ EXPECT_EQ(Path::Segment({ 26, 7 }), URLPath("http://example.com/foo/bar@2x.png?query=foo.bar").extension);
+ EXPECT_EQ(Path::Segment({ 24, 0 }), URLPath("http://example.com/foo/b").extension);
+ EXPECT_EQ(Path::Segment({ 24, 0 }), URLPath("http://example.com/foo/b?query=foo.bar").extension);
+ EXPECT_EQ(Path::Segment({ 23, 0 }), URLPath("http://example.com/foo/").extension);
+ EXPECT_EQ(Path::Segment({ 23, 0 }), URLPath("http://example.com/foo/?query=foo.bar").extension);
+ EXPECT_EQ(Path::Segment({ 22, 0 }), URLPath("http://example.com/foo").extension);
+ EXPECT_EQ(Path::Segment({ 22, 0 }), URLPath("http://example.com/foo?query=foo.bar").extension);
+ EXPECT_EQ(Path::Segment({ 22, 4 }), URLPath("http://example.com/foo.png").extension);
+ EXPECT_EQ(Path::Segment({ 22, 4 }), URLPath("http://example.com/foo.png?query=foo.bar").extension);
+ EXPECT_EQ(Path::Segment({ 19, 0 }), URLPath("http://example.com/").extension);
+ EXPECT_EQ(Path::Segment({ 19, 0 }), URLPath("http://example.com/?query=foo.bar").extension);
+ EXPECT_EQ(Path::Segment({ 18, 0 }), URLPath("http://example.com").extension);
+ EXPECT_EQ(Path::Segment({ 18, 0 }), URLPath("http://example.com?query=foo.bar").extension);
+}
+
+TEST(Path, Filename) {
+ EXPECT_EQ(Path::Segment({ 8, 3 }), Path("foo/bar/baz.ext").filename);
+ EXPECT_EQ(Path::Segment({ 8, 3 }), Path("foo.bar/baz.ext").filename);
+ EXPECT_EQ(Path::Segment({ 8, 3 }), Path("foo.bar/baz").filename);
+ EXPECT_EQ(Path::Segment({ 8, 0 }), Path("foo/bar/.ext").filename);
+ EXPECT_EQ(Path::Segment({ 4, 3 }), Path("foo/bar@2x.png").filename);
+ EXPECT_EQ(Path::Segment({ 4, 1 }), Path("foo/b").filename);
+ EXPECT_EQ(Path::Segment({ 4, 0 }), Path("foo/").filename);
+ EXPECT_EQ(Path::Segment({ 0, 3 }), Path("foo").filename);
+ EXPECT_EQ(Path::Segment({ 0, 3 }), Path("foo.png").filename);
+ EXPECT_EQ(Path::Segment({ 0, 0 }), Path("").filename);
+}
+
+TEST(Path, URLFilename) {
+ EXPECT_EQ(Path::Segment({ 27, 3 }), URLPath("http://example.com/foo/bar/baz.ext").filename);
+ EXPECT_EQ(Path::Segment({ 27, 3 }), URLPath("http://example.com/foo/bar/baz.ext?query=foo.bar").filename);
+ EXPECT_EQ(Path::Segment({ 27, 3 }), URLPath("http://example.com/foo.bar/baz.ext").filename);
+ EXPECT_EQ(Path::Segment({ 27, 3 }), URLPath("http://example.com/foo.bar/baz.ext?query=foo.bar").filename);
+ EXPECT_EQ(Path::Segment({ 27, 3 }), URLPath("http://example.com/foo.bar/baz").filename);
+ EXPECT_EQ(Path::Segment({ 27, 3 }), URLPath("http://example.com/foo.bar/baz?query=foo.bar").filename);
+ EXPECT_EQ(Path::Segment({ 27, 0 }), URLPath("http://example.com/foo/bar/.ext").filename);
+ EXPECT_EQ(Path::Segment({ 27, 0 }), URLPath("http://example.com/foo/bar/.ext?query=foo.bar").filename);
+ EXPECT_EQ(Path::Segment({ 23, 3 }), URLPath("http://example.com/foo/bar@2x.png").filename);
+ EXPECT_EQ(Path::Segment({ 23, 3 }), URLPath("http://example.com/foo/bar@2x.png?query=foo.bar").filename);
+ EXPECT_EQ(Path::Segment({ 23, 1 }), URLPath("http://example.com/foo/b").filename);
+ EXPECT_EQ(Path::Segment({ 23, 1 }), URLPath("http://example.com/foo/b?query=foo.bar").filename);
+ EXPECT_EQ(Path::Segment({ 23, 0 }), URLPath("http://example.com/foo/").filename);
+ EXPECT_EQ(Path::Segment({ 23, 0 }), URLPath("http://example.com/foo/?query=foo.bar").filename);
+ EXPECT_EQ(Path::Segment({ 19, 3 }), URLPath("http://example.com/foo").filename);
+ EXPECT_EQ(Path::Segment({ 19, 3 }), URLPath("http://example.com/foo?query=foo.bar").filename);
+ EXPECT_EQ(Path::Segment({ 19, 3 }), URLPath("http://example.com/foo.png").filename);
+ EXPECT_EQ(Path::Segment({ 19, 3 }), URLPath("http://example.com/foo.png?query=foo.bar").filename);
+ EXPECT_EQ(Path::Segment({ 19, 0 }), URLPath("http://example.com/").filename);
+ EXPECT_EQ(Path::Segment({ 19, 0 }), URLPath("http://example.com/?query=foo.bar").filename);
+ EXPECT_EQ(Path::Segment({ 18, 0 }), URLPath("http://example.com").filename);
+ EXPECT_EQ(Path::Segment({ 18, 0 }), URLPath("http://example.com?query=foo.bar").filename);
+}