LCOV - code coverage report
Current view: top level - http/client - form.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 83.6 % 110 92
Test Date: 2026-11-10 10:38:07 Functions: 87.5 % 16 14

            Line data    Source code
       1              : #include "form.hpp"
       2              : #include "../server/mime_types.hpp"
       3              : #include "../util/url.hpp"
       4              : #include <fstream>
       5              : #include <sstream>
       6              : #include <random>
       7              : #include <algorithm>
       8              : 
       9              : namespace thinger::http {
      10              : 
      11              : namespace {
      12              : 
      13              :     // A field name or filename within the quotes of a Content-Disposition, as browsers (and
      14              :     // curl) write it: '"', CR and LF percent-encoded, so neither can end the parameter or the
      15              :     // header line (WHATWG HTML, multipart/form-data encoding)
      16           40 :     std::string escape_quoted(const std::string& value) {
      17           40 :         std::string result;
      18           40 :         result.reserve(value.size());
      19          358 :         for (char c : value) {
      20          318 :             if (c == '"') result += "%22";
      21          310 :             else if (c == '\r') result += "%0D";
      22          308 :             else if (c == '\n') result += "%0A";
      23          306 :             else result += c;
      24              :         }
      25           40 :         return result;
      26            0 :     }
      27              : 
      28              : }
      29              : 
      30              : // ============================================
      31              : // Field methods
      32              : // ============================================
      33              : 
      34           36 : form& form::field(std::string name, std::string value) {
      35           36 :     fields_.emplace_back(std::move(name), std::move(value));
      36           36 :     return *this;
      37              : }
      38              : 
      39            2 : form& form::fields(std::initializer_list<std::pair<std::string, std::string>> pairs) {
      40            8 :     for (const auto& [name, value] : pairs) {
      41            6 :         fields_.emplace_back(name, value);
      42              :     }
      43            2 :     return *this;
      44              : }
      45              : 
      46              : // ============================================
      47              : // File methods
      48              : // ============================================
      49              : 
      50            0 : form& form::file(std::string name, const std::filesystem::path& path) {
      51            0 :     return file(std::move(name), path, mime_type(path));
      52              : }
      53              : 
      54            0 : form& form::file(std::string name, const std::filesystem::path& path,
      55              :                  std::string content_type) {
      56              :     // Read file content
      57            0 :     std::ifstream ifs(path, std::ios::binary);
      58            0 :     if (!ifs) {
      59            0 :         return *this;
      60              :     }
      61              : 
      62            0 :     std::ostringstream oss;
      63            0 :     oss << ifs.rdbuf();
      64              : 
      65            0 :     files_.push_back({
      66            0 :         std::move(name),
      67            0 :         path.filename().string(),
      68            0 :         std::move(content_type),
      69              :         oss.str()
      70              :     });
      71              : 
      72            0 :     return *this;
      73            0 : }
      74              : 
      75           14 : form& form::file(std::string name, std::string content,
      76              :                  std::string filename, std::string content_type) {
      77           14 :     files_.push_back({
      78           14 :         std::move(name),
      79           14 :         std::move(filename),
      80           14 :         std::move(content_type),
      81           14 :         std::move(content)
      82              :     });
      83           14 :     return *this;
      84           14 : }
      85              : 
      86            2 : form& form::file(std::string name, const void* data, size_t size,
      87              :                  std::string filename, std::string content_type) {
      88            2 :     files_.push_back({
      89            2 :         std::move(name),
      90            2 :         std::move(filename),
      91            2 :         std::move(content_type),
      92              :         std::string(static_cast<const char*>(data), size)
      93              :     });
      94            2 :     return *this;
      95            6 : }
      96              : 
      97              : // ============================================
      98              : // Content-Type and body generation
      99              : // ============================================
     100              : 
     101           13 : std::string form::content_type() const {
     102           13 :     if (is_multipart()) {
     103            7 :         return "multipart/form-data; boundary=" + get_boundary();
     104              :     }
     105           12 :     return "application/x-www-form-urlencoded";
     106              : }
     107              : 
     108           21 : std::string form::body() const {
     109           21 :     if (is_multipart()) {
     110            9 :         return build_multipart();
     111              :     }
     112           12 :     return build_urlencoded();
     113              : }
     114              : 
     115           12 : std::string form::build_urlencoded() const {
     116           12 :     std::string result;
     117           34 :     for (const auto& [name, value] : fields_) {
     118           22 :         if (!result.empty()) {
     119           10 :             result += '&';
     120              :         }
     121           22 :         result += url_encode(name) + '=' + url_encode(value);
     122              :     }
     123           12 :     return result;
     124            0 : }
     125              : 
     126            9 : std::string form::build_multipart() const {
     127            9 :     const std::string& boundary = get_boundary();
     128            9 :     std::ostringstream oss;
     129              : 
     130              :     // Add text fields
     131           25 :     for (const auto& [name, value] : fields_) {
     132           16 :         oss << "--" << boundary << "\r\n";
     133           16 :         oss << "Content-Disposition: form-data; name=\"" << escape_quoted(name) << "\"\r\n";
     134           16 :         oss << "\r\n";
     135           16 :         oss << value << "\r\n";
     136              :     }
     137              : 
     138              :     // Add files
     139           21 :     for (const auto& file : files_) {
     140           12 :         oss << "--" << boundary << "\r\n";
     141            0 :         oss << "Content-Disposition: form-data; name=\"" << escape_quoted(file.name) << "\"; "
     142           12 :             << "filename=\"" << escape_quoted(file.filename) << "\"\r\n";
     143           12 :         oss << "Content-Type: " << file.content_type << "\r\n";
     144           12 :         oss << "\r\n";
     145           12 :         oss << file.content << "\r\n";
     146              :     }
     147              : 
     148              :     // Closing boundary
     149            9 :     oss << "--" << boundary << "--\r\n";
     150              : 
     151           18 :     return oss.str();
     152            9 : }
     153              : 
     154           16 : const std::string& form::get_boundary() const {
     155           16 :     if (boundary_.empty()) {
     156           11 :         boundary_ = generate_boundary();
     157              :     }
     158           16 :     return boundary_;
     159              : }
     160              : 
     161           11 : std::string form::generate_boundary() {
     162              :     static const char chars[] =
     163              :         "0123456789"
     164              :         "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
     165              :         "abcdefghijklmnopqrstuvwxyz";
     166              : 
     167           11 :     std::random_device rd;
     168           11 :     std::mt19937 gen(rd());
     169           11 :     std::uniform_int_distribution<> dis(0, sizeof(chars) - 2);
     170              : 
     171           11 :     std::string boundary = "----ThingerFormBoundary";
     172          187 :     for (int i = 0; i < 16; ++i) {
     173          176 :         boundary += chars[dis(gen)];
     174              :     }
     175              : 
     176           22 :     return boundary;
     177           11 : }
     178              : 
     179              : // ============================================
     180              : // URL encoding/decoding (delegates to util::url)
     181              : // ============================================
     182              : 
     183           58 : std::string form::url_encode(const std::string& str) {
     184              :     // application/x-www-form-urlencoded uses '+' for space instead of '%20'
     185              :     static constexpr char hex[] = "0123456789ABCDEF";
     186           58 :     std::string result;
     187           58 :     result.reserve(str.size());
     188          412 :     for (unsigned char c : str) {
     189          354 :         if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
     190          314 :             result += static_cast<char>(c);
     191           40 :         } else if (c == ' ') {
     192            4 :             result += '+';
     193              :         } else {
     194           36 :             result += '%';
     195           36 :             result += hex[c >> 4];
     196           36 :             result += hex[c & 0x0F];
     197              :         }
     198              :     }
     199           58 :     return result;
     200            0 : }
     201              : 
     202            6 : std::string form::url_decode(const std::string& str) {
     203            6 :     return util::url::url_decode(str);
     204              : }
     205              : 
     206              : // ============================================
     207              : // MIME type detection
     208              : // ============================================
     209              : 
     210           32 : std::string form::mime_type(const std::filesystem::path& path) {
     211              :     // The library's MIME type lookup: application/octet-stream if the extension is unknown or
     212              :     // missing, as curl and browsers send such a file
     213           32 :     return mime_types::extension_to_type(path.extension().string());
     214              : }
     215              : 
     216              : } // namespace thinger::http
        

Generated by: LCOV version 2.0-1