include/boost/burl/parser.hpp

100.0% Lines (16/16) 0.0% List of functions (0/2) 0.0% Branches (0/13)
parser.hpp
f(x) Functions (2)
Line Branch TLA Hits Source Code
1 //
2 // Copyright (c) 2026 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/burl
8 //
9
10 #ifndef BOOST_BURL_PARSER_HPP
11 #define BOOST_BURL_PARSER_HPP
12
13 #include <boost/burl/detail/circular_buffer.hpp>
14 #include <boost/burl/detail/config.hpp>
15 #include <boost/burl/head_parser.hpp>
16 #include <boost/burl/request_head_base.hpp>
17 #include <boost/burl/response_head_base.hpp>
18
19 #include <boost/capy/buffers.hpp>
20 #include <boost/http/metadata.hpp>
21 #include <boost/system/result.hpp>
22
23 #include <array>
24 #include <cstddef>
25 #include <cstdint>
26 #include <memory>
27 #include <span>
28 #include <string_view>
29 #include <system_error>
30
31 namespace boost
32 {
33 namespace burl
34 {
35
36 namespace detail
37 {
38 struct decoder;
39 } // namespace detail
40
41 /** A parser for HTTP/1 messages.
42
43 The parser performs no I/O. Received bytes are
44 handed to it through @ref prepare and @ref
45 commit, and each parsing operation reports @ref
46 http::error::need_data when it requires more.
47 Driving the parser over a stream is the job of
48 @ref message_reader.
49
50 The parser uses a single block of memory
51 allocated during construction and never exceeds
52 it. The space is reused across messages, one at
53 a time, and holds:
54
55 @li raw octets received from the stream,
56 @li the message header, with O(1) access to the
57 start line,
58 @li all or part of the message body, and
59 @li decoded output when the body is encoded.
60
61 @par Operations
62
63 The body can be retrieved three ways, which
64 differ in where the octets end up:
65
66 @li @ref flatten_body returns the whole body in
67 place, without copying,
68 @li @ref read_some copies into caller-supplied
69 memory, or lets the content decoder write
70 into it directly, and
71 @li @ref pull borrows the parser's own buffers,
72 which @ref consume then releases.
73
74 Each parses the header first when it has not
75 been parsed already, so a caller with no
76 interest in the header never has to call @ref
77 parse_header.
78
79 @par Content Decoding
80
81 When @ref config::decode is set, a body whose
82 `Content-Encoding` is `gzip`, `deflate`, `br`,
83 or `zstd` is decoded as it is parsed, using the
84 decode service installed for that coding in the
85 system context. A body whose coding has no
86 installed service, or which the parser does not
87 know, is delivered as sent.
88
89 @par Errors
90
91 Errors which ask for more input are:
92
93 @li @ref http::error::need_data — fill @ref
94 prepare, call @ref commit, and try again.
95 Reported only while @ref prepare has room.
96 @li @ref http::error::in_place_overflow — more
97 input is required but no writable space
98 remains.
99 @li @ref http::error::incomplete — more input is
100 required but @ref commit_eof was called.
101 @li @ref http::error::end_of_stream — the stream
102 closed cleanly before the message began.
103
104 @see
105 @ref message_reader,
106 @ref request_parser,
107 @ref response_parser.
108 */
109 class parser
110 {
111 public:
112 /// Settings which apply for the life of the parser.
113 struct config
114 {
115 /// The limits enforced while parsing a header.
116 header_limits hdr_limits;
117
118 /// The space reserved for buffering received octets.
119 std::size_t in_buffer = 64 * 1024;
120
121 /// The space reserved for decoded output.
122 std::size_t dec_buffer = 8 * 1024;
123
124 /// The default maximum body size.
125 std::uint64_t body_limit = std::uint64_t(-1);
126
127 /** Whether to decode the body.
128
129 When true, a body whose `Content-Encoding`
130 names a coding with a decode service
131 installed in the system context is decoded
132 as it is parsed.
133 */
134 bool decode = true;
135 };
136
137 //--------------------------------------------
138 //
139 // Observers
140 //
141 //--------------------------------------------
142
143 /** Return true if the header has been parsed.
144 */
145 BOOST_BURL_DECL
146 bool
147 got_header() const noexcept;
148
149 /** Return true if the entire message has arrived.
150 */
151 BOOST_BURL_DECL
152 bool
153 got_body() const noexcept;
154
155 /** Return true if octets are buffered past the message.
156
157 Returns true when the buffer holds octets
158 which lie beyond the current message, such
159 as the start of a pipelined message. Returns
160 false before the message is complete, and
161 false for a payload which is delimited by
162 the end of the stream.
163
164 @see @ref buffered_data.
165 */
166 BOOST_BURL_DECL
167 bool
168 has_buffered_data() const noexcept;
169
170 /** Return the unconsumed octets in the buffer.
171
172 The returned octets are raw: no message
173 framing is applied. After a message whose
174 body has been read to completion they are
175 the octets which follow it, which is how the
176 remainder of a tunnel is recovered following
177 a CONNECT request.
178
179 Note that the framing of a response to
180 CONNECT cannot be determined from the
181 response alone, so the parser reports a
182 payload which continues to the end of the
183 stream and @ref has_buffered_data returns
184 false. A caller which knows it issued a
185 CONNECT should use this function directly.
186
187 If the body of a sized payload has only
188 partially been read, the unread remainder is
189 included.
190
191 @par Preconditions
192 `this->got_header() == true`
193
194 @see @ref has_buffered_data.
195 */
196 BOOST_BURL_DECL
197 std::array<capy::const_buffer, 2>
198 buffered_data() const noexcept;
199
200 //--------------------------------------------
201 //
202 // Modifiers
203 //
204 //--------------------------------------------
205
206 /** Prepare for a new stream.
207
208 Discards all parsing state and any buffered
209 octets.
210 */
211 BOOST_BURL_DECL
212 void
213 reset() noexcept;
214
215 /** Set the maximum body size.
216
217 Overrides @ref config::body_limit. The limit
218 is sticky: it applies to every subsequent
219 message until changed, and is not restored
220 by @ref start or @ref reset.
221
222 @param n The body size limit in octets.
223 */
224 BOOST_BURL_DECL
225 void
226 set_body_limit(std::uint64_t n) noexcept;
227
228 /** Return the buffer region for receiving octets.
229
230 The second region is empty unless the buffer
231 has wrapped. Report octets written into it
232 with @ref commit.
233
234 The region may be empty; in that case an
235 operation which requires more input fails
236 with @ref http::error::in_place_overflow
237 rather than asking for it.
238
239 @see @ref commit, @ref commit_eof.
240 */
241 BOOST_BURL_DECL
242 std::array<capy::mutable_buffer, 2>
243 prepare() noexcept;
244
245 /** Report octets received into the buffer.
246
247 @par Preconditions
248 `n <= capy::buffer_size( this->prepare() )`
249
250 @par Postconditions
251 Regions returned by @ref prepare are
252 invalidated.
253
254 @param n The number of octets received.
255
256 @see @ref prepare.
257 */
258 BOOST_BURL_DECL
259 void
260 commit(std::size_t n) noexcept;
261
262 /** Report the end of the stream.
263
264 Call this when the stream has closed and no
265 further octets will arrive.
266
267 @par Postconditions
268 Regions returned by @ref prepare are
269 invalidated.
270
271 @see @ref prepare.
272 */
273 BOOST_BURL_DECL
274 void
275 commit_eof() noexcept;
276
277 /** Return the octets which may be received directly.
278
279 Body octets may be read from the stream
280 straight into caller-supplied memory,
281 bypassing the parser's buffer, when every
282 one of these holds:
283
284 @li the header has been parsed,
285
286 @li the payload has a known size or is
287 delimited by the end of the stream,
288
289 @li no @ref decoder is installed,
290
291 @li the buffer holds no payload octets,
292
293 @li @ref commit_eof has not been called, and
294
295 @li the body limit permits more octets: a
296 known size must fit within what remains of
297 the limit, and a payload delimited by the
298 end of the stream must not have reached it.
299
300 Otherwise body octets must be received
301 through @ref prepare and @ref commit.
302
303 @return The number of octets which may be
304 received directly, or zero when that is not
305 permitted. For a payload of known size this
306 is what remains of the payload; for one
307 delimited by the end of the stream it is
308 what remains of the body limit.
309
310 @see @ref commit_direct.
311 */
312 BOOST_BURL_DECL
313 std::size_t
314 direct_capacity() const noexcept;
315
316 /** Report octets received into caller memory.
317
318 @par Preconditions
319 `n <= this->direct_capacity()`
320
321 @param n The number of octets received.
322
323 @see @ref direct_capacity.
324 */
325 BOOST_BURL_DECL
326 void
327 commit_direct(std::size_t n) noexcept;
328
329 //--------------------------------------------
330 //
331 // Parsing
332 //
333 //--------------------------------------------
334
335 /** Parse the message header.
336
337 Returns as soon as the header is complete,
338 so that @ref set_body_limit can be called
339 before any body octet is parsed. Has no
340 effect once @ref got_header returns true.
341
342 @par Preconditions
343 @ref start has been called.
344
345 @return Nothing on success, otherwise the
346 error.
347 */
348 BOOST_BURL_DECL
349 system::result<void, std::error_code>
350 parse_header();
351
352 /** Flatten the body in place and return it.
353
354 Coalesces the buffered body octets into a
355 contiguous range in the parser's own buffer
356 and returns a view of them, without copying.
357 A chunked payload is de-chunked in place.
358 Parses the header first if @ref got_header
359 returns false.
360
361 @par Preconditions
362 @ref start has been called.
363
364 @return A view of the complete body,
365 otherwise the error.
366
367 */
368 BOOST_BURL_DECL
369 system::result<std::string_view, std::error_code>
370 flatten_body();
371
372 /** Copy body octets into caller-supplied memory.
373
374 When the body is decoded, the decoder writes
375 its output into `buffers` directly. Parses the
376 header first if @ref got_header returns
377 false.
378
379 @par Preconditions
380 @ref start has been called.
381
382 @param buffers The destination.
383
384 @return The number of octets written,
385 otherwise the error. `capy::error::eof`
386 once the body is complete.
387 */
388 template<capy::MutableBufferSequence MB>
389 system::result<std::size_t, std::error_code>
390 read_some(MB const& buffers);
391
392 /** Return available body octets in place.
393
394 Fills `dest` with descriptors referring to
395 the parser's own buffers. Release them with
396 @ref consume. Parses the header first if
397 @ref got_header returns false.
398
399 @par Preconditions
400 @ref start has been called.
401
402 @param dest The descriptors to fill.
403
404 @return The filled prefix of `dest`, valid
405 until the parser is modified, otherwise the
406 error. `capy::error::eof` once the body is
407 complete.
408
409 @see @ref consume.
410 */
411 BOOST_BURL_DECL
412 system::result<
413 std::span<capy::const_buffer>,
414 std::error_code>
415 pull(std::span<capy::const_buffer> dest);
416
417 /** Release body octets returned by @ref pull.
418
419 @par Preconditions
420 `n` does not exceed the octets returned by
421 the last call to @ref pull.
422
423 @param n The number of octets to release.
424
425 @see @ref pull.
426 */
427 BOOST_BURL_DECL
428 void
429 consume(std::size_t n) noexcept;
430
431 /** Copy the trailer fields into a container.
432
433 Appends each field in the trailer section
434 of a chunked payload to `f`, in the order
435 received.
436
437 @par Preconditions
438 `this->got_header() == true`
439
440 @par Exception Safety
441 Basic guarantee. An exception from the
442 container leaves the parser unchanged;
443 fields already appended remain, and the
444 call may be retried.
445
446 @param f The container to append to.
447
448 @return Nothing on success, otherwise the
449 error.
450 */
451 BOOST_BURL_DECL
452 system::result<void, std::error_code>
453 parse_trailer(fields_base& f);
454
455 protected:
456 BOOST_BURL_DECL
457 parser();
458
459 BOOST_BURL_DECL
460 parser(
461 config const& cfg,
462 bool is_req);
463
464 BOOST_BURL_DECL
465 parser(parser&& other) noexcept;
466
467 BOOST_BURL_DECL
468 parser&
469 operator=(parser&& other) noexcept;
470
471 parser(const parser&) = delete;
472
473 parser&
474 operator=(const parser&) = delete;
475
476 BOOST_BURL_DECL
477 ~parser();
478
479 BOOST_BURL_DECL
480 void
481 start(bool head);
482
483 BOOST_BURL_DECL
484 burl::response_head_base const&
485 get_response() const;
486
487 BOOST_BURL_DECL
488 burl::request_head_base const&
489 get_request() const;
490
491 BOOST_BURL_DECL
492 void
493 set_decoder(
494 std::unique_ptr<detail::decoder> dec) noexcept;
495
496 private:
497 struct chunk_fn;
498
499 std::error_code
500 need_more_() const noexcept;
501
502 std::size_t
503 trailer_extent_() const noexcept;
504
505 std::error_code
506 walk_chunks_(chunk_fn f, bool dry = false);
507
508 std::error_code
509 flatten_chunks_();
510
511 BOOST_BURL_DECL
512 system::result<std::size_t, std::error_code>
513 read_some_(capy::mutable_buffer dest);
514
515 system::result<std::size_t, std::error_code>
516 decode_some_(capy::mutable_buffer dest);
517
518 std::unique_ptr<char[]> buf_;
519 head_parser hp_;
520 std::unique_ptr<detail::decoder> dec_;
521 detail::circular_buffer in_;
522 detail::circular_buffer out_;
523 std::uint64_t rem_ = 0;
524 std::uint64_t body_limit_ = 0;
525 std::uint64_t limit_rem_ = 0;
526 std::error_code dec_err_;
527 http::payload payload_ = http::payload::none;
528 bool decode_ : 1 = false;
529 bool head_ : 1 = false;
530 bool started_ : 1 = false;
531 bool got_header_ : 1 = false;
532 bool got_body_ : 1 = false;
533 bool mid_chunk_ : 1 = false;
534 bool fin_chunk_ : 1 = false;
535 bool eof_ : 1 = false;
536 };
537
538 //------------------------------------------------
539
540 template<capy::MutableBufferSequence MB>
541 system::result<std::size_t, std::error_code>
542 9506x parser::
543 read_some(MB const& buffers)
544 {
545 9506x std::size_t n = 0;
546 9506x auto const end = capy::end(buffers);
547
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
9525x for(auto it = capy::begin(buffers); it != end; ++it)
548 {
549 9510x capy::mutable_buffer const b(*it);
550
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
9510x if(b.size() == 0)
551 3x continue;
552
0/1
✗ Branch 1 not taken.
9507x auto const r = read_some_(b);
553
0/2
✗ Branch 1 not taken.
✗ Branch 2 not taken.
9507x if(r.has_error())
554 {
555
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
6287x if(n == 0)
556 6287x return r;
557 // reported on the next call
558 3204x break;
559 }
560
0/1
✗ Branch 1 not taken.
3220x n += *r;
561
0/3
✗ Branch 1 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
3220x if(*r < b.size())
562 3204x break;
563 }
564 3219x return n;
565 }
566
567 } // namespace burl
568 } // namespace boost
569
570 #endif
571