src/server/route_abnf.cpp

93.6% Lines (248/265) 100.0% List of functions (34/34)
route_abnf.cpp
f(x) Functions (34)
Function Calls Lines Blocks
boost::http::detail::(anonymous namespace)::is_special(char) :25 676x 100.0% 100.0% boost::http::detail::(anonymous namespace)::is_reserved(char) :49 203x 100.0% 100.0% boost::http::detail::(anonymous namespace)::is_id_start(char) :68 102x 100.0% 100.0% boost::http::detail::(anonymous namespace)::is_id_continue(char) :78 73x 100.0% 100.0% boost::http::detail::(anonymous namespace)::parser::parser(boost::core::basic_string_view<char>) :96 136x 100.0% 100.0% boost::http::detail::(anonymous namespace)::parser::at_end() const :104 1564x 100.0% 100.0% boost::http::detail::(anonymous namespace)::parser::peek() const :110 1111x 100.0% 100.0% boost::http::detail::(anonymous namespace)::parser::advance() :116 61x 100.0% 100.0% boost::http::detail::(anonymous namespace)::parser::get() :122 745x 100.0% 100.0% boost::http::detail::(anonymous namespace)::parser::parse_identifier() :140 29x 88.9% 84.0% boost::http::detail::(anonymous namespace)::parser::parse_quoted_name() :156 4x 75.0% 71.0% boost::http::detail::(anonymous namespace)::parser::parse_name() :195 35x 100.0% 100.0% boost::http::detail::(anonymous namespace)::parser::parse_text() :212 155x 94.1% 89.0% boost::http::detail::(anonymous namespace)::parser::parse_param() :246 30x 90.0% 76.0% boost::http::detail::(anonymous namespace)::parser::parse_wildcard() :263 5x 90.0% 76.0% boost::http::detail::(anonymous namespace)::parser::parse_group() :280 7x 88.2% 75.0% boost::http::detail::(anonymous namespace)::parser::parse_token() :309 203x 93.3% 90.0% boost::http::detail::(anonymous namespace)::parser::parse_tokens() :339 136x 100.0% 80.0% boost::http::detail::(anonymous namespace)::ci_equal(char, char) :360 502x 100.0% 100.0% boost::http::detail::(anonymous namespace)::ci_starts_with(boost::core::basic_string_view<char>, boost::core::basic_string_view<char>) :370 123x 100.0% 100.0% boost::http::detail::(anonymous namespace)::route_matcher::route_matcher(boost::core::basic_string_view<char>, boost::http::detail::match_options const&) :396 102x 100.0% 100.0% boost::http::detail::(anonymous namespace)::route_matcher::at_end() const :404 76x 100.0% 100.0% boost::http::detail::(anonymous namespace)::route_matcher::pos() const :409 87x 100.0% 100.0% boost::http::detail::(anonymous namespace)::route_matcher::params() const :415 87x 100.0% 100.0% boost::http::detail::(anonymous namespace)::route_matcher::match_text(boost::core::basic_string_view<char>) :421 129x 100.0% 100.0% boost::http::detail::(anonymous namespace)::route_matcher::match_param(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, char) :439 26x 92.3% 81.0% boost::http::detail::(anonymous namespace)::route_matcher::match_wildcard(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) :464 4x 90.0% 68.0% boost::http::detail::(anonymous namespace)::route_matcher::get_stop_char(std::vector<boost::http::detail::route_token, std::allocator<boost::http::detail::route_token> > const&, unsigned long) :485 176x 100.0% 100.0% boost::http::detail::(anonymous namespace)::route_matcher::match_tokens(std::vector<boost::http::detail::route_token, std::allocator<boost::http::detail::route_token> > const&) :500 119x 100.0% 100.0% boost::http::detail::(anonymous namespace)::route_matcher::match_token(boost::http::detail::route_token const&, char) :511 176x 83.3% 93.0% boost::http::detail::(anonymous namespace)::route_matcher::match_group(std::vector<boost::http::detail::route_token, std::allocator<boost::http::detail::route_token> > const&) :533 17x 100.0% 100.0% boost::http::detail::(anonymous namespace)::route_matcher::is_complete() const :550 88x 90.0% 88.0% boost::http::detail::parse_route_pattern(boost::core::basic_string_view<char>) :573 136x 100.0% 73.0% boost::http::detail::match_route(boost::core::basic_string_view<char>, boost::http::detail::route_pattern const&, boost::http::detail::match_options const&) :589 102x 100.0% 83.0%
Line TLA Hits Source Code
1 //
2 // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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 "src/server/route_abnf.hpp"
11 #include <boost/url/grammar/error.hpp>
12
13 namespace boost {
14 namespace http {
15 namespace detail {
16
17 namespace {
18
19 //------------------------------------------------
20 // Character classification
21 //------------------------------------------------
22
23 // Special characters that have meaning in patterns
24 constexpr bool
25 676x is_special(char c) noexcept
26 {
27 676x switch(c)
28 {
29 53x case '{':
30 case '}':
31 case '(':
32 case ')':
33 case '[':
34 case ']':
35 case '+':
36 case '?':
37 case '!':
38 case ':':
39 case '*':
40 case '\\':
41 53x return true;
42 623x default:
43 623x return false;
44 }
45 }
46
47 // Reserved characters (parsed but invalid)
48 constexpr bool
49 203x is_reserved(char c) noexcept
50 {
51 203x switch(c)
52 {
53 5x case '(':
54 case ')':
55 case '[':
56 case ']':
57 case '+':
58 case '?':
59 case '!':
60 5x return true;
61 198x default:
62 198x return false;
63 }
64 }
65
66 // Valid identifier start (ASCII subset of ID_Start)
67 constexpr bool
68 102x is_id_start(char c) noexcept
69 {
70 return
71 102x (c >= 'a' && c <= 'z') ||
72 19x (c >= 'A' && c <= 'Z') ||
73 204x c == '_' || c == '$';
74 }
75
76 // Valid identifier continuation (ASCII subset of ID_Continue)
77 constexpr bool
78 73x is_id_continue(char c) noexcept
79 {
80 return
81 75x is_id_start(c) ||
82 75x (c >= '0' && c <= '9');
83 }
84
85 //------------------------------------------------
86 // Parser state
87 //------------------------------------------------
88
89 class parser
90 {
91 char const* it_;
92 char const* end_;
93 core::string_view original_;
94
95 public:
96 136x parser(core::string_view s)
97 136x : it_(s.data())
98 136x , end_(s.data() + s.size())
99 136x , original_(s)
100 {
101 136x }
102
103 bool
104 1564x at_end() const noexcept
105 {
106 1564x return it_ == end_;
107 }
108
109 char
110 1111x peek() const noexcept
111 {
112 1111x return *it_;
113 }
114
115 void
116 61x advance() noexcept
117 {
118 61x ++it_;
119 61x }
120
121 char
122 745x get() noexcept
123 {
124 745x return *it_++;
125 }
126
127 std::size_t
128 pos() const noexcept
129 {
130 return static_cast<std::size_t>(
131 it_ - original_.data());
132 }
133
134 //--------------------------------------------
135 // Name parsing
136 //--------------------------------------------
137
138 // Parse identifier: id-start *id-continue
139 system::result<std::string>
140 29x parse_identifier()
141 {
142 29x if(at_end() || !is_id_start(peek()))
143 return grammar::error::mismatch;
144
145 29x std::string result;
146 29x result += get();
147
148 85x while(!at_end() && is_id_continue(peek()))
149 56x result += get();
150
151 29x return result;
152 29x }
153
154 // Parse quoted name: DQUOTE *quoted-char DQUOTE
155 system::result<std::string>
156 4x parse_quoted_name()
157 {
158 4x if(at_end() || peek() != '"')
159 return grammar::error::mismatch;
160
161 4x advance(); // skip opening quote
162 4x std::string result;
163
164 36x while(!at_end())
165 {
166 35x char c = peek();
167
168 35x if(c == '"')
169 {
170 3x advance(); // skip closing quote
171 3x if(result.empty())
172 1x return grammar::error::syntax;
173 2x return result;
174 }
175
176 32x if(c == '\\')
177 {
178 advance(); // skip backslash
179 if(at_end())
180 return grammar::error::syntax;
181 result += get();
182 }
183 else
184 {
185 32x result += get();
186 }
187 }
188
189 // Unterminated quote
190 1x return grammar::error::syntax;
191 4x }
192
193 // Parse name: identifier / quoted-name
194 system::result<std::string>
195 35x parse_name()
196 {
197 35x if(at_end())
198 2x return grammar::error::syntax;
199
200 33x if(peek() == '"')
201 4x return parse_quoted_name();
202
203 29x return parse_identifier();
204 }
205
206 //--------------------------------------------
207 // Token parsing
208 //--------------------------------------------
209
210 // Parse text: 1*(char / escaped-char)
211 system::result<route_token>
212 155x parse_text()
213 {
214 155x std::string result;
215
216 783x while(!at_end())
217 {
218 676x char c = peek();
219
220 // Stop at special characters
221 676x if(is_special(c))
222 {
223 53x if(c == '\\')
224 {
225 // Escaped character
226 6x advance();
227 6x if(at_end())
228 1x return grammar::error::syntax;
229 5x result += get();
230 5x continue;
231 }
232 47x break;
233 }
234
235 623x result += get();
236 }
237
238 154x if(result.empty())
239 return grammar::error::mismatch;
240
241 154x return route_token(route_token_type::text, std::move(result));
242 155x }
243
244 // Parse param: ":" name
245 system::result<route_token>
246 30x parse_param()
247 {
248 30x if(at_end() || peek() != ':')
249 return grammar::error::mismatch;
250
251 30x advance(); // skip ':'
252
253 30x auto rv = parse_name();
254 30x if(rv.has_error())
255 3x return rv.error();
256
257 54x return route_token(
258 81x route_token_type::param, std::move(rv.value()));
259 30x }
260
261 // Parse wildcard: "*" name
262 system::result<route_token>
263 5x parse_wildcard()
264 {
265 5x if(at_end() || peek() != '*')
266 return grammar::error::mismatch;
267
268 5x advance(); // skip '*'
269
270 5x auto rv = parse_name();
271 5x if(rv.has_error())
272 1x return rv.error();
273
274 8x return route_token(
275 12x route_token_type::wildcard, std::move(rv.value()));
276 5x }
277
278 // Parse group: "{" *token "}"
279 system::result<route_token>
280 7x parse_group()
281 {
282 7x if(at_end() || peek() != '{')
283 return grammar::error::mismatch;
284
285 7x advance(); // skip '{'
286
287 7x route_token group;
288 7x group.type = route_token_type::group;
289
290 // Parse tokens until '}'
291 17x while(!at_end() && peek() != '}')
292 {
293 10x auto rv = parse_token();
294 10x if(rv.has_error())
295 return rv.error();
296 10x group.children.push_back(std::move(rv.value()));
297 10x }
298
299 7x if(at_end())
300 1x return grammar::error::syntax; // unclosed group
301
302 6x advance(); // skip '}'
303
304 6x return group;
305 7x }
306
307 // Parse single token
308 system::result<route_token>
309 203x parse_token()
310 {
311 203x if(at_end())
312 return grammar::error::syntax;
313
314 203x char c = peek();
315
316 // Check for reserved characters
317 203x if(is_reserved(c))
318 5x return grammar::error::syntax;
319
320 // Try each token type
321 198x if(c == ':')
322 30x return parse_param();
323
324 168x if(c == '*')
325 5x return parse_wildcard();
326
327 163x if(c == '{')
328 7x return parse_group();
329
330 156x if(c == '}')
331 1x return grammar::error::syntax; // unexpected '}'
332
333 // Must be text
334 155x return parse_text();
335 }
336
337 // Parse entire pattern
338 system::result<std::vector<route_token>>
339 136x parse_tokens()
340 {
341 136x std::vector<route_token> tokens;
342
343 317x while(!at_end())
344 {
345 193x auto rv = parse_token();
346 193x if(rv.has_error())
347 12x return rv.error();
348 181x tokens.push_back(std::move(rv.value()));
349 193x }
350
351 124x return tokens;
352 136x }
353 };
354
355 //------------------------------------------------
356 // Case-insensitive comparison
357 //------------------------------------------------
358
359 bool
360 502x ci_equal(char a, char b) noexcept
361 {
362 502x if(a >= 'A' && a <= 'Z')
363 10x a = static_cast<char>(a + 32);
364 502x if(b >= 'A' && b <= 'Z')
365 2x b = static_cast<char>(b + 32);
366 502x return a == b;
367 }
368
369 bool
370 123x ci_starts_with(
371 core::string_view str,
372 core::string_view prefix) noexcept
373 {
374 123x if(prefix.size() > str.size())
375 10x return false;
376 608x for(std::size_t i = 0; i < prefix.size(); ++i)
377 {
378 502x if(!ci_equal(str[i], prefix[i]))
379 7x return false;
380 }
381 106x return true;
382 }
383
384 //------------------------------------------------
385 // Route matcher
386 //------------------------------------------------
387
388 class route_matcher
389 {
390 core::string_view path_;
391 match_options const& opts_;
392 std::vector<std::pair<std::string, std::string>> params_;
393 std::size_t pos_ = 0;
394
395 public:
396 102x route_matcher(
397 core::string_view path,
398 match_options const& opts)
399 102x : path_(path)
400 102x , opts_(opts)
401 {
402 102x }
403
404 76x bool at_end() const noexcept
405 {
406 76x return pos_ >= path_.size();
407 }
408
409 87x std::size_t pos() const noexcept
410 {
411 87x return pos_;
412 }
413
414 std::vector<std::pair<std::string, std::string>> const&
415 87x params() const noexcept
416 {
417 87x return params_;
418 }
419
420 // Match text token
421 129x bool match_text(core::string_view text)
422 {
423 129x auto remaining = path_.substr(pos_);
424 129x if(opts_.case_sensitive)
425 {
426 6x if(!remaining.starts_with(text))
427 3x return false;
428 }
429 else
430 {
431 123x if(!ci_starts_with(remaining, text))
432 17x return false;
433 }
434 109x pos_ += text.size();
435 109x return true;
436 }
437
438 // Match param token - capture until stop_char, '/' or end
439 26x bool match_param(std::string const& name, char stop_char = '\0')
440 {
441 26x if(at_end())
442 return false;
443
444 26x auto start = pos_;
445 95x while(pos_ < path_.size() && path_[pos_] != '/')
446 {
447 // Stop at delimiter if specified
448 70x if(stop_char != '\0' && path_[pos_] == stop_char)
449 1x break;
450 69x ++pos_;
451 }
452
453 // Param must capture at least one character
454 26x if(pos_ == start)
455 1x return false;
456
457 50x params_.emplace_back(
458 name,
459 50x std::string(path_.substr(start, pos_ - start)));
460 25x return true;
461 }
462
463 // Match wildcard token - capture everything to end
464 4x bool match_wildcard(std::string const& name)
465 {
466 4x if(at_end())
467 1x return false;
468
469 3x auto start = pos_;
470 3x pos_ = path_.size();
471
472 // Wildcard must capture at least one character
473 3x if(pos_ == start)
474 return false;
475
476 6x params_.emplace_back(
477 name,
478 6x std::string(path_.substr(start)));
479 3x return true;
480 }
481
482 // Get the first character of the next meaningful token
483 // Returns '\0' if none exists or next token is not text
484 static char
485 176x get_stop_char(
486 std::vector<route_token> const& tokens,
487 std::size_t next_idx)
488 {
489 176x if(next_idx >= tokens.size())
490 113x return '\0';
491
492 63x auto const& next = tokens[next_idx];
493 63x if(next.type == route_token_type::text && !next.value.empty())
494 15x return next.value[0];
495
496 48x return '\0';
497 }
498
499 // Match a sequence of tokens
500 119x bool match_tokens(std::vector<route_token> const& tokens)
501 {
502 273x for(std::size_t i = 0; i < tokens.size(); ++i)
503 {
504 176x if(!match_token(tokens[i], get_stop_char(tokens, i + 1)))
505 22x return false;
506 }
507 97x return true;
508 }
509
510 // Match a single token
511 176x bool match_token(route_token const& token, char stop_char = '\0')
512 {
513 176x switch(token.type)
514 {
515 129x case route_token_type::text:
516 129x return match_text(token.value);
517
518 26x case route_token_type::param:
519 26x return match_param(token.value, stop_char);
520
521 4x case route_token_type::wildcard:
522 4x return match_wildcard(token.value);
523
524 17x case route_token_type::group:
525 17x return match_group(token.children);
526
527 default:
528 return false;
529 }
530 }
531
532 // Match group - try with contents, then without
533 17x bool match_group(std::vector<route_token> const& children)
534 {
535 // Save state before trying group
536 17x auto saved_pos = pos_;
537 17x auto saved_params_size = params_.size();
538
539 // Try matching with group contents
540 17x if(match_tokens(children))
541 9x return true;
542
543 // Restore state and try without group
544 8x pos_ = saved_pos;
545 8x params_.resize(saved_params_size);
546 8x return true; // Group is optional, always succeeds if skipped
547 }
548
549 // Check if match is complete based on options
550 88x bool is_complete() const
551 {
552 88x if(!opts_.end)
553 42x return true; // Prefix match always succeeds
554
555 46x if(opts_.strict)
556 2x return at_end();
557
558 // Non-strict: allow trailing slash
559 44x if(at_end())
560 42x return true;
561 2x if(pos_ == path_.size() - 1 && path_[pos_] == '/')
562 2x return true;
563
564 return false;
565 }
566 };
567
568 } // anonymous namespace
569
570 //------------------------------------------------
571
572 system::result<route_pattern>
573 136x parse_route_pattern(core::string_view pattern)
574 {
575 136x parser p(pattern);
576 136x auto rv = p.parse_tokens();
577 136x if(rv.has_error())
578 12x return rv.error();
579
580 124x route_pattern result;
581 124x result.tokens = std::move(rv.value());
582 124x result.original = std::string(pattern);
583 124x return result;
584 136x }
585
586 //------------------------------------------------
587
588 system::result<match_params>
589 102x match_route(
590 core::string_view path,
591 route_pattern const& pattern,
592 match_options const& opts)
593 {
594 102x route_matcher m(path, opts);
595
596 102x if(!m.match_tokens(pattern.tokens))
597 14x return grammar::error::mismatch;
598
599 88x if(!m.is_complete())
600 1x return grammar::error::mismatch;
601
602 87x match_params result;
603 87x result.params = m.params();
604 87x result.matched_length = m.pos();
605 87x return result;
606 102x }
607
608 } // detail
609 } // http
610 } // boost
611