LCOV - code coverage report
Current view: top level - http/common - multipart_parser.hpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 100.0 % 6 6
Test Date: 2026-11-10 10:38:07 Functions: 100.0 % 6 6

            Line data    Source code
       1              : #ifndef THINGER_HTTP_COMMON_MULTIPART_PARSER_HPP
       2              : #define THINGER_HTTP_COMMON_MULTIPART_PARSER_HPP
       3              : 
       4              : #include <cstddef>
       5              : #include <cstdint>
       6              : #include <limits>
       7              : #include <string>
       8              : #include <string_view>
       9              : #include <utility>
      10              : #include <vector>
      11              : #include "http_response.hpp"
      12              : 
      13              : namespace thinger::http {
      14              : 
      15              :     // Limits of a multipart body, against bodies that are small for the maximum body size
      16              :     // but expensive to process: many parts, large header sections, fields kept in memory.
      17              :     // Content sizes count the bytes of the parts, without delimiters nor headers; the whole
      18              :     // body is bounded by the maximum body size of the route.
      19              :     struct multipart_limits {
      20              :         // Parts in the body, fields and files (Go: 1000)
      21              :         size_t max_parts = 1000;
      22              :         // Parts with a filename (busboy and Go: unlimited)
      23              :         size_t max_files = std::numeric_limits<size_t>::max();
      24              :         // Header section of each part, line endings included (busboy: 16 KB)
      25              :         std::uint64_t max_header_size = 16 * 1024;
      26              :         // Filename of a part, in bytes, as sent (decoded from filename* if present): no
      27              :         // longer than a file name can be in most file systems
      28              :         std::uint64_t max_filename_size = 255;
      29              :         // Content of each part without filename (busboy: 1 MB)
      30              :         std::uint64_t max_field_size = 1024 * 1024;
      31              :         // Content of all the parts without filename, which are usually kept in memory
      32              :         std::uint64_t max_fields_size = 8 * 1024 * 1024;
      33              :         // Content of each part with a filename (busboy and Go: unlimited)
      34              :         std::uint64_t max_file_size = std::numeric_limits<std::uint64_t>::max();
      35              :     };
      36              : 
      37              :     // Why a multipart body failed
      38              :     enum class multipart_error {
      39              :         none,
      40              :         not_multipart,          // 415: the Content-Type is not multipart (or the body has a Content-Encoding)
      41              :         invalid_boundary,       // 400: the boundary parameter is missing or invalid
      42              :         malformed,              // 400: the body does not follow the multipart syntax
      43              :         truncated,              // 400: the body ended before its closing delimiter
      44              :         invalid_body,           // 400: the request body could not be read (framing, connection closed)
      45              :         too_many_parts,         // 413
      46              :         too_many_files,         // 413
      47              :         header_too_large,       // 413: the header section of a part
      48              :         filename_too_large,     // 413
      49              :         field_too_large,        // 413: a part without filename
      50              :         fields_too_large,       // 413: all the parts without filename
      51              :         file_too_large,         // 413: a part with a filename
      52              :         body_too_large,         // 413: over the maximum body size of the route
      53              :         file_failed             // 500: a temporary file could not be written
      54              :     };
      55              : 
      56              :     // Status answering an error (400, 413, 415 or 500), and a message for the client
      57              :     http_response::status multipart_error_status(multipart_error error);
      58              :     const char* multipart_error_message(multipart_error error);
      59              : 
      60              :     // A part of a multipart body: its header section, and what its Content-Disposition and
      61              :     // Content-Type say (RFC 7578)
      62              :     struct multipart_part {
      63              :         // Header fields as received (names keep their case; values without surrounding whitespace)
      64              :         std::vector<std::pair<std::string, std::string>> headers;
      65              : 
      66              :         // Disposition type, lowercase ("form-data"); empty without Content-Disposition
      67              :         std::string disposition;
      68              :         // Field name (the name parameter), empty if none
      69              :         std::string name;
      70              :         // Filename as the client sent it, from filename* (RFC 5987, if its charset is UTF-8,
      71              :         // ISO-8859-1 or US-ASCII) or else filename. It may be a path ("../../etc/passwd",
      72              :         // "C:\\Users\\me\\photo.jpg") or have control characters: never use it as a path,
      73              :         // see safe_filename()
      74              :         std::string filename;
      75              :         // Whether the part has a filename parameter (even an empty one, as browsers send for
      76              :         // a file input without a file): a file, not a field
      77              :         bool has_filename = false;
      78              :         // Content-Type of the part, "text/plain" if it has none (RFC 7578, 4.4)
      79              :         std::string content_type = "text/plain";
      80              : 
      81          198 :         bool is_file() const { return has_filename; }
      82              : 
      83              :         // The value of a header (the first one with that name, case-insensitive), empty if missing
      84              :         std::string header(std::string_view name) const;
      85              : 
      86              :         // The filename reduced to a name to store it with: what follows its last '/' or
      87              :         // '\', without control characters; empty for "." and ".." (and an empty filename).
      88              :         // Still, it can collide with existing files, start with a dot, or be long.
      89           27 :         std::string safe_filename() const { return sanitize_filename(filename); }
      90              : 
      91              :         static std::string sanitize_filename(std::string_view filename);
      92              :     };
      93              : 
      94              :     /**
      95              :      * Incremental parser of a multipart body (RFC 2046, section 5.1; RFC 7578 for
      96              :      * multipart/form-data). It is fed the body in pieces of any size and returns the events
      97              :      * they make, without keeping more than the header section of a part and a partial
      98              :      * delimiter: the content of the parts is handed over as it is found, in pieces pointing
      99              :      * into the input (or into the parser, for bytes held back as a possible delimiter).
     100              :      *
     101              :      * The result does not depend on how the body is split. It is strict as Go's
     102              :      * mime/multipart: CRLF line endings only, transport padding (spaces and tabs) after
     103              :      * delimiters, a closing delimiter required, preamble and epilogue ignored, no header
     104              :      * folding. A part can start with its delimiter right after its header section (an empty
     105              :      * part), as Go reads it. The boundary inside the content of a part is not a delimiter
     106              :      * unless it starts a line and is followed by "--", whitespace or CRLF.
     107              :      */
     108              :     class multipart_parser {
     109              :     public:
     110              :         enum class event {
     111              :             need_more,      // all the input was consumed: feed more, or finish()
     112              :             part,           // a part starts: its header section is in part()
     113              :             data,           // a piece of the content of the current part, in data()
     114              :             part_end,       // the current part ended
     115              :             end,            // the closing delimiter was read (the rest of the body is the epilogue)
     116              :             error           // the body is invalid, or over a limit: see error()
     117              :         };
     118              : 
     119              :         // RFC 2046: 1 to 70 characters, digits, letters and '()+_,-./:=? (and spaces, but not last)
     120              :         static bool valid_boundary(std::string_view boundary);
     121              : 
     122              :         // A parser for a body with this boundary (as the boundary parameter of the
     123              :         // Content-Type gives it). With an invalid boundary, it fails at once (invalid_boundary).
     124              :         explicit multipart_parser(std::string_view boundary, multipart_limits limits = {});
     125              : 
     126              :         /// Parse `input` up to the next event, removing what it consumed from its front.
     127              :         /// After `end` (and `error`) the same event is returned again, and the rest of the
     128              :         /// input (the epilogue) is consumed.
     129              :         event parse(std::string_view& input);
     130              : 
     131              :         /// The body ended: `end` if its closing delimiter was read, else `error` (truncated)
     132              :         event finish();
     133              : 
     134              :         /// Content of a `data` event, valid until the next call
     135       345305 :         std::string_view data() const { return data_; }
     136              : 
     137              :         /// The current part, from its `part` event to the next one
     138       134859 :         const multipart_part& part() const { return part_; }
     139              : 
     140        29034 :         multipart_error error() const { return error_; }
     141              :         bool done() const { return state_ == state::done; }
     142          108 :         bool failed() const { return state_ == state::error; }
     143              : 
     144              :         /// Parts started, and bytes of content of the current part
     145              :         size_t parts() const { return parts_; }
     146              :         std::uint64_t part_size() const { return part_size_; }
     147              : 
     148              :     private:
     149              :         enum class state {
     150              :             preamble,           // before the first delimiter
     151              :             delimiter_line,     // transport padding after a delimiter, then CRLF
     152              :             delimiter_lf,
     153              :             headers,            // header section of a part
     154              :             content,            // content of a part
     155              :             close_line,         // transport padding after the closing delimiter, then CRLF or the end
     156              :             close_lf,
     157              :             done,               // the epilogue
     158              :             error
     159              :         };
     160              : 
     161              :         enum class match { mismatch, partial, delimiter, close };
     162              : 
     163              :         multipart_limits limits_;
     164              :         // CRLF "--" boundary "--": the delimiter, and the two dashes of the closing one
     165              :         std::string delimiter_;
     166              :         size_t boundary_end_ = 0;       // length of CRLF "--" boundary
     167              :         state state_ = state::preamble;
     168              :         multipart_error error_ = multipart_error::none;
     169              : 
     170              :         // Bytes of delimiter_ matched by the input already consumed (held back), from held_from_:
     171              :         // the parts and the body start as if after a CRLF, which is not content
     172              :         size_t matched_ = 0;
     173              :         size_t held_from_ = 0;
     174              : 
     175              :         // Header section of the next part: the line being read, its fields, and its size
     176              :         std::string line_;
     177              :         std::vector<std::pair<std::string, std::string>> headers_;
     178              :         std::uint64_t header_size_ = 0;
     179              : 
     180              :         multipart_part part_;
     181              :         std::string_view data_;
     182              :         size_t parts_ = 0;
     183              :         size_t files_ = 0;
     184              :         std::uint64_t part_size_ = 0;
     185              :         std::uint64_t fields_size_ = 0;
     186              : 
     187              :         event fail(multipart_error error);
     188              :         void start_matching();
     189              :         match match_delimiter(size_t& matched, std::string_view input, size_t& used) const;
     190              :         event scan(std::string_view& input);
     191              :         event on_delimiter(match found);
     192              :         event header_line(std::string_view line);
     193              :         event header_section_end();
     194              :         event content(std::string_view data);
     195              :     };
     196              : 
     197              : }
     198              : 
     199              : #endif
        

Generated by: LCOV version 2.0-1