GCC Code Coverage Report


Directory: ./
File: libs/http/src/server/flat_router.cpp
Date: 2026-01-20 00:11:35
Exec Total Coverage
Lines: 86 90 95.6%
Functions: 10 10 100.0%
Branches: 35 40 87.5%

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