Coverage Report

Created: 2025-11-24 06:56

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/crow/include/crow/multipart.h
Line
Count
Source
1
#pragma once
2
3
#include <string>
4
#include <vector>
5
#include <sstream>
6
7
#include "crow/http_request.h"
8
#include "crow/returnable.h"
9
#include "crow/ci_map.h"
10
#include "crow/exceptions.h"
11
12
namespace crow
13
{
14
15
    /// Encapsulates anything related to processing and organizing `multipart/xyz` messages
16
    namespace multipart
17
    {
18
19
        const std::string dd = "--";
20
21
        /// The first part in a section, contains metadata about the part
22
        struct header
23
        {
24
            std::string value;                                   ///< The first part of the header, usually `Content-Type` or `Content-Disposition`
25
            std::unordered_map<std::string, std::string> params; ///< The parameters of the header, come after the `value`
26
27
0
            operator int() const { return std::stoi(value); }    ///< Returns \ref value as integer
28
0
            operator double() const { return std::stod(value); } ///< Returns \ref value as double
29
        };
30
31
        /// Multipart header map (key is header key).
32
        using mph_map = std::unordered_multimap<std::string, header, ci_hash, ci_key_eq>;
33
34
        /// Find and return the value object associated with the key. (returns an empty class if nothing is found)
35
        template<typename O, typename T>
36
        inline const O& get_header_value_object(const T& headers, const std::string& key)
37
0
        {
38
0
            if (headers.count(key))
39
0
            {
40
0
                return headers.find(key)->second;
41
0
            }
42
0
            static O empty;
43
0
            return empty;
44
0
        }
45
46
        /// Same as \ref get_header_value_object() but for \ref multipart.header
47
        template<typename T>
48
        inline const header& get_header_object(const T& headers, const std::string& key)
49
0
        {
50
0
            return get_header_value_object<header>(headers, key);
51
0
        }
52
53
        ///One part of the multipart message
54
55
        ///
56
        /// It is usually separated from other sections by a `boundary`
57
        struct part
58
        {
59
            mph_map headers;  ///< (optional) The first part before the data, Contains information regarding the type of data and encoding
60
            std::string body; ///< The actual data in the part
61
62
0
            operator int() const { return std::stoi(body); }    ///< Returns \ref body as integer
63
0
            operator double() const { return std::stod(body); } ///< Returns \ref body as double
64
65
            const header& get_header_object(const std::string& key) const
66
0
            {
67
0
                return multipart::get_header_object(headers, key);
68
0
            }
69
        };
70
71
        /// Multipart map (key is the name parameter).
72
        using mp_map = std::unordered_multimap<std::string, part, ci_hash, ci_key_eq>;
73
74
        /// The parsed multipart request/response
75
        struct message : public returnable
76
        {
77
            ci_map headers;          ///< The request/response headers
78
            std::string boundary;    ///< The text boundary that separates different `parts`
79
            std::vector<part> parts; ///< The individual parts of the message
80
            mp_map part_map;         ///< The individual parts of the message, organized in a map with the `name` header parameter being the key
81
82
            const std::string& get_header_value(const std::string& key) const
83
0
            {
84
0
                return crow::get_header_value(headers, key);
85
0
            }
86
87
            part get_part_by_name(const std::string& name)
88
0
            {
89
0
                mp_map::iterator result = part_map.find(name);
90
0
                if (result != part_map.end())
91
0
                    return result->second;
92
0
                else
93
0
                    return {};
94
0
            }
95
96
            /// Represent all parts as a string (**does not include message headers**)
97
            std::string dump() const override
98
0
            {
99
0
                std::stringstream str;
100
0
                std::string delimiter = dd + boundary;
101
0
102
0
                for (unsigned i = 0; i < parts.size(); i++)
103
0
                {
104
0
                    str << delimiter << crlf;
105
0
                    str << dump(i);
106
0
                }
107
0
                str << delimiter << dd << crlf;
108
0
                return str.str();
109
0
            }
110
111
            /// Represent an individual part as a string
112
            std::string dump(int part_) const
113
0
            {
114
0
                std::stringstream str;
115
0
                part item = parts[part_];
116
0
                for (auto& item_h : item.headers)
117
0
                {
118
0
                    str << item_h.first << ": " << item_h.second.value;
119
0
                    for (auto& it : item_h.second.params)
120
0
                    {
121
0
                        str << "; " << it.first << '=' << pad(it.second);
122
0
                    }
123
0
                    str << crlf;
124
0
                }
125
0
                str << crlf;
126
0
                str << item.body << crlf;
127
0
                return str.str();
128
0
            }
129
130
            /// Default constructor using default values
131
            message(const ci_map& headers_, const std::string& boundary_, const std::vector<part>& sections):
132
              returnable("multipart/form-data; boundary=CROW-BOUNDARY"), headers(headers_), boundary(boundary_), parts(sections)
133
0
            {
134
0
                if (!boundary.empty())
135
0
                    content_type = "multipart/form-data; boundary=" + boundary;
136
0
                for (auto& item : parts)
137
0
                {
138
0
                    part_map.emplace(
139
0
                      (get_header_object(item.headers, "Content-Disposition").params.find("name")->second),
140
0
                      item);
141
0
                }
142
0
            }
143
144
            /// Create a multipart message from a request data
145
            explicit message(const request& req):
146
              returnable("multipart/form-data; boundary=CROW-BOUNDARY"),
147
              headers(req.headers),
148
              boundary(get_boundary(get_header_value("Content-Type")))
149
0
            {
150
0
                if (!boundary.empty())
151
0
                {
152
0
                    content_type = "multipart/form-data; boundary=" + boundary;
153
0
                    parse_body(req.body);
154
0
                }
155
0
                else
156
0
                {
157
0
                    throw bad_request("Empty boundary in multipart message");
158
0
                }
159
0
            }
160
161
        private:
162
            std::string get_boundary(const std::string& header) const
163
0
            {
164
0
                constexpr char boundary_text[] = "boundary=";
165
0
                size_t found = header.find(boundary_text);
166
0
                if (found != std::string::npos)
167
0
                {
168
0
                    std::string to_return(header.substr(found + strlen(boundary_text)));
169
0
                    if (to_return[0] == '\"')
170
0
                    {
171
0
                        to_return = to_return.substr(1, to_return.length() - 2);
172
0
                    }
173
0
                    return to_return;
174
0
                }
175
0
                return std::string();
176
0
            }
177
178
            void parse_body(std::string body)
179
0
            {
180
0
                std::string delimiter = dd + boundary;
181
0
182
0
                // TODO(EDev): Exit on error
183
0
                while (body != (crlf))
184
0
                {
185
0
                    size_t found = body.find(delimiter);
186
0
                    if (found == std::string::npos)
187
0
                    {
188
0
                        // did not find delimiter; probably an ill-formed body; throw to indicate the issue to user
189
0
                        throw bad_request("Unable to find delimiter in multipart message. Probably ill-formed body");
190
0
                    }
191
0
                    std::string section = body.substr(0, found);
192
0
193
0
                    // +2 is the CRLF.
194
0
                    // We don't check it and delete it so that the same delimiter can be used for The last delimiter (--delimiter--CRLF).
195
0
                    body.erase(0, found + delimiter.length() + 2);
196
0
                    if (!section.empty())
197
0
                    {
198
0
                        part parsed_section(parse_section(section));
199
0
                        part_map.emplace(
200
0
                          (get_header_object(parsed_section.headers, "Content-Disposition").params.find("name")->second),
201
0
                          parsed_section);
202
0
                        parts.push_back(std::move(parsed_section));
203
0
                    }
204
0
                }
205
0
            }
206
207
            part parse_section(std::string& section)
208
0
            {
209
0
                struct part to_return;
210
0
211
0
                size_t found = section.find(crlf + crlf);
212
0
                std::string head_line = section.substr(0, found + 2);
213
0
                section.erase(0, found + 4);
214
0
215
0
                parse_section_head(head_line, to_return);
216
0
                to_return.body = section.substr(0, section.length() - 2);
217
0
                return to_return;
218
0
            }
219
220
            void parse_section_head(std::string& lines, part& part)
221
0
            {
222
0
                while (!lines.empty())
223
0
                {
224
0
                    header to_add;
225
0
226
0
                    const size_t found_crlf = lines.find(crlf);
227
0
                    std::string line = lines.substr(0, found_crlf);
228
0
                    std::string key;
229
0
                    lines.erase(0, found_crlf + 2);
230
0
                    // Add the header if available
231
0
                    if (!line.empty())
232
0
                    {
233
0
                        const size_t found_semicolon = line.find("; ");
234
0
                        std::string header = line.substr(0, found_semicolon);
235
0
                        if (found_semicolon != std::string::npos)
236
0
                            line.erase(0, found_semicolon + 2);
237
0
                        else
238
0
                            line = std::string();
239
0
240
0
                        size_t header_split = header.find(": ");
241
0
                        key = header.substr(0, header_split);
242
0
243
0
                        to_add.value = header.substr(header_split + 2);
244
0
                    }
245
0
246
0
                    // Add the parameters
247
0
                    while (!line.empty())
248
0
                    {
249
0
                        const size_t found_semicolon = line.find("; ");
250
0
                        std::string param = line.substr(0, found_semicolon);
251
0
                        if (found_semicolon != std::string::npos)
252
0
                            line.erase(0, found_semicolon + 2);
253
0
                        else
254
0
                            line = std::string();
255
0
256
0
                        size_t param_split = param.find('=');
257
0
258
0
                        std::string value = param.substr(param_split + 1);
259
0
260
0
                        to_add.params.emplace(param.substr(0, param_split), trim(value));
261
0
                    }
262
0
                    part.headers.emplace(key, to_add);
263
0
                }
264
0
            }
265
266
            inline std::string trim(std::string& string, const char& excess = '"') const
267
0
            {
268
0
                if (string.length() > 1 && string[0] == excess && string[string.length() - 1] == excess)
269
0
                    return string.substr(1, string.length() - 2);
270
0
                return string;
271
0
            }
272
273
            inline std::string pad(std::string& string, const char& padding = '"') const
274
0
            {
275
0
                return (padding + string + padding);
276
0
            }
277
        };
278
    } // namespace multipart
279
} // namespace crow