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