LCOV - code coverage report
Current view: top level - http/server - response_compression.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 100.0 % 88 88
Test Date: 2026-11-10 10:38:07 Functions: 100.0 % 16 16

            Line data    Source code
       1              : #include "response_compression.hpp"
       2              : #include "../common/http_request.hpp"
       3              : #include "../common/http_response.hpp"
       4              : #include "../common/syntax.hpp"
       5              : #include "../../util/ascii.hpp"
       6              : #include "../../util/compression.hpp"
       7              : 
       8              : namespace thinger::http {
       9              : 
      10              : namespace {
      11              : 
      12              :     using ::thinger::util::ascii::iequals;
      13              : 
      14              :     // Weight of the parameters of an Accept-Encoding element (";q=0.5", the only parameter
      15              :     // defined): 1000 without one, -1 if invalid
      16          141 :     int coding_weight(std::string_view parameters) {
      17          141 :         int quality = 1000;
      18          282 :         while (!parameters.empty() && quality >= 0) {
      19          141 :             auto next = parameters.find(';');
      20          141 :             auto parameter = parameters.substr(0, next);
      21          141 :             parameters = next == std::string_view::npos ? std::string_view{} : parameters.substr(next + 1);
      22          141 :             auto equals = parameter.find('=');
      23          141 :             if (equals != std::string_view::npos && iequals(syntax::trim(parameter.substr(0, equals)), "q")) {
      24          141 :                 quality = syntax::parse_qvalue(syntax::trim(parameter.substr(equals + 1)));
      25              :             }
      26              :         }
      27          141 :         return quality;
      28              :     }
      29              : 
      30              :     // Quality of a content coding in `codings` (`any` for "*"), null if it is not one of them
      31          701 :     int* coding_quality(accepted_codings& codings, int& any, std::string_view coding) {
      32          701 :         if (iequals(coding, "gzip") || iequals(coding, "x-gzip")) return &codings.gzip;
      33          357 :         if (iequals(coding, "deflate")) return &codings.deflate;
      34          172 :         if (iequals(coding, "br")) return &codings.br;
      35           72 :         if (iequals(coding, "identity")) return &codings.identity;
      36           42 :         if (coding == "*") return &any;
      37            9 :         return nullptr;
      38              :     }
      39              : 
      40         1315 :     content_coding negotiate(const accepted_codings& accepted) {
      41         1315 :         int best = std::max(accepted.gzip, accepted.deflate);
      42         1315 :         if (best <= 0 || accepted.identity > best) return content_coding::identity;
      43          242 :         return accepted.gzip >= accepted.deflate ? content_coding::gzip : content_coding::deflate;
      44              :     }
      45              : 
      46         1403 :     bool is_compressible_content_type(std::string_view content_type) {
      47         1403 :         return content_type.starts_with("text/")
      48          342 :             || content_type.starts_with("application/json")
      49          148 :             || content_type.starts_with("application/xml")
      50          146 :             || content_type.starts_with("application/javascript")
      51          122 :             || content_type.starts_with("application/x-javascript")
      52         1745 :             || content_type.starts_with("image/svg+xml");
      53              :     }
      54              : 
      55              :     // Whether a comma-separated header (any of its values) holds `token`, case-insensitive
      56         4063 :     bool has_list_token(const http_response& message, std::string_view name, std::string_view token) {
      57         4320 :         for (const auto& value : message.get_headers_with_key(name)) {
      58          616 :             if (syntax::any_element(value, [&](std::string_view item) { return iequals(item, token); })) return true;
      59         4063 :         }
      60         4023 :         return false;
      61              :     }
      62              : 
      63              :     // Cache-Control: no-transform (RFC 9111, 5.2.2.6): no compression then, as Express
      64         1281 :     bool no_transform(const http_response& message) {
      65         1281 :         return message.has_header(header::cache_control) && has_list_token(message, header::cache_control, "no-transform");
      66              :     }
      67              : 
      68              : }
      69              : 
      70         1866 : accepted_codings parse_accept_encoding(std::string_view accept_encoding) {
      71         1866 :     accepted_codings listed{-1, -1, -1, -1};
      72         1866 :     int any = -1;
      73         1866 :     syntax::for_each_element(accept_encoding, [&](std::string_view element) {
      74          713 :         auto semicolon = element.find(';');
      75          713 :         auto coding = syntax::trim(element.substr(0, semicolon));
      76          725 :         if (coding.empty()) return;
      77          713 :         int quality = semicolon == std::string_view::npos ? 1000 : coding_weight(element.substr(semicolon + 1));
      78          713 :         if (quality < 0) return;
      79              :         // the first time a coding is listed counts
      80          701 :         int* entry = coding_quality(listed, any, coding);
      81          701 :         if (entry && *entry < 0) *entry = quality;
      82              :     });
      83              : 
      84              :     // "*" stands for the codings not listed; identity not listed nor excluded is still
      85              :     // acceptable, as the last choice
      86         9330 :     for (int* quality : {&listed.gzip, &listed.deflate, &listed.br, &listed.identity}) {
      87         7464 :         if (*quality < 0) *quality = any;
      88              :     }
      89         1866 :     return listed;
      90              : }
      91              : 
      92           93 : content_coding negotiate_content_coding(std::string_view accept_encoding) {
      93           93 :     return negotiate(parse_accept_encoding(accept_encoding));
      94              : }
      95              : 
      96              : namespace compression {
      97              : 
      98         1752 :     accepted_codings accepted_by(const http_request& request) {
      99         1752 :         std::string accept_encoding;
     100         2101 :         for (const auto& value : request.get_headers_with_key("Accept-Encoding")) {
     101          349 :             if (!accept_encoding.empty()) accept_encoding += ", ";
     102          349 :             accept_encoding += value;
     103         1752 :         }
     104         3504 :         return parse_accept_encoding(accept_encoding);
     105         1752 :     }
     106              : 
     107         1222 :     content_coding negotiated(const http_request& request) {
     108         1222 :         return negotiate(accepted_by(request));
     109              :     }
     110              : 
     111         2097 :     bool compressible(const http_response& message, size_t size, std::string_view content_type) {
     112         2097 :         return size >= threshold && is_compressible_content_type(content_type) && !no_transform(message);
     113              :     }
     114              : 
     115         1548 :     bool compressible(const http_response& message) {
     116         1548 :         return compressible(message, message.get_content().size(), message.get_content_type());
     117              :     }
     118              : 
     119         2007 :     void add_vary(http_response& message, std::string_view field) {
     120         2007 :         if (has_list_token(message, "Vary", field) || has_list_token(message, "Vary", "*")) return;
     121         1970 :         if (field == "*") {
     122            3 :             message.remove_headers("Vary");
     123           12 :             message.add_header("Vary", "*");
     124            3 :             return;
     125              :         }
     126         1967 :         const auto& vary = message.get_header("Vary");
     127         1967 :         if (vary.empty()) {
     128         9415 :             message.add_header("Vary", std::string(field));
     129              :         } else {
     130          420 :             message.set_header("Vary", vary + ", " + std::string(field));
     131              :         }
     132              :     }
     133              : 
     134         1309 :     void compress(http_response& message, const http_request& request) {
     135         2342 :         if (!compressible(message)) return;
     136              :         // Sent compressed or not depending on the request: caches must tell both apart
     137         1208 :         add_vary(message, "Accept-Encoding");
     138         1208 :         if (message.has_header("Content-Encoding")) return;
     139              : 
     140         1208 :         auto coding = negotiated(request);
     141         1208 :         if (coding == content_coding::identity) return;
     142          175 :         const auto& content = message.get_content();
     143          175 :         auto compressed = coding == content_coding::gzip ? ::thinger::util::gzip::compress(content)
     144          175 :                                                          : ::thinger::util::deflate::compress(content);
     145          175 :         if (compressed) {
     146          175 :             message.set_content(std::move(*compressed));
     147          875 :             message.add_header("Content-Encoding", coding == content_coding::gzip ? "gzip" : "deflate");
     148              :         }
     149          175 :     }
     150              : 
     151              : }
     152              : 
     153              : } // namespace thinger::http
        

Generated by: LCOV version 2.0-1