LCOV - code coverage report
Current view: top level - src/server - route_abnf.cpp (source / functions) Coverage Total Hit Missed
Test: coverage_remapped.info Lines: 93.6 % 265 248 17
Test Date: 2026-06-13 19:44:58 Functions: 100.0 % 34 34

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

Generated by: LCOV version 2.3