LCOV - code coverage report
Current view: top level - util - compression.hpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 95.4 % 108 103
Test Date: 2026-11-10 10:38:07 Functions: 100.0 % 9 9

            Line data    Source code
       1              : #ifndef THINGER_UTIL_COMPRESSION_HPP
       2              : #define THINGER_UTIL_COMPRESSION_HPP
       3              : 
       4              : #include <algorithm>
       5              : #include <cstddef>
       6              : #include <limits>
       7              : #include <optional>
       8              : #include <string>
       9              : #include <string_view>
      10              : #include <zlib.h>
      11              : #include "ascii.hpp"
      12              : 
      13              : namespace thinger::util {
      14              : 
      15              : namespace detail {
      16              : 
      17              :     enum class inflate_format { zlib, gzip, raw };
      18              : 
      19              :     // Inflate `data` up to `max_size` bytes, checked as it is inflated (a decompression bomb
      20              :     // takes no more memory than the limit). gzip decodes every member of the data, one after
      21              :     // another, and accepts zero bytes after the last one (padding, as Node and gzip(1) do);
      22              :     // anything else after the end of the data fails: a zlib or raw deflate stream must take
      23              :     // the whole data, and a gzip member be followed by another one (as Go does).
      24          580 :     inline std::optional<std::string> inflate_data(std::string_view data, inflate_format format,
      25              :                                                    size_t max_size, bool* too_large) {
      26          580 :         if (too_large) *too_large = false;
      27              : 
      28          580 :         int window_bits = format == inflate_format::gzip ? 15 + 16 : format == inflate_format::raw ? -15 : 15;
      29          580 :         z_stream strm{};
      30          580 :         if (inflateInit2(&strm, window_bits) != Z_OK) {
      31            0 :             return std::nullopt;
      32              :         }
      33              :         struct end_guard {
      34              :             z_stream& strm;
      35          580 :             ~end_guard() { inflateEnd(&strm); }
      36          580 :         } guard{strm};
      37              : 
      38              :         // The input is given in pieces fitting in avail_in
      39          580 :         size_t consumed = 0;
      40        49933 :         auto feed = [&] {
      41        49933 :             if (strm.avail_in != 0 || consumed == data.size()) return;
      42          571 :             auto size = std::min<size_t>(data.size() - consumed, std::numeric_limits<uInt>::max());
      43          571 :             strm.next_in = reinterpret_cast<Bytef*>(const_cast<char*>(data.data() + consumed));
      44          571 :             strm.avail_in = static_cast<uInt>(size);
      45          571 :             consumed += size;
      46          580 :         };
      47              : 
      48          580 :         std::string result;
      49              :         char buffer[16384];
      50              :         while (true) {
      51        49933 :             feed();
      52        49933 :             strm.next_out = reinterpret_cast<Bytef*>(buffer);
      53        49933 :             strm.avail_out = sizeof(buffer);
      54              : 
      55              :             // Anything but progress is an error; Z_BUF_ERROR means the input ended before
      56              :             // the compressed stream did (it would never reach Z_STREAM_END)
      57        49933 :             int ret = inflate(&strm, Z_NO_FLUSH);
      58        49933 :             if (ret != Z_OK && ret != Z_STREAM_END) {
      59          138 :                 return std::nullopt;
      60              :             }
      61              : 
      62        49795 :             size_t produced = sizeof(buffer) - strm.avail_out;
      63        49795 :             if (produced > max_size - result.size()) {
      64           81 :                 if (too_large) *too_large = true;
      65           81 :                 return std::nullopt;
      66              :             }
      67        49714 :             result.append(buffer, produced);
      68        49714 :             if (ret != Z_STREAM_END) continue;
      69              : 
      70              :             // End of the stream (or of a gzip member): what follows it, if anything
      71          460 :             auto rest = data.substr(consumed - strm.avail_in);
      72          460 :             if (rest.empty()) return result;
      73          123 :             if (format != inflate_format::gzip) return std::nullopt;
      74          102 :             if (rest.find_first_not_of('\0') == std::string_view::npos) return result;
      75           99 :             if (inflateReset(&strm) != Z_OK) return std::nullopt;
      76        49353 :         }
      77          580 :     }
      78              : 
      79              :     // Whether `data` starts with a zlib header (RFC 1950, 2.2): deflate method, a window of
      80              :     // at most 32 KB, and a check of both bytes
      81          189 :     inline bool zlib_header(std::string_view data) {
      82          189 :         if (data.size() < 2) return false;
      83          189 :         auto cmf = static_cast<unsigned char>(data[0]);
      84          189 :         auto flg = static_cast<unsigned char>(data[1]);
      85          189 :         return (cmf & 0x0f) == 8 && (cmf >> 4) <= 7 && ((cmf << 8) | flg) % 31 == 0;
      86              :     }
      87              : 
      88              : }
      89              : 
      90              : class gzip {
      91              : public:
      92              :     // Compress string to gzip format
      93          436 :     static std::optional<std::string> compress(const std::string& data) {
      94          436 :         z_stream strm{};
      95              :         // windowBits = 15 + 16 enables gzip encoding
      96          436 :         if (deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY) != Z_OK) {
      97            0 :             return std::nullopt;
      98              :         }
      99              : 
     100          436 :         strm.next_in = reinterpret_cast<Bytef*>(const_cast<char*>(data.data()));
     101          436 :         strm.avail_in = static_cast<uInt>(data.size());
     102              : 
     103          436 :         std::string result;
     104          436 :         result.resize(deflateBound(&strm, data.size()));
     105              : 
     106          436 :         strm.next_out = reinterpret_cast<Bytef*>(result.data());
     107          436 :         strm.avail_out = static_cast<uInt>(result.size());
     108              : 
     109          436 :         int ret = deflate(&strm, Z_FINISH);
     110          436 :         deflateEnd(&strm);
     111              : 
     112          436 :         if (ret != Z_STREAM_END) {
     113            0 :             return std::nullopt;
     114              :         }
     115              : 
     116          436 :         result.resize(strm.total_out);
     117          436 :         return result;
     118          436 :     }
     119              : 
     120              :     // Decompress gzip data: all its members, concatenated (RFC 1952, 2.2), and zero bytes
     121              :     // after them (padding). Fails on invalid or truncated data, other bytes after a member,
     122              :     // or if the result would exceed `max_size` bytes (then `too_large` is set, if given).
     123          385 :     static std::optional<std::string> decompress(std::string_view data,
     124              :                                                  size_t max_size = std::numeric_limits<size_t>::max(),
     125              :                                                  bool* too_large = nullptr) {
     126          385 :         return detail::inflate_data(data, detail::inflate_format::gzip, max_size, too_large);
     127              :     }
     128              : 
     129              :     // Check if data might be gzip compressed (by checking magic bytes)
     130              :     static bool is_gzip(const std::string& data) {
     131              :         return data.size() >= 2 &&
     132              :                static_cast<unsigned char>(data[0]) == 0x1f &&
     133              :                static_cast<unsigned char>(data[1]) == 0x8b;
     134              :     }
     135              : };
     136              : 
     137              : class deflate {
     138              : public:
     139              :     // Compress string using deflate (zlib format)
     140          173 :     static std::optional<std::string> compress(const std::string& data) {
     141          173 :         z_stream strm{};
     142              :         // windowBits = 15 for zlib format
     143          173 :         if (deflateInit(&strm, Z_DEFAULT_COMPRESSION) != Z_OK) {
     144            0 :             return std::nullopt;
     145              :         }
     146              : 
     147          173 :         strm.next_in = reinterpret_cast<Bytef*>(const_cast<char*>(data.data()));
     148          173 :         strm.avail_in = static_cast<uInt>(data.size());
     149              : 
     150          173 :         std::string result;
     151          173 :         result.resize(deflateBound(&strm, data.size()));
     152              : 
     153          173 :         strm.next_out = reinterpret_cast<Bytef*>(result.data());
     154          173 :         strm.avail_out = static_cast<uInt>(result.size());
     155              : 
     156          173 :         int ret = ::deflate(&strm, Z_FINISH);
     157          173 :         deflateEnd(&strm);
     158              : 
     159          173 :         if (ret != Z_STREAM_END) {
     160            0 :             return std::nullopt;
     161              :         }
     162              : 
     163          173 :         result.resize(strm.total_out);
     164          173 :         return result;
     165          173 :     }
     166              : 
     167              :     // Decompress deflate data: the zlib format, or raw deflate if it does not start with a
     168              :     // zlib header (as some servers send it; curl and browsers accept it too). Fails on
     169              :     // invalid or truncated data, bytes after its end, or if the result would exceed
     170              :     // `max_size` bytes (then `too_large` is set, if given).
     171          195 :     static std::optional<std::string> decompress(std::string_view data,
     172              :                                                  size_t max_size = std::numeric_limits<size_t>::max(),
     173              :                                                  bool* too_large = nullptr) {
     174          195 :         auto format = data.size() < 2 || detail::zlib_header(data) ? detail::inflate_format::zlib
     175          195 :                                                                     : detail::inflate_format::raw;
     176          195 :         return detail::inflate_data(data, format, max_size, too_large);
     177              :     }
     178              : };
     179              : 
     180              : // Outcome of decode_content()
     181              : enum class content_decoding {
     182              :     decoded,        // the body is decoded (or only listed identity)
     183              :     unsupported,    // a coding other than gzip, x-gzip, deflate or identity: the body is kept
     184              :     invalid,        // not valid data of its codings (corrupt, truncated, trailing bytes), or too many codings
     185              :     too_large       // larger than the maximum size once decoded
     186              : };
     187              : 
     188              : // Most codings a body may be encoded with (as curl and Node's fetch, against chains of
     189              : // decompressions taking a decoded size each)
     190              : inline constexpr size_t max_content_codings = 5;
     191              : 
     192              : // Decode a body, in place, as its Content-Encoding (`codings`: its comma-separated values, of
     193              : // every header) says (RFC 9110, 8.4): the codings were applied in the order listed, so they
     194              : // are undone in reverse. Names are case-insensitive, and x-gzip is gzip (8.4.1.3). Each step
     195              : // stops at `max_size` bytes decoded. A body listing a coding that is not supported is kept as
     196              : // it is (as Node's fetch does), with nothing decoded.
     197          415 : inline content_decoding decode_content(std::string_view codings, std::string& body, size_t max_size) {
     198              :     using ::thinger::util::ascii::iequals;
     199              :     enum class coding { gzip, deflate };
     200              :     coding listed[max_content_codings];
     201          415 :     size_t count = 0;
     202          415 :     bool unsupported = false;
     203          415 :     size_t elements = 0;
     204          920 :     while (!codings.empty()) {
     205          514 :         auto comma = codings.find(',');
     206          514 :         auto element = codings.substr(0, comma);
     207          514 :         codings = comma == std::string_view::npos ? std::string_view{} : codings.substr(comma + 1);
     208          514 :         auto first = element.find_first_not_of(" \t");
     209          514 :         if (first == std::string_view::npos) continue;
     210          508 :         element = element.substr(first, element.find_last_not_of(" \t") - first + 1);
     211              : 
     212          508 :         if (++elements > max_content_codings) return content_decoding::invalid;
     213          499 :         if (iequals(element, "gzip") || iequals(element, "x-gzip")) {
     214          352 :             listed[count++] = coding::gzip;
     215          147 :         } else if (iequals(element, "deflate")) {
     216          114 :             listed[count++] = coding::deflate;
     217           33 :         } else if (!iequals(element, "identity")) {
     218           27 :             unsupported = true;
     219              :         }
     220              :     }
     221          406 :     if (unsupported) return content_decoding::unsupported;
     222              : 
     223          668 :     while (count > 0) {
     224          406 :         bool too_large = false;
     225          406 :         auto decoded = listed[--count] == coding::gzip ? gzip::decompress(body, max_size, &too_large)
     226          406 :                                                        : deflate::decompress(body, max_size, &too_large);
     227          406 :         if (!decoded) return too_large ? content_decoding::too_large : content_decoding::invalid;
     228          289 :         body = std::move(*decoded);
     229          406 :     }
     230          262 :     return content_decoding::decoded;
     231              : }
     232              : 
     233              : } // namespace thinger::util
     234              : 
     235              : #endif // THINGER_UTIL_COMPRESSION_HPP
        

Generated by: LCOV version 2.0-1