diff --git a/src/core/uri/CMakeLists.txt b/src/core/uri/CMakeLists.txt index 0272fe7dc..01ef2d69d 100644 --- a/src/core/uri/CMakeLists.txt +++ b/src/core/uri/CMakeLists.txt @@ -1,7 +1,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME uri PRIVATE_HEADERS error.h SOURCES uri.cc parse.cc accessors.cc setters.cc recompose.cc canonicalize.cc - resolution.cc filesystem.cc escaping.h normalize.h grammar.h) + resolution.cc filesystem.cc query.cc escaping.h normalize.h grammar.h) if(SOURCEMETA_CORE_INSTALL) sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME uri) diff --git a/src/core/uri/accessors.cc b/src/core/uri/accessors.cc index 34a2b7073..37b1ef069 100644 --- a/src/core/uri/accessors.cc +++ b/src/core/uri/accessors.cc @@ -72,10 +72,6 @@ auto URI::fragment() const -> std::optional { return this->fragment_; } -auto URI::query() const -> std::optional { - return this->query_; -} - auto URI::userinfo() const -> std::optional { return this->userinfo_; } diff --git a/src/core/uri/include/sourcemeta/core/uri.h b/src/core/uri/include/sourcemeta/core/uri.h index 8b853b837..081f7222a 100644 --- a/src/core/uri/include/sourcemeta/core/uri.h +++ b/src/core/uri/include/sourcemeta/core/uri.h @@ -10,15 +10,18 @@ // NOLINTEND(misc-include-cleaner) #include // std::convertible_to +#include // std::size_t, std::ptrdiff_t #include // std::uint32_t #include // std::filesystem #include // std::istream +#include // std::forward_iterator_tag #include // std::unique_ptr #include // std::optional #include // std::span #include // std::string #include // std::string_view #include // std::is_same_v +#include // std::pair #include // std::vector /// @defgroup uri URI @@ -311,7 +314,109 @@ class SOURCEMETA_CORE_URI_EXPORT URI { /// ``` auto fragment(const std::string_view fragment) -> URI &; - /// Get the non-dissected query part of the URI, if any. For example: + /// A non-owning, zero-copy view over the RFC 3986 query + /// component of a URI. Provides convenience access to query + /// parameters formatted as `name=value` pairs separated by `&`. + /// For example: + /// + /// ```cpp + /// #include + /// #include + /// + /// const sourcemeta::core::URI + /// uri{"https://www.sourcemeta.com/?foo=bar"}; + /// const auto query{uri.query()}; + /// assert(query.has_value()); + /// assert(query.value().raw() == "foo=bar"); + /// ``` + class SOURCEMETA_CORE_URI_EXPORT Query { + public: + /// Get the raw RFC 3986 query string this view was constructed + /// from. For example: + /// + /// ```cpp + /// #include + /// #include + /// + /// const sourcemeta::core::URI + /// uri{"https://www.sourcemeta.com/?foo=bar&baz=qux"}; + /// assert(uri.query().value().raw() == "foo=bar&baz=qux"); + /// ``` + [[nodiscard]] auto raw() const -> std::string_view; + + /// Look up the value of a query parameter by name. Returns + /// `std::nullopt` if no matching parameter exists. If multiple + /// parameters share the given name, the value of the first one + /// is returned. The returned value is not percent-decoded. For + /// example: + /// + /// ```cpp + /// #include + /// #include + /// + /// const sourcemeta::core::URI + /// uri{"https://www.sourcemeta.com/?foo=bar"}; + /// const auto query{uri.query()}; + /// assert(query.has_value()); + /// assert(query.value().at("foo").value() == "bar"); + /// ``` + [[nodiscard]] auto at(const std::string_view name) const + -> std::optional; + + /// Forward iterator over the `(name, value)` pairs of this + /// query view. Names and values are returned exactly as they + /// appear in the raw query, without percent-decoding + class SOURCEMETA_CORE_URI_EXPORT const_iterator { + public: + using iterator_category = std::forward_iterator_tag; + using value_type = std::pair; + using difference_type = std::ptrdiff_t; + using reference = const value_type &; + using pointer = const value_type *; + + const_iterator() = default; + const_iterator(const std::string_view raw, const std::size_t pair_start); + + [[nodiscard]] auto operator*() const -> reference; + [[nodiscard]] auto operator->() const -> pointer; + auto operator++() -> const_iterator &; + auto operator++(int) -> const_iterator; + [[nodiscard]] auto operator==(const const_iterator &other) const -> bool; + [[nodiscard]] auto operator!=(const const_iterator &other) const -> bool; + + private: +#if defined(_MSC_VER) +#pragma warning(disable : 4251) +#endif + std::string_view raw_{}; + std::size_t pair_start_{std::string_view::npos}; + value_type current_{}; +#if defined(_MSC_VER) +#pragma warning(default : 4251) +#endif + }; + + /// Iterator to the first `(name, value)` pair + [[nodiscard]] auto begin() const -> const_iterator; + + /// Past-the-end iterator + [[nodiscard]] auto end() const -> const_iterator; + + private: + friend class URI; + explicit Query(const std::string_view raw); + +#if defined(_MSC_VER) +#pragma warning(disable : 4251) +#endif + std::string_view raw_; +#if defined(_MSC_VER) +#pragma warning(default : 4251) +#endif + }; + + /// Get the query part of the URI as a navigable view, if any. + /// For example: /// /// ```cpp /// #include @@ -320,9 +425,9 @@ class SOURCEMETA_CORE_URI_EXPORT URI { /// const sourcemeta::core::URI /// uri{"https://www.sourcemeta.com/?foo=bar"}; /// assert(uri.query().has_value()); - /// assert(uri.query().value() == "foo=bar"); + /// assert(uri.query().value().raw() == "foo=bar"); /// ``` - [[nodiscard]] auto query() const -> std::optional; + [[nodiscard]] auto query() const -> std::optional; /// Set the query part of the URI. A leading `?` in the input is stripped. /// Passing an empty string clears the query. For example: @@ -334,7 +439,7 @@ class SOURCEMETA_CORE_URI_EXPORT URI { /// sourcemeta::core::URI uri{"https://www.sourcemeta.com"}; /// uri.query("foo=bar"); /// assert(uri.query().has_value()); - /// assert(uri.query().value() == "foo=bar"); + /// assert(uri.query().value().raw() == "foo=bar"); /// ``` auto query(const std::string_view query) -> URI &; diff --git a/src/core/uri/query.cc b/src/core/uri/query.cc new file mode 100644 index 000000000..dc9358cc9 --- /dev/null +++ b/src/core/uri/query.cc @@ -0,0 +1,105 @@ +#include + +#include // std::size_t, std::string_view::npos +#include // std::optional +#include // std::string_view +#include // std::pair + +namespace { + +auto parse_pair(const std::string_view raw, const std::size_t pair_start) + -> std::pair { + const auto separator{raw.find('&', pair_start)}; + const auto pair_end{separator == std::string_view::npos ? raw.size() + : separator}; + const auto pair_view{raw.substr(pair_start, pair_end - pair_start)}; + const auto equals_index{pair_view.find('=')}; + if (equals_index == std::string_view::npos) { + return {pair_view, std::string_view{}}; + } + return {pair_view.substr(0, equals_index), + pair_view.substr(equals_index + 1)}; +} + +} // namespace + +namespace sourcemeta::core { + +URI::Query::Query(const std::string_view raw) : raw_{raw} {} + +auto URI::Query::raw() const -> std::string_view { return this->raw_; } + +auto URI::query() const -> std::optional { + if (!this->query_.has_value()) { + return std::nullopt; + } + return URI::Query{this->query_.value()}; +} + +URI::Query::const_iterator::const_iterator(const std::string_view raw, + const std::size_t pair_start) + : raw_{raw}, pair_start_{pair_start} { + if (this->pair_start_ != std::string_view::npos) { + this->current_ = parse_pair(this->raw_, this->pair_start_); + } +} + +auto URI::Query::const_iterator::operator*() const -> reference { + return this->current_; +} + +auto URI::Query::const_iterator::operator->() const -> pointer { + return &this->current_; +} + +auto URI::Query::const_iterator::operator++() -> const_iterator & { + const auto separator{this->raw_.find('&', this->pair_start_)}; + if (separator == std::string_view::npos) { + this->pair_start_ = std::string_view::npos; + } else { + this->pair_start_ = separator + 1; + this->current_ = parse_pair(this->raw_, this->pair_start_); + } + return *this; +} + +auto URI::Query::const_iterator::operator++(int) -> const_iterator { + auto previous{*this}; + ++(*this); + return previous; +} + +auto URI::Query::const_iterator::operator==(const const_iterator &other) const + -> bool { + return this->pair_start_ == other.pair_start_; +} + +auto URI::Query::const_iterator::operator!=(const const_iterator &other) const + -> bool { + return !(*this == other); +} + +auto URI::Query::begin() const -> const_iterator { + if (this->raw_.empty()) { + return this->end(); + } + return const_iterator{this->raw_, 0}; +} + +auto URI::Query::end() const -> const_iterator { + return const_iterator{this->raw_, std::string_view::npos}; +} + +// First-wins on duplicates, matching WHATWG `URLSearchParams.get()` +// and most major URL libraries +auto URI::Query::at(const std::string_view name) const + -> std::optional { + for (const auto &pair : *this) { + if (pair.first == name) { + return pair.second; + } + } + return std::nullopt; +} + +} // namespace sourcemeta::core diff --git a/src/core/uri/recompose.cc b/src/core/uri/recompose.cc index ffcd955e5..1717e08a0 100644 --- a/src/core/uri/recompose.cc +++ b/src/core/uri/recompose.cc @@ -164,7 +164,7 @@ auto URI::recompose_without_fragment() const -> std::optional { const auto result_query{this->query()}; if (result_query.has_value()) { result += '?'; - escape_component_to_string(result, result_query.value(), + escape_component_to_string(result, result_query.value().raw(), URIEscapeMode::Fragment); } diff --git a/test/uri/uri_parse_test.cc b/test/uri/uri_parse_test.cc index 83958ddbd..c7e86e809 100644 --- a/test/uri/uri_parse_test.cc +++ b/test/uri/uri_parse_test.cc @@ -79,7 +79,7 @@ TEST(URI_parse, urn_with_query) { sourcemeta::core::URI uri{"urn:example:foo?+bar"}; EXPECT_EQ(uri.scheme().value(), "urn"); EXPECT_EQ(uri.path().value(), "example:foo"); - EXPECT_EQ(uri.query().value(), "+bar"); + EXPECT_EQ(uri.query().value().raw(), "+bar"); EXPECT_EQ(uri.recompose(), "urn:example:foo?+bar"); } @@ -460,7 +460,7 @@ TEST(URI_parse, success_with_equals_percent_3D_stays_encoded) { sourcemeta::core::URI uri{"https://www.example.com?foo%3Dbar"}; // Per RFC 3986 Section 2.2, %3D (=) is a reserved sub-delim // and must not be decoded - EXPECT_EQ(uri.query(), "foo%3Dbar"); + EXPECT_EQ(uri.query().value().raw(), "foo%3Dbar"); EXPECT_EQ(uri.recompose(), "https://www.example.com?foo%3Dbar"); } @@ -631,7 +631,7 @@ TEST(URI_parse, rfc3986_complete_uri) { EXPECT_EQ(uri.host().value(), "example.com"); EXPECT_EQ(uri.port().value(), 8080); EXPECT_EQ(uri.path().value(), "/path/to/resource"); - EXPECT_EQ(uri.query().value(), "query=value&key=data"); + EXPECT_EQ(uri.query().value().raw(), "query=value&key=data"); EXPECT_EQ(uri.fragment().value(), "section"); } @@ -697,7 +697,7 @@ TEST(URI_parse, rfc3986_query_only) { sourcemeta::core::URI uri{"?query=value"}; EXPECT_FALSE(uri.scheme().has_value()); EXPECT_TRUE(uri.query().has_value()); - EXPECT_EQ(uri.query().value(), "query=value"); + EXPECT_EQ(uri.query().value().raw(), "query=value"); EXPECT_EQ(uri.recompose(), "?query=value"); } @@ -797,7 +797,7 @@ TEST(URI_parse, rfc3986_empty_path_with_query) { sourcemeta::core::URI::is_uri_reference("http://example.com?query")); sourcemeta::core::URI uri{"http://example.com?query"}; EXPECT_FALSE(uri.path().has_value()); - EXPECT_EQ(uri.query().value(), "query"); + EXPECT_EQ(uri.query().value().raw(), "query"); } TEST(URI_parse, rfc3986_empty_path_with_fragment) { diff --git a/test/uri/uri_query_test.cc b/test/uri/uri_query_test.cc index 396efbbf8..ae59e7655 100644 --- a/test/uri/uri_query_test.cc +++ b/test/uri/uri_query_test.cc @@ -2,6 +2,9 @@ #include +#include // std::pair +#include // std::vector + TEST(URI_query, https_example_url_no_query) { const sourcemeta::core::URI uri{"https://example.com/test"}; EXPECT_FALSE(uri.query().has_value()); @@ -10,7 +13,7 @@ TEST(URI_query, https_example_url_no_query) { TEST(URI_query, https_example_with_query_single) { const sourcemeta::core::URI uri{"https://example.com/test?foo=bar"}; EXPECT_TRUE(uri.query().has_value()); - EXPECT_EQ(uri.query().value(), "foo=bar"); + EXPECT_EQ(uri.query().value().raw(), "foo=bar"); } // RFC 3986 itself does not understand the query part of a URI as a list of @@ -18,31 +21,31 @@ TEST(URI_query, https_example_with_query_single) { TEST(URI_query, https_example_with_query_double) { const sourcemeta::core::URI uri{"https://example.com/test?foo=bar&bar=baz"}; EXPECT_TRUE(uri.query().has_value()); - EXPECT_EQ(uri.query().value(), "foo=bar&bar=baz"); + EXPECT_EQ(uri.query().value().raw(), "foo=bar&bar=baz"); } TEST(URI_query, rfc3986_query_with_slash) { const sourcemeta::core::URI uri{"http://example.com/path?query/with/slashes"}; EXPECT_TRUE(uri.query().has_value()); - EXPECT_EQ(uri.query().value(), "query/with/slashes"); + EXPECT_EQ(uri.query().value().raw(), "query/with/slashes"); } TEST(URI_query, rfc3986_query_with_question_mark) { const sourcemeta::core::URI uri{"http://example.com/path?query=value?more"}; EXPECT_TRUE(uri.query().has_value()); - EXPECT_EQ(uri.query().value(), "query=value?more"); + EXPECT_EQ(uri.query().value().raw(), "query=value?more"); } TEST(URI_query, rfc3986_query_with_colon) { const sourcemeta::core::URI uri{"http://example.com/path?query:with:colons"}; EXPECT_TRUE(uri.query().has_value()); - EXPECT_EQ(uri.query().value(), "query:with:colons"); + EXPECT_EQ(uri.query().value().raw(), "query:with:colons"); } TEST(URI_query, rfc3986_query_with_at_sign) { const sourcemeta::core::URI uri{"http://example.com/path?query@with@at"}; EXPECT_TRUE(uri.query().has_value()); - EXPECT_EQ(uri.query().value(), "query@with@at"); + EXPECT_EQ(uri.query().value().raw(), "query@with@at"); } TEST(URI_query, rfc3986_query_percent_encoded) { @@ -50,32 +53,32 @@ TEST(URI_query, rfc3986_query_percent_encoded) { "http://example.com/path?query%20with%20spaces"}; EXPECT_TRUE(uri.query().has_value()); // Space is not unreserved, so %20 must not be decoded - EXPECT_EQ(uri.query().value(), "query%20with%20spaces"); + EXPECT_EQ(uri.query().value().raw(), "query%20with%20spaces"); } TEST(URI_query, rfc3986_query_with_fragment) { const sourcemeta::core::URI uri{ "http://example.com/path?query=value#fragment"}; EXPECT_TRUE(uri.query().has_value()); - EXPECT_EQ(uri.query().value(), "query=value"); + EXPECT_EQ(uri.query().value().raw(), "query=value"); } TEST(URI_query, rfc3986_empty_query) { const sourcemeta::core::URI uri{"http://example.com/path?"}; EXPECT_TRUE(uri.query().has_value()); - EXPECT_EQ(uri.query().value(), ""); + EXPECT_EQ(uri.query().value().raw(), ""); } TEST(URI_query, rfc3986_query_only_ampersands) { const sourcemeta::core::URI uri{"http://example.com/path?&&&"}; EXPECT_TRUE(uri.query().has_value()); - EXPECT_EQ(uri.query().value(), "&&&"); + EXPECT_EQ(uri.query().value().raw(), "&&&"); } TEST(URI_query, rfc3986_query_with_subdelims) { const sourcemeta::core::URI uri{"http://example.com/path?!$&'()*+,;="}; EXPECT_TRUE(uri.query().has_value()); - EXPECT_EQ(uri.query().value(), "!$&'()*+,;="); + EXPECT_EQ(uri.query().value().raw(), "!$&'()*+,;="); } TEST(URI_query, rfc3986_query_percent_encoded_brackets) { @@ -83,119 +86,506 @@ TEST(URI_query, rfc3986_query_percent_encoded_brackets) { "http://example.com/path?items%5B0%5D=a&items%5B1%5D=b"}; EXPECT_TRUE(uri.query().has_value()); // [ and ] are gen-delims (reserved), must not be decoded - EXPECT_EQ(uri.query().value(), "items%5B0%5D=a&items%5B1%5D=b"); + EXPECT_EQ(uri.query().value().raw(), "items%5B0%5D=a&items%5B1%5D=b"); } TEST(URI_query, rfc3986_query_no_value) { const sourcemeta::core::URI uri{"http://example.com/path?key"}; EXPECT_TRUE(uri.query().has_value()); - EXPECT_EQ(uri.query().value(), "key"); + EXPECT_EQ(uri.query().value().raw(), "key"); } TEST(URI_query, rfc3986_query_multiple_equals) { const sourcemeta::core::URI uri{"http://example.com/path?key=value=extra"}; EXPECT_TRUE(uri.query().has_value()); - EXPECT_EQ(uri.query().value(), "key=value=extra"); + EXPECT_EQ(uri.query().value().raw(), "key=value=extra"); } TEST(URI_query, encoded_ampersand_preserved) { const sourcemeta::core::URI uri{"http://example.com/path?foo%26bar=baz"}; EXPECT_TRUE(uri.query().has_value()); - EXPECT_EQ(uri.query().value(), "foo%26bar=baz"); + EXPECT_EQ(uri.query().value().raw(), "foo%26bar=baz"); } TEST(URI_query, encoded_hash_not_fragment_delimiter) { const sourcemeta::core::URI uri{"http://example.com/path?foo%23bar"}; EXPECT_TRUE(uri.query().has_value()); - EXPECT_EQ(uri.query().value(), "foo%23bar"); + EXPECT_EQ(uri.query().value().raw(), "foo%23bar"); EXPECT_FALSE(uri.fragment().has_value()); } TEST(URI_query, encoded_equals_preserved) { const sourcemeta::core::URI uri{"http://example.com/path?key%3Dvalue"}; EXPECT_TRUE(uri.query().has_value()); - EXPECT_EQ(uri.query().value(), "key%3Dvalue"); + EXPECT_EQ(uri.query().value().raw(), "key%3Dvalue"); } -TEST(URI_query_setter, set_on_uri_without_query) { +TEST(URI_query, setter_on_uri_without_query) { sourcemeta::core::URI uri{"https://example.com/path"}; uri.query("foo=bar"); EXPECT_TRUE(uri.query().has_value()); - EXPECT_EQ(uri.query().value(), "foo=bar"); + EXPECT_EQ(uri.query().value().raw(), "foo=bar"); EXPECT_EQ(uri.recompose(), "https://example.com/path?foo=bar"); } -TEST(URI_query_setter, replace_existing_query) { +TEST(URI_query, setter_replace_existing_query) { sourcemeta::core::URI uri{"https://example.com/path?old=value"}; uri.query("foo=bar"); EXPECT_TRUE(uri.query().has_value()); - EXPECT_EQ(uri.query().value(), "foo=bar"); + EXPECT_EQ(uri.query().value().raw(), "foo=bar"); EXPECT_EQ(uri.recompose(), "https://example.com/path?foo=bar"); } -TEST(URI_query_setter, clear_with_empty_string) { +TEST(URI_query, setter_clear_with_empty_string) { sourcemeta::core::URI uri{"https://example.com/path?foo=bar"}; uri.query(""); EXPECT_FALSE(uri.query().has_value()); EXPECT_EQ(uri.recompose(), "https://example.com/path"); } -TEST(URI_query_setter, clear_when_already_absent) { +TEST(URI_query, setter_clear_when_already_absent) { sourcemeta::core::URI uri{"https://example.com/path"}; uri.query(""); EXPECT_FALSE(uri.query().has_value()); EXPECT_EQ(uri.recompose(), "https://example.com/path"); } -TEST(URI_query_setter, leading_question_mark_stripped) { +TEST(URI_query, setter_leading_question_mark_stripped) { sourcemeta::core::URI uri{"https://example.com/path"}; uri.query("?foo=bar"); EXPECT_TRUE(uri.query().has_value()); - EXPECT_EQ(uri.query().value(), "foo=bar"); + EXPECT_EQ(uri.query().value().raw(), "foo=bar"); EXPECT_EQ(uri.recompose(), "https://example.com/path?foo=bar"); } -TEST(URI_query_setter, clear_preserves_fragment) { +TEST(URI_query, setter_clear_preserves_fragment) { sourcemeta::core::URI uri{"https://example.com/path?foo=bar#section"}; uri.query(""); EXPECT_FALSE(uri.query().has_value()); EXPECT_EQ(uri.recompose(), "https://example.com/path#section"); } -TEST(URI_query_setter, percent_encoding_applied_on_recompose) { +TEST(URI_query, setter_percent_encoding_applied_on_recompose) { sourcemeta::core::URI uri{"https://example.com/path"}; uri.query("foo=hello world"); EXPECT_TRUE(uri.query().has_value()); - EXPECT_EQ(uri.query().value(), "foo=hello world"); + EXPECT_EQ(uri.query().value().raw(), "foo=hello world"); EXPECT_EQ(uri.recompose(), "https://example.com/path?foo=hello%20world"); } -TEST(URI_query_setter, returns_reference_for_chaining) { +TEST(URI_query, setter_returns_reference_for_chaining) { sourcemeta::core::URI uri{"https://example.com"}; uri.query("a=1").fragment("section"); EXPECT_EQ(uri.recompose(), "https://example.com?a=1#section"); } -TEST(URI_query_setter, normalizes_unreserved_percent_encoding) { +TEST(URI_query, setter_normalizes_unreserved_percent_encoding) { sourcemeta::core::URI uri{"https://example.com"}; uri.query("foo=%7Ebar"); EXPECT_TRUE(uri.query().has_value()); // %7E encodes ~, an unreserved character, so it must be decoded - EXPECT_EQ(uri.query().value(), "foo=~bar"); + EXPECT_EQ(uri.query().value().raw(), "foo=~bar"); } -TEST(URI_query_setter, preserves_reserved_percent_encoding) { +TEST(URI_query, setter_preserves_reserved_percent_encoding) { sourcemeta::core::URI uri{"https://example.com"}; uri.query("foo=%23bar"); EXPECT_TRUE(uri.query().has_value()); // %23 encodes #, a reserved character, so it must not be decoded - EXPECT_EQ(uri.query().value(), "foo=%23bar"); + EXPECT_EQ(uri.query().value().raw(), "foo=%23bar"); } -TEST(URI_query_setter, matches_parsed_uri_for_unreserved_percent) { +TEST(URI_query, setter_matches_parsed_uri_for_unreserved_percent) { sourcemeta::core::URI built{"https://example.com"}; built.query("foo=%7Ebar"); const sourcemeta::core::URI parsed{"https://example.com?foo=%7Ebar"}; - EXPECT_EQ(built.query().value(), parsed.query().value()); + EXPECT_EQ(built.query().value().raw(), parsed.query().value().raw()); EXPECT_EQ(built.recompose(), parsed.recompose()); } + +TEST(URI_query, iterator_no_query_has_no_iterable) { + const sourcemeta::core::URI uri{"https://example.com/path"}; + EXPECT_FALSE(uri.query().has_value()); +} + +TEST(URI_query, iterator_empty_query_yields_no_pairs) { + const sourcemeta::core::URI uri{"https://example.com/path?"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + EXPECT_EQ(query.value().begin(), query.value().end()); +} + +TEST(URI_query, iterator_single_pair) { + const sourcemeta::core::URI uri{"https://example.com/path?foo=bar"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + std::vector> pairs; + for (const auto &pair : query.value()) { + pairs.emplace_back(pair); + } + ASSERT_EQ(pairs.size(), 1); + EXPECT_EQ(pairs[0].first, "foo"); + EXPECT_EQ(pairs[0].second, "bar"); +} + +TEST(URI_query, iterator_two_pairs_in_order) { + const sourcemeta::core::URI uri{"https://example.com/path?foo=bar&baz=qux"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + std::vector> pairs; + for (const auto &pair : query.value()) { + pairs.emplace_back(pair); + } + ASSERT_EQ(pairs.size(), 2); + EXPECT_EQ(pairs[0].first, "foo"); + EXPECT_EQ(pairs[0].second, "bar"); + EXPECT_EQ(pairs[1].first, "baz"); + EXPECT_EQ(pairs[1].second, "qux"); +} + +TEST(URI_query, iterator_key_without_equals) { + const sourcemeta::core::URI uri{"https://example.com/path?foo"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + std::vector> pairs; + for (const auto &pair : query.value()) { + pairs.emplace_back(pair); + } + ASSERT_EQ(pairs.size(), 1); + EXPECT_EQ(pairs[0].first, "foo"); + EXPECT_EQ(pairs[0].second, ""); +} + +TEST(URI_query, iterator_key_with_empty_value) { + const sourcemeta::core::URI uri{"https://example.com/path?foo="}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + std::vector> pairs; + for (const auto &pair : query.value()) { + pairs.emplace_back(pair); + } + ASSERT_EQ(pairs.size(), 1); + EXPECT_EQ(pairs[0].first, "foo"); + EXPECT_EQ(pairs[0].second, ""); +} + +TEST(URI_query, iterator_empty_key_with_value) { + const sourcemeta::core::URI uri{"https://example.com/path?=bar"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + std::vector> pairs; + for (const auto &pair : query.value()) { + pairs.emplace_back(pair); + } + ASSERT_EQ(pairs.size(), 1); + EXPECT_EQ(pairs[0].first, ""); + EXPECT_EQ(pairs[0].second, "bar"); +} + +TEST(URI_query, iterator_only_ampersands_yields_empty_pairs) { + const sourcemeta::core::URI uri{"https://example.com/path?&&&"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + std::vector> pairs; + for (const auto &pair : query.value()) { + pairs.emplace_back(pair); + } + ASSERT_EQ(pairs.size(), 4); + for (const auto &pair : pairs) { + EXPECT_EQ(pair.first, ""); + EXPECT_EQ(pair.second, ""); + } +} + +TEST(URI_query, iterator_multiple_equals_only_first_splits) { + const sourcemeta::core::URI uri{"https://example.com/path?key=value=extra"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + std::vector> pairs; + for (const auto &pair : query.value()) { + pairs.emplace_back(pair); + } + ASSERT_EQ(pairs.size(), 1); + EXPECT_EQ(pairs[0].first, "key"); + EXPECT_EQ(pairs[0].second, "value=extra"); +} + +TEST(URI_query, iterator_percent_encoding_left_untouched) { + const sourcemeta::core::URI uri{"https://example.com/path?foo=%20"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + std::vector> pairs; + for (const auto &pair : query.value()) { + pairs.emplace_back(pair); + } + ASSERT_EQ(pairs.size(), 1); + EXPECT_EQ(pairs[0].first, "foo"); + EXPECT_EQ(pairs[0].second, "%20"); +} + +TEST(URI_query, at_single_match) { + const sourcemeta::core::URI uri{"https://example.com/path?foo=bar"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + const auto value{query.value().at("foo")}; + ASSERT_TRUE(value.has_value()); + EXPECT_EQ(value.value(), "bar"); +} + +TEST(URI_query, at_missing_key_returns_nullopt) { + const sourcemeta::core::URI uri{"https://example.com/path?foo=bar"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + EXPECT_FALSE(query.value().at("missing").has_value()); +} + +TEST(URI_query, at_first_wins_on_duplicates) { + const sourcemeta::core::URI uri{"https://example.com/path?foo=1&foo=2"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + const auto value{query.value().at("foo")}; + ASSERT_TRUE(value.has_value()); + EXPECT_EQ(value.value(), "1"); +} + +TEST(URI_query, at_second_among_distinct_keys) { + const sourcemeta::core::URI uri{"https://example.com/path?foo=bar&baz=qux"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + const auto value{query.value().at("baz")}; + ASSERT_TRUE(value.has_value()); + EXPECT_EQ(value.value(), "qux"); +} + +TEST(URI_query, at_key_with_empty_value_returns_empty_string) { + const sourcemeta::core::URI uri{"https://example.com/path?foo="}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + const auto value{query.value().at("foo")}; + ASSERT_TRUE(value.has_value()); + EXPECT_EQ(value.value(), ""); +} + +TEST(URI_query, at_key_without_equals_returns_empty_string) { + const sourcemeta::core::URI uri{"https://example.com/path?foo"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + const auto value{query.value().at("foo")}; + ASSERT_TRUE(value.has_value()); + EXPECT_EQ(value.value(), ""); +} + +TEST(URI_query, at_empty_key_lookup) { + const sourcemeta::core::URI uri{"https://example.com/path?=bar"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + const auto value{query.value().at("")}; + ASSERT_TRUE(value.has_value()); + EXPECT_EQ(value.value(), "bar"); +} + +TEST(URI_query, at_percent_encoded_value_left_untouched) { + const sourcemeta::core::URI uri{"https://example.com/path?foo=%20bar"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + const auto value{query.value().at("foo")}; + ASSERT_TRUE(value.has_value()); + EXPECT_EQ(value.value(), "%20bar"); +} + +TEST(URI_query, at_empty_query_returns_nullopt) { + const sourcemeta::core::URI uri{"https://example.com/path?"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + EXPECT_FALSE(query.value().at("anything").has_value()); +} + +TEST(URI_query, at_value_view_points_into_uri_storage) { + const sourcemeta::core::URI uri{"https://example.com/path?foo=bar&baz=qux"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + const auto value{query.value().at("baz")}; + ASSERT_TRUE(value.has_value()); + EXPECT_EQ(value.value().data(), + query.value().raw().data() + query.value().raw().find("qux")); +} + +TEST(URI_query, iterator_trailing_ampersand) { + const sourcemeta::core::URI uri{"https://example.com/path?foo=bar&"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + std::vector> pairs; + for (const auto &pair : query.value()) { + pairs.emplace_back(pair); + } + ASSERT_EQ(pairs.size(), 2); + EXPECT_EQ(pairs[0].first, "foo"); + EXPECT_EQ(pairs[0].second, "bar"); + EXPECT_EQ(pairs[1].first, ""); + EXPECT_EQ(pairs[1].second, ""); +} + +TEST(URI_query, iterator_leading_ampersand) { + const sourcemeta::core::URI uri{"https://example.com/path?&foo=bar"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + std::vector> pairs; + for (const auto &pair : query.value()) { + pairs.emplace_back(pair); + } + ASSERT_EQ(pairs.size(), 2); + EXPECT_EQ(pairs[0].first, ""); + EXPECT_EQ(pairs[0].second, ""); + EXPECT_EQ(pairs[1].first, "foo"); + EXPECT_EQ(pairs[1].second, "bar"); +} + +TEST(URI_query, iterator_consecutive_ampersands_between_pairs) { + const sourcemeta::core::URI uri{"https://example.com/path?foo=bar&&baz=qux"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + std::vector> pairs; + for (const auto &pair : query.value()) { + pairs.emplace_back(pair); + } + ASSERT_EQ(pairs.size(), 3); + EXPECT_EQ(pairs[0].first, "foo"); + EXPECT_EQ(pairs[0].second, "bar"); + EXPECT_EQ(pairs[1].first, ""); + EXPECT_EQ(pairs[1].second, ""); + EXPECT_EQ(pairs[2].first, "baz"); + EXPECT_EQ(pairs[2].second, "qux"); +} + +TEST(URI_query, iterator_only_equals_sign) { + const sourcemeta::core::URI uri{"https://example.com/path?="}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + std::vector> pairs; + for (const auto &pair : query.value()) { + pairs.emplace_back(pair); + } + ASSERT_EQ(pairs.size(), 1); + EXPECT_EQ(pairs[0].first, ""); + EXPECT_EQ(pairs[0].second, ""); +} + +TEST(URI_query, iterator_postfix_increment_returns_previous) { + const sourcemeta::core::URI uri{"https://example.com/path?foo=1&bar=2"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + auto iterator{query.value().begin()}; + const auto previous{iterator++}; + EXPECT_EQ(previous->first, "foo"); + EXPECT_EQ(previous->second, "1"); + EXPECT_EQ(iterator->first, "bar"); + EXPECT_EQ(iterator->second, "2"); +} + +TEST(URI_query, iterator_arrow_operator) { + const sourcemeta::core::URI uri{"https://example.com/path?foo=bar"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + const auto iterator{query.value().begin()}; + EXPECT_EQ(iterator->first, "foo"); + EXPECT_EQ(iterator->second, "bar"); +} + +TEST(URI_query, iterator_multipass_forward_guarantee) { + const sourcemeta::core::URI uri{"https://example.com/path?foo=1&bar=2&baz=3"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + const auto saved{query.value().begin()}; + auto advanced{saved}; + ++advanced; + ++advanced; + EXPECT_EQ(saved->first, "foo"); + EXPECT_EQ(saved->second, "1"); + EXPECT_EQ(advanced->first, "baz"); + EXPECT_EQ(advanced->second, "3"); +} + +TEST(URI_query, iterator_pair_views_into_query_storage) { + const sourcemeta::core::URI uri{"https://example.com/path?foo=bar&baz=qux"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + const auto raw{query.value().raw()}; + auto iterator{query.value().begin()}; + EXPECT_EQ(iterator->first.data(), raw.data() + raw.find("foo")); + EXPECT_EQ(iterator->second.data(), raw.data() + raw.find("bar")); + ++iterator; + EXPECT_EQ(iterator->first.data(), raw.data() + raw.find("baz")); + EXPECT_EQ(iterator->second.data(), raw.data() + raw.find("qux")); +} + +TEST(URI_query, iterator_subdelims_in_value) { + const sourcemeta::core::URI uri{"https://example.com/path?foo=!$'()*+,;"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + std::vector> pairs; + for (const auto &pair : query.value()) { + pairs.emplace_back(pair); + } + ASSERT_EQ(pairs.size(), 1); + EXPECT_EQ(pairs[0].first, "foo"); + EXPECT_EQ(pairs[0].second, "!$'()*+,;"); +} + +TEST(URI_query, iterator_pchar_colon_at_in_value) { + const sourcemeta::core::URI uri{ + "https://example.com/path?foo=user:pass@host"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + std::vector> pairs; + for (const auto &pair : query.value()) { + pairs.emplace_back(pair); + } + ASSERT_EQ(pairs.size(), 1); + EXPECT_EQ(pairs[0].first, "foo"); + EXPECT_EQ(pairs[0].second, "user:pass@host"); +} + +TEST(URI_query, at_case_sensitive_no_match) { + const sourcemeta::core::URI uri{"https://example.com/path?foo=bar"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + EXPECT_FALSE(query.value().at("Foo").has_value()); + EXPECT_FALSE(query.value().at("FOO").has_value()); + EXPECT_FALSE(query.value().at("fOo").has_value()); +} + +TEST(URI_query, at_three_duplicates_returns_first) { + const sourcemeta::core::URI uri{"https://example.com/path?foo=1&foo=2&foo=3"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + const auto value{query.value().at("foo")}; + ASSERT_TRUE(value.has_value()); + EXPECT_EQ(value.value(), "1"); +} + +TEST(URI_query, at_first_value_empty_among_duplicates) { + const sourcemeta::core::URI uri{"https://example.com/path?foo=&foo=2"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + const auto value{query.value().at("foo")}; + ASSERT_TRUE(value.has_value()); + EXPECT_EQ(value.value(), ""); +} + +TEST(URI_query, at_value_containing_equals_returned_intact) { + const sourcemeta::core::URI uri{"https://example.com/path?foo=bar=baz"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + const auto value{query.value().at("foo")}; + ASSERT_TRUE(value.has_value()); + EXPECT_EQ(value.value(), "bar=baz"); +} + +TEST(URI_query, at_plus_sign_not_decoded_as_space) { + const sourcemeta::core::URI uri{"https://example.com/path?foo=a+b"}; + const auto query{uri.query()}; + ASSERT_TRUE(query.has_value()); + const auto value{query.value().at("foo")}; + ASSERT_TRUE(value.has_value()); + EXPECT_EQ(value.value(), "a+b"); +}