include/boost/burl/parser.hpp

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