LCOV - code coverage report
Current view: top level - src/server - accepts.cpp (source / functions) Coverage Total Hit Missed
Test: coverage_remapped.info Lines: 86.1 % 316 272 44
Test Date: 2026-06-13 19:44:58 Functions: 100.0 % 24 24

           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 <boost/http/server/accepts.hpp>
      11                 : #include <boost/http/server/mime_types.hpp>
      12                 : #include <boost/http/field.hpp>
      13                 : #include <algorithm>
      14                 : 
      15                 : namespace boost {
      16                 : namespace http {
      17                 : 
      18                 : namespace {
      19                 : 
      20                 : //----------------------------------------------------------
      21                 : // Helpers
      22                 : //----------------------------------------------------------
      23                 : 
      24                 : std::string_view
      25 HIT         142 : trim_ows( std::string_view s ) noexcept
      26                 : {
      27             338 :     while( ! s.empty() &&
      28             169 :         ( s.front() == ' ' || s.front() == '\t' ) )
      29              27 :         s.remove_prefix( 1 );
      30             284 :     while( ! s.empty() &&
      31             142 :         ( s.back() == ' ' || s.back() == '\t' ) )
      32 MIS           0 :         s.remove_suffix( 1 );
      33 HIT         142 :     return s;
      34                 : }
      35                 : 
      36                 : bool
      37              75 : iequals(
      38                 :     std::string_view a,
      39                 :     std::string_view b ) noexcept
      40                 : {
      41              75 :     if( a.size() != b.size() )
      42              26 :         return false;
      43             202 :     for( std::size_t i = 0; i < a.size(); ++i )
      44                 :     {
      45             174 :         unsigned char ca = a[i];
      46             174 :         unsigned char cb = b[i];
      47             174 :         if( ca >= 'A' && ca <= 'Z' )
      48 MIS           0 :             ca += 32;
      49 HIT         174 :         if( cb >= 'A' && cb <= 'Z' )
      50 MIS           0 :             cb += 32;
      51 HIT         174 :         if( ca != cb )
      52              21 :             return false;
      53                 :     }
      54              28 :     return true;
      55                 : }
      56                 : 
      57                 : // Returns quality as integer 0-1000
      58                 : int
      59              16 : parse_q( std::string_view s ) noexcept
      60                 : {
      61              16 :     s = trim_ows( s );
      62              16 :     if( s.empty() )
      63 MIS           0 :         return 1000;
      64 HIT          16 :     if( s[0] == '1' )
      65 MIS           0 :         return 1000;
      66 HIT          16 :     if( s[0] != '0' )
      67               1 :         return 0;
      68              15 :     if( s.size() < 2 || s[1] != '.' )
      69               1 :         return 0;
      70              14 :     int result = 0;
      71              14 :     int mult = 100;
      72              14 :     for( std::size_t i = 2;
      73              28 :         i < s.size() && i < 5; ++i )
      74                 :     {
      75              14 :         if( s[i] < '0' || s[i] > '9' )
      76 MIS           0 :             break;
      77 HIT          14 :         result += ( s[i] - '0' ) * mult;
      78              14 :         mult /= 10;
      79                 :     }
      80              14 :     return result;
      81                 : }
      82                 : 
      83                 : // Extract q-value from parameters after first semicolon
      84                 : int
      85              16 : extract_q( std::string_view params ) noexcept
      86                 : {
      87              16 :     while( ! params.empty() )
      88                 :     {
      89              16 :         auto semi = params.find( ';' );
      90              16 :         auto param = trim_ows(
      91                 :             semi != std::string_view::npos
      92 MIS           0 :                 ? params.substr( 0, semi )
      93                 :                 : params );
      94 HIT          16 :         if( param.size() >= 2 &&
      95              32 :             ( param[0] == 'q' || param[0] == 'Q' ) &&
      96              16 :             param[1] == '=' )
      97                 :         {
      98              16 :             return parse_q( param.substr( 2 ) );
      99                 :         }
     100 MIS           0 :         if( semi != std::string_view::npos )
     101               0 :             params.remove_prefix( semi + 1 );
     102                 :         else
     103               0 :             break;
     104                 :     }
     105               0 :     return 1000;
     106                 : }
     107                 : 
     108                 : //----------------------------------------------------------
     109                 : // Negotiation priority
     110                 : //----------------------------------------------------------
     111                 : 
     112                 : struct priority
     113                 : {
     114                 :     int q;
     115                 :     int specificity;
     116                 :     int order;
     117                 : };
     118                 : 
     119                 : bool
     120 HIT           6 : is_better(
     121                 :     priority const& a,
     122                 :     priority const& b ) noexcept
     123                 : {
     124               6 :     if( a.q != b.q )
     125               4 :         return a.q > b.q;
     126               2 :     if( a.specificity != b.specificity )
     127               1 :         return a.specificity > b.specificity;
     128               1 :     return a.order < b.order;
     129                 : }
     130                 : 
     131                 : //----------------------------------------------------------
     132                 : // Media type parsing (Accept header)
     133                 : //----------------------------------------------------------
     134                 : 
     135                 : struct media_range
     136                 : {
     137                 :     std::string_view type;
     138                 :     std::string_view subtype;
     139                 :     std::string_view full;
     140                 :     int q;
     141                 :     int order;
     142                 : };
     143                 : 
     144                 : std::vector<media_range>
     145              13 : parse_accept( std::string_view header )
     146                 : {
     147              13 :     std::vector<media_range> result;
     148              13 :     int order = 0;
     149                 : 
     150              35 :     while( ! header.empty() )
     151                 :     {
     152              22 :         auto comma = header.find( ',' );
     153                 :         auto entry = ( comma != std::string_view::npos )
     154              22 :             ? header.substr( 0, comma )
     155              13 :             : header;
     156              22 :         if( comma != std::string_view::npos )
     157               9 :             header.remove_prefix( comma + 1 );
     158                 :         else
     159              13 :             header = {};
     160                 : 
     161              22 :         entry = trim_ows( entry );
     162              22 :         if( entry.empty() )
     163 MIS           0 :             continue;
     164                 : 
     165 HIT          22 :         auto semi = entry.find( ';' );
     166              26 :         auto mime_part = trim_ows(
     167                 :             semi != std::string_view::npos
     168               4 :                 ? entry.substr( 0, semi )
     169                 :                 : entry );
     170                 : 
     171              22 :         auto slash = mime_part.find( '/' );
     172              22 :         if( slash == std::string_view::npos )
     173 MIS           0 :             continue;
     174                 : 
     175 HIT          22 :         media_range mr;
     176              22 :         mr.type = mime_part.substr( 0, slash );
     177              22 :         mr.subtype = mime_part.substr( slash + 1 );
     178              22 :         mr.full = mime_part;
     179              22 :         mr.q = ( semi != std::string_view::npos )
     180              22 :             ? extract_q( entry.substr( semi + 1 ) )
     181                 :             : 1000;
     182              22 :         mr.order = order++;
     183              22 :         result.push_back( mr );
     184                 :     }
     185                 : 
     186              13 :     return result;
     187 MIS           0 : }
     188                 : 
     189                 : // Returns specificity (0-6) or -1 for no match
     190                 : int
     191 HIT          16 : match_media(
     192                 :     media_range const& range,
     193                 :     std::string_view type,
     194                 :     std::string_view subtype ) noexcept
     195                 : {
     196              16 :     int s = 0;
     197                 : 
     198              16 :     if( range.type == "*" )
     199                 :     {
     200                 :         // wildcard type
     201                 :     }
     202              15 :     else if( iequals( range.type, type ) )
     203                 :     {
     204               7 :         s |= 4;
     205                 :     }
     206                 :     else
     207                 :     {
     208               8 :         return -1;
     209                 :     }
     210                 : 
     211               8 :     if( range.subtype == "*" )
     212                 :     {
     213                 :         // wildcard subtype
     214                 :     }
     215               5 :     else if( iequals( range.subtype, subtype ) )
     216                 :     {
     217               5 :         s |= 2;
     218                 :     }
     219                 :     else
     220                 :     {
     221 MIS           0 :         return -1;
     222                 :     }
     223                 : 
     224 HIT           8 :     return s;
     225                 : }
     226                 : 
     227                 : //----------------------------------------------------------
     228                 : // Simple token parsing (Accept-Encoding/Charset/Language)
     229                 : //----------------------------------------------------------
     230                 : 
     231                 : struct simple_entry
     232                 : {
     233                 :     std::string_view value;
     234                 :     int q;
     235                 :     int order;
     236                 : };
     237                 : 
     238                 : std::vector<simple_entry>
     239              15 : parse_simple( std::string_view header )
     240                 : {
     241              15 :     std::vector<simple_entry> result;
     242              15 :     int order = 0;
     243                 : 
     244              48 :     while( ! header.empty() )
     245                 :     {
     246              33 :         auto comma = header.find( ',' );
     247                 :         auto entry = ( comma != std::string_view::npos )
     248              33 :             ? header.substr( 0, comma )
     249              15 :             : header;
     250              33 :         if( comma != std::string_view::npos )
     251              18 :             header.remove_prefix( comma + 1 );
     252                 :         else
     253              15 :             header = {};
     254                 : 
     255              33 :         entry = trim_ows( entry );
     256              33 :         if( entry.empty() )
     257 MIS           0 :             continue;
     258                 : 
     259 HIT          33 :         auto semi = entry.find( ';' );
     260              45 :         auto value = trim_ows(
     261                 :             semi != std::string_view::npos
     262              12 :                 ? entry.substr( 0, semi )
     263                 :                 : entry );
     264              33 :         if( value.empty() )
     265 MIS           0 :             continue;
     266                 : 
     267 HIT          33 :         simple_entry se;
     268              33 :         se.value = value;
     269              33 :         se.q = ( semi != std::string_view::npos )
     270              33 :             ? extract_q( entry.substr( semi + 1 ) )
     271                 :             : 1000;
     272              33 :         se.order = order++;
     273              33 :         result.push_back( se );
     274                 :     }
     275                 : 
     276              15 :     return result;
     277 MIS           0 : }
     278                 : 
     279                 : //----------------------------------------------------------
     280                 : // Matching helpers
     281                 : //----------------------------------------------------------
     282                 : 
     283                 : // Exact or wildcard match (encoding, charset)
     284                 : int
     285 HIT          25 : match_exact(
     286                 :     std::string_view spec,
     287                 :     std::string_view offered ) noexcept
     288                 : {
     289              25 :     if( iequals( spec, offered ) )
     290               9 :         return 1;
     291              16 :     if( spec == "*" )
     292               1 :         return 0;
     293              15 :     return -1;
     294                 : }
     295                 : 
     296                 : // Language prefix: "en-US" -> "en"
     297                 : std::string_view
     298              17 : lang_prefix( std::string_view tag ) noexcept
     299                 : {
     300              17 :     auto dash = tag.find( '-' );
     301              17 :     if( dash != std::string_view::npos )
     302               3 :         return tag.substr( 0, dash );
     303              14 :     return tag;
     304                 : }
     305                 : 
     306                 : // Language match with prefix support
     307                 : int
     308              13 : match_language(
     309                 :     std::string_view spec,
     310                 :     std::string_view offered ) noexcept
     311                 : {
     312              13 :     if( iequals( spec, offered ) )
     313               4 :         return 4;
     314               9 :     if( iequals( lang_prefix( spec ), offered ) )
     315               1 :         return 2;
     316               8 :     if( iequals( spec, lang_prefix( offered ) ) )
     317               2 :         return 1;
     318               6 :     if( spec == "*" )
     319 MIS           0 :         return 0;
     320 HIT           6 :     return -1;
     321                 : }
     322                 : 
     323                 : //----------------------------------------------------------
     324                 : // Generic negotiation for simple headers
     325                 : //----------------------------------------------------------
     326                 : 
     327                 : template< class MatchFn >
     328                 : std::string_view
     329              12 : negotiate(
     330                 :     std::vector<simple_entry> const& entries,
     331                 :     std::initializer_list<std::string_view> offered,
     332                 :     MatchFn match )
     333                 : {
     334              12 :     std::string_view best_val;
     335              12 :     priority best_pri{ -1, -1, 0 };
     336              12 :     bool found = false;
     337                 : 
     338              30 :     for( auto const& o : offered )
     339                 :     {
     340              18 :         priority pri{ -1, -1, 0 };
     341              18 :         bool matched = false;
     342                 : 
     343              56 :         for( auto const& e : entries )
     344                 :         {
     345              38 :             if( e.q <= 0 )
     346              21 :                 continue;
     347              38 :             auto s = match( e.value, o );
     348              38 :             if( s < 0 )
     349              21 :                 continue;
     350              17 :             priority p{ e.q, s, e.order };
     351              17 :             if( ! matched ||
     352 MIS           0 :                 p.specificity > pri.specificity ||
     353               0 :                 ( p.specificity == pri.specificity &&
     354               0 :                     p.q > pri.q ) ||
     355               0 :                 ( p.specificity == pri.specificity &&
     356               0 :                     p.q == pri.q &&
     357               0 :                     p.order < pri.order ) )
     358                 :             {
     359 HIT          17 :                 pri = p;
     360              17 :                 matched = true;
     361                 :             }
     362                 :         }
     363                 : 
     364              18 :         if( ! matched || pri.q <= 0 )
     365               1 :             continue;
     366                 : 
     367              17 :         if( ! found || is_better( pri, best_pri ) )
     368                 :         {
     369              13 :             best_val = o;
     370              13 :             best_pri = pri;
     371              13 :             found = true;
     372                 :         }
     373                 :     }
     374                 : 
     375              12 :     return found ? best_val : std::string_view{};
     376                 : }
     377                 : 
     378                 : // Return sorted values from simple entries
     379                 : std::vector<std::string_view>
     380               3 : sorted_values(
     381                 :     std::vector<simple_entry>& entries )
     382                 : {
     383               3 :     std::sort( entries.begin(), entries.end(),
     384              13 :         []( simple_entry const& a,
     385                 :             simple_entry const& b )
     386                 :         {
     387              13 :             if( a.q != b.q )
     388              11 :                 return a.q > b.q;
     389               2 :             return a.order < b.order;
     390                 :         });
     391                 : 
     392               3 :     std::vector<std::string_view> result;
     393               3 :     result.reserve( entries.size() );
     394              12 :     for( auto const& e : entries )
     395                 :     {
     396               9 :         if( e.q <= 0 )
     397 MIS           0 :             continue;
     398 HIT           9 :         result.push_back( e.value );
     399                 :     }
     400               3 :     return result;
     401 MIS           0 : }
     402                 : 
     403                 : } // (anon)
     404                 : 
     405                 : //----------------------------------------------------------
     406                 : 
     407 HIT          26 : accepts::accepts(
     408              26 :     fields_base const& fields ) noexcept
     409              26 :     : fields_( fields )
     410                 : {
     411              26 : }
     412                 : 
     413                 : std::string_view
     414              12 : accepts::type(
     415                 :     std::initializer_list<
     416                 :         std::string_view> offered ) const
     417                 : {
     418              12 :     if( offered.size() == 0 )
     419               1 :         return {};
     420                 : 
     421              11 :     auto accept = fields_.value_or(
     422                 :         field::accept, "" );
     423                 : 
     424              11 :     if( accept.empty() )
     425               1 :         return *offered.begin();
     426                 : 
     427              10 :     auto ranges = parse_accept( accept );
     428              10 :     if( ranges.empty() )
     429 MIS           0 :         return *offered.begin();
     430                 : 
     431 HIT          10 :     std::string_view best_val;
     432              10 :     priority best_pri{ -1, -1, 0 };
     433              10 :     bool found = false;
     434                 : 
     435              22 :     for( auto const& o : offered )
     436                 :     {
     437                 :         // Convert extension to MIME if needed
     438              12 :         std::string_view mime_str = o;
     439              12 :         if( o.find( '/' ) == std::string_view::npos )
     440                 :         {
     441               9 :             auto looked = mime_types::lookup( o );
     442               9 :             if( ! looked.empty() )
     443               8 :                 mime_str = looked;
     444                 :             else
     445               1 :                 continue;
     446                 :         }
     447                 : 
     448              11 :         auto slash = mime_str.find( '/' );
     449              11 :         if( slash == std::string_view::npos )
     450 MIS           0 :             continue;
     451                 : 
     452 HIT          11 :         auto type = mime_str.substr( 0, slash );
     453              11 :         auto subtype = mime_str.substr( slash + 1 );
     454                 : 
     455                 :         // Find best matching range for this type
     456              11 :         priority pri{ -1, -1, 0 };
     457              11 :         bool matched = false;
     458                 : 
     459              29 :         for( auto const& r : ranges )
     460                 :         {
     461              18 :             if( r.q <= 0 )
     462              10 :                 continue;
     463              16 :             auto s = match_media( r, type, subtype );
     464              16 :             if( s < 0 )
     465               8 :                 continue;
     466               8 :             priority p{ r.q, s, r.order };
     467               8 :             if( ! matched ||
     468 MIS           0 :                 p.specificity > pri.specificity ||
     469               0 :                 ( p.specificity == pri.specificity &&
     470               0 :                     p.q > pri.q ) ||
     471               0 :                 ( p.specificity == pri.specificity &&
     472               0 :                     p.q == pri.q &&
     473               0 :                     p.order < pri.order ) )
     474                 :             {
     475 HIT           8 :                 pri = p;
     476               8 :                 matched = true;
     477                 :             }
     478                 :         }
     479                 : 
     480              11 :         if( ! matched || pri.q <= 0 )
     481               3 :             continue;
     482                 : 
     483               8 :         if( ! found || is_better( pri, best_pri ) )
     484                 :         {
     485               8 :             best_val = o;
     486               8 :             best_pri = pri;
     487               8 :             found = true;
     488                 :         }
     489                 :     }
     490                 : 
     491              10 :     return found ? best_val : std::string_view{};
     492              10 : }
     493                 : 
     494                 : std::vector<std::string_view>
     495               4 : accepts::types() const
     496                 : {
     497               4 :     auto accept = fields_.value_or(
     498                 :         field::accept, "" );
     499               4 :     if( accept.empty() )
     500               1 :         return {};
     501                 : 
     502               3 :     auto ranges = parse_accept( accept );
     503                 : 
     504               3 :     std::sort( ranges.begin(), ranges.end(),
     505               6 :         []( media_range const& a,
     506                 :             media_range const& b )
     507                 :         {
     508               6 :             if( a.q != b.q )
     509               6 :                 return a.q > b.q;
     510 MIS           0 :             return a.order < b.order;
     511                 :         });
     512                 : 
     513 HIT           3 :     std::vector<std::string_view> result;
     514               3 :     result.reserve( ranges.size() );
     515               9 :     for( auto const& r : ranges )
     516                 :     {
     517               6 :         if( r.q <= 0 )
     518               1 :             continue;
     519               5 :         result.push_back( r.full );
     520                 :     }
     521               3 :     return result;
     522               3 : }
     523                 : 
     524                 : std::string_view
     525               6 : accepts::encoding(
     526                 :     std::initializer_list<
     527                 :         std::string_view> offered ) const
     528                 : {
     529               6 :     if( offered.size() == 0 )
     530 MIS           0 :         return {};
     531                 : 
     532 HIT           6 :     auto header = fields_.value_or(
     533                 :         field::accept_encoding, "" );
     534                 : 
     535               6 :     if( header.empty() )
     536               1 :         return *offered.begin();
     537                 : 
     538               5 :     auto entries = parse_simple( header );
     539               5 :     if( entries.empty() )
     540 MIS           0 :         return *offered.begin();
     541                 : 
     542 HIT           5 :     return negotiate( entries, offered, match_exact );
     543               5 : }
     544                 : 
     545                 : std::vector<std::string_view>
     546               2 : accepts::encodings() const
     547                 : {
     548               2 :     auto header = fields_.value_or(
     549                 :         field::accept_encoding, "" );
     550               2 :     if( header.empty() )
     551               1 :         return {};
     552                 : 
     553               1 :     auto entries = parse_simple( header );
     554               1 :     return sorted_values( entries );
     555               1 : }
     556                 : 
     557                 : std::string_view
     558               3 : accepts::charset(
     559                 :     std::initializer_list<
     560                 :         std::string_view> offered ) const
     561                 : {
     562               3 :     if( offered.size() == 0 )
     563 MIS           0 :         return {};
     564                 : 
     565 HIT           3 :     auto header = fields_.value_or(
     566                 :         field::accept_charset, "" );
     567                 : 
     568               3 :     if( header.empty() )
     569               1 :         return *offered.begin();
     570                 : 
     571               2 :     auto entries = parse_simple( header );
     572               2 :     if( entries.empty() )
     573 MIS           0 :         return *offered.begin();
     574                 : 
     575 HIT           2 :     return negotiate( entries, offered, match_exact );
     576               2 : }
     577                 : 
     578                 : std::vector<std::string_view>
     579               1 : accepts::charsets() const
     580                 : {
     581               1 :     auto header = fields_.value_or(
     582                 :         field::accept_charset, "" );
     583               1 :     if( header.empty() )
     584 MIS           0 :         return {};
     585                 : 
     586 HIT           1 :     auto entries = parse_simple( header );
     587               1 :     return sorted_values( entries );
     588               1 : }
     589                 : 
     590                 : std::string_view
     591               6 : accepts::language(
     592                 :     std::initializer_list<
     593                 :         std::string_view> offered ) const
     594                 : {
     595               6 :     if( offered.size() == 0 )
     596 MIS           0 :         return {};
     597                 : 
     598 HIT           6 :     auto header = fields_.value_or(
     599                 :         field::accept_language, "" );
     600                 : 
     601               6 :     if( header.empty() )
     602               1 :         return *offered.begin();
     603                 : 
     604               5 :     auto entries = parse_simple( header );
     605               5 :     if( entries.empty() )
     606 MIS           0 :         return *offered.begin();
     607                 : 
     608 HIT           5 :     return negotiate( entries, offered, match_language );
     609               5 : }
     610                 : 
     611                 : std::vector<std::string_view>
     612               1 : accepts::languages() const
     613                 : {
     614               1 :     auto header = fields_.value_or(
     615                 :         field::accept_language, "" );
     616               1 :     if( header.empty() )
     617 MIS           0 :         return {};
     618                 : 
     619 HIT           1 :     auto entries = parse_simple( header );
     620               1 :     return sorted_values( entries );
     621               1 : }
     622                 : 
     623                 : } // http
     624                 : } // boost
        

Generated by: LCOV version 2.3