LCOV - code coverage report
Current view: top level - libs/http/src/server - route_abnf.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 93.6 % 265 248
Test Date: 2026-02-02 17:02:49 Functions: 100.0 % 34 34

            Line data    Source code
       1              : //
       2              : // Copyright (c) 2025 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 "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          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            0 :             return grammar::error::mismatch;
     144              : 
     145           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            0 :             return grammar::error::mismatch;
     160              : 
     161            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            0 :                 advance(); // skip backslash
     179            0 :                 if(at_end())
     180            0 :                     return grammar::error::syntax;
     181            0 :                 result += get();
     182              :             }
     183              :             else
     184              :             {
     185           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            0 :             return grammar::error::mismatch;
     240              : 
     241          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            0 :             return grammar::error::mismatch;
     250              : 
     251           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            0 :             return grammar::error::mismatch;
     267              : 
     268            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            0 :             return grammar::error::mismatch;
     284              : 
     285            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            0 :                 return rv.error();
     296           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            0 :             return grammar::error::syntax;
     313              : 
     314          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            0 :             return false;
     443              : 
     444           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            0 :             return false;
     475              : 
     476            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            0 :         default:
     528            0 :             return false;
     529              :         }
     530              :     }
     531              : 
     532              :     // Match group - try with contents, then without
     533           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            0 :         return false;
     565              :     }
     566              : };
     567              : 
     568              : } // anonymous namespace
     569              : 
     570              : //------------------------------------------------
     571              : 
     572              : system::result<route_pattern>
     573          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