GCC Code Coverage Report


Directory: ./
File: libs/http_proto/src/multipart_form_sink.cpp
Date: 2025-12-23 17:59:41
Exec Total Coverage
Lines: 110 177 62.1%
Functions: 4 5 80.0%
Branches: 67 137 48.9%

Line Branch Exec Source
1 //
2 // Copyright (c) 2025 Mohammad Nejati
3 //
4 // Distributed under the Boost Software License, Version 1.0. (See accompanying
5 // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6 //
7 // Official repository: https://github.com/cppalliance/http_proto
8 //
9
10 #include <boost/http_proto/multipart_form_sink.hpp>
11 #include <boost/http_proto/rfc/content_disposition_rule.hpp>
12
13 #include "src/rfc/detail/rules.hpp"
14
15 #include <boost/url/grammar/ci_string.hpp>
16
17 namespace boost {
18 namespace http_proto {
19
20 namespace {
21 std::string
22 1 unquote(quoted_token_view qtv)
23 {
24
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
1 if(qtv.has_escapes())
25 return { qtv.begin(), qtv.end() };
26
27 1 auto rs = std::string{};
28
2/2
✓ Branch 2 taken 8 times.
✓ Branch 3 taken 1 times.
9 for(auto it = qtv.begin(); it != qtv.end(); it++)
29 {
30
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if(*it == '\\')
31 it++;
32
1/1
✓ Branch 1 taken 8 times.
8 rs.push_back(*it);
33 }
34 1 return rs;
35 1 }
36 } // namespace
37
38 1 multipart_form_sink::
39 multipart_form_sink(
40 1 core::string_view boundary)
41 {
42
1/1
✓ Branch 1 taken 1 times.
1 leftover_.append("\r\n");
43
1/1
✓ Branch 1 taken 1 times.
1 needle_.append("\r\n--");
44
2/2
✓ Branch 1 taken 1 times.
✓ Branch 4 taken 1 times.
1 needle_.append(boundary);
45 1 }
46
47 auto
48 multipart_form_sink::
49 parts() const noexcept
50 -> boost::span<part const>
51 {
52 return parts_;
53 }
54
55 auto
56 1 multipart_form_sink::
57 on_write(
58 buffers::const_buffer b,
59 bool more) -> results
60 {
61 1 system::error_code ec;
62 core::string_view sv(
63 1 static_cast<char const*>(b.data()), b.size());
64
65
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
1 if(!leftover_.empty())
66 {
67
1/2
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
1 for(std::size_t i = 0; i < leftover_.size(); ++i)
68 {
69 1 core::string_view nd(needle_);
70
2/3
✓ Branch 1 taken 1 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 times.
1 if(!nd.starts_with({ &leftover_[i], &*leftover_.cend() }))
71 continue;
72 1 nd.remove_prefix(leftover_.size() - i);
73
1/2
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 if(sv.size() >= nd.size())
74 {
75
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
1 if(!sv.starts_with(nd))
76 continue;
77
1/1
✓ Branch 3 taken 1 times.
1 parse(true, { leftover_.data(), i }, ec);
78
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
1 if(ec.failed())
79 goto upcall;
80 1 leftover_.clear();
81 1 sv.remove_prefix(nd.size());
82 1 goto loop_sv;
83 }
84 if(!more)
85 break;
86 if(!nd.starts_with(sv))
87 continue;
88 parse(false, { leftover_.data(), i }, ec);
89 if(ec.failed())
90 goto upcall;
91 leftover_.erase(0, i);
92 leftover_.append(sv);
93 return { ec , sv.size() };
94 }
95 // leftover_ cannot contain a needle
96 parse(false, leftover_, ec);
97 if(ec.failed())
98 goto upcall;
99 leftover_.clear();
100 }
101
102 1 loop_sv:
103
1/2
✓ Branch 2 taken 62 times.
✗ Branch 3 not taken.
62 for(char const* it = sv.begin(); it != sv.end(); ++it)
104 {
105
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 57 times.
62 if(*it == '\r')
106 {
107 5 core::string_view rm(it, sv.end());
108
2/2
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 1 times.
5 if(rm.size() >= needle_.size())
109 {
110
1/1
✓ Branch 1 taken 4 times.
4 if(std::equal(
111 4 needle_.rbegin(),
112 4 needle_.rend(),
113
2/2
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 3 times.
8 rm.rend() - needle_.size()))
114 {
115
1/1
✓ Branch 3 taken 1 times.
1 parse(true, { sv.begin(), it }, ec);
116
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
1 if(ec.failed())
117 goto upcall;
118 1 sv = { it + needle_.size(), sv.end() };
119 1 goto loop_sv;
120 }
121 3 continue;
122 }
123
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if(!more)
124 1 break;
125 if(!core::string_view(needle_).starts_with(rm))
126 continue;
127 parse(false, { sv.begin(), it }, ec);
128 if(ec.failed())
129 goto upcall;
130 leftover_.append(it, sv.end());
131 sv = {};
132 goto upcall;
133 }
134 }
135
1/1
✓ Branch 1 taken 1 times.
1 parse(false, sv, ec);
136
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
1 if(!ec.failed())
137 1 sv= {};
138
139 upcall:
140 1 return { ec, b.size() - sv.size() };
141 }
142
143 void
144 3 multipart_form_sink::
145 parse(
146 bool match,
147 core::string_view b,
148 system::error_code& ec)
149 {
150 3 loop:
151
5/8
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
6 switch(state_)
152 {
153 1 case state::preamble:
154
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if(match)
155 1 state_ = state::post_boundary0;
156 1 break;
157 2 case state::post_boundary0:
158
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
2 if(b.empty())
159 break;
160
2/3
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 switch(b[0])
161 {
162 1 case '\r':
163 1 state_ = state::post_boundary1;
164 1 break;
165 1 case '-':
166 1 state_ = state::post_boundary2;
167 1 break;
168 default:
169 ec = http_proto::error::bad_payload;
170 return;
171 }
172 2 b.remove_prefix(1);
173 2 goto loop;
174 1 case state::post_boundary1:
175
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
1 if(b.empty())
176 break;
177
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
1 if(b[0] != '\n')
178 {
179 ec = http_proto::error::bad_payload;
180 return;
181 }
182 1 b.remove_prefix(1);
183 1 state_ = state::header;
184 1 goto loop;
185 1 case state::post_boundary2:
186
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
1 if(b.empty())
187 break;
188
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
1 if(b[0] != '-')
189 {
190 ec = http_proto::error::bad_payload;
191 return;
192 }
193 1 b.remove_prefix(1);
194 1 state_ = state::finished;
195 1 break;
196 1 case state::header:
197 {
198 1 auto const s0 = header_.size();
199
2/2
✓ Branch 1 taken 1 times.
✓ Branch 4 taken 1 times.
1 header_.append(b);
200 1 auto const pos = header_.find("\r\n\r\n");
201
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if(pos == std::string::npos)
202 break;
203
1/1
✓ Branch 1 taken 1 times.
1 header_.erase(pos + 4);
204 1 b.remove_prefix(header_.size() - s0);
205
206 // parse fields
207
208 auto const fields = grammar::parse(
209 1 header_,
210
1/1
✓ Branch 3 taken 1 times.
1 grammar::range_rule(detail::field_rule));
211
212
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
1 if(!fields)
213 {
214 ec = http_proto::error::bad_payload;
215 return;
216 }
217
218 1 part part{};
219
220
3/3
✓ Branch 2 taken 1 times.
✓ Branch 9 taken 1 times.
✓ Branch 10 taken 1 times.
2 for(auto&& field : fields.value())
221 {
222
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if(field.has_obs_fold)
223 continue; // TODO
224
225
1/2
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 if(grammar::ci_is_equal(field.name, "Content-Disposition"))
226 {
227 // parse Content-Disposition
228 1 auto cd = grammar::parse(field.value, content_disposition_rule);
229
3/6
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 1 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 1 times.
1 if(!cd || !grammar::ci_is_equal(cd->type, "form-data"))
230 {
231 ec = http_proto::error::bad_payload;
232 return;
233 }
234
2/2
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 1 times.
2 for(auto && param : cd->params)
235 {
236
1/2
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 if(grammar::ci_is_equal(param.name, "name"))
237 {
238
1/1
✓ Branch 1 taken 1 times.
1 part.name = unquote(param.value);
239 }
240 else if(grammar::ci_is_equal(param.name, "filename"))
241 {
242 auto& ff = part.content.emplace<file_field>();
243 ff.name = unquote(param.value);
244
245 // TODO
246 ff.path = ff.name;
247 file_.open(
248 ff.path.c_str(),
249 capy::file_mode::write_new,
250 ec);
251 if(ec.failed())
252 return;
253 }
254 }
255 1 }
256 else if(grammar::ci_is_equal(field.name, "Content-Type"))
257 {
258 part.content_type = field.value;
259 }
260 }
261
262
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
1 if(part.name.empty())
263 {
264 ec = http_proto::error::bad_payload;
265 return;
266 }
267
268 1 header_.clear();
269
1/1
✓ Branch 2 taken 1 times.
1 parts_.push_back(std::move(part));
270 1 state_ = state::content;
271 BOOST_FALLTHROUGH;
272 1 }
273 case state::content:
274
1/2
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
1 if(auto* p = variant2::get_if<text_field>(
275 1 &parts_.back().content))
276 {
277
2/2
✓ Branch 1 taken 1 times.
✓ Branch 4 taken 1 times.
1 p->data.append(b);
278 }
279 else
280 {
281 file_.write(b.data(), b.size(), ec);
282 if(ec.failed())
283 return;
284 if(match)
285 {
286 file_.close(ec);
287 if(ec.failed())
288 return;
289 }
290 }
291
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if(match)
292 1 state_ = state::post_boundary0;
293 1 break;
294 case state::finished:
295 break;
296 }
297 }
298
299 } // http_proto
300 } // boost
301