Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/core/uri/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
4 changes: 0 additions & 4 deletions src/core/uri/accessors.cc
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,6 @@ auto URI::fragment() const -> std::optional<std::string_view> {
return this->fragment_;
}

auto URI::query() const -> std::optional<std::string_view> {
return this->query_;
}

auto URI::userinfo() const -> std::optional<std::string_view> {
return this->userinfo_;
}
Expand Down
113 changes: 109 additions & 4 deletions src/core/uri/include/sourcemeta/core/uri.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,18 @@
// NOLINTEND(misc-include-cleaner)

#include <concepts> // std::convertible_to
#include <cstddef> // std::size_t, std::ptrdiff_t
#include <cstdint> // std::uint32_t
#include <filesystem> // std::filesystem
#include <istream> // std::istream
#include <iterator> // std::forward_iterator_tag
#include <memory> // std::unique_ptr
#include <optional> // std::optional
#include <span> // std::span
#include <string> // std::string
#include <string_view> // std::string_view
#include <type_traits> // std::is_same_v
#include <utility> // std::pair
#include <vector> // std::vector

/// @defgroup uri URI
Expand Down Expand Up @@ -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 <sourcemeta/core/uri.h>
/// #include <cassert>
///
/// 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 <sourcemeta/core/uri.h>
/// #include <cassert>
///
/// 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 <sourcemeta/core/uri.h>
/// #include <cassert>
///
/// 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<std::string_view>;

/// 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<std::string_view, std::string_view>;
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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Use #pragma warning(push/pop) instead of warning(default: 4251) in this header to avoid leaking warning-state changes to includers.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/core/uri/include/sourcemeta/core/uri.h, line 395:

<comment>Use `#pragma warning(push/pop)` instead of `warning(default: 4251)` in this header to avoid leaking warning-state changes to includers.</comment>

<file context>
@@ -385,9 +385,15 @@ class SOURCEMETA_CORE_URI_EXPORT URI {
       std::size_t pair_start_{std::string_view::npos};
       value_type current_{};
+#if defined(_MSC_VER)
+#pragma warning(default : 4251)
+#endif
     };
</file context>

Tip: Review your code locally with the cubic CLI to iterate faster.

#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 <sourcemeta/core/uri.h>
Expand All @@ -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<std::string_view>;
[[nodiscard]] auto query() const -> std::optional<Query>;

/// Set the query part of the URI. A leading `?` in the input is stripped.
/// Passing an empty string clears the query. For example:
Expand All @@ -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 &;

Expand Down
105 changes: 105 additions & 0 deletions src/core/uri/query.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#include <sourcemeta/core/uri.h>

#include <cstddef> // std::size_t, std::string_view::npos
#include <optional> // std::optional
#include <string_view> // std::string_view
#include <utility> // std::pair

namespace {

auto parse_pair(const std::string_view raw, const std::size_t pair_start)
-> std::pair<std::string_view, std::string_view> {
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<URI::Query> {
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 {
Comment thread
jviotti marked this conversation as resolved.
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_;
Comment thread
jviotti marked this conversation as resolved.
}

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<std::string_view> {
for (const auto &pair : *this) {
if (pair.first == name) {
return pair.second;
}
}
return std::nullopt;
}

} // namespace sourcemeta::core
2 changes: 1 addition & 1 deletion src/core/uri/recompose.cc
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ auto URI::recompose_without_fragment() const -> std::optional<std::string> {
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);
}

Expand Down
10 changes: 5 additions & 5 deletions test/uri/uri_parse_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

Expand Down Expand Up @@ -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");
}

Expand Down Expand Up @@ -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");
}

Expand Down Expand Up @@ -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");
}

Expand Down Expand Up @@ -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) {
Expand Down
Loading
Loading