LCOV - code coverage report
Current view: top level - http/client - form.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 84.5 % 103 87
Test Date: 2026-02-20 15:38:22 Functions: 86.7 % 15 13

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

Generated by: LCOV version 2.0-1