include/boost/burl/head_parser.hpp

100.0% Lines (35/35) 100.0% List of functions (10/10) 80.0% Branches (8/10)
head_parser.hpp
f(x) Functions (10)
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_HEAD_PARSER_HPP
11 #define BOOST_BURL_HEAD_PARSER_HPP
12
13 #include <boost/burl/detail/config.hpp>
14 #include <boost/burl/request_head_base.hpp>
15 #include <boost/burl/response_head_base.hpp>
16
17 #include <boost/http/error.hpp>
18 #include <boost/system/error_code.hpp>
19
20 #include <cstdint>
21
22 namespace boost
23 {
24 namespace burl
25 {
26
27 /** Limits enforced while parsing an HTTP message header.
28
29 Every limit is checked by @ref head_parser::parse.
30 */
31 struct header_limits
32 {
33 /// Maximum size of the complete header.
34 std::uint32_t max_size = 8 * 1024;
35
36 /// Maximum number of header fields.
37 std::uint16_t max_fields = 100;
38
39 /// Maximum size of the start line.
40 std::uint16_t max_start_line = 4 * 1024;
41
42 /// Maximum size of a single header field.
43 std::uint16_t max_field = 4 * 1024;
44 };
45
46 /** An in-place parser for HTTP message headers.
47
48 The parser constructs a message header
49 directly in a supplied buffer. Received bytes
50 remain in place throughout parsing.
51
52 The caller owns the buffer and the fill
53 cursor. Bytes are placed at the parse base,
54 the address the header is built at, which is
55 initially the start of the buffer. Their
56 running total is handed to @ref parse, which
57 resumes where the previous call left off.
58 While more input is required, @ref parse
59 reports @ref http::error::need_data. Once the
60 terminating empty line is parsed, the header
61 can be inspected through @ref message_head,
62 @ref request_head, or @ref response_head.
63 Bytes beyond @ref message_head_base::buffer
64 belong to the payload and are left untouched.
65
66 Space for the field lookup table is reserved
67 at the end of the buffer, beginning at
68 @ref ceiling. Nothing may be written there. Use
69 @ref bytes_needed to determine the buffer size
70 required for a given set of limits. If the
71 header cannot complete below @ref ceiling,
72 @ref parse fails with
73 @ref http::error::in_place_overflow.
74
75 Obsolete folded field values (obs-fold) are
76 unfolded in place by replacing each folding
77 CRLF with spaces; this is the only
78 modification made to received bytes. Because
79 the header is built from the bytes where they
80 lie, bytes already consumed by the parser
81 must not be overwritten, and the buffer must
82 remain valid for the lifetime of the parser.
83
84 @see
85 @ref message_head_base,
86 @ref request_head_base,
87 @ref response_head_base.
88 */
89 class head_parser
90 {
91 public:
92 /** Constructor.
93
94 The parser accepts any buffer size. Its
95 usable end is aligned downward as required
96 by the field lookup table. If the resulting
97 usable size is too small for parsing to
98 make progress, @ref parse fails with
99 @ref http::error::in_place_overflow.
100
101 @param is_request True to parse a request
102 header, false to parse a response header.
103
104 @param buf The destination buffer for the
105 parsed header.
106
107 @param n The size of the buffer in bytes.
108
109 @param limits The header size limits
110 enforced during parsing.
111 */
112 BOOST_BURL_DECL
113 head_parser(
114 bool is_request,
115 char* buf,
116 std::size_t n,
117 header_limits const& limits = {}) noexcept;
118
119 /** Constructor.
120
121 Default constructed parsers behave as if
122 constructed with `is_request == true` and
123 a zero-size buffer.
124 */
125 301x head_parser() noexcept
126 301x : head_parser(true, nullptr, 0)
127 {
128 301x }
129
130 /** Constructor.
131
132 The new parser continues over the same
133 buffer from where `other` stopped.
134 Afterwards `other` still references that
135 buffer and must not be used, except to be
136 destroyed or assigned to.
137
138 @param other The parser to move from.
139 */
140 BOOST_BURL_DECL
141 head_parser(head_parser&& other) noexcept;
142
143 /** Assignment.
144
145 The parser continues over the same buffer
146 from where `other` stopped. Afterwards
147 `other` still references that buffer and
148 must not be used, except to be destroyed or
149 assigned to.
150
151 @param other The parser to move from.
152 */
153 BOOST_BURL_DECL
154 head_parser&
155 operator=(head_parser&& other) noexcept;
156
157 head_parser(head_parser const&) = delete;
158 head_parser& operator=(head_parser const&) = delete;
159
160 /** Re-arm the parser for a new header.
161
162 Clears the parsed header and parse state,
163 re-arming the parser to build the next
164 header at `base`. The limits and @ref
165 ceiling are retained; the usable capacity
166 becomes the distance from `base` to
167 @ref ceiling.
168
169 Bytes belonging to the next message may
170 already be present at `base`; they are
171 reported to the next @ref parse like any
172 others. Building the header where its
173 bytes already lie avoids moving them.
174
175 @par Preconditions
176 `base` lies within the buffer supplied at
177 construction and is not greater than
178 @ref ceiling.
179
180 @param base The address to build the
181 header at.
182 */
183 BOOST_BURL_DECL
184 void
185 reset(char* base) noexcept;
186
187 /** Continue building the header at a lower address.
188
189 The caller has already relocated the
190 received bytes to `base`; parsing continues
191 from there, gaining room to receive more
192 input. Parsing may resume normally, whether
193 the header is complete or not.
194
195 The field lookup table does not move.
196 Pointers and views into the header
197 obtained beforehand are invalidated.
198
199 @par Preconditions
200 `base` is not greater than the address the
201 header was built at, and the received bytes
202 have been moved to `base`.
203
204 @param base The address the received
205 bytes were moved to.
206 */
207 BOOST_BURL_DECL
208 void
209 rebase(char* base) noexcept;
210
211 /** Return the end of the writable region.
212
213 Nothing may be written at or beyond the
214 returned address; it is where the field
215 lookup table is reserved. The address is
216 fixed by the buffer supplied at
217 construction and does not move with
218 @ref reset or @ref rebase.
219
220 A buffer too small to hold the table
221 reports the parse base itself, leaving no
222 room to receive anything.
223
224 @par Complexity
225 Constant.
226 */
227 BOOST_BURL_DECL
228 char*
229 ceiling() const noexcept;
230
231 /** Parse the received bytes.
232
233 Parsing resumes where the previous call left off.
234 It continues until the header is complete, more
235 data is required, or an error occurs.
236
237 @par Preconditions
238 `n` bytes are readable at the parse base, and
239 `n` is not less than the count passed to the
240 previous call, nor greater than the distance
241 from the parse base to @ref ceiling.
242
243 @par Complexity
244 Linear in the number of bytes not yet parsed.
245
246 @param n The total number of bytes received at
247 the parse base.
248
249 @param ec Set to:
250 - Zero if the header completed and its
251 payload framing is valid.
252 - @ref http::error::need_data if more input is
253 required and room remains below @ref ceiling.
254 - @ref http::error::in_place_overflow if more
255 input is required but no room remains.
256 - A syntax, framing, or limit error otherwise.
257 */
258 BOOST_BURL_DECL
259 void
260 parse(
261 std::size_t n,
262 system::error_code& ec) noexcept;
263
264 /** Return the limits enforced by the parser.
265
266 These are the limits supplied at
267 construction, with `max_size` capped at
268 @ref fields_base::max_buffer_size.
269 */
270 header_limits const&
271 156x limits() const noexcept
272 {
273 156x return limits_;
274 }
275
276 /** Return the parsed header.
277
278 Returns the parts of the header common to
279 requests and responses. Until @ref parse
280 succeeds, the header holds only the parts
281 parsed so far.
282 */
283 class message_head_base const&
284 59280x message_head() const noexcept
285 {
286 59280x return h_();
287 }
288
289 /** Return the parsed header.
290
291 Until @ref parse succeeds, the header
292 holds only the parts parsed so far.
293
294 @par Preconditions
295 The parser was constructed with
296 `is_request == true`.
297 */
298 class request_head_base const&
299 54x request_head() const noexcept
300 {
301
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 54 times.
54x BOOST_ASSERT(is_req_);
302 54x return s_.req;
303 }
304
305 /** Return the parsed header.
306
307 Until @ref parse succeeds, the header
308 holds only the parts parsed so far.
309
310 @par Preconditions
311 The parser was constructed with
312 `is_request == false`.
313 */
314 class response_head_base const&
315 293x response_head() const noexcept
316 {
317
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 293 times.
293x BOOST_ASSERT(!is_req_);
318 293x return s_.res;
319 }
320
321 /** Return the buffer size to allocate for `limits`.
322
323 The returned size is sufficient for:
324
325 - the largest header permitted by `limits`,
326 - its field lookup table, and
327 - `extra` additional bytes for buffering payload
328 data that follows the header.
329
330 A smaller buffer may still be used; @ref
331 parse simply fails with
332 @ref http::error::in_place_overflow once the
333 header cannot complete within it.
334
335 @param limits The limits the parser will
336 enforce.
337
338 @param extra Bytes to reserve beyond a
339 maximal header.
340 */
341 static constexpr
342 std::size_t
343 321x bytes_needed(
344 header_limits const& limits,
345 std::size_t extra = 0) noexcept
346 {
347 321x constexpr auto align = alignof(message_head_base::entry);
348 321x constexpr auto entry_size = sizeof(message_head_base::entry);
349 321x constexpr auto max_head = fields_base::max_buffer_size;
350 642x constexpr auto clamp = [](std::size_t a, std::size_t b)
351 {
352
2/2
✓ Branch 0 taken 635 times.
✓ Branch 1 taken 7 times.
642x return a < b ? a : b;
353 };
354
355 321x auto const head = clamp(limits.max_size, max_head);
356 321x auto const table = std::size_t(limits.max_fields) * entry_size;
357 321x auto const overhead = table + align - 1;
358 321x auto const avail = std::size_t(-1) - head - overhead;
359
360 321x return head + clamp(extra, avail) + overhead;
361 }
362
363 private:
364 union storage
365 {
366 burl::request_head_base req;
367 burl::response_head_base res;
368 41176x storage() noexcept
369 41176x {
370 41176x }
371 };
372
373 enum class state : unsigned char
374 {
375 start_line,
376 fields,
377 done,
378 };
379
380 storage s_;
381 header_limits limits_;
382 bool is_req_ = false;
383 state st_ = state::start_line;
384
385 burl::message_head_base&
386 211907x h_() noexcept
387 {
388
2/2
✓ Branch 0 taken 204040 times.
✓ Branch 1 taken 7867 times.
211907x if(is_req_)
389 204040x return s_.req;
390 7867x return s_.res;
391 }
392
393 burl::message_head_base const&
394 255131x h_() const noexcept
395 {
396
2/2
✓ Branch 0 taken 247498 times.
✓ Branch 1 taken 7633 times.
255131x if(is_req_)
397 247498x return s_.req;
398 7633x return s_.res;
399 }
400
401 void
402 parse_start_line_(
403 char const*& it,
404 char const* end,
405 system::error_code& ec) noexcept;
406
407 void
408 parse_fields_(
409 char const*& it,
410 char const* end,
411 system::error_code& ec) noexcept;
412 };
413
414 } // namespace burl
415 } // namespace boost
416
417 #endif
418