From 60401eeac8816336d994f166b1cc27845068f95c Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Fri, 15 May 2026 16:22:12 -0400 Subject: [PATCH 1/2] Implement more reader operations on the URI Template router Signed-off-by: Juan Cruz Viotti --- .../sourcemeta/core/uritemplate_router.h | 26 ++ src/core/uritemplate/uritemplate_router.cc | 15 + .../uritemplate/uritemplate_router_view.cc | 229 ++++++++++++++- test/uritemplate/uritemplate_router_test.cc | 59 ++++ .../uritemplate_router_view_test.cc | 261 ++++++++++++++++++ 5 files changed, 588 insertions(+), 2 deletions(-) diff --git a/src/core/uritemplate/include/sourcemeta/core/uritemplate_router.h b/src/core/uritemplate/include/sourcemeta/core/uritemplate_router.h index f8239ca791..982a9613a6 100644 --- a/src/core/uritemplate/include/sourcemeta/core/uritemplate_router.h +++ b/src/core/uritemplate/include/sourcemeta/core/uritemplate_router.h @@ -157,6 +157,11 @@ class SOURCEMETA_CORE_URITEMPLATE_EXPORT URITemplateRouter { [[nodiscard]] auto operation(const std::string_view operation_id) const -> std::pair; + /// Get the operation identifier associated with a registered route + /// identifier + [[nodiscard]] auto operation_id(const Identifier identifier) const + -> std::string_view; + private: Node root_; Node otherwise_; @@ -219,6 +224,27 @@ class SOURCEMETA_CORE_URITEMPLATE_EXPORT URITemplateRouterView { -> std::pair; + /// Get the identifier of the route at the given positional index + [[nodiscard]] auto at(const std::size_t index) const + -> URITemplateRouter::Identifier; + + /// Get the context identifier associated with a registered route + /// identifier + [[nodiscard]] auto + context(const URITemplateRouter::Identifier identifier) const + -> URITemplateRouter::Identifier; + + /// Reconstruct and return the URI Template path string originally registered + /// for the given identifier + [[nodiscard]] auto path(const URITemplateRouter::Identifier identifier) const + -> std::string; + + /// Get the operation identifier associated with a registered route + /// identifier + [[nodiscard]] auto + operation_id(const URITemplateRouter::Identifier identifier) const + -> std::string_view; + private: const std::uint8_t *data_{nullptr}; std::size_t size_{0}; diff --git a/src/core/uritemplate/uritemplate_router.cc b/src/core/uritemplate/uritemplate_router.cc index b6adad2175..a1e61c94f1 100644 --- a/src/core/uritemplate/uritemplate_router.cc +++ b/src/core/uritemplate/uritemplate_router.cc @@ -186,6 +186,21 @@ auto URITemplateRouter::operation(const std::string_view operation_id) const return iterator->second; } +auto URITemplateRouter::operation_id(const Identifier identifier) const + -> std::string_view { + if (identifier == 0) { + return {}; + } + const auto entry = + std::ranges::find_if(this->operations_, [&identifier](const auto &item) { + return item.second.first == identifier; + }); + if (entry == this->operations_.end()) { + return {}; + } + return entry->first; +} + auto URITemplateRouter::otherwise(const Identifier context, const std::span arguments) -> void { diff --git a/src/core/uritemplate/uritemplate_router_view.cc b/src/core/uritemplate/uritemplate_router_view.cc index 95f3c4ae28..8ea60342bb 100644 --- a/src/core/uritemplate/uritemplate_router_view.cc +++ b/src/core/uritemplate/uritemplate_router_view.cc @@ -19,7 +19,7 @@ namespace sourcemeta::core { namespace { constexpr std::uint32_t ROUTER_MAGIC = 0x52544552; // "RTER" -constexpr std::uint32_t ROUTER_VERSION = 8; +constexpr std::uint32_t ROUTER_VERSION = 9; constexpr std::uint32_t NO_CHILD = std::numeric_limits::max(); // Type tags for argument value serialization @@ -39,7 +39,7 @@ struct RouterHeader { std::uint32_t otherwise_context; std::uint32_t base_url_offset; std::uint32_t base_url_length; - std::uint32_t padding; + std::uint32_t paths_offset; }; struct ArgumentEntryHeader { @@ -60,6 +60,18 @@ struct OperationEntry { URITemplateRouter::Identifier context; }; +constexpr std::size_t PATH_ENTRY_SIZE = sizeof(URITemplateRouter::Identifier) + + sizeof(URITemplateRouter::Identifier) + + sizeof(std::uint32_t) + + sizeof(std::uint32_t); + +struct PathEntry { + URITemplateRouter::Identifier identifier; + URITemplateRouter::Identifier context; + std::uint32_t string_offset; + std::uint32_t string_length; +}; + struct alignas(8) SerializedNode { std::uint32_t string_offset; std::uint32_t string_length; @@ -91,6 +103,22 @@ inline auto is_expansion_type(const URITemplateRouter::NodeType type) noexcept type == URITemplateRouter::NodeType::OptionalExpansion; } +inline auto read_path_entry(const std::uint8_t *data, const std::size_t offset) + -> PathEntry { + PathEntry entry{}; + std::memcpy(&entry.identifier, data + offset, sizeof(entry.identifier)); + std::memcpy(&entry.context, data + offset + sizeof(entry.identifier), + sizeof(entry.context)); + std::memcpy(&entry.string_offset, + data + offset + sizeof(entry.identifier) + sizeof(entry.context), + sizeof(entry.string_offset)); + std::memcpy(&entry.string_length, + data + offset + sizeof(entry.identifier) + sizeof(entry.context) + + sizeof(entry.string_offset), + sizeof(entry.string_length)); + return entry; +} + // Binary search for a literal child matching the given segment inline auto binary_search_literal_children( const SerializedNode *nodes, const char *string_table, @@ -303,6 +331,22 @@ auto URITemplateRouterView::save(const URITemplateRouter &router, const auto operation_count = static_cast(operation_entries.size()); + std::vector path_entries; + path_entries.reserve(router.entries_.size()); + for (const auto &[entry_identifier, entry_context, entry_path] : + router.entries_) { + PathEntry entry{}; + entry.identifier = entry_identifier; + entry.context = entry_context; + entry.string_offset = static_cast(string_table.size()); + entry.string_length = static_cast(entry_path.size()); + string_table.append(entry_path.data(), entry_path.size()); + path_entries.push_back(entry); + } + + assert(path_entries.size() <= std::numeric_limits::max()); + const auto path_count = static_cast(path_entries.size()); + // Append the base path to the string table const auto base_path_string_offset = static_cast(string_table.size()); @@ -332,6 +376,9 @@ auto URITemplateRouterView::save(const URITemplateRouter &router, header.otherwise_context = router.otherwise_.context; header.base_url_offset = base_url_string_offset; header.base_url_length = static_cast(base_url_value.size()); + header.paths_offset = static_cast( + header.operations_offset + sizeof(std::uint16_t) + + operation_entries.size() * OPERATION_ENTRY_SIZE); std::ofstream file(path, std::ios::binary); if (!file) { @@ -374,6 +421,18 @@ auto URITemplateRouterView::save(const URITemplateRouter &router, sizeof(entry.context)); } + file.write(reinterpret_cast(&path_count), sizeof(path_count)); + for (const auto &entry : path_entries) { + file.write(reinterpret_cast(&entry.identifier), + sizeof(entry.identifier)); + file.write(reinterpret_cast(&entry.context), + sizeof(entry.context)); + file.write(reinterpret_cast(&entry.string_offset), + sizeof(entry.string_offset)); + file.write(reinterpret_cast(&entry.string_length), + sizeof(entry.string_length)); + } + if (!file) { throw URITemplateRouterSaveError{path, "Failed to write router data to file"}; @@ -918,4 +977,170 @@ auto URITemplateRouterView::operation(const std::string_view operation_id) const return miss; } +auto URITemplateRouterView::at(const std::size_t index) const + -> URITemplateRouter::Identifier { + assert(this->size_ >= sizeof(RouterHeader)); + + const auto *header = reinterpret_cast(this->data_); + assert(header->magic == ROUTER_MAGIC); + assert(header->version == ROUTER_VERSION); + + const auto paths_start = static_cast(header->paths_offset); + assert(paths_start <= this->size_); + assert(this->size_ - paths_start >= sizeof(std::uint16_t)); + + std::uint16_t path_count = 0; + std::memcpy(&path_count, this->data_ + paths_start, sizeof(path_count)); + assert(index < path_count); + + const auto entry_offset = + paths_start + sizeof(path_count) + index * PATH_ENTRY_SIZE; + const auto entry = read_path_entry(this->data_, entry_offset); + return entry.identifier; +} + +auto URITemplateRouterView::context( + const URITemplateRouter::Identifier identifier) const + -> URITemplateRouter::Identifier { + assert(identifier > 0); + assert(this->size_ >= sizeof(RouterHeader)); + + const auto *header = reinterpret_cast(this->data_); + assert(header->magic == ROUTER_MAGIC); + assert(header->version == ROUTER_VERSION); + + const auto paths_start = static_cast(header->paths_offset); + assert(paths_start <= this->size_); + assert(this->size_ - paths_start >= sizeof(std::uint16_t)); + + std::uint16_t path_count = 0; + std::memcpy(&path_count, this->data_ + paths_start, sizeof(path_count)); + + const auto entries_offset = paths_start + sizeof(path_count); + for (std::uint16_t index = 0; index < path_count; ++index) { + const auto entry_offset = entries_offset + index * PATH_ENTRY_SIZE; + const auto entry = read_path_entry(this->data_, entry_offset); + if (entry.identifier == identifier) { + return entry.context; + } + } + + assert(false); + return URITemplateRouter::Identifier{0}; +} + +auto URITemplateRouterView::path( + const URITemplateRouter::Identifier identifier) const -> std::string { + assert(identifier > 0); + assert(this->size_ >= sizeof(RouterHeader)); + + const auto *header = reinterpret_cast(this->data_); + assert(header->magic == ROUTER_MAGIC); + assert(header->version == ROUTER_VERSION); + + const auto paths_start = static_cast(header->paths_offset); + assert(paths_start <= this->size_); + assert(this->size_ - paths_start >= sizeof(std::uint16_t)); + + std::uint16_t path_count = 0; + std::memcpy(&path_count, this->data_ + paths_start, sizeof(path_count)); + + const auto *string_table = + reinterpret_cast(this->data_ + header->string_table_offset); + const auto string_table_size = + header->arguments_offset - header->string_table_offset; + + const auto entries_offset = paths_start + sizeof(path_count); + for (std::uint16_t index = 0; index < path_count; ++index) { + const auto entry_offset = entries_offset + index * PATH_ENTRY_SIZE; + const auto entry = read_path_entry(this->data_, entry_offset); + if (entry.identifier == identifier) { + assert(entry.string_offset <= string_table_size); + assert(entry.string_length <= string_table_size - entry.string_offset); + return std::string{string_table + entry.string_offset, + entry.string_length}; + } + } + + assert(false); + return {}; +} + +auto URITemplateRouterView::operation_id( + const URITemplateRouter::Identifier identifier) const -> std::string_view { + if (identifier == 0) { + return {}; + } + + if (this->size_ < sizeof(RouterHeader)) { + return {}; + } + + const auto *header = reinterpret_cast(this->data_); + if (header->magic != ROUTER_MAGIC || header->version != ROUTER_VERSION) { + return {}; + } + + const auto operations_start = + static_cast(header->operations_offset); + if (operations_start > this->size_ || + this->size_ - operations_start < sizeof(std::uint16_t)) { + return {}; + } + + std::uint16_t entry_count = 0; + std::memcpy(&entry_count, this->data_ + operations_start, + sizeof(entry_count)); + if (entry_count == 0) { + return {}; + } + + const auto entries_offset = operations_start + sizeof(entry_count); + const auto entries_size = + static_cast(entry_count) * OPERATION_ENTRY_SIZE; + if (entries_size > this->size_ - entries_offset) { + return {}; + } + + if (header->string_table_offset > this->size_ || + header->arguments_offset < header->string_table_offset || + header->arguments_offset > this->size_) { + return {}; + } + + const auto *string_table = + reinterpret_cast(this->data_ + header->string_table_offset); + const auto string_table_size = + header->arguments_offset - header->string_table_offset; + + for (std::uint16_t index = 0; index < entry_count; ++index) { + const auto entry_offset = + entries_offset + static_cast(index) * OPERATION_ENTRY_SIZE; + + OperationEntry entry{}; + std::memcpy(&entry.string_offset, this->data_ + entry_offset, + sizeof(entry.string_offset)); + std::memcpy(&entry.string_length, + this->data_ + entry_offset + sizeof(entry.string_offset), + sizeof(entry.string_length)); + std::memcpy(&entry.identifier, + this->data_ + entry_offset + sizeof(entry.string_offset) + + sizeof(entry.string_length), + sizeof(entry.identifier)); + + if (entry.identifier != identifier) { + continue; + } + + if (entry.string_offset > string_table_size || + entry.string_length > string_table_size - entry.string_offset) { + return {}; + } + + return {string_table + entry.string_offset, entry.string_length}; + } + + return {}; +} + } // namespace sourcemeta::core diff --git a/test/uritemplate/uritemplate_router_test.cc b/test/uritemplate/uritemplate_router_test.cc index 48058ac424..aa239c4c4f 100644 --- a/test/uritemplate/uritemplate_router_test.cc +++ b/test/uritemplate/uritemplate_router_test.cc @@ -2472,3 +2472,62 @@ TEST(URITemplateRouter, expansion_consumes_unlike_plain_variable) { EXPECT_EQ(captures_c.size(), 1); EXPECT_ROUTER_CAPTURE(captures_c, 0, "z", "one/two"); } + +TEST(URITemplateRouter, operation_id_round_trip_single_route) { + sourcemeta::core::URITemplateRouter router; + router.add("/users", "list_users", 1); + EXPECT_EQ(router.operation_id(1), "list_users"); +} + +TEST(URITemplateRouter, operation_id_round_trip_multiple_routes) { + sourcemeta::core::URITemplateRouter router; + router.add("/users", "list_users", 1, 11); + router.add("/posts/{id}", "show_post", 2, 22); + router.add("/files/{+rest}", "fetch_files", 3, 33); + EXPECT_EQ(router.operation_id(1), "list_users"); + EXPECT_EQ(router.operation_id(2), "show_post"); + EXPECT_EQ(router.operation_id(3), "fetch_files"); +} + +TEST(URITemplateRouter, operation_id_inverse_of_operation) { + sourcemeta::core::URITemplateRouter router; + router.add("/a", "alpha", 1, 11); + router.add("/b", "beta", 2, 22); + router.add("/c", "gamma", 3, 33); + + EXPECT_EQ(router.operation_id(1), "alpha"); + EXPECT_EQ(router.operation_id(2), "beta"); + EXPECT_EQ(router.operation_id(3), "gamma"); + + const auto resolved_alpha = router.operation(router.operation_id(1)); + EXPECT_EQ(resolved_alpha.first, 1); + EXPECT_EQ(resolved_alpha.second, 11); + + const auto resolved_beta = router.operation(router.operation_id(2)); + EXPECT_EQ(resolved_beta.first, 2); + EXPECT_EQ(resolved_beta.second, 22); + + const auto resolved_gamma = router.operation(router.operation_id(3)); + EXPECT_EQ(resolved_gamma.first, 3); + EXPECT_EQ(resolved_gamma.second, 33); +} + +TEST(URITemplateRouter, operation_id_zero_returns_empty) { + sourcemeta::core::URITemplateRouter router; + router.add("/users", "list_users", 1); + EXPECT_TRUE(router.operation_id(0).empty()); +} + +TEST(URITemplateRouter, operation_id_unregistered_returns_empty) { + sourcemeta::core::URITemplateRouter router; + router.add("/users", "list_users", 1); + EXPECT_TRUE(router.operation_id(99).empty()); +} + +TEST(URITemplateRouter, operation_id_after_overwrite) { + sourcemeta::core::URITemplateRouter router; + router.add("/users", "list_users", 1); + router.add("/users", "list_users_v2", 2); + EXPECT_TRUE(router.operation_id(1).empty()); + EXPECT_EQ(router.operation_id(2), "list_users_v2"); +} diff --git a/test/uritemplate/uritemplate_router_view_test.cc b/test/uritemplate/uritemplate_router_view_test.cc index 4ae56ee3f3..66560992d9 100644 --- a/test/uritemplate/uritemplate_router_view_test.cc +++ b/test/uritemplate/uritemplate_router_view_test.cc @@ -3218,3 +3218,264 @@ TEST_F(URITemplateRouterViewTest, EXPECT_EQ(captures_c.size(), 1); EXPECT_ROUTER_CAPTURE(captures_c, 0, "z", "one/two"); } + +TEST_F(URITemplateRouterViewTest, listing_at_returns_identifiers_in_add_order) { + { + sourcemeta::core::URITemplateRouter router; + router.add("/users", "list_users", 7); + router.add("/posts/{id}", "show_post", 3); + router.add("/{+rest}", "fetch_all", 9); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + EXPECT_EQ(restored.size(), 3); + EXPECT_EQ(restored.at(0), 7); + EXPECT_EQ(restored.at(1), 3); + EXPECT_EQ(restored.at(2), 9); +} + +TEST_F(URITemplateRouterViewTest, listing_context_returns_associated_context) { + { + sourcemeta::core::URITemplateRouter router; + router.add("/users", "list_users", 1, 100); + router.add("/posts/{id}", "show_post", 2, 200); + router.add("/comments", "list_comments", 3, 300); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + EXPECT_EQ(restored.context(1), 100); + EXPECT_EQ(restored.context(2), 200); + EXPECT_EQ(restored.context(3), 300); +} + +TEST_F(URITemplateRouterViewTest, listing_context_default_zero) { + { + sourcemeta::core::URITemplateRouter router; + router.add("/users", "list_users", 1); + router.add("/posts", "list_posts", 2); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + EXPECT_EQ(restored.context(1), 0); + EXPECT_EQ(restored.context(2), 0); +} + +TEST_F(URITemplateRouterViewTest, listing_path_for_literal_route) { + { + sourcemeta::core::URITemplateRouter router; + router.add("/users", "list_users", 1); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + EXPECT_EQ(restored.path(1), "/users"); +} + +TEST_F(URITemplateRouterViewTest, listing_path_for_variable_route) { + { + sourcemeta::core::URITemplateRouter router; + router.add("/users/{id}", "show_user", 1); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + EXPECT_EQ(restored.path(1), "/users/{id}"); +} + +TEST_F(URITemplateRouterViewTest, listing_path_for_multi_variable_route) { + { + sourcemeta::core::URITemplateRouter router; + router.add("/users/{id}/posts/{post_id}", "show_post", 1); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + EXPECT_EQ(restored.path(1), "/users/{id}/posts/{post_id}"); +} + +TEST_F(URITemplateRouterViewTest, listing_path_for_expansion_route) { + { + sourcemeta::core::URITemplateRouter router; + router.add("/files/{+rest}", "fetch_files", 1); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + EXPECT_EQ(restored.path(1), "/files/{+rest}"); +} + +TEST_F(URITemplateRouterViewTest, listing_path_for_optional_expansion_route) { + { + sourcemeta::core::URITemplateRouter router; + router.add("/api/list{/path*}", "list_directory", 1); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + EXPECT_EQ(restored.path(1), "/api/list{/path*}"); +} + +TEST_F(URITemplateRouterViewTest, listing_path_for_root_route) { + { + sourcemeta::core::URITemplateRouter router; + router.add("/", "root_route", 1); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + EXPECT_EQ(restored.path(1), "/"); +} + +TEST_F(URITemplateRouterViewTest, + listing_path_for_multiple_routes_each_correct) { + { + sourcemeta::core::URITemplateRouter router; + router.add("/users", "list_users", 1); + router.add("/users/{id}", "show_user", 2); + router.add("/posts/{id}/comments/{comment_id}", "show_comment", 3); + router.add("/files/{+rest}", "fetch_files", 4); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + EXPECT_EQ(restored.path(1), "/users"); + EXPECT_EQ(restored.path(2), "/users/{id}"); + EXPECT_EQ(restored.path(3), "/posts/{id}/comments/{comment_id}"); + EXPECT_EQ(restored.path(4), "/files/{+rest}"); +} + +TEST_F(URITemplateRouterViewTest, listing_operation_id_round_trip) { + { + sourcemeta::core::URITemplateRouter router; + router.add("/users", "list_users", 1); + router.add("/posts/{id}", "show_post", 2); + router.add("/files/{+rest}", "fetch_files", 3); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + EXPECT_EQ(restored.operation_id(1), "list_users"); + EXPECT_EQ(restored.operation_id(2), "show_post"); + EXPECT_EQ(restored.operation_id(3), "fetch_files"); +} + +TEST_F(URITemplateRouterViewTest, listing_operation_id_zero_returns_empty) { + { + sourcemeta::core::URITemplateRouter router; + router.add("/users", "list_users", 1); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + EXPECT_TRUE(restored.operation_id(0).empty()); +} + +TEST_F(URITemplateRouterViewTest, + listing_operation_id_unregistered_returns_empty) { + { + sourcemeta::core::URITemplateRouter router; + router.add("/users", "list_users", 1); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + EXPECT_TRUE(restored.operation_id(99).empty()); +} + +TEST_F(URITemplateRouterViewTest, listing_operation_id_inverse_of_operation) { + { + sourcemeta::core::URITemplateRouter router; + router.add("/a", "alpha", 1, 11); + router.add("/b", "beta", 2, 22); + router.add("/c", "gamma", 3, 33); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + + EXPECT_EQ(restored.operation_id(1), "alpha"); + EXPECT_EQ(restored.operation_id(2), "beta"); + EXPECT_EQ(restored.operation_id(3), "gamma"); + + const auto resolved_alpha = restored.operation(restored.operation_id(1)); + EXPECT_EQ(resolved_alpha.first, 1); + EXPECT_EQ(resolved_alpha.second, 11); + + const auto resolved_beta = restored.operation(restored.operation_id(2)); + EXPECT_EQ(resolved_beta.first, 2); + EXPECT_EQ(resolved_beta.second, 22); + + const auto resolved_gamma = restored.operation(restored.operation_id(3)); + EXPECT_EQ(resolved_gamma.first, 3); + EXPECT_EQ(resolved_gamma.second, 33); +} + +TEST_F(URITemplateRouterViewTest, listing_matches_writable_router) { + sourcemeta::core::URITemplateRouter router; + router.add("/users", "list_users", 1, 10); + router.add("/posts/{id}", "show_post", 2, 20); + router.add("/files/{+rest}", "fetch_files", 3, 30); + router.add("/api/list{/path*}", "list_directory", 4, 40); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + EXPECT_EQ(restored.size(), router.size()); + + EXPECT_EQ(restored.at(0), router.at(0)); + EXPECT_EQ(restored.at(1), router.at(1)); + EXPECT_EQ(restored.at(2), router.at(2)); + EXPECT_EQ(restored.at(3), router.at(3)); + + EXPECT_EQ(restored.context(1), router.context(1)); + EXPECT_EQ(restored.context(2), router.context(2)); + EXPECT_EQ(restored.context(3), router.context(3)); + EXPECT_EQ(restored.context(4), router.context(4)); + + EXPECT_EQ(restored.path(1), router.path(1)); + EXPECT_EQ(restored.path(2), router.path(2)); + EXPECT_EQ(restored.path(3), router.path(3)); + EXPECT_EQ(restored.path(4), router.path(4)); + + EXPECT_EQ(restored.operation_id(1), router.operation_id(1)); + EXPECT_EQ(restored.operation_id(2), router.operation_id(2)); + EXPECT_EQ(restored.operation_id(3), router.operation_id(3)); + EXPECT_EQ(restored.operation_id(4), router.operation_id(4)); +} + +TEST_F(URITemplateRouterViewTest, listing_empty_router_size_zero) { + { + sourcemeta::core::URITemplateRouter router; + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + EXPECT_EQ(restored.size(), 0); + EXPECT_TRUE(restored.operation_id(0).empty()); + EXPECT_TRUE(restored.operation_id(1).empty()); +} + +TEST_F(URITemplateRouterViewTest, listing_only_otherwise_size_zero) { + { + sourcemeta::core::URITemplateRouter router; + router.otherwise(99); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + EXPECT_EQ(restored.size(), 0); + EXPECT_TRUE(restored.operation_id(0).empty()); +} + +TEST_F(URITemplateRouterViewTest, listing_path_excludes_base_path) { + { + sourcemeta::core::URITemplateRouter router{"/api/v1"}; + router.add("/users/{id}", "show_user", 1); + sourcemeta::core::URITemplateRouterView::save(router, this->path); + } + + const sourcemeta::core::URITemplateRouterView restored{this->path}; + EXPECT_EQ(restored.path(1), "/users/{id}"); +} From 22546764d01b62ef13cd79e537c979ee7f5d1c0f Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Fri, 15 May 2026 16:31:02 -0400 Subject: [PATCH 2/2] Fix Signed-off-by: Juan Cruz Viotti --- .../uritemplate/uritemplate_router_view.cc | 91 +++++++++++++------ 1 file changed, 65 insertions(+), 26 deletions(-) diff --git a/src/core/uritemplate/uritemplate_router_view.cc b/src/core/uritemplate/uritemplate_router_view.cc index 8ea60342bb..9025f7331c 100644 --- a/src/core/uritemplate/uritemplate_router_view.cc +++ b/src/core/uritemplate/uritemplate_router_view.cc @@ -979,22 +979,35 @@ auto URITemplateRouterView::operation(const std::string_view operation_id) const auto URITemplateRouterView::at(const std::size_t index) const -> URITemplateRouter::Identifier { - assert(this->size_ >= sizeof(RouterHeader)); + if (this->size_ < sizeof(RouterHeader)) { + return URITemplateRouter::Identifier{0}; + } const auto *header = reinterpret_cast(this->data_); - assert(header->magic == ROUTER_MAGIC); - assert(header->version == ROUTER_VERSION); + if (header->magic != ROUTER_MAGIC || header->version != ROUTER_VERSION) { + return URITemplateRouter::Identifier{0}; + } const auto paths_start = static_cast(header->paths_offset); - assert(paths_start <= this->size_); - assert(this->size_ - paths_start >= sizeof(std::uint16_t)); + if (paths_start > this->size_ || + this->size_ - paths_start < sizeof(std::uint16_t)) { + return URITemplateRouter::Identifier{0}; + } std::uint16_t path_count = 0; std::memcpy(&path_count, this->data_ + paths_start, sizeof(path_count)); - assert(index < path_count); + if (index >= path_count) { + return URITemplateRouter::Identifier{0}; + } + + const auto entries_offset = paths_start + sizeof(path_count); + const auto entries_size = + static_cast(path_count) * PATH_ENTRY_SIZE; + if (entries_size > this->size_ - entries_offset) { + return URITemplateRouter::Identifier{0}; + } - const auto entry_offset = - paths_start + sizeof(path_count) + index * PATH_ENTRY_SIZE; + const auto entry_offset = entries_offset + index * PATH_ENTRY_SIZE; const auto entry = read_path_entry(this->data_, entry_offset); return entry.identifier; } @@ -1002,21 +1015,31 @@ auto URITemplateRouterView::at(const std::size_t index) const auto URITemplateRouterView::context( const URITemplateRouter::Identifier identifier) const -> URITemplateRouter::Identifier { - assert(identifier > 0); - assert(this->size_ >= sizeof(RouterHeader)); + if (identifier == 0 || this->size_ < sizeof(RouterHeader)) { + return URITemplateRouter::Identifier{0}; + } const auto *header = reinterpret_cast(this->data_); - assert(header->magic == ROUTER_MAGIC); - assert(header->version == ROUTER_VERSION); + if (header->magic != ROUTER_MAGIC || header->version != ROUTER_VERSION) { + return URITemplateRouter::Identifier{0}; + } const auto paths_start = static_cast(header->paths_offset); - assert(paths_start <= this->size_); - assert(this->size_ - paths_start >= sizeof(std::uint16_t)); + if (paths_start > this->size_ || + this->size_ - paths_start < sizeof(std::uint16_t)) { + return URITemplateRouter::Identifier{0}; + } std::uint16_t path_count = 0; std::memcpy(&path_count, this->data_ + paths_start, sizeof(path_count)); const auto entries_offset = paths_start + sizeof(path_count); + const auto entries_size = + static_cast(path_count) * PATH_ENTRY_SIZE; + if (entries_size > this->size_ - entries_offset) { + return URITemplateRouter::Identifier{0}; + } + for (std::uint16_t index = 0; index < path_count; ++index) { const auto entry_offset = entries_offset + index * PATH_ENTRY_SIZE; const auto entry = read_path_entry(this->data_, entry_offset); @@ -1025,44 +1048,60 @@ auto URITemplateRouterView::context( } } - assert(false); return URITemplateRouter::Identifier{0}; } auto URITemplateRouterView::path( const URITemplateRouter::Identifier identifier) const -> std::string { - assert(identifier > 0); - assert(this->size_ >= sizeof(RouterHeader)); + if (identifier == 0 || this->size_ < sizeof(RouterHeader)) { + return {}; + } const auto *header = reinterpret_cast(this->data_); - assert(header->magic == ROUTER_MAGIC); - assert(header->version == ROUTER_VERSION); + if (header->magic != ROUTER_MAGIC || header->version != ROUTER_VERSION) { + return {}; + } const auto paths_start = static_cast(header->paths_offset); - assert(paths_start <= this->size_); - assert(this->size_ - paths_start >= sizeof(std::uint16_t)); + if (paths_start > this->size_ || + this->size_ - paths_start < sizeof(std::uint16_t)) { + return {}; + } - std::uint16_t path_count = 0; - std::memcpy(&path_count, this->data_ + paths_start, sizeof(path_count)); + if (header->string_table_offset > this->size_ || + header->arguments_offset < header->string_table_offset || + header->arguments_offset > this->size_) { + return {}; + } const auto *string_table = reinterpret_cast(this->data_ + header->string_table_offset); const auto string_table_size = header->arguments_offset - header->string_table_offset; + std::uint16_t path_count = 0; + std::memcpy(&path_count, this->data_ + paths_start, sizeof(path_count)); + const auto entries_offset = paths_start + sizeof(path_count); + const auto entries_size = + static_cast(path_count) * PATH_ENTRY_SIZE; + if (entries_size > this->size_ - entries_offset) { + return {}; + } + for (std::uint16_t index = 0; index < path_count; ++index) { const auto entry_offset = entries_offset + index * PATH_ENTRY_SIZE; const auto entry = read_path_entry(this->data_, entry_offset); if (entry.identifier == identifier) { - assert(entry.string_offset <= string_table_size); - assert(entry.string_length <= string_table_size - entry.string_offset); + if (entry.string_offset > string_table_size || + entry.string_length > string_table_size - entry.string_offset) { + return {}; + } return std::string{string_table + entry.string_offset, entry.string_length}; } } - assert(false); return {}; }