From 973aec43ea54bbf95b64fbcb636403401d1ca60e Mon Sep 17 00:00:00 2001 From: Rutger Broekhoff Date: Fri, 28 Aug 2026 18:03:05 +0200 Subject: Import from e4b104792206ee7ea64bf39c6b7d2c0c230f9d14 --- server/src/xml.cppm | 632 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 632 insertions(+) create mode 100644 server/src/xml.cppm (limited to 'server/src/xml.cppm') diff --git a/server/src/xml.cppm b/server/src/xml.cppm new file mode 100644 index 0000000..957f149 --- /dev/null +++ b/server/src/xml.cppm @@ -0,0 +1,632 @@ +module; + +#include +#include + +export module routemon:xml; + +import std; +import :util; + +// XML parsing module. +// +// Makes heavy use of C++20 coroutines. Provides combinators to build +// XML (data format) parsers with. To keep overhead low (and allow for +// HALO/CoroElide), many non-polymorphic definitions are marked inline +// (which is not the default in module units). This also has the added +// benefit of allowing HALO across TU boundaries, which is practically +// necessary to reduce unnecessary allocations when building parsers +// using the provided combinators. + +// Ensure that Expat is speaking UTF-8 +static_assert(std::is_same_v); + +namespace routemon::xml { + + struct qname_view { + std::string_view ns_uri; + std::string_view local; + + auto operator==(qname_view const& rhs) const -> bool; + }; + + namespace detail { + + constexpr auto qname_sep = '\xFF'; + auto split_name(char const* name) noexcept -> qname_view { + auto [l, r] = util::split_on(name, qname_sep); + if (r) return { .ns_uri = l, .local = *r }; + else return { .ns_uri = std::string_view{}, .local = l }; + } + + } // namespace detail + + class attribute_view_iterator { + std::string_view default_ns_uri_; + char const* const* attrs_; + + auto advance() -> void { + attrs_ += 2; + } + + public: + using difference_type = std::ptrdiff_t; + using value_type = std::pair; + + struct sentinel { + friend constexpr auto operator==(attribute_view_iterator const& it, sentinel) noexcept -> bool { + return !*it.attrs_; + } + }; + + inline explicit attribute_view_iterator(std::string_view default_ns_uri, char const* const* attrs) + : default_ns_uri_{default_ns_uri}, attrs_{attrs} + {} + + inline auto operator*() const -> std::pair { + if (!*attrs_) + throw std::runtime_error{"end of attribute list"}; + auto qname = detail::split_name(attrs_[0]); + if (qname.ns_uri.empty()) + qname.ns_uri = default_ns_uri_; + return std::make_pair(qname, attrs_[1]); + } + + // Pre-increment + inline auto operator++() -> attribute_view_iterator& { + advance(); + return *this; + } + + // Post-increment + inline auto operator++(int) -> attribute_view_iterator { + auto pre = *this; + advance(); + return pre; + } + }; + static_assert(std::input_iterator); + + class attribute_view : std::ranges::view_base { + std::string_view default_ns_uri_; + char const* const* attrs_; + + public: + inline explicit attribute_view(std::string_view default_ns_uri, char const** attrs) + : default_ns_uri_{default_ns_uri}, attrs_{const_cast(attrs)} + {} + + [[nodiscard]] inline auto begin() const -> attribute_view_iterator { + return attribute_view_iterator{default_ns_uri_, attrs_}; + } + + [[nodiscard]] inline auto end() const -> attribute_view_iterator::sentinel { + return {}; + } + + inline auto lookup(qname_view want) -> std::optional> { + for (auto const& [name, v] : *this) { + if (name == want) { + return util::not_null{util::lazy_zstring_view{v}}; + } + } + return std::nullopt; + } + }; + static_assert(std::ranges::input_range); + + template class promise; + + template + struct [[clang::coro_await_elidable, clang::coro_return_type]] parser { + using promise_type = promise; + using result_type = promise_type::result_type; + using handle_type = std::coroutine_handle; + + private: + handle_type h_; + + public: + explicit parser(handle_type h) + : h_{h} + { assert(h); } + + parser(const parser&) = delete; + parser(parser&& c) noexcept + : h_{std::exchange(c.h_, nullptr)} + {} + auto operator=(const parser&) -> parser& = delete; + auto operator=(parser&&) -> parser& = delete; + + [[nodiscard]] auto promise() const -> promise_type& { + return h_.promise(); + } + + ~parser() { + if (h_) h_.destroy(); + } + }; + + struct start_element_event { + qname_view name; + attribute_view attrs; + }; + struct end_element_event { + qname_view name; + }; + struct character_data_event { + std::string_view data; + }; + struct processing_instructions_event { + util::lazy_zstring_view target; + util::lazy_zstring_view data; + }; + struct xml_decl_event { + util::lazy_zstring_view version; + util::lazy_zstring_view encoding; + std::optional standalone; + }; + struct eof_event {}; + using event = std::variant; + template + concept event_type = requires(event ev) { std::get(ev); }; + + class executor; + using executor_ref = util::not_null; + + struct current_event_t { + executor_ref executor; + }; + auto current_event(executor_ref executor) -> current_event_t { + return current_event_t{executor}; + } + + class promise_base { + executor_ref executor_; + std::coroutine_handle continuation_ = nullptr; + + public: + // Not having this constructor marked inline messes with coroutine + // HALO. (Hours 'wasted': many) + inline explicit promise_base(executor_ref executor) + : executor_{executor} + {} + + [[nodiscard]] inline auto executor() const -> executor& { + return *executor_; + } + + inline auto base_handle() -> std::coroutine_handle { + return std::coroutine_handle::from_promise(*this); + } + + [[nodiscard]] inline auto continuation() const -> std::coroutine_handle { + return continuation_; + } + inline auto set_continuation(std::coroutine_handle c) -> void { + continuation_ = c; + } + }; + + struct position { + std::size_t line; + std::size_t col; + }; + + class executor { + XML_Parser p_; + std::exception_ptr ex_ = nullptr; + std::coroutine_handle continuation_ = nullptr; + std::vector> default_namespace_; + std::unordered_map> namespaces_; + std::optional ev_; + bool advance_ = true; + + inline auto try_handle_event(event ev) noexcept -> void { + assert(!ex_); + + try { + ev_ = std::move(ev); + } catch (...) { + ex_ = std::current_exception(); + return; + } + advance_ = false; + if (!continuation_) { + // Parser returned (all subparsers are done) and has set the continuation to nullptr. + if (auto s = XML_StopParser(p_, /* resumable */ false); s != XML_STATUS_OK) { + ex_ = std::make_exception_ptr(std::runtime_error{"unexpected error when stopping XML parser"}); + return; + } + ex_ = std::make_exception_ptr(std::runtime_error{"parser did not consume entire XML document"}); + return; + } + continuation_.resume(); + if (ex_) { + // Not sure if it's useful to report this error. + std::ignore = XML_StopParser(p_, /* resumable */ false); + } + } + + static auto handle_start_element(void* ctx, char const* name, char const** attrs) noexcept -> void { + auto qname = detail::split_name(name); + static_cast(ctx)->try_handle_event(start_element_event{ + .name = qname, + .attrs = attribute_view{qname.ns_uri, attrs}, + }); + } + static auto handle_end_element(void* ctx, char const* name) noexcept -> void { + static_cast(ctx)->try_handle_event(end_element_event{ + .name = detail::split_name(name), + }); + } + static auto handle_character_data(void* ctx, char const* s, int len) noexcept -> void { + static_cast(ctx)->try_handle_event(character_data_event{ + .data = std::string_view{s, static_cast(len)}, + }); + } + static auto handle_processing_instructions(void* ctx, char const* target, char const* data) noexcept -> void { + static_cast(ctx)->try_handle_event(processing_instructions_event{ + .target = util::lazy_zstring_view{target}, + .data = util::lazy_zstring_view{data}, + }); + } + static auto handle_external_entity_ref(XML_Parser, char const* /* context */, char const* /* base */, char const* /* system_id */, char const* /* public_id */) noexcept -> int { + return XML_STATUS_ERROR; + } + static auto handle_start_namespace_decl(void* ctx, char const* prefix, char const* uri) noexcept -> void { + if (prefix) { + static_cast(ctx)->namespaces_[std::string_view{prefix}].emplace_back(uri); + } else { + static_cast(ctx)->default_namespace_.push_back(uri ? std::make_optional(uri) : std::nullopt); + } + } + static auto handle_end_namespace_decl(void* ctx, char const* prefix) noexcept -> void { + if (prefix) { + static_cast(ctx)->namespaces_[std::string_view{prefix}].pop_back(); + } else { + static_cast(ctx)->default_namespace_.pop_back(); + } + } + static auto handle_xml_decl(void * ctx, char const* version, char const* encoding, int standalone) noexcept -> void { + static_cast(ctx)->try_handle_event(xml_decl_event{ + .version = util::lazy_zstring_view{version}, + .encoding = util::lazy_zstring_view{encoding}, + .standalone = standalone < 0 ? std::nullopt : std::make_optional(standalone > 0), + }); + } + + inline auto advance_flag() -> bool { + return advance_; + } + inline auto set_exception(std::exception_ptr ex) -> void { + ex_ = std::move(ex); + } + [[nodiscard]] inline auto take_exception() -> std::exception_ptr { + return std::exchange(ex_, nullptr); + } + + template friend class promise; + + public: + executor(); + + executor(executor const&) = delete; + executor(executor&&) = delete; + auto operator=(executor const&) -> executor& = delete; + auto operator=(executor&&) -> executor& = delete; + + ~executor(); + + inline auto set_continuation(std::coroutine_handle c) -> void { + continuation_ = c; + } + inline auto set_advance_flag() -> void { + if (!ev_ || !std::holds_alternative(ev_.value())) { + advance_ = true; + } + } + inline auto event() const -> std::optional const& { + return ev_; + } + inline auto resolve_namespace(std::string_view prefix) -> std::optional { + if (auto it = namespaces_.find(prefix); it != namespaces_.end() && !it->second.empty()) + return it->second.back(); + return std::nullopt; + } + [[nodiscard]] inline auto position() -> position { + return { + .line = XML_GetCurrentLineNumber(p_), + .col = XML_GetCurrentColumnNumber(p_), + }; + } + + auto start() -> void; + auto read(std::string_view xml, bool is_final) -> void; + auto end() -> void; + }; + + template + class promise_returnable : public promise_base { + std::optional returned_value_; + + public: + using result_type = T; + using promise_base::promise_base; + + template + auto return_value(U&& v) -> void { + returned_value_.emplace(std::forward(v)); + } + auto returned_value() -> T&& { + if (!returned_value_) + throw std::runtime_error{"XML coroutine did not return"}; + return std::forward(returned_value_.value()); + } + }; + + template<> + class promise_returnable : public promise_base { + public: + using result_type = void; + using promise_base::promise_base; + + auto return_void() -> void {} + }; + + template + class promise : public promise_returnable { + public: + // Called with all the coroutine's arguments. + // Ignoring all but the first argument, which should be the executor. + template + explicit promise(executor_ref executor, Args&&...) + : promise_returnable{executor} + {} + + auto handle() -> std::coroutine_handle> { + return {parser::handle_type::from_promise(*this)}; + } + + auto get_return_object() -> parser { + return parser{handle()}; + } + + auto initial_suspend() { + return std::suspend_always{}; + } + auto final_suspend() noexcept { + struct awaiter { + std::coroutine_handle<> h_; + + [[nodiscard]] constexpr auto await_ready() const noexcept -> bool { return false; } + auto await_suspend(std::coroutine_handle<>) -> std::coroutine_handle<> { return h_; } + constexpr auto await_resume() const noexcept -> void { return; } + }; + if (this->continuation()) { + return awaiter{this->continuation()}; + } else { + this->executor().set_continuation(nullptr); + return awaiter{std::noop_coroutine()}; + } + } + + auto unhandled_exception() -> void { + this->executor().set_exception(std::current_exception()); + } + + auto await_transform(current_event_t const& req) { + struct awaiter { + executor_ref executor_; + + [[nodiscard]] constexpr auto await_ready() const noexcept -> bool { + return !executor_->advance_flag(); + } + auto await_suspend(std::coroutine_handle> h) -> void { + executor_->set_continuation(h.promise().base_handle()); + } + [[nodiscard]] auto await_resume() const -> event { + assert(executor_->event()); + return executor_->event().value(); + } + }; + return awaiter{req.executor}; + } + + template + auto await_transform(parser const& coro) { + struct [[clang::coro_await_elidable]] awaiter { + util::not_null*> next_; + + [[nodiscard]] constexpr auto await_ready() const noexcept -> bool { return false; } + auto await_suspend(std::coroutine_handle> h) -> std::coroutine_handle<> { + // Passed coroutine handle will be the same as parser::handle_type::from_promise(*this) + next_->set_continuation(h.promise().base_handle()); + return next_->handle(); + } + auto await_resume() -> U { + // Promise is still valid since coroutine frame is still alive (and suspended): + // control was transferred back to this coroutine via symmetric transfer in + // final_suspend(). Assuming that the destructor for coro still needs to run. + if (auto ex = next_->executor().take_exception()) { + std::rethrow_exception(ex); + } else { + if constexpr (!std::is_void_v) { + return std::move(next_->returned_value()); + } + } + } + }; + return awaiter{util::not_null{&coro.promise()}}; + } + }; + + // Helpers for handling XML documents. Non-polymorphic functions + // should be marked inline to allow HALO across TU boundaries. + + template + concept unconstrained = true; + + template concept C> + concept parser_of = requires { + typename T::result_type; + requires std::same_as, T>; + requires C; + }; + + template T> + using parser_result_t = T::result_type; + + template + concept parser_invocable = std::invocable && parser_of, unconstrained>; + + template + requires parser_invocable + using parser_invoke_result_t = parser_result_t>; + + template auto expect_event(executor_ref e) -> parser { + auto ev = co_await current_event(e); + if (!std::holds_alternative(ev)) { + auto pos = e->position(); + throw std::runtime_error{std::format("at {}:{}: unexpected event type, have {}", pos.line, pos.col, ev.index())}; + } + e->set_advance_flag(); + co_return std::get(ev); + } + + inline auto expect_start_element(executor_ref e, qname_view want) -> parser { + auto ev = co_await expect_event(e); + if (ev.name != want) { + auto pos = e->position(); + throw std::runtime_error{std::format("at {}:{}: unexpected element started", pos.line, pos.col)}; + } + co_return ev.attrs; + } + + inline auto allow_start_element(executor_ref e, qname_view want) -> parser> { + auto ev = co_await current_event(e); + if (auto const* pev = std::get_if(&ev)) { + if (pev->name == want) { + e->set_advance_flag(); + co_return pev->attrs; + } + } + co_return std::nullopt; + } + + inline auto expect_end_element(executor_ref e, qname_view want) -> parser { + auto ev = co_await expect_event(e); + if (ev.name != want) { + throw std::runtime_error{"unexpected element ended"}; + } + } + + inline auto ignore_whitespace(executor_ref e) -> parser { + auto all_whitespace = [](std::string_view s) -> bool { + for (auto c : s) + if (c != ' ' && c != '\r' && c != '\n' && c != '\t') + return false; + return true; + }; + + while (true) { + auto ev = co_await current_event(e); + if (auto const* pev = std::get_if(&ev); pev && all_whitespace(pev->data)) { + e->set_advance_flag(); + } else { + co_return; + } + } + } + + auto expect_element(executor_ref e, qname_view want, parser_invocable auto p) + -> parser> + { + co_await ignore_whitespace(e); + auto attrs = co_await expect_start_element(e, want); + auto&& res = co_await p(e, attrs); + co_await expect_end_element(e, want); + co_await ignore_whitespace(e); + co_return std::forward>(res); + } + + auto allow_element(executor_ref e, qname_view want, parser_invocable auto p) + -> parser>> + { + co_await ignore_whitespace(e); + if (auto mattrs = co_await allow_start_element(e, want)) { + auto&& res = co_await p(e, *mattrs); + co_await expect_end_element(e, want); + co_await ignore_whitespace(e); + co_return std::make_optional(std::forward>(res)); + } + co_return std::nullopt; + } + + auto allow_element(executor_ref e, qname_view want, parser_invocable auto p) + -> parser + requires std::is_void_v> + { + co_await ignore_whitespace(e); + if (auto mattrs = co_await allow_start_element(e, want)) { + co_await p(e, *mattrs); + co_await expect_end_element(e, want); + co_await ignore_whitespace(e); + co_return true; + } + co_return false; + } + + inline auto ignore_contents(executor_ref e, std::optional muntil = std::nullopt) -> parser { + std::size_t depth = 0; + while (true) { + auto ev = co_await current_event(e); + if (auto* pev = std::get_if(&ev)) { + if (depth == 0 && muntil && pev->name == *muntil) { + co_return; + } else { + depth++; + } + } else if (std::holds_alternative(ev)) { + if (depth == 0) { + co_return; + } else { + depth--; + } + } + e->set_advance_flag(); + } + } + inline auto ignore_element_contents(executor_ref e, attribute_view) -> parser { + co_await ignore_contents(e); + } + + inline auto read_string(executor_ref e) -> parser { + std::string s; + while (true) { + auto ev = co_await current_event(e); + if (auto* pev = std::get_if(&ev)) { + e->set_advance_flag(); + s += pev->data; + } else { + co_return s; + } + } + } + inline auto read_string_contents(executor_ref e, attribute_view) -> parser { + co_return co_await read_string(e); + } + + template + auto hohalo() { + return [](Args&&... args) -> std::invoke_result_t { + co_return co_await f(std::forward(args)...); + }; + } + +} // namespace routemon::xml -- cgit v1.3