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
17 changes: 14 additions & 3 deletions src/core/uritemplate/include/sourcemeta/core/uritemplate_router.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,18 @@ namespace sourcemeta::core {

/// @ingroup uritemplate
/// A URI Template path router. Keep in mind that the URI Template specification
/// DOES NOT define expansion. So this is an opinionated non-standard adaptation
/// of URI Template for path routing purposes
/// DOES NOT define matching, only expansion. So this is an opinionated
/// non-standard adaptation of URI Template for path routing purposes. The
/// supported operators are:
///
/// - `{var}` for a single path segment (RFC 6570 Level 1 simple expansion)
/// - `{+var}` for greedy capture to the end of the path, requiring at least
/// one trailing segment (RFC 6570 Level 2 reserved expansion)
/// - `{/var*}` for optional greedy capture to the end of the path, where
/// zero trailing segments are also allowed (RFC 6570 Level 3 path-segment
/// operator with Level 4 explode modifier). Must be the last component of
/// the template. The captured value is empty when no trailing segments
/// are present
class SOURCEMETA_CORE_URITEMPLATE_EXPORT URITemplateRouter {
friend class URITemplateRouterView;

Expand All @@ -56,7 +66,8 @@ class SOURCEMETA_CORE_URITEMPLATE_EXPORT URITemplateRouter {
Root = 0,
Literal = 1,
Variable = 2,
Expansion = 3
Expansion = 3,
OptionalExpansion = 4
};

/// A node in the router trie
Expand Down
87 changes: 74 additions & 13 deletions src/core/uritemplate/uritemplate_router.cc
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,15 @@ auto find_or_create_literal_child(std::vector<std::unique_ptr<Node>> &literals,
return result;
}

inline auto is_expansion_type(const NodeType type) noexcept -> bool {
return type == NodeType::Expansion || type == NodeType::OptionalExpansion;
}

auto find_or_create_variable_child(std::unique_ptr<Node> &variable,
const std::string_view name,
const NodeType type) -> Node * {
const NodeType type,
const std::string_view expression)
-> Node * {
if (!variable) {
variable = std::make_unique<Node>();
variable->type = type;
Expand All @@ -59,12 +65,19 @@ auto find_or_create_variable_child(std::unique_ptr<Node> &variable,
throw URITemplateRouterVariableMismatchError{variable->value, name};
}

if (type == NodeType::Expansion) {
if (is_expansion_type(variable->type) && is_expansion_type(type) &&
variable->type != type) {
throw URITemplateRouterInvalidSegmentError{
"Conflicting expansion operators on the same path position",
expression};
}

if (is_expansion_type(type)) {
if (variable->type == NodeType::Variable) {
variable->type = NodeType::Expansion;
variable->type = type;
return variable.get();
}
} else if (variable->type == NodeType::Expansion) {
} else if (is_expansion_type(variable->type)) {
return nullptr;
}

Expand Down Expand Up @@ -296,13 +309,21 @@ auto URITemplateRouter::add(const std::string_view uri_template,
}

NodeType type = NodeType::Variable;
bool path_segment_operator = false;
if (*position == '+') {
type = NodeType::Expansion;
++position;
if (position >= end || *position == '}') {
throw URITemplateRouterInvalidSegmentError{"Empty variable name",
expression};
}
} else if (*position == '/') {
path_segment_operator = true;
++position;
if (position >= end || *position == '}') {
throw URITemplateRouterInvalidSegmentError{"Empty variable name",
expression};
}
} else if (is_operator(*position) && *position != '+') {
throw URITemplateRouterInvalidSegmentError{
"Unsupported URI Template operator", expression};
Expand Down Expand Up @@ -335,6 +356,8 @@ auto URITemplateRouter::add(const std::string_view uri_template,
expression};
}

const char *const varname_end = position;

if (*position == ' ') {
throw URITemplateRouterInvalidSegmentError{
"Space in variable expression", expression};
Expand All @@ -346,8 +369,25 @@ auto URITemplateRouter::add(const std::string_view uri_template,
}

if (*position == '*') {
if (!path_segment_operator) {
throw URITemplateRouterInvalidSegmentError{
"Explode modifier not supported", expression};
}
if (varname_end == varname_start) {
throw URITemplateRouterInvalidSegmentError{"Empty variable name",
expression};
}
type = NodeType::OptionalExpansion;
++position;
if (position >= end || *position != '}') {
throw URITemplateRouterInvalidSegmentError{
"Unexpected characters after explode modifier", expression};
}
} else if (path_segment_operator) {
throw URITemplateRouterInvalidSegmentError{
"Explode modifier not supported", expression};
"Path-segment expansion without the explode modifier is not "
"supported",
expression};
}

if (*position == ',') {
Expand All @@ -356,23 +396,26 @@ auto URITemplateRouter::add(const std::string_view uri_template,
}

const std::string_view varname{
varname_start, static_cast<std::size_t>(position - varname_start)};
varname_start, static_cast<std::size_t>(varname_end - varname_start)};

++position; // skip '}'

if (position < end && *position != '/') {
throw URITemplateRouterInvalidSegmentError{
"Path segment cannot mix literals and variables",
extract_segment(expression_start, end)};
if (*position != '{' || position + 1 >= end || *(position + 1) != '/') {
throw URITemplateRouterInvalidSegmentError{
"Path segment cannot mix literals and variables",
extract_segment(expression_start, end)};
}
}

if (type == NodeType::Expansion && position < end) {
if (is_expansion_type(type) && position < end) {
Copy link
Copy Markdown

@augmentcode augmentcode Bot May 13, 2026

Choose a reason for hiding this comment

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

src/core/uritemplate/uritemplate_router.cc:411: Since is_expansion_type(type) now includes OptionalExpansion, the thrown message ("Reserved expansion must be the last segment") can be raised for {/var*} too, which isn’t a reserved expansion. Consider adjusting the wording so the error remains accurate for both operators.

Severity: low

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

throw URITemplateRouterInvalidSegmentError{
"Reserved expansion must be the last segment", expression};
"Expansion operator must be the last segment", expression};
}

auto &variable = current ? current->variable : this->root_.variable;
auto *result = find_or_create_variable_child(variable, varname, type);
auto *result =
find_or_create_variable_child(variable, varname, type, expression);
if (result == nullptr) {
absorbed = true;
} else {
Expand All @@ -388,6 +431,14 @@ auto URITemplateRouter::add(const std::string_view uri_template,
}

if (position < end && *position == '{') {
if (position + 1 < end && *(position + 1) == '/') {
const std::string_view segment{
segment_start,
static_cast<std::size_t>(position - segment_start)};
auto &literals = current ? current->literals : this->root_.literals;
current = &find_or_create_literal_child(literals, segment);
continue;
}
const char *expr_end = find_expression_end(position, end);
const char *seg_end = expr_end;
while (seg_end < end && *seg_end != '/') {
Expand Down Expand Up @@ -518,7 +569,7 @@ auto URITemplateRouter::match(const std::string_view path,
} else if (*variable_child) {
assert(variable_index <=
std::numeric_limits<URITemplateRouter::Index>::max());
if ((*variable_child)->type == NodeType::Expansion) {
if (is_expansion_type((*variable_child)->type)) {
const std::string_view remaining{
segment_start, static_cast<std::size_t>(path_end - segment_start)};
callback(static_cast<URITemplateRouter::Index>(variable_index),
Expand Down Expand Up @@ -546,6 +597,16 @@ auto URITemplateRouter::match(const std::string_view path,
++position;
}

if (current && current->identifier == 0 && current->variable &&
current->variable->type == NodeType::OptionalExpansion) {
assert(variable_index <=
std::numeric_limits<URITemplateRouter::Index>::max());
callback(static_cast<URITemplateRouter::Index>(variable_index),
current->variable->value, std::string_view{});
return finalize_match(this->otherwise_, current->variable->identifier,
current->variable->context);
}

return current ? finalize_match(this->otherwise_, current->identifier,
current->context)
: finalize_match(this->otherwise_, this->root_.identifier,
Expand Down
36 changes: 31 additions & 5 deletions src/core/uritemplate/uritemplate_router_view.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ namespace sourcemeta::core {
namespace {

constexpr std::uint32_t ROUTER_MAGIC = 0x52544552; // "RTER"
constexpr std::uint32_t ROUTER_VERSION = 6;
constexpr std::uint32_t ROUTER_VERSION = 7;
constexpr std::uint32_t NO_CHILD = std::numeric_limits<std::uint32_t>::max();

// Type tags for argument value serialization
Expand Down Expand Up @@ -83,6 +83,12 @@ finalize_match(const URITemplateRouter::Identifier otherwise_context,
return {identifier, context};
}

inline auto is_expansion_type(const URITemplateRouter::NodeType type) noexcept
-> bool {
return type == URITemplateRouter::NodeType::Expansion ||
type == URITemplateRouter::NodeType::OptionalExpansion;
}

// Binary search for a literal child matching the given segment
inline auto binary_search_literal_children(
const SerializedNode *nodes, const char *string_table,
Expand Down Expand Up @@ -518,8 +524,9 @@ auto URITemplateRouterView::match(
return finalize_match(otherwise_context, 0, 0);
}

// Check if this is an expansion (catch-all)
if (variable_node.type == URITemplateRouter::NodeType::Expansion) {
// Both Expansion and OptionalExpansion consume the rest of the path
// verbatim
if (is_expansion_type(variable_node.type)) {
const auto remaining_length =
static_cast<std::uint32_t>(path_end - segment_start);
callback(static_cast<URITemplateRouter::Index>(variable_index),
Expand Down Expand Up @@ -548,8 +555,27 @@ auto URITemplateRouterView::match(
return finalize_match(otherwise_context, 0, 0);
}

return finalize_match(otherwise_context, nodes[current_node].identifier,
nodes[current_node].context);
const auto &final_node = nodes[current_node];
if (final_node.identifier == 0 && final_node.variable_child != NO_CHILD &&
final_node.variable_child < header->node_count) {
const auto &variable_node = nodes[final_node.variable_child];
if (variable_node.type == URITemplateRouter::NodeType::OptionalExpansion) {
if (variable_node.string_offset > string_table_size ||
variable_node.string_length >
string_table_size - variable_node.string_offset) {
return finalize_match(otherwise_context, 0, 0);
}
callback(static_cast<URITemplateRouter::Index>(variable_index),
{string_table + variable_node.string_offset,
variable_node.string_length},
std::string_view{});
return finalize_match(otherwise_context, variable_node.identifier,
variable_node.context);
}
}

return finalize_match(otherwise_context, final_node.identifier,
final_node.context);
}

auto URITemplateRouterView::arguments(
Expand Down
Loading
Loading