| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // | ||
| 2 | // Copyright (c) 2026 Vinnie Falco (vinnie dot falco at gmail dot com) | ||
| 3 | // | ||
| 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying | ||
| 5 | // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) | ||
| 6 | // | ||
| 7 | // Official repository: https://github.com/cppalliance/http | ||
| 8 | // | ||
| 9 | |||
| 10 | #include <boost/http/server/flat_router.hpp> | ||
| 11 | #include <boost/http/server/router.hpp> | ||
| 12 | #include <boost/http/detail/except.hpp> | ||
| 13 | |||
| 14 | #include "src/server/detail/router_base.hpp" | ||
| 15 | #include "src/server/detail/pct_decode.hpp" | ||
| 16 | #include "src/server/detail/route_match.hpp" | ||
| 17 | |||
| 18 | #include <system_error> | ||
| 19 | |||
| 20 | namespace boost { | ||
| 21 | namespace http { | ||
| 22 | |||
| 23 | //------------------------------------------------ | ||
| 24 | |||
| 25 | struct flat_router::impl | ||
| 26 | { | ||
| 27 | using entry = detail::router_base::entry; | ||
| 28 | using layer = detail::router_base::layer; | ||
| 29 | using handler = detail::router_base::handler; | ||
| 30 | using matcher = detail::router_base::matcher; | ||
| 31 | using opt_flags = detail::router_base::opt_flags; | ||
| 32 | using handler_ptr = detail::router_base::handler_ptr; | ||
| 33 | using match_result = route_params_base::match_result; | ||
| 34 | |||
| 35 | std::vector<entry> entries; | ||
| 36 | std::vector<matcher> matchers; | ||
| 37 | |||
| 38 | // RAII scope tracker sets matcher's skip_ when scope ends | ||
| 39 | struct scope_tracker | ||
| 40 | { | ||
| 41 | std::vector<matcher>& matchers_; | ||
| 42 | std::vector<entry>& entries_; | ||
| 43 | std::size_t matcher_idx_; | ||
| 44 | |||
| 45 | 99 | scope_tracker( | |
| 46 | std::vector<matcher>& m, | ||
| 47 | std::vector<entry>& e, | ||
| 48 | std::size_t idx) | ||
| 49 | 99 | : matchers_(m) | |
| 50 | 99 | , entries_(e) | |
| 51 | 99 | , matcher_idx_(idx) | |
| 52 | { | ||
| 53 | 99 | } | |
| 54 | |||
| 55 | 99 | ~scope_tracker() | |
| 56 | { | ||
| 57 | 99 | matchers_[matcher_idx_].skip_ = entries_.size(); | |
| 58 | 99 | } | |
| 59 | }; | ||
| 60 | |||
| 61 | static opt_flags | ||
| 62 | 84 | compute_effective_opts( | |
| 63 | opt_flags parent, | ||
| 64 | opt_flags child) | ||
| 65 | { | ||
| 66 | 84 | opt_flags result = parent; | |
| 67 | |||
| 68 | // case_sensitive: bits 1-2 (2=true, 4=false) | ||
| 69 |
2/2✓ Branch 0 taken 3 times.
✓ Branch 1 taken 81 times.
|
84 | if(child & 2) |
| 70 | 3 | result = (result & ~6) | 2; | |
| 71 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 80 times.
|
81 | else if(child & 4) |
| 72 | 1 | result = (result & ~6) | 4; | |
| 73 | |||
| 74 | // strict: bits 3-4 (8=true, 16=false) | ||
| 75 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 84 times.
|
84 | if(child & 8) |
| 76 | ✗ | result = (result & ~24) | 8; | |
| 77 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 84 times.
|
84 | else if(child & 16) |
| 78 | ✗ | result = (result & ~24) | 16; | |
| 79 | |||
| 80 | 84 | return result; | |
| 81 | } | ||
| 82 | |||
| 83 | void | ||
| 84 | 60 | flatten(detail::router_base::impl& src) | |
| 85 | { | ||
| 86 | 60 | flatten_recursive(src, opt_flags{}, 0); | |
| 87 | 60 | } | |
| 88 | |||
| 89 | void | ||
| 90 | 84 | flatten_recursive( | |
| 91 | detail::router_base::impl& src, | ||
| 92 | opt_flags parent_opts, | ||
| 93 | std::uint32_t depth) | ||
| 94 | { | ||
| 95 | 84 | opt_flags eff = compute_effective_opts(parent_opts, src.opt); | |
| 96 | |||
| 97 |
2/2✓ Branch 5 taken 99 times.
✓ Branch 6 taken 84 times.
|
183 | for(auto& layer : src.layers) |
| 98 | { | ||
| 99 | // Move matcher, set flat router fields | ||
| 100 | 99 | std::size_t matcher_idx = matchers.size(); | |
| 101 |
1/1✓ Branch 2 taken 99 times.
|
99 | matchers.emplace_back(std::move(layer.match)); |
| 102 | 99 | auto& m = matchers.back(); | |
| 103 | 99 | m.first_entry_ = entries.size(); | |
| 104 | 99 | m.effective_opts_ = eff; | |
| 105 | 99 | m.depth_ = depth; | |
| 106 | // m.skip_ set by scope_tracker dtor | ||
| 107 | |||
| 108 | 99 | scope_tracker scope(matchers, entries, matcher_idx); | |
| 109 | |||
| 110 |
2/2✓ Branch 5 taken 112 times.
✓ Branch 6 taken 99 times.
|
211 | for(auto& e : layer.entries) |
| 111 | { | ||
| 112 |
2/2✓ Branch 1 taken 24 times.
✓ Branch 2 taken 88 times.
|
112 | if(e.h->kind == detail::router_base::is_router) |
| 113 | { | ||
| 114 | // Recurse into nested router | ||
| 115 | 24 | auto* nested = e.h->get_router(); | |
| 116 |
2/4✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
|
24 | if(nested && nested->impl_) |
| 117 |
1/1✓ Branch 1 taken 24 times.
|
24 | flatten_recursive(*nested->impl_, eff, depth + 1); |
| 118 | } | ||
| 119 | else | ||
| 120 | { | ||
| 121 | // Set matcher_idx, then move entire entry | ||
| 122 | 88 | e.matcher_idx = matcher_idx; | |
| 123 |
1/1✓ Branch 2 taken 88 times.
|
88 | entries.emplace_back(std::move(e)); |
| 124 | } | ||
| 125 | } | ||
| 126 | // ~scope_tracker sets matchers[matcher_idx].skip_ | ||
| 127 | 99 | } | |
| 128 | 84 | } | |
| 129 | |||
| 130 | // Restore path to a given base_path length | ||
| 131 | static void | ||
| 132 | 20 | restore_path( | |
| 133 | route_params_base& p, | ||
| 134 | std::size_t base_len) | ||
| 135 | { | ||
| 136 | 20 | p.base_path = { p.decoded_path_.data(), base_len }; | |
| 137 | // Account for the addedSlash_ when computing path length | ||
| 138 |
2/2✓ Branch 1 taken 15 times.
✓ Branch 2 taken 5 times.
|
20 | auto const path_len = p.decoded_path_.size() - (p.addedSlash_ ? 1 : 0); |
| 139 |
2/2✓ Branch 0 taken 19 times.
✓ Branch 1 taken 1 times.
|
20 | if(base_len < path_len) |
| 140 | 19 | p.path = { p.decoded_path_.data() + base_len, | |
| 141 | path_len - base_len }; | ||
| 142 | else | ||
| 143 | 2 | p.path = { p.decoded_path_.data() + | |
| 144 | 1 | p.decoded_path_.size() - 1, 1 }; // soft slash | |
| 145 | 20 | } | |
| 146 | |||
| 147 | capy::task<route_result> | ||
| 148 |
1/1✓ Branch 1 taken 58 times.
|
58 | dispatch_loop(route_params_base& p) const |
| 149 | { | ||
| 150 | // All checks happen BEFORE co_await to minimize coroutine launches. | ||
| 151 | // Avoid touching p.ep_ (expensive atomic on Windows) - use p.kind_ for mode checks. | ||
| 152 | |||
| 153 | std::size_t last_matched = SIZE_MAX; | ||
| 154 | std::uint32_t current_depth = 0; | ||
| 155 | |||
| 156 | // Stack of base_path lengths at each depth level. | ||
| 157 | // path_stack[d] = base_path.size() before any matcher at depth d was tried. | ||
| 158 | std::size_t path_stack[detail::router_base::max_path_depth]; | ||
| 159 | path_stack[0] = 0; | ||
| 160 | |||
| 161 | // Track which matcher index is matched at each depth level. | ||
| 162 | // matched_at_depth[d] = matcher index that successfully matched at depth d. | ||
| 163 | std::size_t matched_at_depth[detail::router_base::max_path_depth]; | ||
| 164 | for(std::size_t d = 0; d < detail::router_base::max_path_depth; ++d) | ||
| 165 | matched_at_depth[d] = SIZE_MAX; | ||
| 166 | |||
| 167 | for(std::size_t i = 0; i < entries.size(); ) | ||
| 168 | { | ||
| 169 | auto const& e = entries[i]; | ||
| 170 | auto const& m = matchers[e.matcher_idx]; | ||
| 171 | auto const target_depth = m.depth_; | ||
| 172 | |||
| 173 | //-------------------------------------------------- | ||
| 174 | // Pre-invoke checks (no coroutine yet) | ||
| 175 | //-------------------------------------------------- | ||
| 176 | |||
| 177 | // For nested routes: verify ancestors at depths 0..target_depth-1 are matched. | ||
| 178 | // For siblings: if moving to same depth with different matcher, restore path. | ||
| 179 | bool ancestors_ok = true; | ||
| 180 | |||
| 181 | // Check if we need to match new ancestor matchers for this entry. | ||
| 182 | // We iterate through matchers from last_matched+1 to e.matcher_idx, | ||
| 183 | // but ONLY process those that are at depths we need (ancestors or self). | ||
| 184 | std::size_t start_idx = (last_matched == SIZE_MAX) ? 0 : last_matched + 1; | ||
| 185 | |||
| 186 | for(std::size_t check_idx = start_idx; | ||
| 187 | check_idx <= e.matcher_idx && ancestors_ok; | ||
| 188 | ++check_idx) | ||
| 189 | { | ||
| 190 | auto const& cm = matchers[check_idx]; | ||
| 191 | |||
| 192 | // Only check matchers that are: | ||
| 193 | // 1. Ancestors (depth < target_depth) that we haven't matched yet, or | ||
| 194 | // 2. The entry's own matcher | ||
| 195 | bool is_needed_ancestor = (cm.depth_ < target_depth) && | ||
| 196 | (matched_at_depth[cm.depth_] == SIZE_MAX); | ||
| 197 | bool is_self = (check_idx == e.matcher_idx); | ||
| 198 | |||
| 199 | if(!is_needed_ancestor && !is_self) | ||
| 200 | continue; | ||
| 201 | |||
| 202 | // Restore path if moving to same or shallower depth | ||
| 203 | if(cm.depth_ <= current_depth && current_depth > 0) | ||
| 204 | { | ||
| 205 | restore_path(p, path_stack[cm.depth_]); | ||
| 206 | } | ||
| 207 | |||
| 208 | // In error/exception mode, skip end routes | ||
| 209 | if(cm.end_ && p.kind_ != detail::router_base::is_plain) | ||
| 210 | { | ||
| 211 | i = cm.skip_; | ||
| 212 | ancestors_ok = false; | ||
| 213 | break; | ||
| 214 | } | ||
| 215 | |||
| 216 | // Apply effective_opts for this matcher | ||
| 217 | p.case_sensitive = (cm.effective_opts_ & 2) != 0; | ||
| 218 | p.strict = (cm.effective_opts_ & 8) != 0; | ||
| 219 | |||
| 220 | // Save path state before trying this matcher | ||
| 221 | if(cm.depth_ < detail::router_base::max_path_depth) | ||
| 222 | path_stack[cm.depth_] = p.base_path.size(); | ||
| 223 | |||
| 224 | match_result mr; | ||
| 225 | if(!cm(p, mr)) | ||
| 226 | { | ||
| 227 | // Clear matched_at_depth for this depth and deeper | ||
| 228 | for(std::size_t d = cm.depth_; d < detail::router_base::max_path_depth; ++d) | ||
| 229 | matched_at_depth[d] = SIZE_MAX; | ||
| 230 | i = cm.skip_; | ||
| 231 | ancestors_ok = false; | ||
| 232 | break; | ||
| 233 | } | ||
| 234 | |||
| 235 | // Mark this depth as matched | ||
| 236 | if(cm.depth_ < detail::router_base::max_path_depth) | ||
| 237 | matched_at_depth[cm.depth_] = check_idx; | ||
| 238 | |||
| 239 | last_matched = check_idx; | ||
| 240 | current_depth = cm.depth_ + 1; | ||
| 241 | |||
| 242 | // Save state for next depth level | ||
| 243 | if(current_depth < detail::router_base::max_path_depth) | ||
| 244 | path_stack[current_depth] = p.base_path.size(); | ||
| 245 | } | ||
| 246 | |||
| 247 | if(!ancestors_ok) | ||
| 248 | continue; | ||
| 249 | |||
| 250 | // Check method match (only for end routes) | ||
| 251 | if(m.end_ && !e.match_method( | ||
| 252 | const_cast<route_params_base&>(p))) | ||
| 253 | { | ||
| 254 | ++i; | ||
| 255 | continue; | ||
| 256 | } | ||
| 257 | |||
| 258 | // Check kind match (cheap char comparison) | ||
| 259 | if(e.h->kind != p.kind_) | ||
| 260 | { | ||
| 261 | ++i; | ||
| 262 | continue; | ||
| 263 | } | ||
| 264 | |||
| 265 | //-------------------------------------------------- | ||
| 266 | // Invoke handler (coroutine starts here) | ||
| 267 | //-------------------------------------------------- | ||
| 268 | |||
| 269 | route_result rv; | ||
| 270 | try | ||
| 271 | { | ||
| 272 | rv = co_await e.h->invoke( | ||
| 273 | const_cast<route_params_base&>(p)); | ||
| 274 | } | ||
| 275 | catch(...) | ||
| 276 | { | ||
| 277 | // Only touch ep_ when actually catching | ||
| 278 | p.ep_ = std::current_exception(); | ||
| 279 | p.kind_ = detail::router_base::is_exception; | ||
| 280 | ++i; | ||
| 281 | continue; | ||
| 282 | } | ||
| 283 | |||
| 284 | //-------------------------------------------------- | ||
| 285 | // Handle result | ||
| 286 | // | ||
| 287 | // Coroutines invert control - handler does the send. | ||
| 288 | // Success = !rv.failed() (handler completed request) | ||
| 289 | // route::next = continue to next handler | ||
| 290 | // route::next_route = skip to next route | ||
| 291 | // Failing error_code = enter error mode | ||
| 292 | //-------------------------------------------------- | ||
| 293 | |||
| 294 | if(rv == route::next) | ||
| 295 | { | ||
| 296 | ++i; | ||
| 297 | continue; | ||
| 298 | } | ||
| 299 | |||
| 300 | if(rv == route::next_route) | ||
| 301 | { | ||
| 302 | // next_route only valid for end routes, not middleware | ||
| 303 | if(!m.end_) | ||
| 304 | co_return make_error_code(std::errc::invalid_argument); | ||
| 305 | i = m.skip_; | ||
| 306 | continue; | ||
| 307 | } | ||
| 308 | |||
| 309 | if(!rv.failed()) | ||
| 310 | { | ||
| 311 | // Success - handler completed the request | ||
| 312 | co_return rv; | ||
| 313 | } | ||
| 314 | |||
| 315 | // Failing error_code - transition to error mode | ||
| 316 | p.ec_ = rv; | ||
| 317 | p.kind_ = detail::router_base::is_error; | ||
| 318 | |||
| 319 | if(m.end_) | ||
| 320 | { | ||
| 321 | // End routes don't have error handlers | ||
| 322 | i = m.skip_; | ||
| 323 | continue; | ||
| 324 | } | ||
| 325 | |||
| 326 | ++i; | ||
| 327 | } | ||
| 328 | |||
| 329 | // Final state | ||
| 330 | if(p.kind_ == detail::router_base::is_exception) | ||
| 331 | co_return error::unhandled_exception; | ||
| 332 | if(p.kind_ == detail::router_base::is_error) | ||
| 333 | co_return p.ec_; | ||
| 334 | |||
| 335 | co_return route::next; // no handler matched | ||
| 336 | 116 | } | |
| 337 | }; | ||
| 338 | |||
| 339 | //------------------------------------------------ | ||
| 340 | |||
| 341 | 60 | flat_router:: | |
| 342 | flat_router( | ||
| 343 | 60 | detail::router_base&& src) | |
| 344 | 60 | : impl_(std::make_shared<impl>()) | |
| 345 | { | ||
| 346 |
1/1✓ Branch 2 taken 60 times.
|
60 | impl_->flatten(*src.impl_); |
| 347 | 60 | } | |
| 348 | |||
| 349 | capy::task<route_result> | ||
| 350 | 54 | flat_router:: | |
| 351 | dispatch( | ||
| 352 | http::method verb, | ||
| 353 | urls::url_view const& url, | ||
| 354 | route_params_base& p) const | ||
| 355 | { | ||
| 356 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 53 times.
|
54 | if(verb == http::method::unknown) |
| 357 | 1 | detail::throw_invalid_argument(); | |
| 358 | |||
| 359 | // Initialize params | ||
| 360 | 53 | p.kind_ = detail::router_base::is_plain; | |
| 361 | 53 | p.verb_ = verb; | |
| 362 | 53 | p.verb_str_.clear(); | |
| 363 | 53 | p.ec_.clear(); | |
| 364 | 53 | p.ep_ = nullptr; | |
| 365 |
1/1✓ Branch 2 taken 53 times.
|
53 | p.decoded_path_ = detail::pct_decode_path(url.encoded_path()); |
| 366 | 53 | p.base_path = { p.decoded_path_.data(), 0 }; | |
| 367 | 53 | p.path = p.decoded_path_; | |
| 368 |
2/2✓ Branch 1 taken 25 times.
✓ Branch 2 taken 28 times.
|
53 | if(p.decoded_path_.back() != '/') |
| 369 | { | ||
| 370 | 25 | p.decoded_path_.push_back('/'); | |
| 371 | 25 | p.addedSlash_ = true; | |
| 372 | } | ||
| 373 | else | ||
| 374 | { | ||
| 375 | 28 | p.addedSlash_ = false; | |
| 376 | } | ||
| 377 | |||
| 378 | 53 | return impl_->dispatch_loop(p); | |
| 379 | } | ||
| 380 | |||
| 381 | capy::task<route_result> | ||
| 382 | 6 | flat_router:: | |
| 383 | dispatch( | ||
| 384 | std::string_view verb, | ||
| 385 | urls::url_view const& url, | ||
| 386 | route_params_base& p) const | ||
| 387 | { | ||
| 388 |
2/2✓ Branch 1 taken 1 times.
✓ Branch 2 taken 5 times.
|
6 | if(verb.empty()) |
| 389 | 1 | detail::throw_invalid_argument(); | |
| 390 | |||
| 391 | // Initialize params | ||
| 392 | 5 | p.kind_ = detail::router_base::is_plain; | |
| 393 |
1/1✓ Branch 2 taken 5 times.
|
5 | p.verb_ = http::string_to_method(verb); |
| 394 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1 times.
|
5 | if(p.verb_ == http::method::unknown) |
| 395 | 4 | p.verb_str_ = verb; | |
| 396 | else | ||
| 397 | 1 | p.verb_str_.clear(); | |
| 398 | 5 | p.ec_.clear(); | |
| 399 | 5 | p.ep_ = nullptr; | |
| 400 |
1/1✓ Branch 2 taken 5 times.
|
5 | p.decoded_path_ = detail::pct_decode_path(url.encoded_path()); |
| 401 | 5 | p.base_path = { p.decoded_path_.data(), 0 }; | |
| 402 | 5 | p.path = p.decoded_path_; | |
| 403 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
|
5 | if(p.decoded_path_.back() != '/') |
| 404 | { | ||
| 405 | ✗ | p.decoded_path_.push_back('/'); | |
| 406 | ✗ | p.addedSlash_ = true; | |
| 407 | } | ||
| 408 | else | ||
| 409 | { | ||
| 410 | 5 | p.addedSlash_ = false; | |
| 411 | } | ||
| 412 | |||
| 413 | 5 | return impl_->dispatch_loop(p); | |
| 414 | } | ||
| 415 | |||
| 416 | } // http | ||
| 417 | } // boost | ||
| 418 | |||
| 419 |