include/boost/burl/parser.hpp
100.0% Lines (16/16)
0.0% List of functions (0/2)
0.0% Branches (0/13)
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 |