diff --git a/example/usage.cpp b/example/usage.cpp index df6ea9c..42cee00 100644 --- a/example/usage.cpp +++ b/example/usage.cpp @@ -68,10 +68,10 @@ inspect_response(corosio::tls_context tls_ctx) if(ec) throw std::system_error(ec); - std::cout << "status: " << r.status_int() << '\n'; - std::cout << "reason: " << r.reason() << '\n'; - std::cout << "headers: " << r.headers() << '\n'; - std::cout << "body: " << co_await r.as() << '\n'; + std::cout << "status: " << r.status_int() << '\n'; + std::cout << "reason: " << r.reason() << '\n'; + std::cout << "headers:\n" << r.headers(); + std::cout << "body: " << co_await r.as() << '\n'; // end::inspect_response[] } diff --git a/include/boost/burl.hpp b/include/boost/burl.hpp index 19959b8..6674682 100644 --- a/include/boost/burl.hpp +++ b/include/boost/burl.hpp @@ -11,15 +11,26 @@ #define BOOST_BURL_HPP #include -#include #include +#include #include +#include +#include #include +#include #include +#include #include #include #include +#include +#include #include +#include +#include +#include +#include +#include #include #include diff --git a/include/boost/burl/client.hpp b/include/boost/burl/client.hpp index b0d7d70..010c338 100644 --- a/include/boost/burl/client.hpp +++ b/include/boost/burl/client.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -21,8 +22,6 @@ #include #include #include -#include -#include #include #include @@ -315,7 +314,7 @@ class client private: config config_; std::shared_ptr pool_; - http::fields headers_; + fields headers_; burl::cookie_jar cookie_jar_; public: @@ -388,7 +387,7 @@ class client c.headers().set(http::field::user_agent, "BoostBurl/1.0"); @endcode */ - http::fields& + fields_base& headers() noexcept { return headers_; @@ -401,7 +400,7 @@ class client precedence over default headers with the same name. */ - const http::fields& + const fields_base& headers() const noexcept { return headers_; diff --git a/include/boost/burl/detail/parser.hpp b/include/boost/burl/detail/parser.hpp index a58f44b..32ee112 100644 --- a/include/boost/burl/detail/parser.hpp +++ b/include/boost/burl/detail/parser.hpp @@ -11,6 +11,7 @@ #define BOOST_BURL_DETAIL_PARSER_HPP #include +#include #include #include @@ -19,10 +20,7 @@ #include #include #include -#include -#include -#include -#include +#include #include @@ -56,7 +54,7 @@ class parser struct config { - http::header_limits hdr_limits; + header_limits hdr_limits; std::size_t in_buffer = 64 * 1024; std::size_t dec_buffer = 8 * 1024; @@ -106,7 +104,7 @@ class parser parser( config const& cfg, - http::detail::kind kind, + bool is_request, capy::any_read_stream stream = {}); parser(parser&& other) noexcept = default; @@ -124,10 +122,10 @@ class parser void start(bool head); - http::static_response const& + burl::response_head_base const& get_response() const; - http::static_request const& + burl::request_head_base const& get_request() const; private: @@ -145,9 +143,6 @@ class parser std::size_t payload_rem() const noexcept; - std::size_t - table_reserve() const noexcept; - capy::io_task<> refill(); @@ -165,22 +160,9 @@ class parser do_read_some( std::span buffers); - struct header_deleter - { - void - operator()( - http::detail::header* h) const noexcept - { - ::operator delete(h); - } - }; - - using header_ptr = std::unique_ptr< - http::detail::header, header_deleter>; - - http::header_limits hdr_limits_; capy::any_read_stream stream_; - header_ptr h_; + std::unique_ptr buf_; + head_parser hp_; decoder * dec_ = nullptr; circular_buffer in_; circular_buffer out_; @@ -188,8 +170,10 @@ class parser std::uint64_t transferred_ = 0; std::uint64_t decoded_ = 0; std::uint64_t body_limit_ = 0; + std::uint64_t payload_size_ = 0; std::error_code dec_err_; http::payload payload_ = http::payload::none; + bool is_req_ = true; bool head_ = false; bool started_ = false; bool got_header_ = false; diff --git a/include/boost/burl/detail/request_parser.hpp b/include/boost/burl/detail/request_parser.hpp index 65628dc..7f2b94c 100644 --- a/include/boost/burl/detail/request_parser.hpp +++ b/include/boost/burl/detail/request_parser.hpp @@ -12,8 +12,6 @@ #include -#include - #include namespace boost @@ -33,7 +31,7 @@ class request_parser request_parser( config const& cfg, capy::any_read_stream stream = {}) - : parser(cfg, http::detail::kind::request, std::move(stream)) + : parser(cfg, true, std::move(stream)) { } @@ -48,7 +46,7 @@ class request_parser parser::start(false); } - http::static_request const& + burl::request_head_base const& get() const { return get_request(); diff --git a/include/boost/burl/detail/response_parser.hpp b/include/boost/burl/detail/response_parser.hpp index ab20d51..5177a9f 100644 --- a/include/boost/burl/detail/response_parser.hpp +++ b/include/boost/burl/detail/response_parser.hpp @@ -12,8 +12,6 @@ #include -#include - #include namespace boost @@ -33,7 +31,7 @@ class response_parser response_parser( config const& cfg, capy::any_read_stream stream = {}) - : parser(cfg, http::detail::kind::response, std::move(stream)) + : parser(cfg, false, std::move(stream)) { } @@ -48,7 +46,7 @@ class response_parser parser::start(head); } - http::static_response const& + burl::response_head_base const& get() const { return get_response(); diff --git a/include/boost/burl/fields.hpp b/include/boost/burl/fields.hpp new file mode 100644 index 0000000..6910b51 --- /dev/null +++ b/include/boost/burl/fields.hpp @@ -0,0 +1,353 @@ +// +// Copyright (c) 2021 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +#ifndef BOOST_BURL_FIELDS_HPP +#define BOOST_BURL_FIELDS_HPP + +#include +#include + +#include + +namespace boost +{ +namespace burl +{ + +/** A dynamic container of HTTP fields. + + This container owns a sequence of HTTP fields in + a single allocation which grows as needed. + + A default-constructed container refers to a static + two-byte buffer holding the final "\r\n" and does + not allocate. + + @see @ref fields_base for the shared observers, + lookup functions, and modifiers. +*/ +class fields : public fields_base +{ +public: + //-------------------------------------------- + // + // Special members + // + //-------------------------------------------- + + /** Constructor. + + The container is empty and does not + allocate; @ref buffer returns "\r\n". + + @par Postconditions + @code + this->capacity_in_bytes() == 0 + @endcode + + @par Complexity + Constant. + */ + BOOST_BURL_DECL + fields() noexcept; + + /** Destructor. + + Any allocated memory is released. All + iterators, references, and views obtained + from the container are invalidated. + + @par Complexity + Constant. + */ + BOOST_BURL_DECL + ~fields(); + + /** Constructor. + + The container acquires ownership of the + contents of `other`, which is left in its + default-constructed state. Views obtained + from `other` remain valid; its iterators + are invalidated. + + @par Postconditions + @code + other.capacity_in_bytes() == 0 + @endcode + + @par Complexity + Constant. + */ + BOOST_BURL_DECL + fields(fields&& other) noexcept; + + /** Constructor. + + The contents of `other` are copied into + an exact-fit allocation. No allocation + occurs when `other` is empty. + + @par Complexity + Linear in `other.buffer().size()`. + + @par Exception Safety + Strong guarantee. + Calls to allocate may throw. + */ + BOOST_BURL_DECL + fields(fields const& other); + + /** Constructor. + + The field section of `other` is copied into + an exact-fit allocation. When `other` is a + message header, only its fields are copied; + the start line is discarded. No allocation + occurs when `other` is empty. + + This constructor is `explicit`: converting a + non-owning view into an owning container + allocates, and discarding a header's start + line should be deliberate. + + @par Complexity + Linear in `other.buffer().size()`. + + @par Exception Safety + Strong guarantee. + Calls to allocate may throw. + + @param other The fields to copy. + */ + BOOST_BURL_DECL + explicit + fields(fields_base const& other); + + /** Constructor. + + The container is constructed with a copy + of the fields in `init`, equivalent to + calling @ref append for each element in + order. Duplicate names are allowed and + their relative order is preserved. + + The storage is allocated exactly once. + No allocation occurs when `init` is + empty. + + @par Example + @code + fields f = { + { http::field::host, "example.com" }, + { "X-Request-Id", "42" }, + }; + @endcode + + @par Complexity + Linear in the total size of the names + and values in `init`. + + @par Exception Safety + Strong guarantee. + Calls to allocate may throw. + Exception thrown if a size limit would be + exceeded. + + @throw std::length_error + A size limit would be exceeded; see + @ref max_name_size, @ref max_value_size, + and @ref max_buffer_size. + + @param init The fields to insert. + */ + BOOST_BURL_DECL + fields(std::initializer_list init); + + /** Assignment. + + The container acquires ownership of the + contents of `other`, which is left in its + default-constructed state. The previous + contents are destroyed. + + @par Complexity + Constant. + */ + BOOST_BURL_DECL + fields& + operator=(fields&& other) noexcept; + + /** Assignment. + + The contents are replaced with a copy of + `other`. The existing allocation is reused + when it is large enough. + + @par Complexity + Linear in `other.buffer().size()`. + + @par Exception Safety + Strong guarantee. + Calls to allocate may throw. + */ + BOOST_BURL_DECL + fields& + operator=(fields const& other); + + /** Assignment. + + The contents are replaced with a copy of the + field section of `other`. When `other` is a + message header, only its fields are copied; + the start line is discarded. + + @par Complexity + Linear in `other.buffer().size()`. + + @par Exception Safety + Strong guarantee. + Calls to allocate may throw. + + @param other The fields to copy. + */ + BOOST_BURL_DECL + fields& + operator=(fields_base const& other); + + /** Swap the contents. + + The contents of the two containers are + exchanged. No allocation occurs and no + bytes are copied. + + Views obtained from either container remain + valid; they follow the contents into the + other container. Iterators are invalidated: + an iterator stays bound to the container it + was obtained from, which now holds different + fields. + + If `this == &other`, this function call has + no effect. + + @par Complexity + Constant. + + @par Exception Safety + No-throw guarantee. + + @param other The container to swap with. + */ + BOOST_BURL_DECL + void + swap(fields& other) noexcept; + + /** Swap the contents. + + The contents of the two containers are + exchanged. No allocation occurs and no + bytes are copied. + + If `&v0 == &v1`, this function call has no + effect. + + @par Effects + @code + v0.swap(v1); + @endcode + + @par Complexity + Constant. + + @par Exception Safety + No-throw guarantee. + + @param v0 The first container to swap. + + @param v1 The second container to swap. + + @see @ref fields::swap + */ + friend + void + swap( + fields& v0, + fields& v1) noexcept + { + v0.swap(v1); + } + + //-------------------------------------------- + // + // Capacity + // + //-------------------------------------------- + + /** Reserve storage. + + This function ensures that @ref buffer + can grow to `bytes` bytes, and the + number of fields to `count`, without + a reallocation. Has no effect if the + current capacity is already sufficient. + + All references and views are invalidated + when a reallocation occurs. + + @par Exception Safety + Strong guarantee. + Calls to allocate may throw. + Exception thrown if a size limit would be + exceeded. + + @throw std::length_error + `bytes` exceeds @ref max_buffer_size, or the + table for `count` fields does not fit + alongside `bytes` within @ref max_buffer_size. + + @param bytes The serialized size in bytes. + + @param count The number of fields. + */ + BOOST_BURL_DECL + void + reserve( + std::size_t bytes, + std::size_t count); + + /** Remove excess capacity. + + An empty container releases its + allocation entirely. + + All references and views are invalidated + when a reallocation occurs. + + @par Exception Safety + Strong guarantee. + Calls to allocate may throw. + */ + BOOST_BURL_DECL + void + shrink_to_fit(); + +private: + bool + static_() const noexcept override + { + return false; + } +}; + +} // namespace burl +} // namespace boost + +#endif diff --git a/include/boost/burl/fields_base.hpp b/include/boost/burl/fields_base.hpp new file mode 100644 index 0000000..6671817 --- /dev/null +++ b/include/boost/burl/fields_base.hpp @@ -0,0 +1,1766 @@ +// +// Copyright (c) 2021 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +#ifndef BOOST_BURL_FIELDS_BASE_HPP +#define BOOST_BURL_FIELDS_BASE_HPP + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace boost +{ +namespace burl +{ + +/** An order-preserving container of HTTP fields. + + This is the common base for every message container in + the library. It provides read access and modifiers over + a serialized field section kept in a single buffer, + preceded by a prefix region and followed by free space + and a lookup table which grows downwards from the end: + + @code + +----------+------------+--------+------+-----------------------------+ + | headroom | start line | fields | free | entry[count-1] ... entry[0] | + +----------+------------+--------+------+-----------------------------+ + ^ ^ ^ ^ ^ + | | buf buf + size buf + cap + | buf - prefix_size + buf - prefix_cap + @endcode + + The prefix holds the start line of message headers, + flush against `buf` so the whole header is one + contiguous run of bytes; the headroom lets the start + line change size without moving the fields. For + @ref fields the prefix is empty. The field section + always ends with an empty line, so @ref buffer can be + written to the wire directly. + + Where the storage comes from, whether it can grow, and + the object lifetime are determined by the derived + class: @ref fields, @ref request_head, and + @ref response_head own a dynamic allocation which + grows as needed, while @ref static_fields, + @ref static_request_head, @ref static_response_head, + and the headers produced by @ref head_parser refer to + an externally provided fixed buffer which cannot grow. + + Fields are kept in insertion order; duplicate names + are allowed and their relative order is preserved. + Lookups by name are case-insensitive. + + Names and values are stored verbatim: member + functions perform no validation, folding, or + trimming. The caller is responsible for ensuring + that arguments do not contain octets which would + corrupt the serialized form (such as CR or LF). + + @par Invalidation + Every member function which modifies the container + invalidates all subranges, references, and buffers + previously obtained from it. Iterators survive + reallocation: they are invalidated only at and + after the position where a field is inserted or + erased. Arguments which are views into the + container itself remain valid to pass to any + modifier. + + @par Thread safety + Distinct objects: safe. Shared objects: only + const member functions may be called concurrently. +*/ +class fields_base +{ +protected: + struct entry + { + std::uint32_t of; + std::uint16_t id; + std::uint16_t nn; + std::uint16_t ws; + std::uint16_t vn; + }; + +public: + //-------------------------------------------- + // + // Types + // + //-------------------------------------------- + + /** A view to an HTTP field. + + Objects of this type are returned when + dereferencing an iterator. The views are + invalidated when the underlying container + is modified. + + They are also formed implicitly from the + elements of an initializer list when + constructing a @ref fields container; the + field name may be given as a string or as + a field constant: + + @code + fields f = { + { http::field::host, "example.com" }, + { "X-Request-Id", "42" }, + }; + @endcode + + The caller is responsible for ensuring + that the lifetime of the viewed characters + extends until the view is no longer + referenced. + */ + struct field_view + { + /// A view to the field name, as stored + std::string_view name; + + /// A view to the field value + std::string_view value; + + /// The field name constant + http::field id; + + /** Constructor. + + @param name_ The field name. + + @param value_ The field value. + */ + field_view( + std::string_view name_, + std::string_view value_) noexcept + : name(name_) + , value(value_) + , id(unknown_field) + { + } + + /** Constructor. + + @param id_ The field name constant. + Must not be the zero constant. + + @param value_ The field value. + */ + field_view( + http::field id_, + std::string_view value_) noexcept + : name(http::to_string(id_)) + , value(value_) + , id(id_) + { + BOOST_ASSERT(id_ != unknown_field); + } + + private: + friend class fields_base; + + field_view( + std::string_view name_, + std::string_view value_, + http::field id_) noexcept + : name(name_) + , value(value_) + , id(id_) + { + } + }; + + /** A random-access iterator to HTTP fields. + + Dereferencing returns a @ref field_view by + value; the views it holds point into the + container. + + The iterator is invalidated when the + underlying container is modified. + */ + class iterator + { + fields_base const* f_ = nullptr; + std::uint32_t i_ = 0; + + friend class fields_base; + + iterator( + fields_base const* f, + std::uint32_t i) noexcept + : f_(f) + , i_(i) + { + } + + public: + using value_type = fields_base::field_view; + using reference = fields_base::field_view; + using difference_type = std::ptrdiff_t; + using pointer = void; + using iterator_category = std::input_iterator_tag; + using iterator_concept = std::random_access_iterator_tag; + + /// Proxy returned by `operator->` + struct arrow + { + fields_base::field_view ref; + + fields_base::field_view const* + operator->() const noexcept + { + return &ref; + } + }; + + iterator() = default; + + reference + operator*() const noexcept + { + return f_->ref_(i_); + } + + arrow + operator->() const noexcept + { + return arrow{ f_->ref_(i_) }; + } + + reference + operator[](difference_type n) const noexcept + { + return f_->ref_(i_ + static_cast(n)); + } + + iterator& + operator++() noexcept + { + ++i_; + return *this; + } + + iterator + operator++(int) noexcept + { + auto it = *this; + ++i_; + return it; + } + + iterator& + operator--() noexcept + { + --i_; + return *this; + } + + iterator + operator--(int) noexcept + { + auto it = *this; + --i_; + return it; + } + + iterator& + operator+=(difference_type n) noexcept + { + i_ += static_cast(n); + return *this; + } + + iterator& + operator-=(difference_type n) noexcept + { + i_ -= static_cast(n); + return *this; + } + + friend iterator + operator+( + iterator it, + difference_type n) noexcept + { + it += n; + return it; + } + + friend iterator + operator+( + difference_type n, + iterator it) noexcept + { + it += n; + return it; + } + + friend iterator + operator-( + iterator it, + difference_type n) noexcept + { + it -= n; + return it; + } + + friend difference_type + operator-( + iterator const& a, + iterator const& b) noexcept + { + return static_cast(a.i_) - + static_cast(b.i_); + } + + bool + operator==(iterator const& other) const noexcept = default; + + std::strong_ordering + operator<=>(iterator const& other) const noexcept + { + BOOST_ASSERT(f_ == other.f_); + return i_ <=> other.i_; + } + }; + + /// @copydoc iterator + using const_iterator = iterator; + + /** A reverse random-access iterator to HTTP fields. + */ + using reverse_iterator = std::reverse_iterator; + + /// @copydoc reverse_iterator + using const_reverse_iterator = reverse_iterator; + + /** A forward range of values for all matching fields. + + Objects of this type are returned by + @ref find_all. Dereferencing an iterator + returns the field value as a + `std::string_view`. + + Advancing skips fields whose name differs; + when the fields are known, only field ids + are compared and the field names are never + touched. + + The range is invalidated when the + underlying container is modified. + */ + class subrange + { + fields_base const* f_ = nullptr; + std::uint32_t i_ = 0; + + friend class fields_base; + + subrange( + fields_base const* f, + std::uint32_t i) noexcept + : f_(f) + , i_(i) + { + } + + public: + /// A forward iterator over the values + class iterator + { + fields_base const* f_ = nullptr; + std::uint32_t i_ = 0; + + friend class subrange; + + iterator( + fields_base const* f, + std::uint32_t i) noexcept + : f_(f) + , i_(i) + { + } + + public: + using value_type = std::string_view; + using reference = std::string_view; + using difference_type = std::ptrdiff_t; + using pointer = void; + using iterator_category = std::input_iterator_tag; + using iterator_concept = std::forward_iterator_tag; + + iterator() = default; + + std::string_view + operator*() const noexcept + { + return f_->ref_(i_).value; + } + + BOOST_BURL_DECL + iterator& + operator++() noexcept; + + iterator + operator++(int) noexcept + { + auto it = *this; + ++*this; + return it; + } + + bool + operator==(iterator const& other) const noexcept = default; + }; + + /// @copydoc iterator + using const_iterator = iterator; + + /// The type of each element + using value_type = std::string_view; + + /** Constructor. + + Default-constructed subranges are empty. + */ + subrange() = default; + + /// Return an iterator to the beginning + iterator + begin() const noexcept + { + return iterator(f_, i_); + } + + /// Return an iterator to the end + iterator + end() const noexcept + { + return iterator(f_, f_ ? f_->count_ : 0); + } + + /// Return true if the range is empty + bool + empty() const noexcept + { + return f_ == nullptr || i_ == f_->count_; + } + }; + + /** The type of each element. + + This is an alias for @ref field_view: + elements are views, and copying one does + not copy the underlying characters. + */ + using value_type = field_view; + + /// The type used to represent sizes + using size_type = std::size_t; + + /// The type used to represent iterator distances + using difference_type = std::ptrdiff_t; + + /// Maximum allowed name size + static constexpr std::size_t max_name_size = 65535; + + /// Maximum allowed value size + static constexpr std::size_t max_value_size = 65535; + + /// Maximum allowed size of the storage. + static constexpr std::size_t max_buffer_size = 0x7FFFFFFF; + + /// Maximum allowed size of the start line + static constexpr std::size_t max_start_line_size = 65535; + + /// The alignment an external buffer must end on. + static constexpr std::size_t buffer_alignment = alignof(entry); + + /** Return the buffer space needed for a field table. + + Every field consumes a fixed amount of + space at the end of the buffer for its + lookup table entry, in addition to its + serialized text. Callers sizing an + external buffer for @ref head_parser must + reserve this much space for `count` fields. + + @param count The number of fields. + */ + static + constexpr + std::size_t + table_space(std::size_t count) noexcept + { + return sizeof(entry) * count; + } + + /** Return the size of the storage space, in bytes. + + @par Complexity + Constant. + */ + std::size_t + capacity_in_bytes() const noexcept + { + if(! default_()) + return std::size_t(prefix_cap_) + cap_; + return 0; + } + + //-------------------------------------------- + // + // Observers + // + //-------------------------------------------- + + /** Return a string view representing the field section. + + The returned view references every field + line followed by the final empty line: + + @code + "Host: example.com\r\nAccept: text/html\r\n\r\n" + @endcode + + Header containers provide their own + @ref message_head_base::buffer which additionally + includes the start line. + + The view is invalidated when the container + is modified. + + @par Complexity + Constant. + */ + std::string_view + buffer() const noexcept + { + return { buf_, size_ }; + } + + /** Format the fields to an output stream. + + Each field is written as `name: value` + followed by a newline: + + @code + "Host: example.com\nAccept: text/html\n" + @endcode + + This form is for diagnostics; the wire + form is available from @ref buffer. + + @par Complexity + Linear in `f.buffer().size()`. + + @par Exception Safety + Basic guarantee. + + @return A reference to the output stream. + + @param os The output stream to write to. + + @param f The container to write. + */ + friend + BOOST_BURL_DECL + std::ostream& + operator<<( + std::ostream& os, + fields_base const& f); + + /** Return the number of fields in the container. + + @par Complexity + Constant. + */ + std::size_t + size() const noexcept + { + return count_; + } + + /** Return true if the container has no fields. + + @par Complexity + Constant. + */ + bool + empty() const noexcept + { + return count_ == 0; + } + + /** Return an iterator to the beginning. + */ + iterator + begin() const noexcept + { + return iterator(this, 0); + } + + /** Return an iterator to the end. + */ + iterator + end() const noexcept + { + return iterator(this, count_); + } + + /** Return a reverse iterator to the beginning. + */ + reverse_iterator + rbegin() const noexcept + { + return reverse_iterator(end()); + } + + /** Return a reverse iterator to the end. + */ + reverse_iterator + rend() const noexcept + { + return reverse_iterator(begin()); + } + + //-------------------------------------------- + // + // Lookup + // + //-------------------------------------------- + + /** Return an iterator to the matching element if it exists. + + If more than one field with the specified + name exists, the first field defined by + insertion order is returned. + + @par Complexity + Linear in `this->size()`. + + @return An iterator to the field, or + @ref end if no field matches. + + @param id The field name constant. + */ + iterator + find(http::field id) const noexcept + { + return iterator(this, find_(0, id)); + } + + /** Return an iterator to the matching element if it exists. + + If more than one field with the specified + name exists, the first field defined by + insertion order is returned. The comparison + is case-insensitive. + + If `name` refers to a known field, it is + faster to call @ref find with a field id + instead of a string. + + @par Complexity + Linear in `this->size()`. + + @return An iterator to the field, or + @ref end if no field matches. + + @param name The field name. + */ + iterator + find(std::string_view name) const noexcept + { + return iterator(this, find_(0, name)); + } + + /** Return an iterator to the matching element if it exists. + + The search starts at `from` and runs + forwards. + + @par Complexity + Linear in `this->size()`. + + @return An iterator to the field, or + @ref end if no field matches. + + @param from The position to begin the + search from. This can be `end()`. + + @param id The field name constant. + */ + iterator + find( + iterator from, + http::field id) const noexcept + { + BOOST_ASSERT(from.f_ == this); + return iterator(this, find_(from.i_, id)); + } + + /** Return an iterator to the matching element if it exists. + + The search starts at `from` and runs + forwards. The comparison is + case-insensitive. + + If `name` refers to a known field, it is + faster to call @ref find with a field id + instead of a string. + + @par Complexity + Linear in `this->size()`. + + @return An iterator to the field, or + @ref end if no field matches. + + @param from The position to begin the + search from. This can be `end()`. + + @param name The field name. + */ + iterator + find( + iterator from, + std::string_view name) const noexcept + { + BOOST_ASSERT(from.f_ == this); + return iterator(this, find_(from.i_, name)); + } + + /** Return an iterator to the last matching element if it exists. + + The search runs backwards, beginning with + the field immediately preceding `before`. + + @par Complexity + Linear in `this->size()`. + + @return An iterator to the field, or + @ref end if no field matches. + + @param before One past the position to + begin the search from. This can be `end()`. + + @param id The field name constant. + */ + iterator + find_last( + iterator before, + http::field id) const noexcept + { + BOOST_ASSERT(before.f_ == this); + return iterator(this, find_last_(before.i_, id)); + } + + /** Return an iterator to the last matching element if it exists. + + The search runs backwards, beginning with + the field immediately preceding `before`. + The comparison is case-insensitive. + + If `name` refers to a known field, it is + faster to call @ref find_last with a field + id instead of a string. + + @par Complexity + Linear in `this->size()`. + + @return An iterator to the field, or + @ref end if no field matches. + + @param before One past the position to + begin the search from. This can be `end()`. + + @param name The field name. + */ + iterator + find_last( + iterator before, + std::string_view name) const noexcept + { + BOOST_ASSERT(before.f_ == this); + return iterator(this, find_last_(before.i_, name)); + } + + /** Return a forward range containing values for all matching fields. + + @par Complexity + Constant; advancing the iterators of the + returned range is linear in `this->size()`. + + @param id The field name constant. + */ + subrange + find_all(http::field id) const noexcept + { + return subrange(this, find_(0, id)); + } + + /** Return a forward range containing values for all matching fields. + + If `name` refers to a known field, it is + faster to call @ref find_all with a field + id instead of a string. + + @par Complexity + Constant; advancing the iterators of the + returned range is linear in `this->size()`. + + @param name The field name. + */ + subrange + find_all(std::string_view name) const noexcept + { + return subrange(this, find_(0, name)); + } + + /** Return the value of a field or a default if missing. + + If more than one field with the specified + name exists, the value of the first field + defined by insertion order is returned. + + @par Complexity + Linear in `this->size()`. + + @param id The field name constant. + + @param s The value to be returned if the + field does not exist. + */ + std::string_view + value_or( + http::field id, + std::string_view s) const noexcept + { + auto const i = find_(0, id); + if(i != count_) + return ref_(i).value; + return s; + } + + /** Return the value of a field or a default if missing. + + If more than one field with the specified + name exists, the value of the first field + defined by insertion order is returned. + + If `name` refers to a known field, it is + faster to call @ref value_or with a field + id instead of a string. + + @par Complexity + Linear in `this->size()`. + + @param name The field name. + + @param s The value to be returned if the + field does not exist. + */ + std::string_view + value_or( + std::string_view name, + std::string_view s) const noexcept + { + auto const i = find_(0, name); + if(i != count_) + return ref_(i).value; + return s; + } + + /** Return the value of a field, or throw an exception. + + If more than one field with the specified + name exists, the value of the first field + defined by insertion order is returned. + + @par Complexity + Linear in `this->size()`. + + @par Exception Safety + Strong guarantee. + + @throw std::out_of_range + Field is not found. + + @param id The field name constant. + */ + BOOST_BURL_DECL + std::string_view + at(http::field id) const; + + /** Return the value of a field, or throw an exception. + + If more than one field with the specified + name exists, the value of the first field + defined by insertion order is returned. + + If `name` refers to a known field, it is + faster to call @ref at with a field id + instead of a string. + + @par Complexity + Linear in `this->size()`. + + @par Exception Safety + Strong guarantee. + + @throw std::out_of_range + Field is not found. + + @param name The field name. + */ + BOOST_BURL_DECL + std::string_view + at(std::string_view name) const; + + /** Return the number of matching fields. + + @par Complexity + Linear in `this->size()`. + + @param id The field name constant. + */ + BOOST_BURL_DECL + std::size_t + count(http::field id) const noexcept; + + /** Return the number of matching fields. + + If `name` refers to a known field, it is + faster to call @ref count with a field id + instead of a string. + + @par Complexity + Linear in `this->size()`. + + @param name The field name. + */ + BOOST_BURL_DECL + std::size_t + count(std::string_view name) const noexcept; + + /** Return true if a field exists. + + @par Complexity + Linear in `this->size()`. + + @param id The field name constant. + */ + bool + contains(http::field id) const noexcept + { + return find_(0, id) != count_; + } + + /** Return true if a field exists. + + If `name` refers to a known field, it is + faster to call @ref contains with a field + id instead of a string. + + @par Complexity + Linear in `this->size()`. + + @param name The field name. + */ + bool + contains(std::string_view name) const noexcept + { + return find_(0, name) != count_; + } + + //-------------------------------------------- + // + // Modifiers + // + //-------------------------------------------- + + /** Append a field. + + This function appends a new field using + the canonical name of `id`. Existing + fields with the same name are not changed. + + The value is stored verbatim. + + All references and views are invalidated. + No iterators are invalidated. + + @par Example + @code + f.append( http::field::user_agent, "Boost" ); + @endcode + + @par Complexity + Linear in `to_string( id ).size() + value.size()`, + amortized. + + @par Exception Safety + Strong guarantee. + Exception thrown if the storage cannot + grow, or if a size limit would be + exceeded. + + @throw std::length_error + The storage cannot accommodate the field; + see @ref max_name_size, @ref max_value_size, + and @ref max_buffer_size. + + @param id The field name constant. Must + not be the zero constant. + + @param value The field value. + */ + void + append( + http::field id, + std::string_view value) + { + BOOST_ASSERT(id != unknown_field); + insert_( + count_, + static_cast(id), + http::to_string(id), + value); + } + + /** Append a field. + + This function appends a new field. + Existing fields with the same name are not + changed. + + The name and value are stored verbatim. + When `name` equals a known field name, the + corresponding field constant is stored + with it, and subsequent lookups by id + succeed regardless of how the field was + added. + + All references and views are invalidated. + No iterators are invalidated. + + @par Example + @code + f.append( "User-Agent", "Boost" ); + @endcode + + @par Complexity + Linear in `name.size() + value.size()`, + amortized. + + @par Exception Safety + Strong guarantee. + Exception thrown if the storage cannot + grow, or if a size limit would be + exceeded. + + @throw std::length_error + The storage cannot accommodate the field; + see @ref max_name_size, @ref max_value_size, + and @ref max_buffer_size. + + @param name The field name. + + @param value The field value. + */ + void + append( + std::string_view name, + std::string_view value) + { + insert_(count_, resolve_(name), name, value); + } + + /** Append fields. + + This function appends the fields in `init` + in order, as if by calling @ref append for + each element. Existing fields with the same + names are not changed. + + The names and values are stored verbatim. + + All references and views are invalidated. + No iterators are invalidated. + + @par Example + @code + f.append({ + { http::field::host, "example.com" }, + { "User-Agent", "Boost" } }); + @endcode + + @par Complexity + Linear in the total size of the names and + values in `init`. + + @par Exception Safety + Strong guarantee. + Exception thrown if the storage cannot + grow, or if a size limit would be + exceeded. + + @throw std::length_error + The storage cannot accommodate the fields; + see @ref max_name_size, @ref max_value_size, + and @ref max_buffer_size. + + @param init The fields to append. + */ + BOOST_BURL_DECL + void + append(std::initializer_list init); + + /** Insert a field. + + If a matching field with the same name + exists, it is not replaced. Instead, an + additional field is inserted using the + canonical name of `id`. + + The value is stored verbatim. + + All references and views are invalidated, as + are iterators at or after `before`. + + @par Example + @code + f.insert( f.begin(), http::field::user_agent, "Boost" ); + @endcode + + @par Complexity + Linear in `to_string( id ).size() + value.size()` + plus the size of the fields after `before`. + + @par Exception Safety + Strong guarantee. + Exception thrown if the storage cannot + grow, or if a size limit would be + exceeded. + + @throw std::length_error + The storage cannot accommodate the field; + see @ref max_name_size, @ref max_value_size, + and @ref max_buffer_size. + + @return An iterator to the newly inserted field. + + @param before Position to insert before. + + @param id The field name constant. Must + not be the zero constant. + + @param value The field value. + */ + iterator + insert( + iterator before, + http::field id, + std::string_view value) + { + BOOST_ASSERT(before.f_ == this); + BOOST_ASSERT(id != unknown_field); + return iterator( + this, + insert_( + before.i_, + static_cast(id), + http::to_string(id), + value)); + } + + /** Insert a field. + + If a matching field with the same name + exists, it is not replaced. Instead, an + additional field with the same name is + inserted. + + The name and value are stored verbatim. + When `name` equals a known field name, the + corresponding field constant is stored + with it, and subsequent lookups by id + succeed regardless of how the field was + added. + + All references and views are invalidated, as + are iterators at or after `before`. + + @par Example + @code + f.insert( f.begin(), "User-Agent", "Boost" ); + @endcode + + @par Complexity + Linear in `name.size() + value.size()` + plus the size of the fields after `before`. + + @par Exception Safety + Strong guarantee. + Exception thrown if the storage cannot + grow, or if a size limit would be + exceeded. + + @throw std::length_error + The storage cannot accommodate the field; + see @ref max_name_size, @ref max_value_size, + and @ref max_buffer_size. + + @return An iterator to the newly inserted field. + + @param before Position to insert before. + + @param name The field name. + + @param value The field value. + */ + iterator + insert( + iterator before, + std::string_view name, + std::string_view value) + { + BOOST_ASSERT(before.f_ == this); + return iterator(this, insert_(before.i_, resolve_(name), name, value)); + } + + /** Set a field value. + + Uses the given value to overwrite the + current one in the field pointed to by + the iterator. No other fields are + affected. + + The value is stored verbatim. + + All references and views are invalidated. + No iterators are invalidated. + + @par Complexity + Linear in `this->buffer().size()`. + + @par Exception Safety + Strong guarantee. + Exception thrown if the storage cannot + grow, or if a size limit would be + exceeded. + + @throw std::length_error + The storage cannot accommodate the value; + see @ref max_value_size and + @ref max_buffer_size. + + @param it The iterator to the field. Must + be dereferenceable. + + @param value The field value. + */ + void + set( + iterator it, + std::string_view value) + { + BOOST_ASSERT(it.f_ == this); + BOOST_ASSERT(it.i_ < count_); + replace_value_(it.i_, value); + } + + /** Set a field value, removing duplicates. + + The container is modified to contain + exactly one field with the specified id + set to the given value: if a matching + field exists, its value is replaced in + place and the remaining duplicates are + removed; otherwise the field is appended. + + The value is stored verbatim. + + All references and views are invalidated. + Iterators at or after the first removed + duplicate are invalidated; if there are no + duplicates, no iterators are invalidated. + + @par Postconditions + @code + this->count( id ) == 1 && this->at( id ) == value + @endcode + + @par Complexity + Linear in `this->buffer().size()`. + + @par Exception Safety + Strong guarantee. + Exception thrown if the storage cannot + grow, or if a size limit would be + exceeded. + + @throw std::length_error + The storage cannot accommodate the value; + see @ref max_value_size and + @ref max_buffer_size. + + @param id The field name constant. Must + not be the zero constant. + + @param value The field value. + */ + BOOST_BURL_DECL + void + set( + http::field id, + std::string_view value); + + /** Set a field value, removing duplicates. + + The container is modified to contain + exactly one field with the specified name + set to the given value: if a matching + field exists, its value is replaced in + place and the remaining duplicates are + removed; otherwise the field is appended. + The comparison is case-insensitive. + + The name and value are stored verbatim. + + All references and views are invalidated. + Iterators at or after the first removed + duplicate are invalidated; if there are no + duplicates, no iterators are invalidated. + + @par Postconditions + @code + this->count( name ) == 1 && this->at( name ) == value + @endcode + + @par Complexity + Linear in `this->buffer().size()`. + + @par Exception Safety + Strong guarantee. + Exception thrown if the storage cannot + grow, or if a size limit would be + exceeded. + + @throw std::length_error + The storage cannot accommodate the field; + see @ref max_name_size, @ref max_value_size, + and @ref max_buffer_size. + + @param name The field name. + + @param value The field value. + */ + BOOST_BURL_DECL + void + set( + std::string_view name, + std::string_view value); + + /** Erase all matching fields. + + This removes all fields whose name + constant is equal to `id`. + + If any fields are erased, all references and + views are invalidated, as are iterators at or + after the first erased field. Otherwise, + nothing is invalidated. + + @par Complexity + Linear in `this->buffer().size()`. + + @return The number of fields erased. + + @param id The field name constant. + */ + BOOST_BURL_DECL + std::size_t + erase(http::field id) noexcept; + + /** Erase all matching fields. + + This removes all fields with a matching + name, using a case-insensitive comparison. + + If any fields are erased, all references and + views are invalidated, as are iterators at or + after the first erased field. Otherwise, + nothing is invalidated. + + If `name` refers to a known field, it is + faster to call @ref erase with a field id + instead of a string. + + @par Complexity + Linear in `this->buffer().size()`. + + @return The number of fields erased. + + @param name The field name. + */ + BOOST_BURL_DECL + std::size_t + erase(std::string_view name) noexcept; + + /** Erase a field. + + This removes the field pointed to by + `pos`. + + All references and views are invalidated, as + are iterators at or after `pos`. + + @par Complexity + Linear in the size of the fields after + `pos`. + + @return An iterator to one past the + removed element. + + @param pos The iterator to the element to + erase. Must be dereferenceable. + */ + iterator + erase(iterator pos) noexcept + { + BOOST_ASSERT(pos.f_ == this); + BOOST_ASSERT(pos.i_ < count_); + erase_at_(pos.i_); + return iterator(this, pos.i_); + } + + /** Erase all fields. + + The start line of header containers is + preserved. The capacity is unchanged. + + All iterators, references, and views are + invalidated. + + @par Postconditions + @code + this->size() == 0 + @endcode + + @par Complexity + Constant. + */ + BOOST_BURL_DECL + void + clear() noexcept; + +protected: + BOOST_BURL_DECL + fields_base() noexcept; + + fields_base( + char* buf, + std::uint32_t cap, + std::uint32_t size, + std::uint32_t count, + std::uint16_t prefix_cap = 0, + std::uint16_t prefix_size = 0) noexcept + : buf_(buf) + , cap_(cap) + , size_(size) + , count_(count) + , prefix_cap_(prefix_cap) + , prefix_size_(prefix_size) + { + } + + fields_base(fields_base const&) = default; + + BOOST_BURL_DECL + fields_base& + operator=(fields_base const& other); + + ~fields_base() = default; + + BOOST_BURL_DECL + void + swap_(fields_base& other) noexcept; + + virtual + bool + static_() const noexcept + { + return true; + } + + BOOST_BURL_DECL + virtual + char* + default_buf_() const noexcept; + + virtual + void + on_clear_() noexcept + { + } + + virtual + void + on_special_(http::field) noexcept + { + } + + char* + base_() const noexcept + { + return buf_ - prefix_size_; + } + + void + detach_() + { + if(default_()) + realloc_(size_ + table_space(count_), prefix_cap_); + } + + BOOST_BURL_DECL + void + release_() noexcept; + + BOOST_BURL_DECL + void + init_static_( + char* storage, + std::size_t n); + + BOOST_BURL_DECL + char* + splice_start_( + std::uint16_t pos, + std::uint16_t on, + std::size_t nn, + std::string_view* s0 = nullptr, + std::string_view* s1 = nullptr); + + BOOST_BURL_DECL + void + assign_( + fields_base const& other, + std::uint16_t prefix_size); + + BOOST_BURL_DECL + void + reserve_( + std::size_t bytes, + std::size_t count, + std::size_t prefix_cap); + + BOOST_BURL_DECL + void + shrink_to_fit_(); + + entry& + ent_(std::uint32_t i) const noexcept + { + return ent_(tab_(), i); + } + + BOOST_BURL_DECL + static std::uint16_t + resolve_(std::string_view name) noexcept; + + char* buf_; + std::uint32_t cap_; + std::uint32_t size_; + std::uint32_t count_; + std::uint16_t prefix_cap_; + std::uint16_t prefix_size_; + +private: + struct alloc_; + + constexpr static http::field unknown_field = + static_cast(0); + + bool + default_() const noexcept + { + return buf_ == default_buf_(); + } + + bool + owns_() const noexcept + { + return ! default_() && ! static_(); + } + + entry* + tab_() const noexcept + { + return reinterpret_cast(buf_ + cap_); + } + + static entry& + ent_( + entry* t, + std::uint32_t i) noexcept + { + return *(t - 1 - static_cast(i)); + } + + field_view + ref_(std::uint32_t i) const noexcept + { + auto const& e = ent_(i); + return + { + { buf_ + e.of, e.nn }, + { buf_ + e.of + e.nn + e.ws, e.vn }, + static_cast(e.id) + }; + } + + std::uint32_t + line_len_(std::uint32_t i) const noexcept + { + auto const of = ent_(i).of; + if(i + 1 == count_) + return size_ - 2 - of; + return ent_(i + 1).of - of; + } + + std::size_t + off_in_(std::string_view s) const noexcept + { + if(s.data() >= base_() && s.data() < buf_ + size_) + return static_cast(s.data() - base_()); + return std::size_t(-1); + } + + BOOST_BURL_DECL + void + realloc_( + std::size_t need, + std::uint16_t prefix_cap); + + std::size_t + grow_size_(std::size_t need) const; + + char* + realloc_splice_( + std::size_t need, + std::size_t text_pos, + std::size_t on, + std::size_t nn, + std::uint16_t new_prefix_size, + std::uint16_t new_prefix_cap, + std::string_view* s0, + std::string_view* s1); + + void + grow_gap_( + std::size_t need, + std::size_t pos, + std::size_t len, + std::string_view* s0, + std::string_view* s1 = nullptr); + + static void + move_chars_( + char* dest, + char const* src, + std::size_t n, + std::string_view* s0, + std::string_view* s1 = nullptr) noexcept; + + BOOST_BURL_DECL + std::uint32_t + find_( + std::uint32_t from, + http::field id) const noexcept; + + BOOST_BURL_DECL + std::uint32_t + find_( + std::uint32_t from, + std::string_view name) const noexcept; + + BOOST_BURL_DECL + std::uint32_t + find_last_( + std::uint32_t before, + http::field id) const noexcept; + + BOOST_BURL_DECL + std::uint32_t + find_last_( + std::uint32_t before, + std::string_view name) const noexcept; + + void + notify_(std::uint16_t id) noexcept; + + BOOST_BURL_DECL + std::uint32_t + insert_( + std::uint32_t i, + std::uint16_t id, + std::string_view name, + std::string_view value); + + BOOST_BURL_DECL + void + replace_value_( + std::uint32_t i, + std::string_view value); + + BOOST_BURL_DECL + void + erase_at_(std::uint32_t i) noexcept; + + template + std::uint32_t + erase_all_( + std::uint32_t i, + std::uint16_t id, + Match const& match) noexcept; + + std::uint32_t + erase_all_( + std::uint32_t i, + std::uint16_t id) noexcept; + + std::uint32_t + erase_all_( + std::uint32_t i, + std::string_view name) noexcept; + + void + erase_dups_(std::uint32_t i) noexcept; +}; + +} // namespace burl +} // namespace boost + +#endif diff --git a/include/boost/burl/head_parser.hpp b/include/boost/burl/head_parser.hpp new file mode 100644 index 0000000..12f9677 --- /dev/null +++ b/include/boost/burl/head_parser.hpp @@ -0,0 +1,438 @@ +// +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +#ifndef BOOST_BURL_HEAD_PARSER_HPP +#define BOOST_BURL_HEAD_PARSER_HPP + +#include +#include +#include + +#include +#include +#include + +#include + +namespace boost +{ +namespace burl +{ + +/** Configurable limits for parsing HTTP message headers. + + Every limit is enforced by @ref head_parser::parse. +*/ +struct header_limits +{ + /** Largest allowed size of the complete header. + + Bounds the start line plus every field line + and the trailing empty line, counting all + delimiters. + */ + std::uint32_t max_size = 8 * 1024; + + /// Largest allowed number of fields. + std::uint32_t max_fields = 100; + + /// Largest allowed size of the start line. + std::uint16_t max_start_line = 4 * 1024; + + /// Largest allowed size of a single field line. + std::uint16_t max_field = 4 * 1024; +}; + +/** An in-place parser for HTTP message headers. + + This type parses a message header directly in + an externally provided buffer, leaving every + received byte in its original location. The + caller reads from the peer into the region + returned by @ref prepare, reports the count + through @ref commit, and calls @ref parse; + parsing resumes where the previous call + stopped. + + While the header is incomplete, @ref parse + sets @ref http::error::need_data. Once the + final empty line is parsed it returns success + and the header can be inspected through + @ref request_head_base or @ref response_head_base; + field names and values are views into the + buffer, exactly as received, with trailing + whitespace excluded from the values. Bytes + which follow the header in the buffer belong + to the message payload and are never touched; + @ref leftovers is the region holding those + which have arrived. + + A field line which an obs-fold continues onto + further lines is resolved where it lies: each + folding CRLF is overwritten with spaces, so the + value stays one contiguous view of the buffer. + This is the only case in which a received byte + is modified. + + The field lookup table grows down from the end + of the buffer, and @ref prepare withholds the + room it needs: space for + @ref header_limits::max_fields entries, which + @ref buffer_size accounts for when sizing a + buffer. When the header cannot complete within + the writable region, @ref parse fails with + @ref http::error::in_place_overflow. + + A parse error leaves the parser where the error + was found and stores nothing: @ref parse may be + called again, and derives the same error from + the same bytes for as long as it is asked. A + header rejected by the framing rules is + complete, and can be inspected. + + The buffer must stay valid for the lifetime of + the parser. Bytes the parser has consumed must + not be written again, as doing so would undo a + resolved obs-fold. +*/ +class head_parser +{ +public: + /** Constructor. + + Any buffer may be given: the end is aligned + down as the field lookup table requires, and + a size too small to make progress in only + causes @ref parse to fail with + @ref http::error::in_place_overflow. Use + @ref buffer_size to size the buffer for a + given set of limits. + + @param is_request True to parse a request + header, false for a response. + + @param buf The buffer which receives the + header. + + @param n The size of the buffer. + + @param limits The header size limits to + enforce while parsing. + */ + BOOST_BURL_DECL + head_parser( + bool is_request, + char* buf, + std::size_t n, + header_limits const& limits = {}) noexcept; + + /** Constructor. + + Default constructed parsers behave as if + constructed with a zero-size buffer. + */ + head_parser() noexcept + : head_parser(true, nullptr, 0) + { + } + + /** Constructor. + + The constructed parser continues parsing + the same buffer from where `other` + stopped. Afterwards `other` still + references that buffer and must not be + used, except to be destroyed or assigned + to. + + @param other The parser to move from. + */ + BOOST_BURL_DECL + head_parser(head_parser&& other) noexcept; + + /** Assignment. + + The parser continues parsing the same + buffer from where `other` stopped. + Afterwards `other` still references that + buffer and must not be used, except to be + destroyed or assigned to. + + @param other The parser to move from. + */ + BOOST_BURL_DECL + head_parser& + operator=(head_parser&& other) noexcept; + + head_parser(head_parser const&) = delete; + head_parser& operator=(head_parser const&) = delete; + + /** Re-arm the parser for a new header. + + Clears the parsed header and parse state in + place, re-arming the parser to parse a fresh + header over the same buffer. The configured + limits are retained. Equivalent to + reconstructing the parser with the original + buffer and limits, but touches no storage + outside the parser itself. + + Bytes of a following message may already + have arrived: the caller moves them to the + front of the buffer and passes their count, + which the parser adopts as if reported + through @ref commit. + + @param leftovers The number of received + bytes at the front of the buffer. Must not + exceed the size of the fresh @ref prepare + region. + */ + BOOST_BURL_DECL + void + reset(std::size_t leftovers = 0) noexcept; + + /** Return the buffer region which receives bytes. + + The region begins where the bytes received + so far end, and stops short of the space + the field lookup table requires. It may be + empty: a @ref parse which cannot proceed + without further room fails with + @ref http::error::in_place_overflow. + + @par Complexity + Constant. + */ + BOOST_BURL_DECL + capy::mutable_buffer + prepare() noexcept; + + /** Report bytes received into the buffer. + + The bytes become visible to the next call + to @ref parse. + + @par Preconditions + @code + n <= this->prepare().size() + @endcode + + @param n The number of bytes received at + the front of @ref prepare. + */ + BOOST_BURL_DECL + void + commit(std::size_t n) noexcept; + + /** Parse the received bytes. + + Parsing resumes from where the previous + call stopped and continues until the + header is complete, more data is needed, + or an error is found. + + @par Complexity + Linear in the size of @ref leftovers. + + @param ec Set to the error, if any: + @ref http::error::need_data when the + header is incomplete and @ref prepare has + room to receive the rest, + @ref http::error::in_place_overflow when + it does not, or a syntax or limit error. + */ + BOOST_BURL_DECL + void + parse(system::error_code& ec) noexcept; + + /** Return true if any bytes were received. + + This becomes true when the first byte of + the message arrives, through @ref commit or + carried by @ref reset, and false after a + reset which carries nothing. When end of + stream is seen, it distinguishes a + connection closed between messages from a + header cut short. + + @par Complexity + Constant. + */ + bool + got_some() const noexcept + { + return in_size_ != 0; + } + + /** Return the received but unconsumed bytes. + + Once @ref parse succeeds this region holds + the payload bytes which followed the header + into the buffer, beginning at the end of + @ref message_head_base::buffer; together + with @ref prepare it forms the part of the + buffer past the header. Before that it + holds the bytes of the unfinished line. + + @par Complexity + Constant. + */ + capy::mutable_buffer + leftovers() noexcept + { + auto& h = h_(); + return { + h.buf_ + h.size_, + in_size_ - + (std::size_t(h.prefix_size_) + h.size_) }; + } + + /** Return the configured header size limits. + + These are the limits supplied at + construction and enforced by @ref parse. + */ + header_limits const& + limits() const noexcept + { + return limits_; + } + + /** Return the parsed header. + + Returns the parts of the header common to + requests and responses. The header is + empty until @ref parse succeeds. + */ + class message_head_base const& + message_head() const noexcept + { + return h_(); + } + + /** Return the parsed header. + + The parser must have been constructed with + `is_request == true`. The header is empty + until @ref parse succeeds. + */ + class request_head_base const& + request_head() const noexcept + { + BOOST_ASSERT(is_req_); + return s_.req; + } + + /** Return the parsed header. + + The parser must have been constructed with + `is_request == false`. The header is empty + until @ref parse succeeds. + */ + class response_head_base const& + response_head() const noexcept + { + BOOST_ASSERT(!is_req_); + return s_.res; + } + + /** Return the buffer size to allocate for `limits`. + + Returns the size of a buffer that can hold + the largest header permitted by `limits` — + its `max_size` bytes of header text, the + space the field lookup table reserves at the + end of the buffer, and `extra` bytes beyond + the header (e.g. to buffer a pipelined + payload). The result is rounded so that, + given a suitably aligned base, the end of + the buffer satisfies the alignment the field + table requires. + + A smaller buffer may still be used; @ref + parse simply fails with + @ref http::error::in_place_overflow once + the header cannot complete within it. + + @param limits The limits the parser will + enforce. + + @param extra Bytes to reserve beyond a + maximal header. + */ + static constexpr + std::size_t + buffer_size( + header_limits const& limits, + std::size_t extra = 0) noexcept + { + constexpr auto align = alignof(message_head_base::entry); + auto const data = + (std::size_t(limits.max_size) + extra + align - 1) & + ~(align - 1); + return data + fields_base::table_space(limits.max_fields); + } + +private: + union storage + { + burl::request_head_base req; + burl::response_head_base res; + storage() noexcept + { + } + }; + + enum class state : unsigned char + { + start_line, + fields, + done, + }; + + storage s_; + header_limits limits_; + std::uint32_t in_size_ = 0; + bool is_req_ = false; + state st_ = state::start_line; + + burl::message_head_base& + h_() noexcept + { + if(is_req_) + return s_.req; + return s_.res; + } + + burl::message_head_base const& + h_() const noexcept + { + if(is_req_) + return s_.req; + return s_.res; + } + + void + parse_start_line_( + char const*& it, + char const* end, + system::error_code& ec) noexcept; + + void + parse_fields_( + char const*& it, + char const* end, + system::error_code& ec) noexcept; +}; + +} // namespace burl +} // namespace boost + +#endif diff --git a/include/boost/burl/message_head_base.hpp b/include/boost/burl/message_head_base.hpp new file mode 100644 index 0000000..18b9101 --- /dev/null +++ b/include/boost/burl/message_head_base.hpp @@ -0,0 +1,366 @@ +// +// Copyright (c) 2021 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +#ifndef BOOST_BURL_MESSAGE_HEAD_BASE_HPP +#define BOOST_BURL_MESSAGE_HEAD_BASE_HPP + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +namespace boost +{ +namespace burl +{ + +/** The common part of an HTTP message header. + + This type extends @ref fields_base with a start + line and framing-related observers and modifiers. + The start line is stored immediately before the + field section, and @ref buffer returns the whole + header. + + Users cannot construct, copy, or destroy objects + of this type: headers are obtained by reference + from a @ref head_parser, which builds them over + the received bytes in their original location, or + built directly for sending through the owning + @ref request_head and @ref response_head or their + fixed-storage counterparts @ref static_request_head + and @ref static_response_head. + + The framing observers @ref payload, + @ref content_length, and @ref keep_alive are + constant-time: the facts they depend on are + cached and kept current as the header is + parsed or modified. +*/ +class message_head_base : public fields_base +{ + friend class head_parser; + + void + push_start_line_( + std::string_view method, + std::string_view target, + http::version v, + std::uint16_t n) noexcept; + + void + push_start_line_( + http::version v, + std::uint16_t status_int, + std::string_view reason, + std::uint16_t n) noexcept; + + http::error + push_field_( + std::string_view name, + std::string_view value, + std::uint16_t n) noexcept; + + http::error + validate_framing_() const noexcept; + + static + std::uint16_t + conn_flags_(std::string_view value) noexcept; + +protected: + enum : std::uint16_t + { + f_req = 1 << 0, + f_http_1_1 = 1 << 1, + f_content_length = 1 << 2, + f_transfer_encoding = 1 << 3, + f_chunked = 1 << 4, + f_conn_close = 1 << 5, + f_conn_keep_alive = 1 << 6, + f_conn_upgrade = 1 << 7, + f_upgrade = 1 << 8, + f_exp_100 = 1 << 9 + }; + + struct req_t + { + std::uint16_t method_len_ = 3; // "GET" + std::uint16_t target_len_ = 1; // "/" + http::method method_ = http::method::get; + }; + + struct res_t + { + std::uint16_t status_int_ = 200; + http::status status_ = http::status::ok; + }; + + std::uint64_t content_length_v_ = 0; + union + { + req_t req_; + res_t res_; + }; + std::uint16_t flags_ = f_http_1_1; + + BOOST_BURL_DECL + message_head_base( + bool is_request, + char* buf, + std::uint32_t cap, + std::uint32_t size, + std::uint16_t prefix_size) noexcept; + + message_head_base(message_head_base const&) = default; + + message_head_base& + operator=(message_head_base const&) = default; + + ~message_head_base() = default; + + BOOST_BURL_DECL + void + swap_(message_head_base& other) noexcept; + + BOOST_BURL_DECL + void + set_version_(http::version v) noexcept; + + BOOST_BURL_DECL + void + on_special_(http::field id) noexcept override; + + BOOST_BURL_DECL + void + on_clear_() noexcept override; + +public: + /** Return a string view representing the header. + + The returned view references the start + line, every field line, and the final + empty line, forming a complete message + header: + + @code + "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n" + @endcode + + The view is invalidated when the header is + modified. + + @par Complexity + Constant. + */ + std::string_view + buffer() const noexcept + { + return { base_(), std::size_t(prefix_size_) + size_ }; + } + + /** Format the header to an output stream. + + The start line is written first, then each + field as `name: value`, every line followed + by a newline: + + @code + "GET / HTTP/1.1\nHost: example.com\n" + @endcode + + This form is for diagnostics; the wire + form is available from @ref buffer. + + @par Complexity + Linear in `h.buffer().size()`. + + @par Exception Safety + Basic guarantee. + + @return A reference to the output stream. + + @param os The output stream to write to. + + @param h The header to write. + */ + friend + BOOST_BURL_DECL + std::ostream& + operator<<( + std::ostream& os, + message_head_base const& h); + + /** Return the type of payload framing. + + The result is derived from the + Transfer-Encoding and Content-Length + fields, and for responses from the status + code, following RFC 9112. + + @ref http::payload::error is returned when + those fields cannot be reconciled. Such a + header is rejected by @ref head_parser, so + this only arises for a header built by hand. + + @par Complexity + Constant. + */ + BOOST_BURL_DECL + http::payload + payload() const noexcept; + + /** Return the payload size stated by Content-Length. + + `std::nullopt` is returned when there is no + such field, or its value is not a single + decimal number, or it appears more than + once. Such a header is rejected by + @ref head_parser, so this only arises for a + header built by hand. + + @par Complexity + Constant. + */ + BOOST_BURL_DECL + std::optional + content_length() const noexcept; + + /** Return true if the payload uses chunked framing. + + This is a shorthand for + `payload() == @ref http::payload::chunked`, + true when a final "chunked" transfer coding + determines the payload framing. + + @par Complexity + Constant. + */ + BOOST_BURL_DECL + bool + chunked() const noexcept; + + /** Return true if the connection should be kept open. + + The result is derived from the HTTP + version, the Connection field, and + @ref payload. + + @par Complexity + Constant. + */ + BOOST_BURL_DECL + bool + keep_alive() const noexcept; + + /** Return true if the message proposes a protocol switch. + + The result is true when an Upgrade field + is present and the Connection field + contains the "upgrade" token, following + RFC 9110. The proposed protocols are + listed in the Upgrade field. + + Recipients of HTTP/1.0 messages must + ignore the proposal. + + @par Complexity + Constant. + */ + bool + upgrade() const noexcept + { + return (flags_ & f_upgrade && flags_ & f_conn_upgrade); + } + + /** Return the HTTP version of the message. + + @par Complexity + Constant. + */ + http::version + version() const noexcept + { + using enum http::version; + return (flags_ & f_http_1_1) ? http_1_1 : http_1_0; + } + + //-------------------------------------------- + + /** Set the Content-Length field. + + Any chunked Transfer-Encoding is removed + first, as if by `set_chunked(false)`. + + @par Exception Safety + Basic guarantee. + + @throw std::length_error + The storage cannot accommodate the field. + + @param n The payload size. + */ + BOOST_BURL_DECL + void + set_content_length(std::uint64_t n); + + /** Add or remove the chunked transfer coding. + + When `value` is true, any Content-Length + fields are removed and "chunked" is added + as the final transfer coding if not + already present. When `value` is false, a + final "chunked" coding is removed. + + @par Exception Safety + Basic guarantee. + + @throw std::length_error + The storage cannot accommodate the field. + + @param value Whether the payload is chunked. + */ + BOOST_BURL_DECL + void + set_chunked(bool value); + + /** Set whether the connection should be kept open. + + The Connection field is rewritten: the + "close" and "keep-alive" tokens are + removed, other tokens are preserved, and + the token required by the HTTP version and + `value` is added. + + @par Exception Safety + Basic guarantee. + + @throw std::length_error + The storage cannot accommodate the field. + + @param value Whether the connection should + be kept open. + */ + BOOST_BURL_DECL + void + set_keep_alive(bool value); +}; + +} // namespace burl +} // namespace boost + +#endif diff --git a/include/boost/burl/request.hpp b/include/boost/burl/request.hpp index 8df6c9d..0097a4e 100644 --- a/include/boost/burl/request.hpp +++ b/include/boost/burl/request.hpp @@ -12,8 +12,8 @@ #include #include +#include -#include #include #include @@ -90,7 +90,7 @@ struct request default headers of the @ref client with the same name. */ - http::fields headers; + fields headers; /** The request body. diff --git a/include/boost/burl/request_head.hpp b/include/boost/burl/request_head.hpp new file mode 100644 index 0000000..c3fcd70 --- /dev/null +++ b/include/boost/burl/request_head.hpp @@ -0,0 +1,324 @@ +// +// Copyright (c) 2021 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +#ifndef BOOST_BURL_REQUEST_HEAD_HPP +#define BOOST_BURL_REQUEST_HEAD_HPP + +#include +#include + +#include +#include + +namespace boost +{ +namespace burl +{ + +/** A dynamic container for an HTTP request header. + + This container builds a request header in a single + allocation which grows as needed, mirroring + @ref fields for the field section while adding the + request line. It owns its storage and never runs + out of room. + + A default-constructed object holds the default + request line ("GET / HTTP/1.1") and no fields; + the request line and fields are filled in through + the inherited @ref request_head_base interface. + Like @ref fields, a default-constructed object is a + non-owning view over a shared buffer and does not + allocate until it is first modified. + + @ref message_head_base::buffer returns the complete + header bytes ready for the wire. + + @see @ref request_head_base for the request-line + observers and modifiers. +*/ +class request_head : public request_head_base +{ +public: + /** Constructor. + + The header holds the default request line and + no fields. No allocation is performed until the + header is first modified. + + @par Exception Safety + No-throw guarantee. + */ + BOOST_BURL_DECL + request_head() noexcept; + + /** Destructor. + + Releases the allocation. All views obtained + from the header are invalidated. + */ + BOOST_BURL_DECL + ~request_head(); + + /** Constructor. + + The container acquires ownership of the + contents of `other`, which is left in a + valid but unspecified state and must not be + used except to be destroyed or assigned to. + + @par Complexity + Constant. + */ + BOOST_BURL_DECL + request_head(request_head&& other) noexcept; + + /** Constructor. + + The contents of `other` are copied into an + exact-fit allocation. + + @par Complexity + Linear in `other.buffer().size()`. + + @par Exception Safety + Strong guarantee. Calls to allocate may throw. + */ + BOOST_BURL_DECL + request_head(request_head const& other); + + /** Constructor. + + The contents of `other` are copied into an + exact-fit allocation, taking ownership of a + header that may otherwise refer to external + storage, such as the one produced by + @ref head_parser. + + The conversion is lossless and implicit: an + owning @ref request_head carries exactly the + state of a @ref request_head_base. + + @par Complexity + Linear in `other.buffer().size()`. + + @par Exception Safety + Strong guarantee. Calls to allocate may throw. + + @param other The header to copy. + */ + BOOST_BURL_DECL + request_head(request_head_base const& other); + + /** Constructor. + + The header holds a request line with the + given method, target, and version, and no + fields. + + @par Exception Safety + Strong guarantee. Calls to allocate may throw. + + @throw std::length_error + The storage cannot accommodate the request line. + + @param m The method constant. Must not be + @ref http::method::unknown. + + @param t The request-target. Must not be + empty. + + @param v The version. + */ + request_head( + http::method m, + std::string_view t, + http::version v = + http::version::http_1_1) + : request_head() + { + set_start_line(m, t, v); + } + + /** Assignment. + + The container acquires ownership of the + contents of `other`, which is left in a + valid but unspecified state. The previous + contents are released. + + @par Complexity + Constant. + */ + BOOST_BURL_DECL + request_head& + operator=(request_head&& other) noexcept; + + /** Assignment. + + The contents are replaced with a copy of + `other`. + + @par Complexity + Linear in `other.buffer().size()`. + + @par Exception Safety + Strong guarantee. Calls to allocate may throw. + */ + BOOST_BURL_DECL + request_head& + operator=(request_head const& other); + + /** Assignment. + + The contents are replaced with a copy of + `other`, taking ownership of a header that + may otherwise refer to external storage. + + @par Complexity + Linear in `other.buffer().size()`. + + @par Exception Safety + Strong guarantee. Calls to allocate may throw. + + @param other The header to copy. + */ + BOOST_BURL_DECL + request_head& + operator=(request_head_base const& other); + + /** Swap the contents. + + The contents of the two headers are + exchanged. No allocation occurs and no + bytes are copied. + + Views obtained from either header remain + valid; they follow the contents into the + other header. Iterators are invalidated: an + iterator stays bound to the header it was + obtained from, which now holds different + fields. + + If `this == &other`, this function call has + no effect. + + @par Complexity + Constant. + + @par Exception Safety + No-throw guarantee. + + @param other The header to swap with. + */ + BOOST_BURL_DECL + void + swap(request_head& other) noexcept; + + /** Swap the contents. + + The contents of the two headers are + exchanged. No allocation occurs and no + bytes are copied. + + If `&v0 == &v1`, this function call has no + effect. + + @par Effects + @code + v0.swap(v1); + @endcode + + @par Complexity + Constant. + + @par Exception Safety + No-throw guarantee. + + @param v0 The first header to swap. + + @param v1 The second header to swap. + + @see @ref request_head::swap + */ + friend + void + swap( + request_head& v0, + request_head& v1) noexcept + { + v0.swap(v1); + } + + /** Reserve storage. + + Ensures the field section can grow to `bytes` + bytes and the number of fields to `count`, and + the request line to `start_line` bytes, without + reallocating. Has no effect if the current + allocation is already sufficient. + + Reserving room for the request line lets it be + set or changed later, up to `start_line` bytes, + without shifting the field section. + + All views are invalidated when a reallocation + occurs. + + @par Exception Safety + Strong guarantee. Calls to allocate may throw. + + @throw std::length_error + `bytes` exceeds @ref max_buffer_size, the table + for `count` fields does not fit alongside + `bytes` within @ref max_buffer_size, or + `start_line` exceeds + @ref fields_base::max_start_line_size. + + @param bytes The serialized field-section size, + as returned by `fields_base::buffer().size()`. + + @param count The number of fields. + + @param start_line The request-line size to + reserve room for, including its trailing CRLF, + as it appears in @ref message_head_base::buffer. + */ + BOOST_BURL_DECL + void + reserve( + std::size_t bytes, + std::size_t count, + std::size_t start_line); + + /** Remove excess capacity. + + All views are invalidated when a reallocation + occurs. + + @par Exception Safety + Strong guarantee. Calls to allocate may throw. + */ + BOOST_BURL_DECL + void + shrink_to_fit(); + +private: + bool + static_() const noexcept override + { + return false; + } +}; + +} // namespace burl +} // namespace boost + +#endif diff --git a/include/boost/burl/request_head_base.hpp b/include/boost/burl/request_head_base.hpp new file mode 100644 index 0000000..4172a72 --- /dev/null +++ b/include/boost/burl/request_head_base.hpp @@ -0,0 +1,266 @@ +// +// Copyright (c) 2021 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +#ifndef BOOST_BURL_REQUEST_HEAD_BASE_HPP +#define BOOST_BURL_REQUEST_HEAD_BASE_HPP + +#include +#include + +namespace boost +{ +namespace burl +{ + +/** The header of an HTTP request. + + This type extends @ref message_head_base with observers + and modifiers for the request line + + @code + method SP request-target SP HTTP-version CRLF + @endcode + + Users cannot construct or copy objects of this + type; see @ref message_head_base. To build a request + header for sending, use @ref request_head. + + Strings passed to the start-line modifiers are + stored verbatim; arguments which are views into + the message itself remain valid to pass. +*/ +class request_head_base : public message_head_base +{ + friend class head_parser; + + BOOST_BURL_DECL + void + set_method_( + std::string_view s, + http::method m); + + BOOST_BURL_DECL + void + set_start_line_( + std::string_view ms, + http::method m, + std::string_view t, + http::version v); + + char* + default_buf_() const noexcept override; + +protected: + BOOST_BURL_DECL + request_head_base() noexcept; + + request_head_base( + char* buf, + std::uint32_t cap, + std::uint32_t size = 0, + std::uint16_t prefix_size = 0) noexcept + : message_head_base( + true, buf, cap, size, prefix_size) + { + } + + request_head_base(request_head_base const&) = default; + + request_head_base& + operator=(request_head_base const&) = default; + + ~request_head_base() = default; + + void + swap_(request_head_base& other) noexcept + { + message_head_base::swap_(other); + } + +public: + /** Return the method as a constant. + + If the stored method string is not a + recognized verb, @ref http::method::unknown + is returned and @ref method_text holds the + original characters. + + @par Complexity + Constant. + */ + http::method + method() const noexcept + { + return req_.method_; + } + + /** Return the method as it appears in the start line. + + @par Complexity + Constant. + */ + std::string_view + method_text() const noexcept + { + return { base_(), req_.method_len_ }; + } + + /** Return the request-target. + + @par Complexity + Constant. + */ + std::string_view + target() const noexcept + { + return { base_() + req_.method_len_ + 1, req_.target_len_ }; + } + + /** Return true if the Expect field is 100-continue. + + @par Complexity + Constant. + */ + bool + expect_100_continue() const noexcept + { + return (flags_ & f_exp_100) != 0; + } + + //-------------------------------------------- + + /** Set the method using its canonical name. + + @par Exception Safety + Strong guarantee. + + @throw std::length_error + The storage cannot accommodate the change. + + @param m The method constant. Must not be + @ref http::method::unknown. + */ + void + set_method(http::method m) + { + BOOST_ASSERT(m != http::method::unknown); + set_method_(http::to_string(m), m); + } + + /** Set the method from a string. + + The string is stored verbatim. When it + equals a known method name, the + corresponding constant is returned by + @ref method. + + @par Exception Safety + Strong guarantee. + + @throw std::length_error + The storage cannot accommodate the change. + + @param s The method string. Must not be + empty. + */ + void + set_method(std::string_view s) + { + set_method_(s, http::string_to_method(s)); + } + + /** Set the request-target. + + The string is stored verbatim. + + @par Exception Safety + Strong guarantee. + + @throw std::length_error + The storage cannot accommodate the change. + + @param s The request-target. Must not be + empty. + */ + BOOST_BURL_DECL + void + set_target(std::string_view s); + + /** Set the HTTP version. + + @par Exception Safety + Strong guarantee. + + @param v The version. + */ + BOOST_BURL_DECL + void + set_version(http::version v); + + /** Set the entire request line. + + The method, target, and version are + replaced in a single operation. + + @par Exception Safety + Strong guarantee. + + @throw std::length_error + The storage cannot accommodate the change. + + @param m The method constant. Must not be + @ref http::method::unknown. + + @param t The request-target. Must not be + empty. + + @param v The version. + */ + void + set_start_line( + http::method m, + std::string_view t, + http::version v = + http::version::http_1_1) + { + BOOST_ASSERT(m != http::method::unknown); + set_start_line_(http::to_string(m), m, t, v); + } + + /// @copydoc set_start_line(http::method,std::string_view,http::version) + void + set_start_line( + std::string_view m, + std::string_view t, + http::version v = + http::version::http_1_1) + { + set_start_line_(m, http::string_to_method(m), t, v); + } + + /** Add or remove the Expect: 100-continue field. + + @par Exception Safety + Strong guarantee. + + @throw std::length_error + The storage cannot accommodate the field. + + @param b Whether the field should be present. + */ + BOOST_BURL_DECL + void + set_expect_100_continue(bool b); +}; + +} // namespace burl +} // namespace boost + +#endif diff --git a/include/boost/burl/response.hpp b/include/boost/burl/response.hpp index 72f8ddf..fd4e695 100644 --- a/include/boost/burl/response.hpp +++ b/include/boost/burl/response.hpp @@ -18,7 +18,6 @@ #include #include #include -#include #include #include #include @@ -66,7 +65,7 @@ namespace burl throw std::system_error(ec); std::cout << "status: " << r.status_int() << '\n'; - std::cout << "headers: " << r.headers() << '\n'; + std::cout << "headers: " << r.headers().buffer() << '\n'; std::cout << "body: " << co_await r.as() << '\n'; @endcode @@ -225,7 +224,7 @@ class response /** Return the response headers. */ - const http::fields_base& + const fields_base& headers() const noexcept { return parser_.get(); @@ -233,17 +232,17 @@ class response /** Return the payload size, if known. - Returns the size of the message payload when - it is determined by the message metadata. - Otherwise returns an empty optional, such as - for chunked messages. + Returns the value stated by the + Content-Length field. Otherwise returns an + empty optional, such as for chunked + messages. A response to a HEAD request + states the size of the representation even + though no payload follows. */ std::optional content_length() const noexcept { - if(parser_.get().payload() == http::payload::size) - return parser_.get().payload_size(); - return std::nullopt; + return parser_.get().content_length(); } /** Asynchronously read the entire body in place. diff --git a/include/boost/burl/response_head.hpp b/include/boost/burl/response_head.hpp new file mode 100644 index 0000000..c583999 --- /dev/null +++ b/include/boost/burl/response_head.hpp @@ -0,0 +1,321 @@ +// +// Copyright (c) 2021 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +#ifndef BOOST_BURL_RESPONSE_HEAD_HPP +#define BOOST_BURL_RESPONSE_HEAD_HPP + +#include +#include + +#include + +namespace boost +{ +namespace burl +{ + +/** A dynamic container for an HTTP response header. + + This container builds a response header in a single + allocation which grows as needed, mirroring + @ref fields for the field section while adding the + status line. It owns its storage and never runs + out of room. + + A default-constructed object holds the default + status line ("HTTP/1.1 200 OK") and no fields; + the status line and fields are filled in through + the inherited @ref response_head_base interface. + Like @ref fields, a default-constructed object is a + non-owning view over a shared buffer and does not + allocate until it is first modified. + + @ref message_head_base::buffer returns the complete + header bytes ready for the wire. + + @see @ref response_head_base for the status-line + observers and modifiers. +*/ +class response_head : public response_head_base +{ +public: + /** Constructor. + + The header holds the default status line and + no fields. No allocation is performed until the + header is first modified. + + @par Exception Safety + No-throw guarantee. + */ + BOOST_BURL_DECL + response_head() noexcept; + + /** Destructor. + + Releases the allocation. All views obtained + from the header are invalidated. + */ + BOOST_BURL_DECL + ~response_head(); + + /** Constructor. + + The container acquires ownership of the + contents of `other`, which is left in a + valid but unspecified state and must not be + used except to be destroyed or assigned to. + + @par Complexity + Constant. + */ + BOOST_BURL_DECL + response_head(response_head&& other) noexcept; + + /** Constructor. + + The contents of `other` are copied into an + exact-fit allocation. + + @par Complexity + Linear in `other.buffer().size()`. + + @par Exception Safety + Strong guarantee. Calls to allocate may throw. + */ + BOOST_BURL_DECL + response_head(response_head const& other); + + /** Constructor. + + The contents of `other` are copied into an + exact-fit allocation, taking ownership of a + header that may otherwise refer to external + storage, such as the one produced by + @ref head_parser. + + The conversion is lossless and implicit: an + owning @ref response_head carries exactly the + state of a @ref response_head_base. + + @par Complexity + Linear in `other.buffer().size()`. + + @par Exception Safety + Strong guarantee. Calls to allocate may throw. + + @param other The header to copy. + */ + BOOST_BURL_DECL + response_head(response_head_base const& other); + + /** Constructor. + + The header holds a status line with the given + status code and version, and no fields. The + reason-phrase is set to the standard text for + the status code. + + @par Exception Safety + Strong guarantee. Calls to allocate may throw. + + @throw std::length_error + The storage cannot accommodate the status line. + + @param sc The status code. Must have an + integer value in [100, 999]. + + @param v The version. + */ + explicit + response_head( + http::status sc, + http::version v = + http::version::http_1_1) + : response_head() + { + set_start_line(sc, v); + } + + /** Assignment. + + The container acquires ownership of the + contents of `other`, which is left in a + valid but unspecified state. The previous + contents are released. + + @par Complexity + Constant. + */ + BOOST_BURL_DECL + response_head& + operator=(response_head&& other) noexcept; + + /** Assignment. + + The contents are replaced with a copy of + `other`. + + @par Complexity + Linear in `other.buffer().size()`. + + @par Exception Safety + Strong guarantee. Calls to allocate may throw. + */ + BOOST_BURL_DECL + response_head& + operator=(response_head const& other); + + /** Assignment. + + The contents are replaced with a copy of + `other`, taking ownership of a header that + may otherwise refer to external storage. + + @par Complexity + Linear in `other.buffer().size()`. + + @par Exception Safety + Strong guarantee. Calls to allocate may throw. + + @param other The header to copy. + */ + BOOST_BURL_DECL + response_head& + operator=(response_head_base const& other); + + /** Swap the contents. + + The contents of the two headers are + exchanged. No allocation occurs and no + bytes are copied. + + Views obtained from either header remain + valid; they follow the contents into the + other header. Iterators are invalidated: an + iterator stays bound to the header it was + obtained from, which now holds different + fields. + + If `this == &other`, this function call has + no effect. + + @par Complexity + Constant. + + @par Exception Safety + No-throw guarantee. + + @param other The header to swap with. + */ + BOOST_BURL_DECL + void + swap(response_head& other) noexcept; + + /** Swap the contents. + + The contents of the two headers are + exchanged. No allocation occurs and no + bytes are copied. + + If `&v0 == &v1`, this function call has no + effect. + + @par Effects + @code + v0.swap(v1); + @endcode + + @par Complexity + Constant. + + @par Exception Safety + No-throw guarantee. + + @param v0 The first header to swap. + + @param v1 The second header to swap. + + @see @ref response_head::swap + */ + friend + void + swap( + response_head& v0, + response_head& v1) noexcept + { + v0.swap(v1); + } + + /** Reserve storage. + + Ensures the field section can grow to `bytes` + bytes and the number of fields to `count`, and + the status line to `start_line` bytes, without + reallocating. Has no effect if the current + allocation is already sufficient. + + Reserving room for the status line lets it be + set or changed later, up to `start_line` bytes, + without shifting the field section. + + All views are invalidated when a reallocation + occurs. + + @par Exception Safety + Strong guarantee. Calls to allocate may throw. + + @throw std::length_error + `bytes` exceeds @ref max_buffer_size, the table + for `count` fields does not fit alongside + `bytes` within @ref max_buffer_size, or + `start_line` exceeds + @ref fields_base::max_start_line_size. + + @param bytes The serialized field-section size, + as returned by `fields_base::buffer().size()`. + + @param count The number of fields. + + @param start_line The status-line size to + reserve room for, including its trailing CRLF, + as it appears in @ref message_head_base::buffer. + */ + BOOST_BURL_DECL + void + reserve( + std::size_t bytes, + std::size_t count, + std::size_t start_line); + + /** Remove excess capacity. + + All views are invalidated when a reallocation + occurs. + + @par Exception Safety + Strong guarantee. Calls to allocate may throw. + */ + BOOST_BURL_DECL + void + shrink_to_fit(); + +private: + bool + static_() const noexcept override + { + return false; + } +}; + +} // namespace burl +} // namespace boost + +#endif diff --git a/include/boost/burl/response_head_base.hpp b/include/boost/burl/response_head_base.hpp new file mode 100644 index 0000000..61d99bc --- /dev/null +++ b/include/boost/burl/response_head_base.hpp @@ -0,0 +1,227 @@ +// +// Copyright (c) 2021 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +#ifndef BOOST_BURL_RESPONSE_HEAD_BASE_HPP +#define BOOST_BURL_RESPONSE_HEAD_BASE_HPP + +#include +#include + +namespace boost +{ +namespace burl +{ + +/** The header of an HTTP response. + + This type extends @ref message_head_base with observers + and modifiers for the status line + + @code + HTTP-version SP status-code SP [ reason-phrase ] CRLF + @endcode + + Users cannot construct or copy objects of this + type; see @ref message_head_base. To build a response + header for sending, use @ref response_head. + + Strings passed to the start-line modifiers are + stored verbatim; arguments which are views into + the message itself remain valid to pass. +*/ +class response_head_base : public message_head_base +{ + friend class head_parser; + + char* + default_buf_() const noexcept override; + +protected: + BOOST_BURL_DECL + response_head_base() noexcept; + + response_head_base( + char* buf, + std::uint32_t cap, + std::uint32_t size = 0, + std::uint16_t prefix_size = 0) noexcept + : message_head_base( + false, buf, cap, size, prefix_size) + { + } + + response_head_base(response_head_base const&) = default; + + response_head_base& + operator=(response_head_base const&) = default; + + ~response_head_base() = default; + + void + swap_(response_head_base& other) noexcept + { + message_head_base::swap_(other); + } + +public: + /** Return the reason-phrase. + + For serialized responses this is the text + chosen by the caller; for parsed responses + it is the text as received, which may be + empty. + + @par Complexity + Constant. + */ + std::string_view + reason() const noexcept + { + // "HTTP/1.x NNN reason\r\n"; parsed status + // lines may omit the space and the phrase + if(prefix_size_ < 15) + return {}; + return { base_() + 13, std::size_t(prefix_size_) - 15 }; + } + + /** Return the status code as a constant. + + If the received code is not recognized, + @ref http::status::unknown is returned and + @ref status_int holds the original value. + + @par Complexity + Constant. + */ + http::status + status() const noexcept + { + return res_.status_; + } + + /** Return the status code as an integer. + + @par Complexity + Constant. + */ + unsigned short + status_int() const noexcept + { + return res_.status_int_; + } + + //-------------------------------------------- + + /** Set the status line. + + The reason-phrase is set to the standard + text for the status code. + + @par Exception Safety + Strong guarantee. + + @throw std::length_error + The storage cannot accommodate the change. + + @param sc The status code. Must have an + integer value in [100, 999]. + + @param v The version. + */ + void + set_start_line( + http::status sc, + http::version v = + http::version::http_1_1) + { + BOOST_ASSERT(in_range_(sc)); + set_start_line( + static_cast(sc), + http::to_string(sc), + v); + } + + /** Set the status line. + + The reason-phrase is stored verbatim. + + @par Exception Safety + Strong guarantee. + + @throw std::invalid_argument + `si` is not in [100, 999]. + + @throw std::length_error + The storage cannot accommodate the change. + + @param si The status code, in [100, 999]. + + @param reason The reason-phrase. + + @param v The version. + */ + BOOST_BURL_DECL + void + set_start_line( + unsigned short si, + std::string_view reason, + http::version v = + http::version::http_1_1); + + /** Set the HTTP version. + + @par Exception Safety + Strong guarantee. + + @param v The version. + */ + BOOST_BURL_DECL + void + set_version(http::version v); + + /** Set the status code. + + The reason-phrase is set to the standard + text for the status code. The version is + unchanged. + + @par Exception Safety + Strong guarantee. + + @throw std::length_error + The storage cannot accommodate the change. + + @param sc The status code. Must have an + integer value in [100, 999]. + */ + void + set_status(http::status sc) + { + BOOST_ASSERT(in_range_(sc)); + set_start_line( + static_cast(sc), + http::to_string(sc), + version()); + } + +private: + static + bool + in_range_(http::status sc) noexcept + { + auto const si = static_cast(sc); + return si >= 100 && si <= 999; + } +}; + +} // namespace burl +} // namespace boost + +#endif diff --git a/include/boost/burl/static_fields.hpp b/include/boost/burl/static_fields.hpp new file mode 100644 index 0000000..bbe510b --- /dev/null +++ b/include/boost/burl/static_fields.hpp @@ -0,0 +1,156 @@ +// +// Copyright (c) 2021 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +#ifndef BOOST_BURL_STATIC_FIELDS_HPP +#define BOOST_BURL_STATIC_FIELDS_HPP + +#include +#include + +#include + +namespace boost +{ +namespace burl +{ + +/** A static container of HTTP fields. + + This container holds a sequence of HTTP fields + in an externally provided buffer with fixed + capacity, and performs no allocations during its + lifetime. Modifiers throw `std::length_error` + when the storage is exhausted. + + A newly constructed container is empty; + @ref buffer returns "\r\n". Fields are filled in + through the inherited @ref fields_base interface. + + The caller is responsible for ensuring that the + lifetime of the storage extends until the + container is destroyed. + + @par Example + @code + char buf[256]; + static_fields f(buf, sizeof(buf)); + + f.set(http::field::host, "example.com"); + + assert(f.buffer() == + "Host: example.com\r\n" + "\r\n"); + @endcode + + @see + @ref fields, + @ref fields_base. +*/ +class static_fields : public fields_base +{ +public: + /** Constructor. + + The container uses the given storage and + holds no fields. + + @par Exception Safety + Strong guarantee. + + @throw std::length_error + The storage cannot accommodate the empty + field section. + + @param storage The storage to use. + + @param n The size of the storage. + */ + BOOST_BURL_DECL + static_fields( + char* storage, + std::size_t n); + + /** Constructor (deleted). + */ + static_fields( + static_fields const&) = delete; + + /** Constructor. + + The newly constructed object refers to the + storage of `other`, which is left in a valid + but unspecified state where the only safe + operation is destruction: it no longer refers + to the storage, and modifiers and assignment + throw `std::length_error`. + + @par Complexity + Constant. + + @param other The container to move from. + */ + BOOST_BURL_DECL + static_fields( + static_fields&& other) noexcept; + + /** Assignment. + + The contents are replaced with a copy of + `other`. The storage is retained. + + @par Complexity + Linear in `other.buffer().size()`. + + @par Exception Safety + Strong guarantee. + + @throw std::length_error + The storage cannot accommodate the contents + of `other`. + + @param other The fields to copy. + + @return A reference to this object. + */ + BOOST_BURL_DECL + static_fields& + operator=(static_fields const& other); + + /** Assignment. + + The contents are replaced with a copy of the + field section of `other`. When `other` is a + message header, only its fields are copied; + the start line is discarded. The storage is + retained. + + @par Complexity + Linear in `other.buffer().size()`. + + @par Exception Safety + Strong guarantee. + + @throw std::length_error + The storage cannot accommodate the contents + of `other`. + + @param other The fields to copy. + + @return A reference to this object. + */ + BOOST_BURL_DECL + static_fields& + operator=(fields_base const& other); +}; + +} // namespace burl +} // namespace boost + +#endif diff --git a/include/boost/burl/static_request_head.hpp b/include/boost/burl/static_request_head.hpp new file mode 100644 index 0000000..1d79b23 --- /dev/null +++ b/include/boost/burl/static_request_head.hpp @@ -0,0 +1,158 @@ +// +// Copyright (c) 2021 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +#ifndef BOOST_BURL_STATIC_REQUEST_HEAD_HPP +#define BOOST_BURL_STATIC_REQUEST_HEAD_HPP + +#include +#include + +#include + +namespace boost +{ +namespace burl +{ + +/** A static container for an HTTP request header. + + This container builds a request header in an + externally provided buffer with fixed capacity, + and performs no allocations during its lifetime. + Modifiers throw `std::length_error` when the + storage is exhausted. + + A newly constructed object holds the default + request line ("GET / HTTP/1.1") and no fields; + the request line and fields are filled in through + the inherited @ref request_head_base interface. + + The caller is responsible for ensuring that the + lifetime of the storage extends until the header + is destroyed. + + @par Example + @code + char buf[256]; + static_request_head h(buf, sizeof(buf)); + + h.set_start_line(http::method::get, "/"); + h.set(http::field::host, "example.com"); + + assert(h.buffer() == + "GET / HTTP/1.1\r\n" + "Host: example.com\r\n" + "\r\n"); + @endcode + + @see + @ref request_head, + @ref request_head_base. +*/ +class static_request_head : public request_head_base +{ +public: + /** Constructor. + + The header uses the given storage and holds + the default request line and no fields. + + @par Exception Safety + Strong guarantee. + + @throw std::length_error + The storage cannot accommodate the default + request line. + + @param storage The storage to use. + + @param n The size of the storage. + */ + BOOST_BURL_DECL + static_request_head( + char* storage, + std::size_t n); + + /** Constructor (deleted). + */ + static_request_head( + static_request_head const&) = delete; + + /** Constructor. + + The newly constructed object refers to the + storage of `other`, which is left in a valid + but unspecified state where the only safe + operation is destruction: it no longer refers + to the storage, and modifiers and assignment + throw `std::length_error`. + + @par Complexity + Constant. + + @param other The header to move from. + */ + BOOST_BURL_DECL + static_request_head( + static_request_head&& other) noexcept; + + /** Assignment. + + The contents are replaced with a copy of + `other`. The storage is retained. + + @par Complexity + Linear in `other.buffer().size()`. + + @par Exception Safety + Strong guarantee. + + @throw std::length_error + The storage cannot accommodate the contents + of `other`. + + @param other The header to copy. + + @return A reference to this object. + */ + BOOST_BURL_DECL + static_request_head& + operator=(static_request_head const& other); + + /** Assignment. + + The contents are replaced with a copy of + `other`, which may be an owning + @ref request_head or a header produced by + @ref head_parser. The storage is retained. + + @par Complexity + Linear in `other.buffer().size()`. + + @par Exception Safety + Strong guarantee. + + @throw std::length_error + The storage cannot accommodate the contents + of `other`. + + @param other The header to copy. + + @return A reference to this object. + */ + BOOST_BURL_DECL + static_request_head& + operator=(request_head_base const& other); +}; + +} // namespace burl +} // namespace boost + +#endif diff --git a/include/boost/burl/static_response_head.hpp b/include/boost/burl/static_response_head.hpp new file mode 100644 index 0000000..9a664cd --- /dev/null +++ b/include/boost/burl/static_response_head.hpp @@ -0,0 +1,158 @@ +// +// Copyright (c) 2021 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +#ifndef BOOST_BURL_STATIC_RESPONSE_HEAD_HPP +#define BOOST_BURL_STATIC_RESPONSE_HEAD_HPP + +#include +#include + +#include + +namespace boost +{ +namespace burl +{ + +/** A static container for an HTTP response header. + + This container builds a response header in an + externally provided buffer with fixed capacity, + and performs no allocations during its lifetime. + Modifiers throw `std::length_error` when the + storage is exhausted. + + A newly constructed object holds the default + status line ("HTTP/1.1 200 OK") and no fields; + the status line and fields are filled in through + the inherited @ref response_head_base interface. + + The caller is responsible for ensuring that the + lifetime of the storage extends until the header + is destroyed. + + @par Example + @code + char buf[256]; + static_response_head h(buf, sizeof(buf)); + + h.set_start_line(http::status::not_found); + h.set(http::field::content_type, "text/html"); + + assert(h.buffer() == + "HTTP/1.1 404 Not Found\r\n" + "Content-Type: text/html\r\n" + "\r\n"); + @endcode + + @see + @ref response_head, + @ref response_head_base. +*/ +class static_response_head : public response_head_base +{ +public: + /** Constructor. + + The header uses the given storage and holds + the default status line and no fields. + + @par Exception Safety + Strong guarantee. + + @throw std::length_error + The storage cannot accommodate the default + status line. + + @param storage The storage to use. + + @param n The size of the storage. + */ + BOOST_BURL_DECL + static_response_head( + char* storage, + std::size_t n); + + /** Constructor (deleted). + */ + static_response_head( + static_response_head const&) = delete; + + /** Constructor. + + The newly constructed object refers to the + storage of `other`, which is left in a valid + but unspecified state where the only safe + operation is destruction: it no longer refers + to the storage, and modifiers and assignment + throw `std::length_error`. + + @par Complexity + Constant. + + @param other The header to move from. + */ + BOOST_BURL_DECL + static_response_head( + static_response_head&& other) noexcept; + + /** Assignment. + + The contents are replaced with a copy of + `other`. The storage is retained. + + @par Complexity + Linear in `other.buffer().size()`. + + @par Exception Safety + Strong guarantee. + + @throw std::length_error + The storage cannot accommodate the contents + of `other`. + + @param other The header to copy. + + @return A reference to this object. + */ + BOOST_BURL_DECL + static_response_head& + operator=(static_response_head const& other); + + /** Assignment. + + The contents are replaced with a copy of + `other`, which may be an owning + @ref response_head or a header produced by + @ref head_parser. The storage is retained. + + @par Complexity + Linear in `other.buffer().size()`. + + @par Exception Safety + Strong guarantee. + + @throw std::length_error + The storage cannot accommodate the contents + of `other`. + + @param other The header to copy. + + @return A reference to this object. + */ + BOOST_BURL_DECL + static_response_head& + operator=(response_head_base const& other); +}; + +} // namespace burl +} // namespace boost + +#endif diff --git a/include/boost/burl/test/response_factory.hpp b/include/boost/burl/test/response_factory.hpp index a10fcf2..922e2d7 100644 --- a/include/boost/burl/test/response_factory.hpp +++ b/include/boost/burl/test/response_factory.hpp @@ -13,11 +13,12 @@ #include #include #include +#include #include #include #include -#include +#include #include namespace boost @@ -68,7 +69,7 @@ class response_factory { using clock = std::chrono::steady_clock; - http::response msg_; + response_head head_; std::vector body_; urls::url url_; std::optional deadline_; @@ -89,7 +90,7 @@ class response_factory response_factory( http::status sc = http::status::ok, http::version v = http::version::http_1_1) - : msg_(sc, v) + : head_(sc, v) { } @@ -109,7 +110,7 @@ class response_factory response_factory& header(http::field field, core::string_view value) { - msg_.append(field, value); + head_.append(field, value); return *this; } @@ -127,7 +128,7 @@ class response_factory response_factory& header(core::string_view name, core::string_view value) { - msg_.append(name, value); + head_.append(name, value); return *this; } @@ -167,7 +168,7 @@ class response_factory response_factory& content_length(std::uint64_t value) { - msg_.set_content_length(value); + head_.set_content_length(value); return *this; } @@ -186,7 +187,7 @@ class response_factory response_factory& chunked(bool value) { - msg_.set_chunked(value); + head_.set_chunked(value); return *this; } @@ -240,9 +241,9 @@ class response_factory response create(capy::test::fuse fuse = {}) const { - auto msg = msg_; + auto head = head_; std::vector chunks; - if(msg.chunked()) + if(head.chunked()) { for(auto const& piece : body_) chunks.push_back(frame_chunk(piece)); @@ -250,12 +251,12 @@ class response_factory } else { - if(msg.payload() != http::payload::size) + if(head.payload() != http::payload::size) { std::uint64_t content_length = 0; for(auto const& piece : body_) content_length += piece.size(); - msg.set_content_length(content_length); + head.set_content_length(content_length); } chunks = body_; } @@ -264,7 +265,7 @@ class response_factory std::make_unique( std::move(chunks), std::move(fuse), - msg.buffer()), + std::string{ head.buffer() }), {}, {}); diff --git a/src/client.cpp b/src/client.cpp index 4c6bf8e..8a3a1a8 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -9,10 +9,12 @@ #include #include +#include #include "detail/base64.hpp" #include "detail/can_reuse_conn.hpp" #include "detail/connection_pool.hpp" +#include "detail/content_coding.hpp" #include "detail/decoders.hpp" #include "detail/drain_body.hpp" #include "detail/redirect.hpp" @@ -23,10 +25,6 @@ #include #include #include -#include -#include -#include -#include #include #include @@ -43,7 +41,7 @@ namespace void set_accept_encoding( - http::request& headers, + request_head& head, client::config const& cfg) { std::string accept_encoding; @@ -67,17 +65,17 @@ set_accept_encoding( accept("zstd"); if(!accept_encoding.empty()) - headers.set(http::field::accept_encoding, accept_encoding); + head.set(http::field::accept_encoding, accept_encoding); } void -set_target(http::request& headers, const urls::url_view& url) +set_target(request_head& head, const urls::url_view& url) { auto target = url.encoded_target(); if(url.path().empty()) - headers.set_target("/" + std::string(target)); + head.set_target("/" + std::string(target)); else - headers.set_target(target); + head.set_target(target); } } // namespace @@ -192,38 +190,39 @@ client::execute_impl( { using field = http::field; - http::request headers(request.method, "/", config_.version); + request_head head(request.method, "/", config_.version); for(auto f : headers_) - if(!request.headers.exists(f.name)) - headers.append(f.name, f.value); + if(!request.headers.contains(f.name)) + head.append(f.name, f.value); for(auto f : request.headers) - headers.append(f.name, f.value); + head.append(f.name, f.value); if(request.body.has_value()) { // Use the body's content type only if the caller did not set one. - if(!headers.exists(field::content_type)) + if(!head.contains(field::content_type)) { if(auto ct = request.body.content_type()) - headers.set(field::content_type, ct.value()); + head.set(field::content_type, ct.value()); } // Content length is always derived from the body. if(auto cl = request.body.content_length()) - headers.set_content_length(cl.value()); + head.set_content_length(cl.value()); else - headers.set_chunked(true); + head.set_chunked(true); } - auto const head = headers.method() == http::method::head; - auto const auto_decode = !headers.exists(field::accept_encoding); + auto const is_head = head.method() == http::method::head; + auto const auto_decode = !head.contains(field::accept_encoding); if(auto_decode) - set_accept_encoding(headers, config_); + set_accept_encoding(head, config_); detail::response_parser parser( { + .hdr_limits = {}, .in_buffer = config_.response_inplace_buffer, .dec_buffer = config_.response_inplace_buffer, .body_limit = config_.response_body_limit @@ -238,21 +237,21 @@ client::execute_impl( auto request_cookies = request.headers.value_or(field::cookie, ""); for(;;) { - set_target(headers, url); - headers.set(field::host, url.encoded_host_and_port()); + set_target(head, url); + head.set(field::host, url.encoded_host_and_port()); // set cookies - headers.erase(field::cookie); + head.erase(field::cookie); if(!request_cookies.empty()) { if(trusted) - headers.set(field::cookie, request_cookies); + head.set(field::cookie, request_cookies); } else if(config_.cookies) { auto cookies = cookie_jar_.cookie_header(url); if(!cookies.empty()) - headers.set(field::cookie, cookies); + head.set(field::cookie, cookies); } auto [cec, conn] = co_await pool_->acquire(url); @@ -262,7 +261,7 @@ client::execute_impl( // TODO: expect100timeout auto stream = conn.stream(); - sr.reset(&stream, &headers); + sr.reset(&stream, &head); if(request.body.has_value()) { http::any_buffer_sink sink(&sr); @@ -276,7 +275,7 @@ client::execute_impl( } parser.reset(std::move(stream)); - parser.start(head); + parser.start(is_head); auto [rec] = co_await parser.read_header(); if(rec) @@ -304,10 +303,10 @@ client::execute_impl( ec = std::error_code(status_int, burl_category()); std::unique_ptr dec; - if(auto_decode && !head) + if(auto_decode && !is_head) { - auto const& md = parser.get().metadata(); - dec = detail::make_decoder(md.content_encoding.coding); + dec = detail::make_decoder( + detail::content_coding(parser.get())); parser.set_decoder(dec.get()); } @@ -334,7 +333,7 @@ client::execute_impl( auto referer = url; referer.remove_userinfo(); referer.remove_fragment(); - headers.set(field::referer, referer); + head.set(field::referer, referer.buffer()); } // Prepare the next request to follow the redirect @@ -343,14 +342,14 @@ client::execute_impl( co_return { error::bad_redirect_response, {} }; // Change the method according to RFC 9110, Section 15.4.4. - if(need_method_change && headers.method() != http::method::head) + if(need_method_change && head.method() != http::method::head) { - headers.set_method(http::method::get); - headers.erase(field::content_length); - headers.erase(field::transfer_encoding); - headers.erase(field::content_encoding); - headers.erase(field::content_type); - headers.erase(field::expect); + head.set_method(http::method::get); + head.erase(field::content_length); + head.erase(field::transfer_encoding); + head.erase(field::content_encoding); + head.erase(field::content_type); + head.erase(field::expect); request.body = {}; // drop the body } @@ -359,8 +358,8 @@ client::execute_impl( if(!trusted) { - headers.erase(field::authorization); - headers.erase(field::proxy_authorization); + head.erase(field::authorization); + head.erase(field::proxy_authorization); // cookies are removed on each iteration } } diff --git a/src/detail/content_coding.cpp b/src/detail/content_coding.cpp new file mode 100644 index 0000000..12f683e --- /dev/null +++ b/src/detail/content_coding.cpp @@ -0,0 +1,54 @@ +// +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +#include "content_coding.hpp" + +#include +#include +#include +#include + +namespace boost +{ +namespace burl +{ +namespace detail +{ + +http::content_coding +content_coding(fields_base const& headers) noexcept +{ + using enum http::content_coding; + using urls::grammar::ci_is_equal; + + auto const it = headers.find(http::field::content_encoding); + if(it == headers.end()) + return identity; + + if(auto rv = urls::grammar::parse( + it->value, http::token_rule)) + { + if(ci_is_equal(*rv, "identity")) + return identity; + if(ci_is_equal(*rv, "deflate")) + return deflate; + if(ci_is_equal(*rv, "gzip")) + return gzip; + if(ci_is_equal(*rv, "br")) + return br; + if(ci_is_equal(*rv, "zstd")) + return zstd; + } + + return unknown; +} + +} // namespace detail +} // namespace burl +} // namespace boost diff --git a/src/detail/content_coding.hpp b/src/detail/content_coding.hpp new file mode 100644 index 0000000..002b4bf --- /dev/null +++ b/src/detail/content_coding.hpp @@ -0,0 +1,31 @@ +// +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +#ifndef BOOST_BURL_SRC_DETAIL_CONTENT_CODING_HPP +#define BOOST_BURL_SRC_DETAIL_CONTENT_CODING_HPP + +#include + +#include + +namespace boost +{ +namespace burl +{ +namespace detail +{ + +http::content_coding +content_coding(fields_base const& headers) noexcept; + +} // namespace detail +} // namespace burl +} // namespace boost + +#endif diff --git a/src/detail/decoders.cpp b/src/detail/decoders.cpp index e743d6f..7b14c2a 100644 --- a/src/detail/decoders.cpp +++ b/src/detail/decoders.cpp @@ -128,7 +128,7 @@ class brotli_decoder final process( capy::mutable_buffer out, capy::const_buffer in, - bool eof) override + bool) override { auto* next_in = static_cast(in.data()); auto available_in = in.size(); @@ -184,7 +184,7 @@ class zstd_decoder final process( capy::mutable_buffer out, capy::const_buffer in, - bool eof) override + bool) override { ZSTD_inBuffer in_buf{ in.data(), in.size(), 0 }; ZSTD_outBuffer out_buf{ out.data(), out.size(), 0 }; diff --git a/src/detail/except.cpp b/src/detail/except.cpp new file mode 100644 index 0000000..eba9c63 --- /dev/null +++ b/src/detail/except.cpp @@ -0,0 +1,52 @@ +// +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +#include "except.hpp" + +#include + +#include + +namespace boost +{ +namespace burl +{ +namespace detail +{ + +void +throw_invalid_argument( + char const* what, + source_location const& loc) +{ + throw_exception( + std::invalid_argument(what), loc); +} + +void +throw_length_error( + char const* what, + source_location const& loc) +{ + throw_exception( + std::length_error(what), loc); +} + +void +throw_out_of_range( + char const* what, + source_location const& loc) +{ + throw_exception( + std::out_of_range(what), loc); +} + +} // namespace detail +} // namespace burl +} // namespace boost diff --git a/src/detail/except.hpp b/src/detail/except.hpp new file mode 100644 index 0000000..b4f5ffe --- /dev/null +++ b/src/detail/except.hpp @@ -0,0 +1,41 @@ +// +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +#ifndef BOOST_BURL_SRC_DETAIL_EXCEPT_HPP +#define BOOST_BURL_SRC_DETAIL_EXCEPT_HPP + +#include + +namespace boost +{ +namespace burl +{ +namespace detail +{ + +[[noreturn]] void +throw_invalid_argument( + char const* what, + source_location const& loc = BOOST_CURRENT_LOCATION); + +[[noreturn]] void +throw_length_error( + char const* what, + source_location const& loc = BOOST_CURRENT_LOCATION); + +[[noreturn]] void +throw_out_of_range( + char const* what, + source_location const& loc = BOOST_CURRENT_LOCATION); + +} // namespace detail +} // namespace burl +} // namespace boost + +#endif diff --git a/src/detail/http_tunnel.cpp b/src/detail/http_tunnel.cpp index 26a6d2f..028fde9 100644 --- a/src/detail/http_tunnel.cpp +++ b/src/detail/http_tunnel.cpp @@ -11,16 +11,13 @@ #include #include +#include #include "base64.hpp" #include "effective_port.hpp" #include #include -#include -#include -#include -#include #include @@ -41,7 +38,7 @@ open_http_tunnel( host_port += ':'; host_port += effective_port(target); - http::request req(http::method::connect, host_port); + request_head req(http::method::connect, host_port); req.set(http::field::host, host_port); req.set(http::field::proxy_connection, "keep-alive"); diff --git a/src/detail/parser.cpp b/src/detail/parser.cpp index 209c5db..85e8754 100644 --- a/src/detail/parser.cpp +++ b/src/detail/parser.cpp @@ -21,7 +21,6 @@ #include #include -#include #include #include @@ -32,14 +31,14 @@ namespace burl namespace detail { +using http::condition::need_more_input; using http::error::bad_payload; using http::error::body_too_large; +using http::error::end_of_stream; using http::error::in_place_overflow; using http::error::incomplete; using http::error::need_data; -using http::condition::need_more_input; -using header = http::detail::header; using payload = http::payload; namespace @@ -302,27 +301,19 @@ struct parser::chunk_fn parser:: parser( config const& cfg, - http::detail::kind kind, + bool is_request, capy::any_read_stream stream) - : hdr_limits_(cfg.hdr_limits) - , stream_(std::move(stream)) + : stream_(std::move(stream)) , body_limit_(cfg.body_limit) + , is_req_(is_request) { - constexpr auto align = alignof(header::entry); - auto const h_cap = hdr_limits_.valid_space_needed() + - align * ((cfg.in_buffer + align - 1) / align); - auto* const p = static_cast(::operator new( - sizeof(http::static_response) + h_cap + cfg.dec_buffer)); - in_ = { p + sizeof(http::static_response), h_cap - table_reserve() }; - out_ = { in_.ptr + h_cap, cfg.dec_buffer }; - if(kind == http::detail::kind::request) - h_.reset(reinterpret_cast( - ::new(static_cast(p)) - http::static_request(in_.ptr, h_cap))); - else - h_.reset(reinterpret_cast( - ::new(static_cast(p)) - http::static_response(in_.ptr, h_cap))); + auto const h_cap = head_parser::buffer_size( + cfg.hdr_limits, cfg.in_buffer); + buf_ = std::make_unique_for_overwrite( + h_cap + cfg.dec_buffer); + hp_ = { is_req_, buf_.get(), h_cap, cfg.hdr_limits }; + in_ = { buf_.get(), 0 }; + out_ = { buf_.get() + h_cap, cfg.dec_buffer }; } bool @@ -388,15 +379,7 @@ parser:: payload_rem() const noexcept { BOOST_ASSERT(payload_sized()); - return clamp(h_->md.payload_size - transferred_); -} - -std::size_t -parser:: -table_reserve() const noexcept -{ - return header::table_space( - hdr_limits_.max_fields); + return clamp(payload_size_ - transferred_); } void @@ -408,58 +391,47 @@ start(bool head) if(payload_sized() && got_body_) in_.consume(payload_rem()); - auto* const base = - reinterpret_cast(h_.get()) - + sizeof(http::static_response); - move_leftovers(base, in_.data()); - in_ = { base, static_cast( - out_.ptr - base) - table_reserve(), 0, in_.size() }; - out_ = { out_.ptr, out_.cap }; - - *h_ = header{ http::detail::empty{ h_->kind } }; - h_->buf = in_.ptr; - h_->cbuf = in_.ptr; - h_->cap = in_.cap + table_reserve(); - - dec_ = nullptr; - chunk_rem_ = 0; - transferred_ = 0; - decoded_ = 0; - dec_err_ = {}; - payload_ = payload::none; - head_ = head; - started_ = true; - got_header_ = false; - got_body_ = false; - mid_chunk_ = false; - fin_chunk_ = false; + move_leftovers(buf_.get(), in_.data()); + hp_.reset(in_.size()); // pass leftovers + in_ = { buf_.get(), 0 }; + + dec_ = nullptr; + chunk_rem_ = 0; + transferred_ = 0; + decoded_ = 0; + payload_size_ = 0; + dec_err_ = {}; + payload_ = payload::none; + head_ = head; + started_ = true; + got_header_ = false; + got_body_ = false; + mid_chunk_ = false; + fin_chunk_ = false; } void parser:: reset(capy::any_read_stream stream) noexcept { - auto* const base = - reinterpret_cast(h_.get()) - + sizeof(http::static_response); - in_ = { base, static_cast( - out_.ptr - base) - table_reserve() }; - out_ = { out_.ptr, out_.cap }; - - stream_ = std::move(stream); - dec_ = nullptr; - chunk_rem_ = 0; - transferred_ = 0; - decoded_ = 0; - dec_err_ = {}; - payload_ = payload::none; - head_ = false; - started_ = false; - got_header_ = false; - got_body_ = false; - mid_chunk_ = false; - fin_chunk_ = false; - eof_ = false; + hp_.reset(); + in_ = { buf_.get(), 0 }; + + stream_ = std::move(stream); + dec_ = nullptr; + chunk_rem_ = 0; + transferred_ = 0; + decoded_ = 0; + payload_size_ = 0; + dec_err_ = {}; + payload_ = payload::none; + head_ = false; + started_ = false; + got_header_ = false; + got_body_ = false; + mid_chunk_ = false; + fin_chunk_ = false; + eof_ = false; } capy::io_task<> @@ -626,25 +598,38 @@ read_header() for(;;) { system::error_code ec; - h_->parse(in_.size(), hdr_limits_, ec); + hp_.parse(ec); if(ec) { if(ec != need_more_input) co_return { ec }; - if(eof_ && in_.empty()) - co_return { http::error::end_of_stream }; - if(auto [rec] = co_await refill(); rec) - co_return rec; + if(eof_) + { + if(!hp_.got_some()) + co_return { end_of_stream }; + co_return { incomplete }; + } + auto [rec, n] = co_await stream_.read_some(hp_.prepare()); + hp_.commit(n); + if(rec == capy::cond::eof) + eof_ = true; + else if(rec) + co_return { rec }; continue; } // TODO: resize out_ based on payload and decoder - in_.ptr += h_->size; - in_.len -= h_->size; - in_.cap -= h_->size; - - payload_ = head_ ? payload::none : h_->md.payload; - got_header_ = true; + auto const leftovers = hp_.leftovers(); + in_ = { + static_cast(leftovers.data()), + leftovers.size() + hp_.prepare().size(), + 0, + leftovers.size() }; + + auto const& h = hp_.message_head(); + got_header_ = true; + payload_ = head_ ? payload::none : h.payload(); + payload_size_ = h.content_length().value_or(0); switch(payload_) { @@ -764,22 +749,18 @@ read_body() } } -http::static_response const& +burl::response_head_base const& parser:: get_response() const { - BOOST_ASSERT(h_); - return reinterpret_cast< - http::static_response const&>(*h_); + return hp_.response_head(); } -http::static_request const& +burl::request_head_base const& parser:: get_request() const { - BOOST_ASSERT(h_); - return reinterpret_cast< - http::static_request const&>(*h_); + return hp_.request_head(); } capy::io_task diff --git a/src/detail/redirect.cpp b/src/detail/redirect.cpp index b7c2794..a799bd3 100644 --- a/src/detail/redirect.cpp +++ b/src/detail/redirect.cpp @@ -45,7 +45,7 @@ is_redirect( urls::url resolve_location( - http::response_base const& response, + fields_base const& response, const urls::url_view& base) { auto it = response.find(http::field::location); diff --git a/src/detail/redirect.hpp b/src/detail/redirect.hpp index e50a3f0..cefa3d2 100644 --- a/src/detail/redirect.hpp +++ b/src/detail/redirect.hpp @@ -11,8 +11,8 @@ #define BOOST_BURL_SRC_DETAIL_REDIRECT_HPP #include +#include -#include #include #include #include @@ -37,7 +37,7 @@ is_redirect( urls::url resolve_location( - http::response_base const& response, + fields_base const& response, const urls::url_view& base); } // namespace detail diff --git a/src/detail/serializer.cpp b/src/detail/serializer.cpp index 2c687d8..74fd5fa 100644 --- a/src/detail/serializer.cpp +++ b/src/detail/serializer.cpp @@ -82,7 +82,7 @@ void serializer:: reset( capy::any_write_stream* stream, - http::message_base* msg, + message_head_base* msg, encoder* enc, bool head) noexcept { @@ -333,9 +333,9 @@ flush( { auto const decl = [&]()-> std::uint64_t { - if(head_ || msg_->payload() != http::payload::size) + if(head_) return 0; - return msg_->payload_size(); + return msg_->content_length().value_or(0); }(); auto const prod = total_body_ + len + tail_len; diff --git a/src/detail/serializer.hpp b/src/detail/serializer.hpp index 979eb0e..65f8e31 100644 --- a/src/detail/serializer.hpp +++ b/src/detail/serializer.hpp @@ -11,13 +11,13 @@ #define BOOST_BURL_SRC_DETAIL_SERIALIZER_HPP #include +#include #include #include #include #include #include -#include namespace boost { @@ -84,7 +84,7 @@ class serializer return stream_; } - http::message_base* + message_head_base* message() const noexcept { return msg_; @@ -93,13 +93,13 @@ class serializer void reset( capy::any_write_stream* stream, - http::message_base* msg, + message_head_base* msg, encoder* enc = nullptr, bool head = false) noexcept; void reset( - http::message_base* msg, + message_head_base* msg, encoder* enc = nullptr, bool head = false) noexcept { @@ -168,7 +168,7 @@ class serializer static constexpr std::size_t margin = 24; capy::any_write_stream* stream_; - http::message_base* msg_ = nullptr; + message_head_base* msg_ = nullptr; encoder* enc_ = nullptr; std::size_t min_prepare_; std::size_t direct_thr_; diff --git a/src/fields.cpp b/src/fields.cpp new file mode 100644 index 0000000..b285c78 --- /dev/null +++ b/src/fields.cpp @@ -0,0 +1,115 @@ +// +// Copyright (c) 2021 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +#include + +#include + +namespace boost +{ +namespace burl +{ + +fields:: +fields() noexcept + : fields_base() +{ +} + +fields:: +~fields() +{ + release_(); +} + +fields:: +fields(fields&& other) noexcept + : fields() +{ + swap_(other); +} + +fields:: +fields(fields_base const& other) + : fields() +{ + assign_(other, 0); +} + +fields:: +fields(fields const& other) + : fields(static_cast(other)) +{ +} + +fields:: +fields(std::initializer_list init) + : fields() +{ + append(init); +} + +fields& +fields:: +operator=(fields&& other) noexcept +{ + fields tmp(std::move(other)); + swap_(tmp); + return *this; +} + +fields& +fields:: +operator=(fields_base const& other) +{ + if(this != &other) + assign_(other, 0); + return *this; +} + +fields& +fields:: +operator=(fields const& other) +{ + return *this = static_cast(other); +} + +void +fields:: +swap(fields& other) noexcept +{ + swap_(other); +} + +void +fields:: +reserve( + std::size_t bytes, + std::size_t count) +{ + reserve_(bytes, count, 0); +} + +void +fields:: +shrink_to_fit() +{ + if(count_ == 0) + { + // release the allocation entirely + fields tmp; + swap_(tmp); + return; + } + shrink_to_fit_(); +} + +} // namespace burl +} // namespace boost diff --git a/src/fields_base.cpp b/src/fields_base.cpp new file mode 100644 index 0000000..afb64c2 --- /dev/null +++ b/src/fields_base.cpp @@ -0,0 +1,993 @@ +// +// Copyright (c) 2021 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +#include + +#include "detail/except.hpp" +#include "detail/util.hpp" + +#include + +#include +#include +#include +#include + +namespace boost +{ +namespace burl +{ + +namespace +{ + +char buf[] = "\r\n"; + +constexpr char +to_lower(char c) noexcept +{ + if(c >= 'A' && c <= 'Z') + return static_cast(c - 'A' + 'a'); + return c; +} + +bool +ci_equal_rev( + char const* p1, + char const* p2, + std::size_t n) noexcept +{ + p1 += n; + p2 += n; + char a, b; + // fast loop + while(n != 0) + { + --n; + a = *--p1; + b = *--p2; + if(a != b) + goto slow; + } + return true; + for(;;) + { + a = *--p1; + b = *--p2; + slow: + if(to_lower(a) != to_lower(b)) + return false; + if(n == 0) + break; + --n; + } + return true; +} + +} // namespace + +fields_base:: +fields_base() noexcept + : fields_base(buf, 2, 2, 0) +{ +} + +char* +fields_base:: +default_buf_() const noexcept +{ + return buf; +} + +struct fields_base::alloc_ +{ + std::uint16_t prefix; + std::size_t n; + char* p; + + alloc_(std::uint16_t prefix_, std::size_t n_) + : prefix(prefix_) + , n(size_for(prefix, n_)) + , p(static_cast(::operator new(n))) + { + } + + alloc_(alloc_ const&) = delete; + + ~alloc_() + { + if(p) + ::operator delete(p); + } + + void + adopt(fields_base * f) noexcept + { + f->release_(); + f->cap_ = static_cast(n - prefix); + f->buf_ = p + prefix; + f->prefix_cap_ = prefix; + p = nullptr; + } + + static + std::size_t + size_for( + std::uint16_t prefix, + std::size_t n) noexcept + { + return (prefix + n + 7) & ~std::size_t{ 7 }; + } +}; + +//------------------------------------------------ +// +// Owning storage +// +//------------------------------------------------ + +void +fields_base:: +release_() noexcept +{ + if(owns_()) + ::operator delete(buf_ - prefix_cap_); +} + +void +fields_base:: +init_static_( + char* storage, + std::size_t n) +{ + n = detail::clamp(n, max_buffer_size); + auto const a = reinterpret_cast(storage); + auto const e = (a + n) - (a + n) % buffer_alignment; + // the storage end is aligned down as the field + // table requires; it must hold the current contents + auto const need = std::size_t(prefix_size_) + size_; + if(e < a + need) + detail::throw_length_error("buffer too small"); + std::memcpy(storage, base_(), need); + buf_ = storage + prefix_size_; + cap_ = static_cast(e - a) - prefix_size_; + prefix_cap_ = prefix_size_; +} + +void +fields_base:: +realloc_( + std::size_t need, + std::uint16_t prefix_cap) +{ + if(static_()) + detail::throw_length_error("buffer limit exceeded"); + auto const tn = table_space(count_); + alloc_ a(prefix_cap, need); + std::memcpy( + a.p + prefix_cap - prefix_size_, + base_(), + std::size_t(prefix_size_) + size_); + std::memcpy(a.p + a.n - tn, buf_ + cap_ - tn, tn); + a.adopt(this); +} + +std::size_t +fields_base:: +grow_size_(std::size_t need) const +{ + if(static_() || need > max_buffer_size) + detail::throw_length_error("buffer limit exceeded"); + auto const dbl = + detail::clamp(2 * std::uint64_t(cap_), + max_buffer_size); + if(need > dbl) + return need; + return dbl; +} + +fields_base& +fields_base:: +operator=(fields_base const& other) +{ + if(this != &other) + assign_(other, other.prefix_size_); + return *this; +} + +void +fields_base::swap_(fields_base& other) noexcept +{ + std::swap(buf_, other.buf_); + std::swap(cap_, other.cap_); + std::swap(size_, other.size_); + std::swap(count_, other.count_); + std::swap(prefix_cap_, other.prefix_cap_); + std::swap(prefix_size_, other.prefix_size_); +} + +void +fields_base:: +assign_( + fields_base const& other, + std::uint16_t prefix_size) +{ + BOOST_ASSERT(this != &other); + BOOST_ASSERT(prefix_size <= other.prefix_size_); + if(prefix_size == 0 && prefix_size_ == 0 && other.count_ == 0) + { + clear(); + return; + } + auto const tn = table_space(other.count_); + auto const need = std::size_t(other.size_) + tn; + if(static_()) + { + if(default_()) + detail::throw_length_error("buffer limit exceeded"); + if(prefix_size > prefix_cap_ || need > cap_) + { + auto const total = std::size_t(prefix_cap_) + cap_; + if(prefix_size >= total || total - prefix_size < need) + detail::throw_length_error("buffer limit exceeded"); + buf_ = buf_ - prefix_cap_ + prefix_size; + cap_ = static_cast(total - prefix_size); + prefix_cap_ = prefix_size; + } + } + else if(other.default_()) + { + release_(); + buf_ = default_buf_(); + cap_ = other.cap_; + size_ = other.size_; + count_ = other.count_; + prefix_cap_ = prefix_size; + prefix_size_ = prefix_size; + return; + } + else if(default_() || prefix_size > prefix_cap_ || need > cap_) + { + // a copy is compact: no reserved start-line headroom. + alloc_ a(prefix_size, need); + a.adopt(this); + } + size_ = other.size_; + count_ = other.count_; + prefix_size_ = prefix_size; + std::memcpy(base_(), other.buf_ - prefix_size, prefix_size + size_); + std::memcpy(buf_ + cap_ - tn, other.buf_ + other.cap_ - tn, tn); +} + +void +fields_base:: +reserve_( + std::size_t bytes, + std::size_t count, + std::size_t prefix_cap) +{ + if(bytes > max_buffer_size) + detail::throw_length_error("buffer limit exceeded"); + if(count > (max_buffer_size - bytes) / sizeof(entry)) + detail::throw_length_error("field count too large"); + if(prefix_cap > max_start_line_size) + detail::throw_length_error("start line too large"); + if(bytes < size_) + bytes = size_; + if(count < count_) + count = count_; + // prefix_cap_ >= prefix_size_ already; only grow it + auto new_prefix_cap = std::size_t(prefix_cap_); + if(prefix_cap > new_prefix_cap) + new_prefix_cap = prefix_cap; + auto need = bytes + table_space(count); + // growing the prefix must not give back field capacity + if(need < cap_) + need = cap_; + if(need > cap_ || new_prefix_cap > prefix_cap_) + realloc_(need, static_cast(new_prefix_cap)); +} + +void +fields_base:: +shrink_to_fit_() +{ + if(! owns_()) + return; + auto const need = table_space(count_) + size_; + if(alloc_::size_for(prefix_size_, need) < + std::size_t(prefix_cap_) + cap_) + realloc_(need, prefix_size_); +} + +char* +fields_base:: +realloc_splice_( + std::size_t need, + std::size_t text_pos, + std::size_t on, + std::size_t nn, + std::uint16_t new_prefix_size, + std::uint16_t new_prefix_cap, + std::string_view* s0, + std::string_view* s1) +{ + BOOST_ASSERT(nn >= on); + auto const d = nn - on; + alloc_ a( + new_prefix_cap, + grow_size_(need) - (new_prefix_size - prefix_size_)); + auto* const base = a.p + new_prefix_cap - new_prefix_size; + std::memcpy(base, base_(), text_pos); + std::memcpy( + base + text_pos + nn, + base_() + text_pos + on, + std::size_t(prefix_size_) + size_ - text_pos - on); + std::memcpy(base + text_pos + d, base_() + text_pos, on); + auto const tn = table_space(count_); + std::memcpy(a.p + a.n - tn, buf_ + cap_ - tn, tn); + auto const rebase = [&](std::string_view* s) + { + auto const of = s ? off_in_(*s) : std::size_t(-1); + if(of == std::size_t(-1)) + return; + *s = { base + (of < text_pos ? of : of + d), s->size() }; + }; + rebase(s0); + rebase(s1); + prefix_size_ = new_prefix_size; + a.adopt(this); + return base + text_pos; +} + +char* +fields_base:: +splice_start_( + std::uint16_t pos, + std::uint16_t on, + std::size_t nn, + std::string_view* s0, + std::string_view* s1) +{ + BOOST_ASSERT(pos + on <= prefix_size_); + auto const keep = std::size_t(prefix_size_ - on); + if(nn > max_start_line_size - keep) + detail::throw_length_error("start line too large"); + auto const new_prefix_size = static_cast(keep + nn); + if(new_prefix_size <= prefix_cap_) + { + detach_(); + auto* obase = base_(); + if(nn < on) + { + // a shrink slides the head over the leading + // bytes of the replaced text; an argument + // viewing them is salvaged to the tail of the + // replaced region, which only the caller's + // subsequent copy overwrites + auto const salvage = [&](std::string_view* s) + { + if(s == nullptr || s->size() > nn) + return; + if(s->data() >= obase + pos + (std::size_t(on) - nn) || + s->data() + s->size() <= obase + pos) + return; + auto* to = obase + pos + on - s->size(); + std::memmove(to, s->data(), s->size()); + *s = { to, s->size() }; + }; + salvage(s0); + salvage(s1); + } + // the retained head shifts to sit flush against buf_ + move_chars_(buf_ - new_prefix_size, obase, pos, s0, s1); + prefix_size_ = new_prefix_size; + return base_() + pos; + } + // only a grow reaches here: new_prefix_size exceeds + // prefix_cap_, which is at least prefix_size_ + auto const d = std::uint32_t(new_prefix_size - prefix_size_); + auto const need = table_space(count_) + size_ + d; + auto new_prefix_cap = prefix_cap_ + d; + if(need <= cap_ && new_prefix_cap <= max_start_line_size) + { + // enough field-region slack: the rest of the start + // line and the field section are contiguous and + // shift together; entry offsets stay valid because + // buf_ moves with its text + auto* base = base_(); + move_chars_( + base + pos + nn, + base + pos + on, + size_ + std::uint32_t(prefix_size_ - pos - on), + s0, + s1); + buf_ += d; + cap_ -= d; + prefix_size_ = new_prefix_size; + prefix_cap_ = static_cast(new_prefix_cap); + return base + pos; + } + if(new_prefix_cap > max_start_line_size) + new_prefix_cap = max_start_line_size; + return realloc_splice_( + need, + pos, + on, + nn, + new_prefix_size, + static_cast(new_prefix_cap), + s0, + s1); +} + +void +fields_base::notify_(std::uint16_t id) noexcept +{ + auto const f = static_cast(id); + switch(f) + { + case http::field::connection: + case http::field::content_length: + case http::field::expect: + case http::field::transfer_encoding: + case http::field::upgrade: + on_special_(f); + break; + default: + break; + } +} + +//------------------------------------------------ +// +// Lookup +// +//------------------------------------------------ + +std::uint16_t +fields_base:: +resolve_(std::string_view name) noexcept +{ + auto const f = http::string_to_field(name); + if(f) + return static_cast(*f); + return 0; +} + +std::uint32_t +fields_base:: +find_( + std::uint32_t from, + http::field id) const noexcept +{ + if(from >= count_) + return count_; + auto const v = static_cast(id); + auto const* e = &ent_(from); + for(auto i = from; i != count_; ++i, --e) + if(e->id == v) + return i; + return count_; +} + +std::uint32_t +fields_base:: +find_( + std::uint32_t from, + std::string_view name) const noexcept +{ + if(from >= count_) + return count_; + auto const n = name.size(); + auto const* e = &ent_(from); + for(auto i = from; i != count_; ++i, --e) + if(e->nn == n && ci_equal_rev(buf_ + e->of, name.data(), n)) + return i; + return count_; +} + +std::uint32_t +fields_base:: +find_last_( + std::uint32_t before, + http::field id) const noexcept +{ + BOOST_ASSERT(before <= count_); + auto const v = static_cast(id); + auto i = before; + while(i != 0) + { + --i; + if(ent_(i).id == v) + return i; + } + return count_; +} + +std::uint32_t +fields_base:: +find_last_( + std::uint32_t before, + std::string_view name) const noexcept +{ + BOOST_ASSERT(before <= count_); + auto const n = name.size(); + auto i = before; + while(i != 0) + { + --i; + auto const& e = ent_(i); + if(e.nn == n && ci_equal_rev(buf_ + e.of, name.data(), n)) + return i; + } + return count_; +} + +std::string_view +fields_base:: +at(http::field id) const +{ + auto const i = find_(0, id); + if(i == count_) + detail::throw_out_of_range("field not found"); + return ref_(i).value; +} + +std::string_view +fields_base:: +at(std::string_view name) const +{ + auto const i = find_(0, name); + if(i == count_) + detail::throw_out_of_range("field not found"); + return ref_(i).value; +} + +std::size_t +fields_base:: +count(http::field id) const noexcept +{ + if(count_ == 0) + return 0; + auto const v = static_cast(id); + auto const* e = &ent_(0); + std::size_t n = 0; + for(std::uint32_t i = 0; i != count_; ++i, --e) + if(e->id == v) + ++n; + return n; +} + +std::size_t +fields_base:: +count(std::string_view name) const noexcept +{ + if(count_ == 0) + return 0; + auto const nn = name.size(); + auto const* e = &ent_(0); + std::size_t n = 0; + for(std::uint32_t i = 0; i != count_; ++i, --e) + if(e->nn == nn && ci_equal_rev(buf_ + e->of, name.data(), nn)) + ++n; + return n; +} + +auto +fields_base:: +subrange::iterator::operator++() noexcept -> iterator& +{ + BOOST_ASSERT(f_ != nullptr); + BOOST_ASSERT(i_ < f_->count_); + auto const& c = *f_; + auto const* e = &c.ent_(i_); + auto const id = e->id; + if(id != 0) + { + // known field; ids alone identify the name + while(++i_ != c.count_) + { + --e; + if(e->id == id) + break; + } + return *this; + } + auto const nn = e->nn; + auto const* p = c.buf_ + e->of; + while(++i_ != c.count_) + { + --e; + if(e->id != 0 || e->nn != nn) + continue; + if(ci_equal_rev(c.buf_ + e->of, p, nn)) + break; + } + return *this; +} + +//------------------------------------------------ +// +// Modifiers +// +//------------------------------------------------ + +void +fields_base:: +append(std::initializer_list init) +{ + // validate and total up front, so that + // nothing can throw once we start inserting + auto bytes = std::size_t(size_); + for(auto const& e : init) + { + if(e.name.size() > max_name_size) + detail::throw_length_error("field name too large"); + if(e.value.size() > max_value_size) + detail::throw_length_error("field value too large"); + bytes += e.name.size() + e.value.size() + 4; + } + reserve_(bytes, count_ + init.size(), 0); + for(auto const& e : init) + { + auto const id = (e.id == unknown_field) + ? resolve_(e.name) + : static_cast(e.id); + insert_(count_, id, e.name, e.value); + } +} + +void +fields_base:: +grow_gap_( + std::size_t need, + std::size_t pos, + std::size_t len, + std::string_view* s0, + std::string_view* s1) +{ + BOOST_ASSERT(pos <= size_); + if(need > max_buffer_size) + detail::throw_length_error("buffer limit exceeded"); + if(need <= cap_) + { + // open the gap in place + move_chars_( + buf_ + pos + len, buf_ + pos, size_ - pos, s0, s1); + return; + } + realloc_splice_( + need, + std::size_t(prefix_size_) + pos, + 0, + len, + prefix_size_, + prefix_cap_, + s0, + s1); +} + +void +fields_base:: +move_chars_( + char* dest, + char const* src, + std::size_t n, + std::string_view* s0, + std::string_view* s1) noexcept +{ + auto const follow = [&](std::string_view* s) + { + if(s != nullptr && s->data() >= src && s->data() < src + n) + *s = { s->data() + (dest - src), s->size() }; + }; + follow(s0); + follow(s1); + std::memmove(dest, src, n); +} + +std::uint32_t +fields_base:: +insert_( + std::uint32_t i, + std::uint16_t id, + std::string_view name, + std::string_view value) +{ + BOOST_ASSERT(i <= count_); + BOOST_ASSERT(size_ >= 2); + if(name.size() > max_name_size) + detail::throw_length_error("field name too large"); + if(value.size() > max_value_size) + detail::throw_length_error("field value too large"); + auto const nn = static_cast(name.size()); + auto const vn = static_cast(value.size()); + auto const len = nn + vn + std::uint32_t(4); + auto const pos = (i == count_) ? size_ - 2 : ent_(i).of; + grow_gap_( + table_space(count_ + 1) + size_ + len, + pos, + len, + &name, + &value); + char* p = buf_ + pos; + if(nn != 0) + std::memcpy(p, name.data(), nn); + p[nn] = ':'; + p[nn + 1] = ' '; + if(vn != 0) + std::memcpy(p + nn + 2, value.data(), vn); + p[len - 2] = '\r'; + p[len - 1] = '\n'; + auto* et = tab_(); + for(auto j = count_; j > i; --j) + { + auto e = ent_(et, j - 1); + e.of += len; + ent_(et, j) = e; + } + auto& e = ent_(et, i); + e.of = pos; + e.id = id; + e.nn = nn; + e.ws = 2; + e.vn = vn; + size_ += len; + ++count_; + notify_(id); + return i; +} + +void +fields_base:: +replace_value_( + std::uint32_t i, + std::string_view value) +{ + BOOST_ASSERT(i < count_); + if(value.size() > max_value_size) + detail::throw_length_error("field value too large"); + auto const vn2 = static_cast(value.size()); + auto const of = ent_(i).of; + auto const vp = of + ent_(i).nn + ent_(i).ws; + auto const old_len = line_len_(i); + auto const new_len = vp - of + vn2 + 2; + auto const tail = of + old_len; + auto const tail_n = size_ - tail; + if(new_len <= old_len) + { + // shrink: write the value first, so arguments + // viewing the old text stay readable, then + // close the gap + auto const d = old_len - new_len; + if(vn2 != 0) + std::memmove(buf_ + vp, value.data(), vn2); + buf_[vp + vn2] = '\r'; + buf_[vp + vn2 + 1] = '\n'; + if(d != 0) + { + std::memmove(buf_ + tail - d, buf_ + tail, tail_n); + auto* et = tab_(); + for(auto j = i + 1; j < count_; ++j) + ent_(et, j).of -= d; + size_ -= d; + } + ent_(i).vn = vn2; + notify_(ent_(i).id); + return; + } + auto const d = new_len - old_len; + grow_gap_( + table_space(count_) + size_ + d, + tail, + d, + &value); + BOOST_ASSERT(vn2 != 0); + std::memmove(buf_ + vp, value.data(), vn2); + buf_[vp + vn2] = '\r'; + buf_[vp + vn2 + 1] = '\n'; + auto* et = tab_(); + for(auto j = i + 1; j < count_; ++j) + ent_(et, j).of += d; + ent_(i).vn = vn2; + size_ += d; + notify_(ent_(i).id); +} + +void +fields_base:: +erase_at_(std::uint32_t i) noexcept +{ + BOOST_ASSERT(i < count_); + auto const id = ent_(i).id; + auto const of = ent_(i).of; + auto const len = line_len_(i); + std::memmove(buf_ + of, buf_ + of + len, size_ - of - len); + auto* et = tab_(); + for(auto j = i + 1; j < count_; ++j) + { + auto e = ent_(et, j); + e.of -= len; + ent_(et, j - 1) = e; + } + size_ -= len; + --count_; + notify_(id); +} + +template +std::uint32_t +fields_base:: +erase_all_( + std::uint32_t i, + std::uint16_t id, + Match const& match) noexcept +{ + BOOST_ASSERT(i < count_); + // single pass, relocating runs of surviving + // lines as whole blocks, so each character + // moves at most once + auto* et = tab_(); + auto cw = ent_(et, i).of; + auto ew = i; + while(i != count_) + { + auto e = ent_(et, i); + if(match(e)) + { + ++i; + continue; + } + std::uint32_t const rp = e.of; + std::uint32_t rn = 0; + for(;;) + { + auto const len = line_len_(i); + e.of = cw + rn; + ent_(et, ew) = e; + rn += len; + ++ew; + if(++i == count_) + break; + e = ent_(et, i); + if(match(e)) + break; + } + std::memmove(buf_ + cw, buf_ + rp, rn); + cw += rn; + } + buf_[cw] = '\r'; + buf_[cw + 1] = '\n'; + auto const n = count_ - ew; + size_ = cw + 2; + count_ = ew; + notify_(id); + return n; +} + +std::uint32_t +fields_base:: +erase_all_( + std::uint32_t i, + std::uint16_t id) noexcept +{ + return erase_all_(i, id, + [id](entry const& e) noexcept + { + return e.id == id; + }); +} + +std::uint32_t +fields_base:: +erase_all_( + std::uint32_t i, + std::string_view name) noexcept +{ + return erase_all_(i, ent_(i).id, + [this, name](entry const& e) noexcept + { + return e.nn == name.size() && + ci_equal_rev(buf_ + e.of, name.data(), name.size()); + }); +} + +void +fields_base:: +erase_dups_(std::uint32_t i) noexcept +{ + auto const& e = ent_(i); + if(e.id != 0) + { + auto const j = find_(i + 1, static_cast(e.id)); + if(j != count_) + erase_all_(j, e.id); + } + else + { + std::string_view const name(buf_ + e.of, e.nn); + auto const j = find_(i + 1, name); + if(j != count_) + erase_all_(j, name); + } +} + +void +fields_base:: +set( + http::field id, + std::string_view value) +{ + auto const i = find_(0, id); + if(i == count_) + return append(id, value); + replace_value_(i, value); + erase_dups_(i); +} + +void +fields_base:: +set( + std::string_view name, + std::string_view value) +{ + auto const i = find_(0, name); + if(i == count_) + return append(name, value); + replace_value_(i, value); + erase_dups_(i); +} + +std::size_t +fields_base:: +erase(http::field id) noexcept +{ + auto const i = find_(0, id); + if(i == count_) + return 0; + return erase_all_(i, static_cast(id)); +} + +std::size_t +fields_base:: +erase(std::string_view name) noexcept +{ + auto const i = find_(0, name); + if(i == count_) + return 0; + return erase_all_(i, name); +} + +void +fields_base:: +clear() noexcept +{ + if(count_ == 0) + return; + buf_[0] = '\r'; + buf_[1] = '\n'; + size_ = 2; + count_ = 0; + on_clear_(); +} + +std::ostream& +operator<<( + std::ostream& os, + fields_base const& f) +{ + for(auto fv : f) + os << fv.name << ": " << fv.value << '\n'; + return os; +} + +} // namespace burl +} // namespace boost diff --git a/src/head_parser.cpp b/src/head_parser.cpp new file mode 100644 index 0000000..073f247 --- /dev/null +++ b/src/head_parser.cpp @@ -0,0 +1,746 @@ +// +// Copyright (c) 2021 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +#include + +#include "detail/util.hpp" + +#include + +#include +#include + +namespace boost +{ +namespace burl +{ + +// assert relying facts +static_assert( + std::is_same_v< + decltype(header_limits::max_size), std::uint32_t>); + +static_assert( + std::is_same_v< + decltype(header_limits::max_fields), std::uint32_t>); + +static_assert( + std::is_same_v< + decltype(header_limits::max_field), std::uint16_t>); + +static_assert( + std::is_same_v< + decltype(header_limits::max_start_line), std::uint16_t>); + +using error = http::error; +using version = http::version; + +namespace +{ + +std::uint16_t +distance_u16( + char const* it, char const* end) noexcept +{ + return static_cast(end - it); +} + +char const* +trim_front( + char const* it, char const* end) noexcept +{ + while(it != end) + { + if(*it != ' ' && *it != '\t') + break; + ++it; + } + return it; +} + +char const* +trim_back( + char const* it, char const* first) noexcept +{ + while(it != first) + { + auto const c = it[-1]; + if(c != ' ' && c != '\t') + break; + --it; + } + return it; +} + +bool +is_token_char(char c) noexcept +{ + /* + tchar = "!" | "#" | "$" | "%" | "&" | + "'" | "*" | "+" | "-" | "." | + "^" | "_" | "`" | "|" | "~" | + DIGIT | ALPHA + */ + static char constexpr tab[] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 + 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, // 112 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 128 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 144 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 160 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 176 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 192 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 208 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 224 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // 240 + }; + static_assert(sizeof(tab) == 256); + return tab[static_cast(c)]; +} + +bool +is_target_char(char c) noexcept +{ + auto const u = static_cast(c); + return u >= 0x21 && u <= 0x7e; +} + +bool +is_digit(char c) noexcept +{ + return c >= '0' && c <= '9'; +} + +bool +is_print(char c) noexcept +{ + return static_cast(c-32) < 95; +} + +char const* +parse_token_to_eol( + char const* it, + char const* end, + char const*& token_end, + system::error_code& ec) noexcept +{ + for(;; ++it) + { + if(it >= end) + { + ec = error::need_data; + return it; + } + if(BOOST_UNLIKELY(! is_print(*it))) + if((BOOST_LIKELY(static_cast< + unsigned char>(*it) < '\040') && + BOOST_LIKELY(*it != 9)) || + BOOST_UNLIKELY(*it == 127)) + goto found_control; + } +found_control: + if(BOOST_LIKELY(*it == '\r')) + { + if(++it >= end) + { + ec = error::need_data; + return end; + } + if(*it++ != '\n') + { + ec = error::bad_line_ending; + return end; + } + token_end = it - 2; + } + else if(*it == '\n') + { + // bare LF + ec = error::bad_line_ending; + return end; + } + else + { + // invalid character + return nullptr; + } + return it; +} + +void +parse_method( + char const*& it, + char const* end, + std::string_view& result, + system::error_code& ec) +{ + // parse token SP + auto const first = it; + for(;; ++it) + { + if(it + 1 > end) + { + ec = error::need_data; + return; + } + if(! is_token_char(*it)) + break; + } + if(*it != ' ') + { + ec = error::bad_method; + return; + } + if(it == first) + { + // cannot be empty + ec = error::bad_method; + return; + } + result = { first, it++ }; +} + +void +parse_target( + char const*& it, + char const* end, + std::string_view& result, + system::error_code& ec) +{ + // parse target SP + auto const first = it; + for(;; ++it) + { + if(it + 1 > end) + { + ec = error::need_data; + return; + } + if(! is_target_char(*it)) + break; + } + if(*it != ' ') + { + ec = error::bad_request_target; + return; + } + if(it == first) + { + // cannot be empty + ec = error::bad_request_target; + return; + } + result = { first, it++ }; +} + +void +parse_version( + char const*& it, + char const* end, + version& result, + system::error_code& ec) +{ + if(it + 8 > end) + { + ec = error::need_data; + return; + } + if(std::memcmp(it, "HTTP/1.1", 8) == 0) + { + it += 8; + result = version::http_1_1; + return; + } + if(std::memcmp(it, "HTTP/1.0", 8) == 0) + { + it += 8; + result = version::http_1_0; + return; + } + ec = error::bad_version; +} + +void +parse_status( + char const*& it, + char const* end, + std::uint16_t& result, + system::error_code& ec) +{ + // parse 3(digit) SP + if(it + 4 > end) + { + ec = error::need_data; + return; + } + if(! is_digit(*it)) + { + ec = error::bad_status_code; + return; + } + result = 100 * (*it++ - '0'); + if(! is_digit(*it)) + { + ec = error::bad_status_code; + return; + } + result += 10 * (*it++ - '0'); + if(! is_digit(*it)) + { + ec = error::bad_status_code; + return; + } + result += *it++ - '0'; + if(*it == ' ') + { + ++it; + } + else if(*it != '\r') + { + ec = error::bad_status_code; + } +} + +void +parse_reason( + char const*& it, + char const* end, + std::string_view& result, + system::error_code& ec) +{ + auto const first = it; + char const* token_end = nullptr; + auto p = parse_token_to_eol( + it, end, token_end, ec); + if(ec) + return; + if(! p) + { + ec = error::bad_reason; + return; + } + result = { first, token_end }; + it = p; +} + +void +parse_field( + char const*& it, + char const* end, + std::string_view& name, + std::string_view& value, + system::error_code& ec) +{ +/* header-field = field-name ":" OWS field-value OWS + + field-name = token + field-value = *( field-content / obs-fold ) + field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] + field-vchar = VCHAR / obs-text + + obs-fold = CRLF 1*( SP / HTAB ) + ; obsolete line folding + ; see Section 3.2.4 + + token = 1* + CHAR = + sep = "(" | ")" | "<" | ">" | "@" + | "," | ";" | ":" | "\" | <"> + | "/" | "[" | "]" | "?" | "=" + | "{" | "}" | SP | HT +*/ + + auto first = it; + while(it != end && is_token_char(*it)) + { + ++it; + } + if(it == end) + { + ec = error::need_data; + return; + } + if(it == first || *it != ':') + { + ec = error::bad_field_name; + return; + } + name = { first, it }; + ++it; // eat ':' + first = it; + char const* token_end = nullptr; + for(;;) + { + // parse to CRLF + it = parse_token_to_eol(it, end, token_end, ec); + if(ec) + return; + if(! it) + { + ec = error::bad_field_value; + return; + } + // Look 1 char past the CRLF to handle obs-fold. + if(it == end) + { + ec = error::need_data; + return; + } + if(*it != ' ' && *it != '\t') + { + first = trim_front(first, token_end); + value = { first, trim_back(token_end, first) }; + return; + } + // obs-fold: resolve in place, CRLF -> SP SP + auto const q = const_cast(it); + q[-2] = ' '; + q[-1] = ' '; + } +} + +template +void +parse_limited( + Parse&& parse, + char const*& it, + char const* end, + std::size_t limit, + error limit_err, + system::error_code& ec) noexcept +{ + bool const limited = [&]() + { + if(static_cast(end - it) >= limit) + { + end = it + limit; + return true; + } + return false; + }(); + parse(it, end, ec); + if(ec == error::need_data && limited) + ec = limit_err; +} + +} // namespace + +head_parser:: +head_parser( + bool is_request, + char* buf, + std::size_t n, + header_limits const& limits) noexcept + : limits_(limits) + , is_req_(is_request) +{ + n = detail::clamp(n, fields_base::max_buffer_size); + auto const a = reinterpret_cast(buf); + auto const e = (a + n) - + (a + n) % fields_base::buffer_alignment; + std::uint32_t cap = 0; + if(e > a) + cap = static_cast(e - a); + else + buf = nullptr; + + if(is_req_) + ::new(static_cast(&s_.req)) + class request_head_base(buf, cap); + else + ::new(static_cast(&s_.res)) + class response_head_base(buf, cap); +} + +head_parser:: +head_parser(head_parser&& other) noexcept + : limits_(other.limits_) + , in_size_(other.in_size_) + , is_req_(other.is_req_) + , st_(other.st_) +{ + if(is_req_) + ::new(static_cast(&s_.req)) + class request_head_base(other.s_.req); + else + ::new(static_cast(&s_.res)) + class response_head_base(other.s_.res); +} + +head_parser& +head_parser:: +operator=(head_parser&& other) noexcept +{ + if(this == &other) + return *this; + limits_ = other.limits_; + in_size_ = other.in_size_; + is_req_ = other.is_req_; + st_ = other.st_; + if(is_req_) + ::new(static_cast(&s_.req)) + class request_head_base(other.s_.req); + else + ::new(static_cast(&s_.res)) + class response_head_base(other.s_.res); + return *this; +} + +void +head_parser:: +reset(std::size_t leftovers) noexcept +{ + auto& h = h_(); + auto const usable = static_cast( + std::size_t(h.cap_) + h.prefix_size_); + if(is_req_) + ::new(static_cast(&s_.req)) + class request_head_base(h.base_(), usable); + else + ::new(static_cast(&s_.res)) + class response_head_base(h.base_(), usable); + BOOST_ASSERT(leftovers <= prepare().size()); + in_size_ = leftovers; + st_ = state::start_line; +} + +capy::mutable_buffer +head_parser:: +prepare() noexcept +{ + auto& h = h_(); + auto const reserve = + fields_base::table_space(limits_.max_fields); + auto const cap = std::size_t(h.prefix_size_) + h.cap_; + if(reserve + in_size_ >= cap) + return { h.base_() + in_size_, 0 }; + return { h.base_() + in_size_, cap - reserve - in_size_ }; +} + +void +head_parser:: +commit(std::size_t n) noexcept +{ + BOOST_ASSERT(n <= prepare().size()); + in_size_ += static_cast(n); +} + +void +head_parser:: +parse(system::error_code& ec) noexcept +{ + ec.clear(); + auto const& h = h_(); + char const* it = h.buf_ + h.size_; + auto* const end = it + + (in_size_ - h.prefix_size_ - h.size_); + + // a parser constructed with no usable + // storage cannot make progress + if(it == nullptr) + { + ec = error::in_place_overflow; + return; + } + + switch(st_) + { + case state::start_line: + { + parse_limited( + [this](auto& it, auto end, auto& ec) + { + parse_start_line_(it, end, ec); + }, + it, + end, + detail::clamp(limits_.max_start_line, limits_.max_size), + error::start_line_limit, + ec); + if(ec) + break; + st_ = state::fields; + BOOST_FALLTHROUGH; + } + case state::fields: + { + parse_limited( + [this](auto& it, auto end, auto& ec) + { + parse_fields_(it, end, ec); + }, + it, + end, + limits_.max_size - h.prefix_size_ - h.size_, + error::headers_limit, + ec); + if(ec) + break; + st_ = state::done; + BOOST_FALLTHROUGH; + } + case state::done: + { + ec = h.validate_framing_(); + } + } + + if(ec == error::need_data && prepare().size() == 0) + ec = error::in_place_overflow; +} + +void +head_parser:: +parse_start_line_( + char const*& it, + char const* end, + system::error_code& ec) noexcept +{ + auto& h = h_(); + auto const first = it; + if(is_req_) + { + /* + request-line = method SP request-target SP HTTP-version CRLF + method = token + */ + + std::string_view m; + parse_method(it, end, m, ec); + if(ec) + return; + + std::string_view t; + parse_target(it, end, t, ec); + if(ec) + return; + + version v; + parse_version(it, end, v, ec); + if(ec) + return; + + if(it + 2 > end) + { + ec = error::need_data; + return; + } + if(it[0] != '\r' || it[1] != '\n') + { + ec = error::bad_line_ending; + return; + } + it += 2; + h.push_start_line_( + m, t, v, distance_u16(first, it)); + } + else + { + /* + status-line = HTTP-version SP status-code SP reason-phrase CRLF + status-code = 3*DIGIT + reason-phrase = *( HTAB / SP / VCHAR / obs-text ) + */ + + version v; + parse_version(it, end, v, ec); + if(ec) + return; + + // SP + if(it + 1 > end) + { + ec = error::need_data; + return; + } + if(*it++ != ' ') + { + ec = error::bad_version; + return; + } + + std::uint16_t s; + parse_status(it, end, s, ec); + if(ec) + return; + + // parse reason CRLF + std::string_view r; + parse_reason(it, end, r, ec); + if(ec) + return; + h.push_start_line_( + v, s, r, distance_u16(first, it)); + } +} + +void +head_parser:: +parse_fields_( + char const*& it, + char const* end, + system::error_code& ec) noexcept +{ + auto& h = h_(); + std::string_view name; + std::string_view value; + for(;;) + { + if(it + 2 > end) + { + ec = error::need_data; + return; + } + if(it[0] == '\r') + { + if(it[1] != '\n') + { + ec = error::bad_line_ending; + return; + } + // terminating CRLF + it += 2; + h.size_ += 2; + return; + } + + if(h.size() >= limits_.max_fields) + { + ec = error::fields_limit; + return; + } + + auto const first = it; + parse_limited( + [&name, &value](auto& it, auto end, auto& ec) + { + parse_field(it, end, name, value, ec); + }, + it, + end, + limits_.max_field + 1u, // 1u for obs lookahead + error::field_size_limit, + ec); + if(ec) + return; + ec = h.push_field_( + name, value, distance_u16(first, it)); + if(ec) + return; + } +} + +} // namespace burl +} // namespace boost diff --git a/src/message_head_base.cpp b/src/message_head_base.cpp new file mode 100644 index 0000000..3adb989 --- /dev/null +++ b/src/message_head_base.cpp @@ -0,0 +1,534 @@ +// +// Copyright (c) 2021 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace boost +{ +namespace burl +{ + +using error = http::error; +using field = http::field; +using payload = http::payload; + +namespace grammar = urls::grammar; +using grammar::ci_is_equal; + +namespace +{ + +bool +parse_dec( + std::string_view s, + std::uint64_t& n) noexcept +{ + if(s.empty()) + return false; + n = 0; + for(char const c : s) + { + if(c < '0' || c > '9') + return false; + auto const d = static_cast(c - '0'); + if(n > ((std::numeric_limits::max)() - d) / 10) + return false; + n = n * 10 + d; + } + return true; +} + +constexpr auto token_list_rule = + http::list_rule(http::token_rule); + +bool +token_list_contains( + std::string_view value, + core::string_view token) noexcept +{ + auto const rv = grammar::parse(value, token_list_rule); + if(rv) + { + for(auto t : *rv) + if(ci_is_equal(t, token)) + return true; + } + return false; +} + +bool +last_coding_is_chunked( + std::string_view value) noexcept +{ + auto const rv = grammar::parse(value, token_list_rule); + auto chunked = false; + if(rv) + { + for(auto t : *rv) + chunked = ci_is_equal(t, "chunked"); + } + return chunked; +} + +bool +is_ws(char c) noexcept +{ + return c == ' ' || c == '\t'; +} + +} // namespace + +std::uint16_t +message_head_base:: +conn_flags_(std::string_view value) noexcept +{ + std::uint16_t f = 0; + auto const rv = grammar::parse(value, token_list_rule); + if(rv) + { + for(auto t : *rv) + { + if(ci_is_equal(t, "close")) + f |= f_conn_close; + else if(ci_is_equal(t, "keep-alive")) + f |= f_conn_keep_alive; + else if(ci_is_equal(t, "upgrade")) + f |= f_conn_upgrade; + } + } + return f; +} + +message_head_base:: +message_head_base( + bool is_request, + char* buf, + std::uint32_t cap, + std::uint32_t size, + std::uint16_t prefix_size) noexcept + : fields_base(buf, cap, size, 0, prefix_size, prefix_size) +{ + BOOST_ASSERT( + cap == 2 || + (reinterpret_cast(buf) + cap) % alignof(entry) == 0); + if(is_request) + { + flags_ |= f_req; + req_ = req_t{}; + } + else + res_ = res_t{}; +} + +void +message_head_base:: +swap_(message_head_base& other) noexcept +{ + fields_base::swap_(other); + if(flags_ & f_req) + std::swap(req_, other.req_); + else + std::swap(res_, other.res_); + std::swap(content_length_v_, other.content_length_v_); + std::swap(flags_, other.flags_); +} + +void +message_head_base:: +set_version_(http::version v) noexcept +{ + if(v == http::version::http_1_1) + flags_ |= f_http_1_1; + else + flags_ &= ~f_http_1_1; +} + +void +message_head_base:: +push_start_line_( + std::string_view method, + std::string_view target, + http::version v, + std::uint16_t n) noexcept +{ + BOOST_ASSERT(flags_ & f_req); + req_.method_len_ = static_cast(method.size()); + req_.target_len_ = static_cast(target.size()); + req_.method_ = http::string_to_method(method); + set_version_(v); + prefix_size_ = n; + prefix_cap_ = n; + buf_ += n; + cap_ -= n; +} + +void +message_head_base:: +push_start_line_( + http::version v, + std::uint16_t status_int, + std::string_view, + std::uint16_t n) noexcept +{ + BOOST_ASSERT(!(flags_ & f_req)); + res_.status_int_ = status_int; + res_.status_ = http::int_to_status(status_int); + set_version_(v); + prefix_size_ = n; + prefix_cap_ = n; + buf_ += n; + cap_ -= n; +} + +http::error +message_head_base:: +push_field_( + std::string_view name, + std::string_view value, + std::uint16_t n) noexcept +{ + auto const id = resolve_(name); + + switch(static_cast(id)) + { + case field::connection: + flags_ |= conn_flags_(value); + break; + case field::content_length: + { + if(flags_ & f_content_length) + return error::multiple_content_length; + std::uint64_t n = 0; + if(!parse_dec(value, n)) + return error::bad_content_length; + if(flags_ & f_transfer_encoding) + return error::bad_payload; + flags_ |= f_content_length; + content_length_v_ = n; + break; + } + case field::expect: + if(token_list_contains(value, "100-continue")) + flags_ |= f_exp_100; + break; + case field::transfer_encoding: + { + if(flags_ & f_chunked) + return error::bad_transfer_encoding; + if(!(flags_ & f_http_1_1)) + return error::bad_transfer_encoding; + auto const rv = grammar::parse(value, token_list_rule); + if(!rv) + return error::bad_transfer_encoding; + std::size_t n_chunked = 0; + auto is_last = false; + for(auto t : *rv) + { + is_last = ci_is_equal(t, "chunked"); + if(is_last) + ++n_chunked; + } + if(n_chunked > (is_last ? 1u : 0u)) + return error::bad_transfer_encoding; + if(flags_ & f_content_length) + return error::bad_payload; + flags_ |= f_transfer_encoding; + if(is_last) + flags_ |= f_chunked; + break; + } + case field::upgrade: + flags_ |= f_upgrade; + break; + default: + break; + } + auto& e = ent_(count_++); + e.of = size_; + e.id = id; + e.nn = static_cast(name.size()); + e.ws = static_cast(value.data() - name.data() - name.size()); + e.vn = static_cast(value.size()); + size_ += n; // includes CRLF + return error::success; +} + +http::error +message_head_base:: +validate_framing_() const noexcept +{ + if( (flags_ & f_req) && + (flags_ & f_transfer_encoding) && + !(flags_ & f_chunked)) + return error::bad_transfer_encoding; + return error::success; +} + +void +message_head_base:: +on_special_(field id) noexcept +{ + switch(id) + { + case field::connection: + { + std::uint16_t f = 0; + for(auto v : find_all(field::connection)) + f |= conn_flags_(v); + flags_ &= ~(f_conn_close | + f_conn_keep_alive | f_conn_upgrade); + flags_ |= f; + break; + } + case field::content_length: + flags_ &= ~f_content_length; + content_length_v_ = 0; + for(auto v : find_all(field::content_length)) + { + if((flags_ & f_content_length) || + !parse_dec(v, content_length_v_)) + { + flags_ &= ~f_content_length; + content_length_v_ = 0; + break; + } + flags_ |= f_content_length; + } + break; + case field::expect: + flags_ &= ~f_exp_100; + for(auto v : find_all(field::expect)) + { + if(token_list_contains(v, "100-continue")) + { + flags_ |= f_exp_100; + break; + } + } + break; + case field::transfer_encoding: + { + flags_ &= ~(f_transfer_encoding | f_chunked); + auto const it = find_last(end(), field::transfer_encoding); + if(it == end()) + break; + flags_ |= f_transfer_encoding; + if(last_coding_is_chunked((*it).value)) + flags_ |= f_chunked; + break; + } + case field::upgrade: + flags_ &= ~f_upgrade; + if(contains(field::upgrade)) + flags_ |= f_upgrade; + break; + default: + break; + } +} + +void +message_head_base:: +on_clear_() noexcept +{ + // the start line, and the bits derived from + // it, are preserved + content_length_v_ = 0; + flags_ &= f_req | f_http_1_1; +} + +//------------------------------------------------ +// +// Observers +// +//------------------------------------------------ + +payload +message_head_base:: +payload() const noexcept +{ + if(!(flags_ & f_req)) + { + auto const i = res_.status_int_; + if((i >= 100 && i <= 199) || i == 204 || i == 304) + return payload::none; + } + if(flags_ & f_transfer_encoding) + { + if(flags_ & f_content_length) + return payload::error; + if(flags_ & f_chunked) + return payload::chunked; + if(flags_ & f_req) + return payload::error; + return payload::to_eof; + } + if(flags_ & f_content_length) + { + if(content_length_v_ == 0) + return payload::none; + return payload::size; + } + if(flags_ & f_req) + return payload::none; + return payload::to_eof; +} + +std::optional +message_head_base:: +content_length() const noexcept +{ + if(flags_ & f_content_length) + return content_length_v_; + return std::nullopt; +} + +bool +message_head_base:: +chunked() const noexcept +{ + return payload() == payload::chunked; +} + +bool +message_head_base:: +keep_alive() const noexcept +{ + auto const p = payload(); + if(p == payload::error || p == payload::to_eof) + return false; + if(!(flags_ & f_http_1_1)) + return (flags_ & f_conn_keep_alive) != 0; + return !(flags_ & f_conn_close); +} + +//------------------------------------------------ +// +// Modifiers +// +//------------------------------------------------ + +void +message_head_base:: +set_content_length(std::uint64_t n) +{ + set_chunked(false); + char tmp[20]; + auto const r = std::to_chars(tmp, tmp + sizeof(tmp), n); + set( + field::content_length, + { tmp, static_cast(r.ptr - tmp) }); +} + +void +message_head_base:: +set_chunked(bool value) +{ + if(value) + { + erase(field::content_length); + auto const it = find_last(end(), field::transfer_encoding); + if(it != end() && last_coding_is_chunked((*it).value)) + return; + append(field::transfer_encoding, "chunked"); + return; + } + auto const it = find_last(end(), field::transfer_encoding); + if(it == end()) + return; + auto const v = (*it).value; + if(!last_coding_is_chunked(v)) + return; + auto const comma = v.rfind(','); + if(comma == std::string_view::npos) + { + erase(it); + return; + } + std::string_view head = v.substr(0, comma); + while(!head.empty() && is_ws(head.back())) + head.remove_suffix(1); + set(it, head); +} + +void +message_head_base:: +set_keep_alive(bool value) +{ + // TODO: Use static buffer + std::string list; + for(auto v : find_all(field::connection)) + { + auto const rv = grammar::parse(v, token_list_rule); + if(rv) + { + for(auto t : *rv) + { + if(ci_is_equal(t, "close")) + continue; + if(ci_is_equal(t, "keep-alive")) + continue; + if(!list.empty()) + list += ", "; + list += t; + } + } + } + std::string_view add; + if(flags_ & f_http_1_1) + { + if(!value) + add = "close"; + } + else + { + if(value) + add = "keep-alive"; + } + if(!add.empty()) + { + if(!list.empty()) + list += ", "; + list += add; + } + if(list.empty()) + erase(field::connection); + else + set(field::connection, list); +} + +std::ostream& +operator<<( + std::ostream& os, + message_head_base const& h) +{ + // in-place parsing leaves the start line + // empty until it has been parsed + if(h.prefix_size_ >= 2) + os << std::string_view{ + h.base_(), + std::size_t(h.prefix_size_) - 2 } << '\n'; + return os << static_cast(h); +} + +} // namespace burl +} // namespace boost diff --git a/src/request_head.cpp b/src/request_head.cpp new file mode 100644 index 0000000..2f7f691 --- /dev/null +++ b/src/request_head.cpp @@ -0,0 +1,101 @@ +// +// Copyright (c) 2021 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +#include + +#include + +namespace boost +{ +namespace burl +{ + +request_head:: +request_head() noexcept + : request_head_base() +{ +} + +request_head:: +~request_head() +{ + release_(); +} + +request_head:: +request_head(request_head&& other) noexcept + : request_head() +{ + swap_(other); +} + +request_head:: +request_head(request_head_base const& other) + : request_head() +{ + request_head_base::operator=(other); +} + +request_head:: +request_head(request_head const& other) + : request_head(static_cast(other)) +{ +} + +request_head& +request_head:: +operator=(request_head&& other) noexcept +{ + request_head tmp(std::move(other)); + swap_(tmp); + return *this; +} + +request_head& +request_head:: +operator=(request_head_base const& other) +{ + request_head_base::operator=(other); + return *this; +} + +request_head& +request_head:: +operator=(request_head const& other) +{ + return *this = static_cast(other); +} + +void +request_head:: +swap(request_head& other) noexcept +{ + swap_(other); +} + +void +request_head:: +reserve( + std::size_t bytes, + std::size_t count, + std::size_t start_line) +{ + reserve_(bytes, count, start_line); +} + +void +request_head:: +shrink_to_fit() +{ + shrink_to_fit_(); +} + +} // namespace burl +} // namespace boost diff --git a/src/request_head_base.cpp b/src/request_head_base.cpp new file mode 100644 index 0000000..03d19e3 --- /dev/null +++ b/src/request_head_base.cpp @@ -0,0 +1,132 @@ +// +// Copyright (c) 2021 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +#include + +#include + +namespace boost +{ +namespace burl +{ + +namespace +{ +char buf[] = "GET / HTTP/1.1\r\n\r\n"; +} // namespace + +request_head_base:: +request_head_base() noexcept + : request_head_base(buf + 16, 2, 2, 16) +{ +} + +char* +request_head_base:: +default_buf_() const noexcept +{ + return buf + 16; +} + +void +request_head_base:: +set_method_( + std::string_view s, + http::method m) +{ + BOOST_ASSERT(!s.empty()); + auto* dest = splice_start_( + 0, + req_.method_len_, + s.size(), + &s); + std::memmove(dest, s.data(), s.size()); + req_.method_ = m; + req_.method_len_ = static_cast(s.size()); +} + +void +request_head_base:: +set_start_line_( + std::string_view ms, + http::method m, + std::string_view t, + http::version v) +{ + BOOST_ASSERT(!ms.empty()); + BOOST_ASSERT(!t.empty()); + auto const vs = http::to_string(v); + auto* dest = splice_start_( + 0, + prefix_size_, + ms.size() + 1 + t.size() + 1 + 8 + 2, + &ms, + &t); + auto* tp = dest + ms.size() + 1; + if(ms.data() + ms.size() > tp && ms.data() < tp + t.size()) + { + std::memmove(dest, ms.data(), ms.size()); + std::memmove(tp, t.data(), t.size()); + } + else + { + std::memmove(tp, t.data(), t.size()); + std::memmove(dest, ms.data(), ms.size()); + } + dest += ms.size(); + *dest++ = ' '; + dest += t.size(); + *dest++ = ' '; + std::memcpy(dest, vs.data(), 8); + dest += 8; + *dest++ = '\r'; + *dest = '\n'; + req_.method_ = m; + req_.method_len_ = static_cast(ms.size()); + req_.target_len_ = static_cast(t.size()); + set_version_(v); +} + +void +request_head_base:: +set_target(std::string_view s) +{ + BOOST_ASSERT(!s.empty()); + auto* dest = splice_start_( + static_cast(req_.method_len_ + 1), + req_.target_len_, + s.size(), + &s); + std::memmove(dest, s.data(), s.size()); + req_.target_len_ = static_cast(s.size()); +} + +void +request_head_base:: +set_version(http::version v) +{ + detach_(); + auto const s = http::to_string(v); + std::memcpy(buf_ - 10, s.data(), 8); + set_version_(v); +} + +void +request_head_base:: +set_expect_100_continue(bool b) +{ + if(b) + set(http::field::expect, "100-continue"); + else + erase(http::field::expect); +} + +} // namespace burl +} // namespace boost diff --git a/src/response_head.cpp b/src/response_head.cpp new file mode 100644 index 0000000..35a9695 --- /dev/null +++ b/src/response_head.cpp @@ -0,0 +1,101 @@ +// +// Copyright (c) 2021 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +#include + +#include + +namespace boost +{ +namespace burl +{ + +response_head:: +response_head() noexcept + : response_head_base() +{ +} + +response_head:: +~response_head() +{ + release_(); +} + +response_head:: +response_head(response_head&& other) noexcept + : response_head() +{ + swap_(other); +} + +response_head:: +response_head(response_head_base const& other) + : response_head() +{ + response_head_base::operator=(other); +} + +response_head:: +response_head(response_head const& other) + : response_head(static_cast(other)) +{ +} + +response_head& +response_head:: +operator=(response_head&& other) noexcept +{ + response_head tmp(std::move(other)); + swap_(tmp); + return *this; +} + +response_head& +response_head:: +operator=(response_head_base const& other) +{ + response_head_base::operator=(other); + return *this; +} + +response_head& +response_head:: +operator=(response_head const& other) +{ + return *this = static_cast(other); +} + +void +response_head:: +swap(response_head& other) noexcept +{ + swap_(other); +} + +void +response_head:: +reserve( + std::size_t bytes, + std::size_t count, + std::size_t start_line) +{ + reserve_(bytes, count, start_line); +} + +void +response_head:: +shrink_to_fit() +{ + shrink_to_fit_(); +} + +} // namespace burl +} // namespace boost diff --git a/src/response_head_base.cpp b/src/response_head_base.cpp new file mode 100644 index 0000000..6caa034 --- /dev/null +++ b/src/response_head_base.cpp @@ -0,0 +1,82 @@ +// +// Copyright (c) 2021 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +#include + +#include "detail/except.hpp" + +#include + +namespace boost +{ +namespace burl +{ + +namespace +{ +char buf[] = "HTTP/1.1 200 OK\r\n\r\n"; +} // namespace + +response_head_base:: +response_head_base() noexcept + : response_head_base(buf + 17, 2, 2, 17) +{ +} + +char* +response_head_base:: +default_buf_() const noexcept +{ + return buf + 17; +} + +void +response_head_base:: +set_start_line( + unsigned short si, + std::string_view reason, + http::version v) +{ + if(si < 100 || si > 999) + detail::throw_invalid_argument("invalid status code"); + auto const vs = http::to_string(v); + auto* dest = splice_start_( + 0, + prefix_size_, + 15 + reason.size(), + &reason); + std::memmove(dest + 13, reason.data(), reason.size()); + std::memcpy(dest, vs.data(), 8); + dest[8] = ' '; + dest[9] = static_cast('0' + si / 100); + dest[10] = static_cast('0' + (si / 10) % 10); + dest[11] = static_cast('0' + si % 10); + dest[12] = ' '; + dest[13 + reason.size()] = '\r'; + dest[14 + reason.size()] = '\n'; + set_version_(v); + res_.status_int_ = si; + res_.status_ = http::int_to_status(si); +} + +void +response_head_base:: +set_version(http::version v) +{ + auto const s = http::to_string(v); + BOOST_ASSERT(s.size() == 8); + // this writes the start line in place + detach_(); + std::memcpy(base_(), s.data(), 8); + set_version_(v); +} + +} // namespace burl +} // namespace boost diff --git a/src/static_fields.cpp b/src/static_fields.cpp new file mode 100644 index 0000000..a858736 --- /dev/null +++ b/src/static_fields.cpp @@ -0,0 +1,51 @@ +// +// Copyright (c) 2021 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +#include + +namespace boost +{ +namespace burl +{ + +static_fields:: +static_fields( + char* storage, + std::size_t n) + : fields_base() +{ + init_static_(storage, n); +} + +static_fields:: +static_fields(static_fields&& other) noexcept + : fields_base() +{ + swap_(other); +} + +static_fields& +static_fields:: +operator=(static_fields const& other) +{ + return *this = static_cast(other); +} + +static_fields& +static_fields:: +operator=(fields_base const& other) +{ + if(this != &other) + assign_(other, 0); + return *this; +} + +} // namespace burl +} // namespace boost diff --git a/src/static_request_head.cpp b/src/static_request_head.cpp new file mode 100644 index 0000000..ec851a7 --- /dev/null +++ b/src/static_request_head.cpp @@ -0,0 +1,50 @@ +// +// Copyright (c) 2021 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +#include + +namespace boost +{ +namespace burl +{ + +static_request_head:: +static_request_head( + char* storage, + std::size_t n) + : request_head_base() +{ + init_static_(storage, n); +} + +static_request_head:: +static_request_head(static_request_head&& other) noexcept + : request_head_base() +{ + swap_(other); +} + +static_request_head& +static_request_head:: +operator=(request_head_base const& other) +{ + request_head_base::operator=(other); + return *this; +} + +static_request_head& +static_request_head:: +operator=(static_request_head const& other) +{ + return *this = static_cast(other); +} + +} // namespace burl +} // namespace boost diff --git a/src/static_response_head.cpp b/src/static_response_head.cpp new file mode 100644 index 0000000..ca18f1e --- /dev/null +++ b/src/static_response_head.cpp @@ -0,0 +1,50 @@ +// +// Copyright (c) 2021 Vinnie Falco (vinnie.falco@gmail.com) +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +#include + +namespace boost +{ +namespace burl +{ + +static_response_head:: +static_response_head( + char* storage, + std::size_t n) + : response_head_base() +{ + init_static_(storage, n); +} + +static_response_head:: +static_response_head(static_response_head&& other) noexcept + : response_head_base() +{ + swap_(other); +} + +static_response_head& +static_response_head:: +operator=(response_head_base const& other) +{ + response_head_base::operator=(other); + return *this; +} + +static_response_head& +static_response_head:: +operator=(static_response_head const& other) +{ + return *this = static_cast(other); +} + +} // namespace burl +} // namespace boost diff --git a/test/unit/detail/connection_pool.cpp b/test/unit/detail/connection_pool.cpp index a8826e2..e52b159 100644 --- a/test/unit/detail/connection_pool.cpp +++ b/test/unit/detail/connection_pool.cpp @@ -426,7 +426,7 @@ class connection_pool_test corosio::tls_context{}, client::config{}); - for(auto i : { 0, 1 }) + for(auto _ : { 0, 1 }) { auto [aec, pc] = co_await pool->acquire(server.url("http")); BOOST_TEST(!aec); @@ -503,7 +503,7 @@ class connection_pool_test corosio::tls_context{}, client::config{}); - for(auto i : { 0, 1 }) + for(auto _ : { 0, 1 }) { auto [aec, pc] = co_await pool->acquire(server.url("https")); BOOST_TEST(!aec); @@ -610,7 +610,7 @@ class connection_pool_test corosio::tls_context{}, std::move(cfg)); - for(auto i : { 0, 1 }) + for(auto _ : { 0, 1 }) { auto [aec, pc] = co_await pool->acquire("http://example.com"); BOOST_TEST(!aec); @@ -690,7 +690,7 @@ class connection_pool_test corosio::tls_context{}, std::move(cfg)); - for(auto i : { 0, 1 }) + for(auto _ : { 0, 1 }) { auto [aec, pc] = co_await pool->acquire("http://127.0.0.1"); BOOST_TEST(!aec); @@ -744,7 +744,7 @@ class connection_pool_test corosio::tls_context{}, std::move(cfg)); - for(auto i : { 0, 1 }) + for(auto _ : { 0, 1 }) { auto [aec, pc] = co_await pool->acquire("http://example.com"); BOOST_TEST(!aec); diff --git a/test/unit/detail/content_coding.cpp b/test/unit/detail/content_coding.cpp new file mode 100644 index 0000000..e7cbcf8 --- /dev/null +++ b/test/unit/detail/content_coding.cpp @@ -0,0 +1,77 @@ +// +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +// Test that header file is self-contained. +#include "src/detail/content_coding.hpp" + +#include + +#include "test_suite.hpp" + +#include + +namespace boost +{ +namespace burl +{ +namespace detail +{ + +class content_coding_test +{ + static + http::content_coding + coding_for(std::string_view value) + { + fields f; + f.set(http::field::content_encoding, value); + return content_coding(f); + } + +public: + void + run() + { + using enum http::content_coding; + + // no Content-Encoding means identity + fields f; + BOOST_TEST(content_coding(f) == identity); + + // the known codings, case-insensitively + BOOST_TEST(coding_for("identity") == identity); + BOOST_TEST(coding_for("deflate") == deflate); + BOOST_TEST(coding_for("gzip") == gzip); + BOOST_TEST(coding_for("GZip") == gzip); + BOOST_TEST(coding_for("br") == br); + BOOST_TEST(coding_for("zstd") == zstd); + + // an unrecognized coding + BOOST_TEST(coding_for("compress") == unknown); + + // values which are not a single token: an + // empty value, and a list of codings + BOOST_TEST(coding_for("") == unknown); + BOOST_TEST(coding_for("gzip, br") == unknown); + + // only the first field is consulted + fields g; + g.append(http::field::content_encoding, "gzip"); + g.append(http::field::content_encoding, "br"); + BOOST_TEST(content_coding(g) == gzip); + } +}; + +TEST_SUITE( + content_coding_test, + "boost.burl.detail.content_coding"); + +} // namespace detail +} // namespace burl +} // namespace boost diff --git a/test/unit/detail/parser.cpp b/test/unit/detail/parser.cpp index 5abfa9d..7b09d82 100644 --- a/test/unit/detail/parser.cpp +++ b/test/unit/detail/parser.cpp @@ -36,7 +36,7 @@ struct test_parser : parser test_parser( config const& cfg, capy::any_read_stream* stream = nullptr) - : parser(cfg, http::detail::kind::response, stream) + : parser(cfg, false, stream) { } @@ -46,7 +46,7 @@ struct test_parser : parser parser::start(head); } - http::static_response const& + response_head_base const& get() const { return get_response(); @@ -309,8 +309,10 @@ class parser_test capy::test::run_blocking()([&]() -> capy::task<> { - // Content-Length together with Transfer-Encoding makes - // the payload undefined + // Content-Length together with Transfer-Encoding + // makes the payload undefined; the header parser + // rejects it as the second of the two is parsed, + // so the header never completes server.provide( "HTTP/1.1 200 OK\r\n" "Content-Length: 5\r\n" @@ -321,11 +323,7 @@ class parser_test pr.start(); auto [ec] = co_await pr.read_header(); BOOST_TEST(ec == http::error::bad_payload); - - // the parsed header stays accessible - BOOST_TEST(pr.got_header()); - BOOST_TEST_EQ(pr.get().status_int(), 200); - BOOST_TEST(!pr.get().keep_alive()); + BOOST_TEST(!pr.got_header()); BOOST_TEST(!pr.got_body()); }()); } @@ -1373,10 +1371,15 @@ class parser_test "Transfer-Encoding: chunked\r\n" "\r\n"; - // dec_buffer smaller than the dechunked body + // the input region holds the first chunk's data + // but not the whole dechunked body, so the second + // chunk overflows the in-place decode and falls + // back to streaming; max_fields leaves no table + // reserve to reclaim parser::config cfg; cfg.hdr_limits.max_size = hdr.size(); - cfg.in_buffer = 5; + cfg.hdr_limits.max_fields = 1; + cfg.in_buffer = 9; capy::test::read_stream server; capy::any_read_stream stream(&server); test_parser pr(cfg, &stream); @@ -1440,7 +1443,7 @@ class parser_test pr.start(); auto [ec] = co_await pr.read_header(); BOOST_TEST(!ec); - BOOST_TEST(pr.get().metadata().payload == + BOOST_TEST(pr.get().payload() == http::payload::to_eof); BOOST_TEST(!pr.got_body()); @@ -2510,7 +2513,7 @@ class parser_test } // move-construct mid-body: pr2 must adopt the in-progress state - // (and the static_response the buffer points to), and the + // (and the parsed header the buffer points to), and the // moved-from pr must remain safely destructible. test_parser pr2(std::move(pr)); BOOST_TEST(pr2.got_header()); diff --git a/test/unit/detail/redirect.cpp b/test/unit/detail/redirect.cpp index 41a58ba..afa2e5f 100644 --- a/test/unit/detail/redirect.cpp +++ b/test/unit/detail/redirect.cpp @@ -12,8 +12,8 @@ #include "test_suite.hpp" +#include #include -#include #include #include #include @@ -33,7 +33,7 @@ class redirect_test static std::string resolve(std::string_view location, urls::url_view base) { - http::response response; + fields response; response.set(http::field::location, location); return resolve_location(response, base).buffer(); } @@ -147,7 +147,7 @@ class redirect_test void testResolveNoLocation() { - http::response response; + fields response; BOOST_TEST( resolve_location(response, "http://a.test/").empty()); } diff --git a/test/unit/detail/serializer.cpp b/test/unit/detail/serializer.cpp index 6fbfe34..e6db226 100644 --- a/test/unit/detail/serializer.cpp +++ b/test/unit/detail/serializer.cpp @@ -11,13 +11,13 @@ #include "src/detail/serializer.hpp" #include +#include #include #include #include #include #include -#include #include @@ -44,18 +44,18 @@ class serializer_test .enc_buffer = 32, .enc_thr = 8 }; - static http::request + static request_head make_request() { - http::request req; + request_head req; req.set_chunked(true); return req; } - static http::request + static request_head make_request(std::size_t cl) { - http::request req; + request_head req; req.set_content_length(cl); return req; } @@ -301,7 +301,7 @@ class serializer_test // flushed, so chunked encoding is replaced with // Content-Length and the body is sent unframed. BOOST_TEST(!req.chunked()); - BOOST_TEST_EQ(req.payload_size(), 11u); + BOOST_TEST_EQ(req.content_length().value(), 11u); BOOST_TEST( server.data().find("Transfer-Encoding") == std::string_view::npos); @@ -329,7 +329,7 @@ class serializer_test }()); BOOST_TEST(!req.chunked()); - BOOST_TEST_EQ(req.payload_size(), 5u); + BOOST_TEST_EQ(req.content_length().value(), 5u); BOOST_TEST_EQ( server.data(), std::string(req.buffer()) + "hello"); @@ -979,7 +979,7 @@ class serializer_test BOOST_TEST_EQ(enc.calls, 0u); BOOST_TEST(!req.chunked()); - BOOST_TEST_EQ(req.payload_size(), 5u); + BOOST_TEST_EQ(req.content_length().value(), 5u); BOOST_TEST( server.data().find("Content-Encoding") == std::string_view::npos); @@ -1054,7 +1054,7 @@ class serializer_test BOOST_TEST_EQ(enc.calls, 0u); BOOST_TEST(!req.chunked()); - BOOST_TEST_EQ(req.payload_size(), 5u); + BOOST_TEST_EQ(req.content_length().value(), 5u); BOOST_TEST_EQ( server.data(), std::string(req.buffer()) + "hello"); @@ -1090,7 +1090,7 @@ class serializer_test // encoded length while Content-Encoding is kept BOOST_TEST(enc.finished); BOOST_TEST(!req.chunked()); - BOOST_TEST_EQ(req.payload_size(), 8u); + BOOST_TEST_EQ(req.content_length().value(), 8u); BOOST_TEST( server.data().find("Content-Encoding: test\r\n") != std::string_view::npos); @@ -1121,7 +1121,7 @@ class serializer_test }()); BOOST_TEST_EQ(enc.calls, 0u); - BOOST_TEST_EQ(req.payload_size(), 7u); + BOOST_TEST_EQ(req.content_length().value(), 7u); BOOST_TEST( server.data().find("Content-Encoding") == std::string_view::npos); @@ -1163,7 +1163,7 @@ class serializer_test }()); BOOST_TEST(enc.finished); - BOOST_TEST_EQ(req.payload_size(), 8u); + BOOST_TEST_EQ(req.content_length().value(), 8u); BOOST_TEST_EQ( server.data(), std::string(req.buffer()) + encoded("abcdefgh")); @@ -1279,7 +1279,7 @@ class serializer_test BOOST_TEST_EQ(enc.calls, 2u); BOOST_TEST(enc.finished); BOOST_TEST(!req.chunked()); - BOOST_TEST_EQ(req.payload_size(), 54u); + BOOST_TEST_EQ(req.content_length().value(), 54u); BOOST_TEST_EQ( server.data(), std::string(req.buffer()) + encoded(body)); @@ -1555,7 +1555,7 @@ class serializer_test }()); BOOST_TEST(enc.finished); - BOOST_TEST_EQ(req.payload_size(), 12u); + BOOST_TEST_EQ(req.content_length().value(), 12u); BOOST_TEST_EQ( server.data(), std::string(req.buffer()) + @@ -1594,7 +1594,7 @@ class serializer_test BOOST_TEST(enc.finished); BOOST_TEST(!req.chunked()); - BOOST_TEST_EQ(req.payload_size(), 24u); + BOOST_TEST_EQ(req.content_length().value(), 24u); BOOST_TEST_EQ( server.data(), std::string(req.buffer()) + encoded(body)); @@ -1640,7 +1640,7 @@ class serializer_test }()); BOOST_TEST(enc.finished); - BOOST_TEST_EQ(req.payload_size(), 8u); + BOOST_TEST_EQ(req.content_length().value(), 8u); BOOST_TEST_EQ( server.data(), std::string(req.buffer()) + encoded("abcdefgh")); @@ -1914,7 +1914,7 @@ class serializer_test BOOST_TEST(!ec3); }()); - BOOST_TEST_EQ(req.payload_size(), 5u); + BOOST_TEST_EQ(req.content_length().value(), 5u); BOOST_TEST_EQ(server1.data(), std::string(req.buffer())); BOOST_TEST_EQ( server2.data(), diff --git a/test/unit/fields.cpp b/test/unit/fields.cpp new file mode 100644 index 0000000..f0d0eff --- /dev/null +++ b/test/unit/fields.cpp @@ -0,0 +1,1100 @@ +// +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +// Test that header file is self-contained. +#include + +#include + +#include "test_suite.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace boost +{ +namespace burl +{ + +static_assert(std::is_nothrow_swappable_v); +static_assert(std::random_access_iterator); +static_assert(std::ranges::random_access_range); +static_assert(std::forward_iterator); +static_assert(std::ranges::forward_range); + +class fields_test +{ +public: + void + testDefault() + { + fields f; + BOOST_TEST(f.empty()); + BOOST_TEST_EQ(f.size(), 0u); + BOOST_TEST_EQ(f.buffer(), "\r\n"); + BOOST_TEST_EQ(f.capacity_in_bytes(), 0u); + BOOST_TEST(f.begin() == f.end()); + BOOST_TEST(f.find(http::field::host) == f.end()); + BOOST_TEST(f.find("Host") == f.end()); + BOOST_TEST_EQ(f.count("Host"), 0u); + BOOST_TEST_EQ(f.count(http::field::host), 0u); + BOOST_TEST(!f.contains("Host")); + BOOST_TEST(!f.contains(http::field::host)); + BOOST_TEST(f.find_all(http::field::host).empty()); + BOOST_TEST(f.find_all("Host").empty()); + BOOST_TEST_EQ(f.value_or(http::field::host, "?"), "?"); + + // clear and shrink_to_fit on a default + // container are no-ops + f.clear(); + f.shrink_to_fit(); + BOOST_TEST_EQ(f.buffer(), "\r\n"); + BOOST_TEST_EQ(f.capacity_in_bytes(), 0u); + + // all default containers share the static buffer + fields g; + BOOST_TEST_EQ( + static_cast(f.buffer().data()), + static_cast(g.buffer().data())); + + // default subrange + fields::subrange sr; + BOOST_TEST(sr.empty()); + BOOST_TEST(sr.begin() == sr.end()); + } + + void + testAppend() + { + fields f; + f.append(http::field::host, "example.com"); + f.append("User-Agent", "burl"); + f.append("X-Custom", "1"); + f.append(http::field::accept, ""); + BOOST_TEST_EQ(f.size(), 4u); + BOOST_TEST(!f.empty()); + BOOST_TEST_EQ( + f.buffer(), + "Host: example.com\r\n" + "User-Agent: burl\r\n" + "X-Custom: 1\r\n" + "Accept: \r\n" + "\r\n"); + + BOOST_TEST_EQ(f.begin()[0].name, "Host"); + BOOST_TEST_EQ(f.begin()[0].value, "example.com"); + BOOST_TEST(f.begin()[0].id == http::field::host); + + // a known name inserted as a string resolves its id + BOOST_TEST(f.begin()[1].id == http::field::user_agent); + BOOST_TEST(f.find(http::field::user_agent) != f.end()); + + BOOST_TEST_EQ(static_cast(f.begin()[2].id), 0); + BOOST_TEST_EQ(f.begin()[3].value, ""); + + // lookups are case-insensitive + BOOST_TEST(f.find("hOsT") != f.end()); + BOOST_TEST_EQ(f.find("x-custom")->value, "1"); + BOOST_TEST_EQ(f.value_or(http::field::host, "?"), "example.com"); + BOOST_TEST_EQ(f.value_or("USER-AGENT", "?"), "burl"); + BOOST_TEST_EQ(f.value_or("missing", "?"), "?"); + BOOST_TEST(f.contains("X-CUSTOM")); + + // verbatim storage of degenerate names + fields g; + g.append("", "v"); + BOOST_TEST_EQ(g.buffer(), ": v\r\n\r\n"); + BOOST_TEST_EQ(g.begin()[0].name, ""); + BOOST_TEST(g.find("") == g.begin()); + } + + void + testAppendList() + { + fields f; + f.append("X", "0"); + f.append({ + { http::field::host, "example.com" }, + { "User-Agent", "burl" }, + { "X-Custom", "1" }, + }); + BOOST_TEST_EQ( + f.buffer(), + "X: 0\r\n" + "Host: example.com\r\n" + "User-Agent: burl\r\n" + "X-Custom: 1\r\n" + "\r\n"); + BOOST_TEST(f.begin()[1].id == http::field::host); + // a known name inserted as a string resolves its id + BOOST_TEST(f.begin()[2].id == http::field::user_agent); + + // an empty list is a no-op; a default + // container performs no allocation + fields g; + g.append({}); + BOOST_TEST_EQ(g.buffer(), "\r\n"); + BOOST_TEST_EQ(g.capacity_in_bytes(), 0u); + + // an oversized element leaves the container + // unchanged, even when preceded by valid ones + auto const before = std::string(f.buffer()); + BOOST_TEST_THROWS( + f.append({ + { "T", "*" }, + { std::string(fields::max_name_size + 1, 'x'), "v" }, + }), + std::length_error); + BOOST_TEST_EQ(f.buffer(), before); + BOOST_TEST_THROWS( + f.append({ + { "T", "*" }, + { "V", std::string(fields::max_value_size + 1, 'x') }, + }), + std::length_error); + BOOST_TEST_EQ(f.buffer(), before); + } + + void + testFindFrom() + { + fields f = { + { http::field::set_cookie, "a=1" }, + { http::field::content_type, "text/html" }, + { http::field::set_cookie, "b=2" }, + { "X-Dup", "x" }, + { "Y-Dup", "y" }, // same length, different name + { "X-Dup", "z" }, + }; + + auto it = f.find(http::field::set_cookie); + BOOST_TEST_EQ(it->value, "a=1"); + it = f.find(std::next(it), http::field::set_cookie); + BOOST_TEST_EQ(it->value, "b=2"); + it = f.find(std::next(it), http::field::set_cookie); + BOOST_TEST(it == f.end()); + + it = f.find("x-dup"); + BOOST_TEST_EQ(it->value, "x"); + it = f.find(std::next(it), "x-dup"); + BOOST_TEST_EQ(it->value, "z"); + + BOOST_TEST_EQ(f.count(http::field::set_cookie), 2u); + BOOST_TEST_EQ(f.count("SET-COOKIE"), 2u); + BOOST_TEST_EQ(f.count("X-DUP"), 2u); + BOOST_TEST_EQ(f.count("y-dup"), 1u); + BOOST_TEST_EQ(f.count(http::field::host), 0u); + } + + void + testFindAll() + { + fields f = { + { http::field::set_cookie, "a" }, + { http::field::content_type, "t" }, + { http::field::set_cookie, "b" }, + { "X-Tok", "1" }, + { http::field::range, "r" }, // known, same length as X-Tok + { "Y-Tok", "no" }, // unknown, same length + { "x-tok", "2" }, // case-variant duplicate + { http::field::set_cookie, "c" }, + }; + + std::string s; + for(auto sv : f.find_all(http::field::set_cookie)) + { + s.append(sv); + s.push_back(';'); + } + BOOST_TEST_EQ(s, "a;b;c;"); + + // unknown names must skip known fields and + // reject same-length unknown names + s.clear(); + for(auto sv : f.find_all("X-Tok")) + { + s.append(sv); + s.push_back(';'); + } + BOOST_TEST_EQ(s, "1;2;"); + + // find_all with a string of a known name + s.clear(); + for(auto sv : f.find_all("set-cookie")) + { + s.append(sv); + s.push_back(';'); + } + BOOST_TEST_EQ(s, "a;b;c;"); + + BOOST_TEST(f.find_all("nope").empty()); + BOOST_TEST(f.find_all(http::field::host).empty()); + } + + void + testSet() + { + fields f = { + { http::field::accept, "1" }, + { http::field::host, "a" }, + { http::field::accept, "2" }, + { http::field::accept, "3" }, + }; + + // replaces the first in place, erases the rest + f.set(http::field::accept, "final"); + BOOST_TEST_EQ(f.size(), 2u); + BOOST_TEST_EQ(f.begin()[0].name, "Accept"); + BOOST_TEST_EQ(f.begin()[0].value, "final"); + BOOST_TEST_EQ(f.begin()[1].name, "Host"); + BOOST_TEST_EQ(f.buffer(), "Accept: final\r\nHost: a\r\n\r\n"); + + // set on an absent field appends + f.set(http::field::user_agent, "u"); + BOOST_TEST_EQ(f.size(), 3u); + BOOST_TEST_EQ(f.begin()[2].name, "User-Agent"); + + // by name, case-insensitive + f.set("HOST", "b"); + BOOST_TEST_EQ(f.begin()[1].value, "b"); + BOOST_TEST_EQ(f.count(http::field::host), 1u); + + // longer, shorter, and equal-length replacement + f.set(http::field::host, "muchlongervalue.example.com"); + BOOST_TEST_EQ(f.begin()[1].value, "muchlongervalue.example.com"); + BOOST_TEST_EQ(f.begin()[2].value, "u"); + f.set(http::field::host, "x"); + BOOST_TEST_EQ(f.begin()[1].value, "x"); + BOOST_TEST_EQ(f.begin()[2].value, "u"); + f.set(http::field::host, "y"); + BOOST_TEST_EQ( + f.buffer(), "Accept: final\r\nHost: y\r\nUser-Agent: u\r\n\r\n"); + + // set by unknown name replaces case-variant duplicates + f.append("X-Trace", "1"); + f.append("x-trace", "2"); + f.set("X-TRACE", "3"); + BOOST_TEST_EQ(f.count("x-trace"), 1u); + BOOST_TEST_EQ(f.find("X-Trace")->value, "3"); + } + + void + testInsert() + { + fields f = { + { http::field::host, "h" }, + { http::field::accept, "a" }, + }; + + auto it = f.insert(f.begin(), http::field::user_agent, "u"); + BOOST_TEST(it == f.begin()); + BOOST_TEST_EQ(it->name, "User-Agent"); + + it = f.insert(f.begin() + 2, "X-Mid", "m"); + BOOST_TEST_EQ((*it).value, "m"); + BOOST_TEST_EQ( + f.buffer(), + "User-Agent: u\r\nHost: h\r\nX-Mid: m\r\nAccept: a\r\n\r\n"); + + it = f.insert(f.end(), "X-End", "e"); + BOOST_TEST_EQ(f.size(), 5u); + BOOST_TEST_EQ(f.begin()[4].name, "X-End"); + BOOST_TEST(it == f.end() - 1); + } + + void + testErase() + { + fields f = { + { http::field::accept, "1" }, + { http::field::host, "h" }, + { http::field::accept, "2" }, + { "X-A", "x" }, + { "x-a", "y" }, + }; + + auto it = f.erase(f.begin()); + BOOST_TEST_EQ(it->name, "Host"); + BOOST_TEST_EQ(f.size(), 4u); + + BOOST_TEST_EQ(f.erase(http::field::accept), 1u); + BOOST_TEST_EQ(f.erase("X-A"), 2u); + BOOST_TEST_EQ(f.size(), 1u); + BOOST_TEST_EQ(f.erase("missing"), 0u); + BOOST_TEST_EQ(f.erase(http::field::accept), 0u); + BOOST_TEST_EQ(f.buffer(), "Host: h\r\n\r\n"); + + f.erase(f.begin()); + BOOST_TEST(f.empty()); + BOOST_TEST_EQ(f.buffer(), "\r\n"); + BOOST_TEST(f.capacity_in_bytes() > 0); + } + + void + testEraseAll() + { + // erase-all compacts in a single pass + fields f = { + { http::field::set_cookie, "a" }, + { http::field::host, "h" }, + { http::field::set_cookie, "bb" }, + { "X-K", "1" }, + { http::field::set_cookie, "ccc" }, + }; + BOOST_TEST_EQ(f.erase(http::field::set_cookie), 3u); + BOOST_TEST_EQ(f.buffer(), "Host: h\r\nX-K: 1\r\n\r\n"); + BOOST_TEST_EQ(f.size(), 2u); + BOOST_TEST(f.begin()[0].id == http::field::host); + BOOST_TEST_EQ(f.begin()[1].name, "X-K"); + + // unknown-name erase: case variants match, a + // known name of equal length does not + f.append("X-Del", "1"); + f.append(http::field::range, "r"); + f.append("x-del", "2"); + BOOST_TEST_EQ(f.erase("X-DEL"), 2u); + BOOST_TEST_EQ(f.buffer(), "Host: h\r\nX-K: 1\r\nRange: r\r\n\r\n"); + + // erasing every field leaves the empty section + BOOST_TEST_EQ(f.erase("host"), 1u); + BOOST_TEST_EQ(f.erase("x-k"), 1u); + BOOST_TEST_EQ(f.erase(http::field::range), 1u); + BOOST_TEST_EQ(f.buffer(), "\r\n"); + BOOST_TEST(f.empty()); + } + + // The fields which drive framing metadata in + // message heads carry no semantics here: a plain + // container stores and removes them like any + // other field. + void + testSpecialFields() + { + fields f; + f.set(http::field::connection, "close"); + f.append(http::field::content_length, "42"); + f.insert( + f.begin(), http::field::transfer_encoding, "chunked"); + f.set(http::field::expect, "100-continue"); + f.append(http::field::upgrade, "h2c"); + BOOST_TEST_EQ( + f.buffer(), + "Transfer-Encoding: chunked\r\n" + "Connection: close\r\n" + "Content-Length: 42\r\n" + "Expect: 100-continue\r\n" + "Upgrade: h2c\r\n" + "\r\n"); + BOOST_TEST_EQ(f.erase(http::field::transfer_encoding), 1u); + BOOST_TEST_EQ(f.erase(http::field::content_length), 1u); + BOOST_TEST_EQ(f.size(), 3u); + } + + void + testAt() + { + fields f = { + { http::field::host, "h" }, + { "X-A", "1" }, + }; + BOOST_TEST_EQ(f.at(http::field::host), "h"); + BOOST_TEST_EQ(f.at("HOST"), "h"); + BOOST_TEST_EQ(f.at("x-a"), "1"); + BOOST_TEST_THROWS(f.at(http::field::age), std::out_of_range); + BOOST_TEST_THROWS(f.at("missing"), std::out_of_range); + } + + void + testSetIterator() + { + fields f = { + { http::field::host, "h" }, + { "X-A", "1" }, + { "X-A", "2" }, + }; + + // only the addressed field changes; no dedup + auto it = f.find(std::next(f.find("X-A")), "X-A"); + f.set(it, "two"); + BOOST_TEST_EQ(f.begin()[2].value, "two"); + BOOST_TEST_EQ(f.begin()[1].value, "1"); + BOOST_TEST_EQ(f.count("x-a"), 2u); + + // growing and shrinking a value in place + f.reserve(128, 8); + f.set(f.begin(), "a-considerably-longer-host-name"); + BOOST_TEST_EQ(f.begin()[0].value, "a-considerably-longer-host-name"); + BOOST_TEST_EQ(f.begin()[2].value, "two"); + f.set(f.begin(), "s"); + BOOST_TEST_EQ(f.begin()[0].value, "s"); + BOOST_TEST_EQ(f.buffer(), "Host: s\r\nX-A: 1\r\nX-A: two\r\n\r\n"); + } + + void + testFindLast() + { + fields f = { + { http::field::set_cookie, "a" }, + { "X-A", "1" }, + { http::field::set_cookie, "b" }, + { "x-a", "2" }, + }; + + auto it = f.find_last(f.end(), http::field::set_cookie); + BOOST_TEST_EQ(it->value, "b"); + it = f.find_last(it, http::field::set_cookie); + BOOST_TEST_EQ(it->value, "a"); + BOOST_TEST(f.find_last(it, http::field::set_cookie) == f.end()); + + it = f.find_last(f.end(), "X-A"); + BOOST_TEST_EQ(it->value, "2"); + it = f.find_last(it, "X-A"); + BOOST_TEST_EQ(it->value, "1"); + BOOST_TEST(f.find_last(it, "X-A") == f.end()); + + BOOST_TEST(f.find_last(f.begin(), "x-a") == f.end()); + BOOST_TEST(f.find_last(f.end(), "nope") == f.end()); + BOOST_TEST(f.find_last(f.end(), http::field::host) == f.end()); + + fields g; + BOOST_TEST(g.find_last(g.end(), http::field::host) == g.end()); + } + + void + testReverseIterators() + { + fields f = { + { "A", "1" }, + { "B", "2" }, + { "C", "3" }, + }; + + std::string s; + for(auto it = f.rbegin(); it != f.rend(); ++it) + { + s.append((*it).name); + s.push_back(';'); + } + BOOST_TEST_EQ(s, "C;B;A;"); + BOOST_TEST_EQ(f.rend() - f.rbegin(), 3); + BOOST_TEST_EQ((*(f.rbegin() + 2)).name, "A"); + + fields g; + BOOST_TEST(g.rbegin() == g.rend()); + } + + void + testCopyMove() + { + fields a = { + { http::field::host, "h" }, + { "X-C", "c" }, + }; + + fields b(a); + BOOST_TEST_EQ(b.buffer(), a.buffer()); + BOOST_TEST(b.buffer().data() != a.buffer().data()); + + fields c(std::move(a)); + BOOST_TEST_EQ(c.buffer(), b.buffer()); + + // moved-from equals a default-constructed container + BOOST_TEST(a.empty()); + BOOST_TEST_EQ(a.buffer(), "\r\n"); + BOOST_TEST_EQ(a.capacity_in_bytes(), 0u); + a.append(http::field::accept, "r"); + BOOST_TEST_EQ(a.size(), 1u); + + fields d; + d = b; + BOOST_TEST_EQ(d.buffer(), b.buffer()); + d = fields(); + BOOST_TEST(d.empty()); + BOOST_TEST_EQ(d.capacity_in_bytes(), 0u); + + // copying an empty container allocates nothing + fields e(d); + BOOST_TEST_EQ(e.capacity_in_bytes(), 0u); + + // self-assignment + auto& br = b; + b = br; + BOOST_TEST_EQ(b.size(), 2u); + b = std::move(br); + BOOST_TEST_EQ(b.size(), 2u); + + // assigning empty over non-empty keeps capacity + fields g = { { "A", "1" } }; + auto const cap = g.capacity_in_bytes(); + g = d; + BOOST_TEST(g.empty()); + BOOST_TEST_EQ(g.buffer(), "\r\n"); + BOOST_TEST_EQ(g.capacity_in_bytes(), cap); + + // assigning into sufficient capacity reuses it + fields h; + h.reserve(256, 8); + auto const* p = h.buffer().data(); + h = b; + BOOST_TEST_EQ(h.buffer(), b.buffer()); + BOOST_TEST_EQ( + static_cast(h.buffer().data()), + static_cast(p)); + + // an empty source which still owns storage behaves + // like an empty value: a default target does not + // allocate, an owning target keeps its capacity + fields src = { { "A", "1" } }; + src.clear(); + fields k; + k = src; + BOOST_TEST(k.empty()); + BOOST_TEST_EQ(k.capacity_in_bytes(), 0u); + fields m = { { "B", "2" }, { "C", "3" } }; + auto const mcap = m.capacity_in_bytes(); + m = src; + BOOST_TEST(m.empty()); + BOOST_TEST_EQ(m.buffer(), "\r\n"); + BOOST_TEST_EQ(m.capacity_in_bytes(), mcap); + } + + void + testSwap() + { + { + fields a = { { http::field::host, "a" } }; + fields b = { + { http::field::host, "b" }, + { "X-C", "c" }, + }; + auto const acap = a.capacity_in_bytes(); + auto const bcap = b.capacity_in_bytes(); + // a view follows the contents into the + // other container + auto const av = a.begin()[0].value; + + a.swap(b); + BOOST_TEST_EQ(a.size(), 2u); + BOOST_TEST_EQ(a.buffer(), + "Host: b\r\n" + "X-C: c\r\n" + "\r\n"); + BOOST_TEST_EQ(a.capacity_in_bytes(), bcap); + BOOST_TEST_EQ(b.size(), 1u); + BOOST_TEST_EQ(b.buffer(), "Host: a\r\n\r\n"); + BOOST_TEST_EQ(b.capacity_in_bytes(), acap); + BOOST_TEST_EQ(av, "a"); + BOOST_TEST_EQ( + static_cast(av.data()), + static_cast( + b.begin()[0].value.data())); + + // both containers remain usable + a.append("X-A", "1"); + b.append("X-B", "2"); + BOOST_TEST_EQ(a.size(), 3u); + BOOST_TEST_EQ(b.size(), 2u); + } + + // ADL and std::swap find the hidden friend + { + fields a = { { "A", "1" } }; + fields b = { { "B", "2" } }; + swap(a, b); + BOOST_TEST_EQ(a.buffer(), "B: 2\r\n\r\n"); + std::swap(a, b); + BOOST_TEST_EQ(a.buffer(), "A: 1\r\n\r\n"); + } + + // swapping with a default container transfers the + // allocation and the shared static buffer + { + fields a = { { "A", "1" } }; + fields b; + auto const acap = a.capacity_in_bytes(); + a.swap(b); + BOOST_TEST(a.empty()); + BOOST_TEST_EQ(a.buffer(), "\r\n"); + BOOST_TEST_EQ(a.capacity_in_bytes(), 0u); + BOOST_TEST_EQ(b.buffer(), "A: 1\r\n\r\n"); + BOOST_TEST_EQ(b.capacity_in_bytes(), acap); + + // the container which received the static + // buffer allocates on its next modification + a.append("X-A", "2"); + BOOST_TEST_EQ(a.buffer(), "X-A: 2\r\n\r\n"); + } + + // two default containers + { + fields a; + fields b; + a.swap(b); + BOOST_TEST_EQ(a.buffer(), "\r\n"); + BOOST_TEST_EQ(a.capacity_in_bytes(), 0u); + BOOST_TEST_EQ(b.buffer(), "\r\n"); + BOOST_TEST_EQ(b.capacity_in_bytes(), 0u); + } + + // self-swap has no effect + { + fields a = { { "A", "1" } }; + auto& ar = a; + a.swap(ar); + BOOST_TEST_EQ(a.size(), 1u); + BOOST_TEST_EQ(a.buffer(), "A: 1\r\n\r\n"); + } + } + + void + testFromFieldsBase() + { + // the conversion is explicit: turning a view into an + // owning container allocates, and dropping a header's + // start line should be deliberate + static_assert( + std::is_constructible_v); + static_assert( + ! std::is_convertible_v); + + // an owning header is a fields_base carrying a start + // line; converting to fields copies only the fields + request_head h; + h.set_target("/orig"); + h.append(http::field::host, "example.com"); + h.append("X-A", "1"); + h.append("X-A", "2"); + + fields f{ static_cast(h) }; + BOOST_TEST_EQ( + f.buffer(), + "Host: example.com\r\n" + "X-A: 1\r\n" + "X-A: 2\r\n" + "\r\n"); + BOOST_TEST(f.buffer().find("GET") == std::string_view::npos); + BOOST_TEST_EQ(f.count("X-A"), 2u); + // independent storage; field ids survive the copy + BOOST_TEST(f.buffer().data() != h.buffer().data()); + BOOST_TEST(f.begin()[0].id == http::field::host); + + // mutating the source leaves the copy intact + h.set_target("/changed"); + h.set(http::field::host, "other.com"); + BOOST_TEST_EQ(f.at(http::field::host), "example.com"); + + // copying an empty header allocates nothing + request_head e; + fields ef{ static_cast(e) }; + BOOST_TEST(ef.empty()); + BOOST_TEST_EQ(ef.capacity_in_bytes(), 0u); + + // a plain fields is also a fields_base (no prefix) + fields src = { { "A", "1" } }; + fields ff{ static_cast(src) }; + BOOST_TEST_EQ(ff.buffer(), src.buffer()); + } + + void + testAssignFieldsBase() + { + request_head h; + h.append(http::field::host, "example.com"); + h.append("X-A", "1"); + + // assignment drops the prior contents and the start line + fields f = { { "Old", "v" } }; + f = static_cast(h); + BOOST_TEST_EQ( + f.buffer(), + "Host: example.com\r\n" + "X-A: 1\r\n" + "\r\n"); + BOOST_TEST(! f.contains("Old")); + + // assigning an empty header clears + request_head e; + f = static_cast(e); + BOOST_TEST(f.empty()); + BOOST_TEST_EQ(f.buffer(), "\r\n"); + + // self-assignment through a fields_base reference + fields g = { { "A", "1" }, { "B", "2" } }; + fields_base const& gb = g; + g = gb; + BOOST_TEST_EQ(g.size(), 2u); + BOOST_TEST_EQ(g.buffer(), "A: 1\r\nB: 2\r\n\r\n"); + } + + void + testCapacity() + { + fields f; + f.reserve(4096, 64); + auto const cap = f.capacity_in_bytes(); + BOOST_TEST(cap >= 4096 + 64 * 12); + + auto const* p = f.buffer().data(); + for(int i = 0; i < 64; ++i) + f.append("X-Header-Name", "some-value-here"); + BOOST_TEST_EQ(f.size(), 64u); + + // no reallocation happened + BOOST_TEST_EQ(f.capacity_in_bytes(), cap); + BOOST_TEST_EQ( + static_cast(f.buffer().data()), + static_cast(p)); + + f.shrink_to_fit(); + BOOST_TEST(f.capacity_in_bytes() < cap); + BOOST_TEST_EQ(f.size(), 64u); + BOOST_TEST_EQ(f.begin()[63].name, "X-Header-Name"); + BOOST_TEST_EQ(f.begin()[63].value, "some-value-here"); + + // clear keeps the allocation + auto const cap2 = f.capacity_in_bytes(); + f.clear(); + BOOST_TEST(f.empty()); + BOOST_TEST_EQ(f.buffer(), "\r\n"); + BOOST_TEST_EQ(f.capacity_in_bytes(), cap2); + + // shrinking an empty container releases it + f.shrink_to_fit(); + BOOST_TEST_EQ(f.capacity_in_bytes(), 0u); + BOOST_TEST_EQ(f.buffer(), "\r\n"); + + // no-op reserve + fields g; + g.reserve(0, 0); + BOOST_TEST_EQ(g.capacity_in_bytes(), 0u); + g.reserve(2, 0); + BOOST_TEST_EQ(g.capacity_in_bytes(), 0u); + } + + void + testSelfReference() + { + // arguments viewing the container remain valid + fields f; + f.append(http::field::host, "example.com"); + + // growth path: the old buffer is read + f.append("X-Copy", f.begin()[0].value); + BOOST_TEST_EQ(f.begin()[1].value, "example.com"); + + // in-place, source before the insertion point + f.reserve(1024, 16); + f.append("X-Copy2", f.begin()[0].value); + BOOST_TEST_EQ(f.begin()[2].value, "example.com"); + + // in-place, source shifted by the insertion + f.insert(f.begin(), "X-First", f.begin()[1].value); + BOOST_TEST_EQ(f.begin()[0].name, "X-First"); + BOOST_TEST_EQ(f.begin()[0].value, "example.com"); + + // self-referenced name; the id still resolves + f.insert(f.begin(), f.begin()[1].name, "n"); + BOOST_TEST_EQ(f.begin()[0].name, "Host"); + BOOST_TEST(f.begin()[0].id == http::field::host); + + // set from a substring of the field's own value + fields g; + g.append(http::field::host, "www.example.com"); + g.set(http::field::host, g.begin()[0].value.substr(4)); + BOOST_TEST_EQ(g.begin()[0].value, "example.com"); + + // set from a later field's value: shrinking + g.append("X-Tail", "tail-value"); + g.set(http::field::host, g.begin()[1].value); + BOOST_TEST_EQ(g.begin()[0].value, "tail-value"); + BOOST_TEST_EQ(g.begin()[1].value, "tail-value"); + + // set from a later field's value: growing + g.append("X-Long", "a-much-longer-value-than-before"); + g.set(http::field::host, g.begin()[2].value); + BOOST_TEST_EQ(g.begin()[0].value, "a-much-longer-value-than-before"); + BOOST_TEST_EQ(g.begin()[2].value, "a-much-longer-value-than-before"); + BOOST_TEST_EQ(g.begin()[1].value, "tail-value"); + + // set from a later field's value: equal length + g.set("X-Tail", "value-tail"); + g.set(http::field::host, g.begin()[1].value); + BOOST_TEST_EQ(g.begin()[0].value, "value-tail"); + } + + void + testIterators() + { + fields f = { + { "A", "1" }, + { "B", "2" }, + { "C", "3" }, + }; + + auto it = f.begin(); + BOOST_TEST_EQ((*it).name, "A"); + BOOST_TEST_EQ(it->name, "A"); + BOOST_TEST_EQ(it[2].name, "C"); + BOOST_TEST_EQ((it + 2)->name, "C"); + BOOST_TEST_EQ((2 + it)->name, "C"); + + auto it2 = f.end(); + BOOST_TEST_EQ(it2 - it, 3); + --it2; + BOOST_TEST_EQ(it2->name, "C"); + it2 -= 1; + BOOST_TEST_EQ(it2->name, "B"); + it2 = it2 - 1; + BOOST_TEST(it2 == f.begin()); + BOOST_TEST(it < f.end()); + BOOST_TEST(f.end() > it); + BOOST_TEST(it <= f.begin()); + BOOST_TEST(it >= f.begin()); + + it++; + it--; + BOOST_TEST(it == f.begin()); + BOOST_TEST(it != f.end()); + + std::string s; + for(auto r : f) + { + s.append(r.name); + s.push_back('='); + s.append(r.value); + s.push_back(';'); + } + BOOST_TEST_EQ(s, "A=1;B=2;C=3;"); + + // default-constructed iterators compare equal + fields::iterator d1; + fields::iterator d2; + BOOST_TEST(d1 == d2); + } + + void + testBig() + { + fields f; + std::string expected; + for(int i = 0; i < 200; ++i) + { + auto const name = "X-Field-" + std::to_string(i); + auto const value = "v" + std::to_string(i * 7); + f.append(name, value); + expected += name + ": " + value + "\r\n"; + } + expected += "\r\n"; + BOOST_TEST_EQ(f.size(), 200u); + BOOST_TEST_EQ(f.buffer(), expected); + + for(int i = 0; i < 200; ++i) + { + auto const name = "x-field-" + std::to_string(i); + auto it = f.find(name); + BOOST_TEST(it != f.end()); + BOOST_TEST_EQ(it->value, "v" + std::to_string(i * 7)); + } + + // erase every other field + for(int i = 0; i < 200; i += 2) + BOOST_TEST_EQ(f.erase("X-Field-" + std::to_string(i)), 1u); + BOOST_TEST_EQ(f.size(), 100u); + + expected.clear(); + for(int i = 1; i < 200; i += 2) + expected += "X-Field-" + std::to_string(i) + ": v" + + std::to_string(i * 7) + "\r\n"; + expected += "\r\n"; + BOOST_TEST_EQ(f.buffer(), expected); + + // erase by iterator until empty + while(!f.empty()) + f.erase(f.begin()); + BOOST_TEST_EQ(f.buffer(), "\r\n"); + } + + void + testLimits() + { + fields f = { { "X-A", "1" } }; + std::string const big_name(fields::max_name_size + 1, 'a'); + std::string const big_value(fields::max_value_size + 1, 'b'); + + BOOST_TEST_THROWS(f.append(big_name, "v"), std::length_error); + BOOST_TEST_THROWS(f.append("X-B", big_value), std::length_error); + BOOST_TEST_THROWS( + f.append(http::field::host, big_value), std::length_error); + BOOST_TEST_THROWS( + f.insert(f.begin(), big_name, "v"), std::length_error); + BOOST_TEST_THROWS(f.set(f.begin(), big_value), std::length_error); + BOOST_TEST_THROWS(f.set("X-A", big_value), std::length_error); + BOOST_TEST_THROWS( + f.set(http::field::host, big_value), std::length_error); + + // failed operations leave the container unchanged + BOOST_TEST_EQ(f.buffer(), "X-A: 1\r\n\r\n"); + + // impossible reservations; exhausting the total + // buffer size is not tested, as reaching it would + // have to allocate it + fields h; + BOOST_TEST_THROWS( + h.reserve(fields::max_buffer_size + 1, 0), + std::length_error); + BOOST_TEST_THROWS( + h.reserve(std::size_t(-1), 0), + std::length_error); + BOOST_TEST_THROWS( + h.reserve(0, std::size_t(-1) / 12 + 1), + std::length_error); + BOOST_TEST_THROWS( + h.reserve(0, fields::max_buffer_size / 12 + 1), + std::length_error); + // the table shares the limit with the text + BOOST_TEST_THROWS( + h.reserve(fields::max_buffer_size, 1), + std::length_error); + BOOST_TEST_THROWS( + h.reserve( + fields::max_buffer_size - fields::table_space(1) + 1, 1), + std::length_error); + BOOST_TEST_EQ(h.capacity_in_bytes(), 0u); + BOOST_TEST_EQ(h.buffer(), "\r\n"); + } + + void + testInitializerList() + { + // empty list + { + fields f = {}; + BOOST_TEST_EQ(f.buffer(), "\r\n"); + BOOST_TEST_EQ(f.capacity_in_bytes(), 0u); + } + + // mixed constants and strings + { + fields f = { + { http::field::host, "example.com" }, + { "X-Request-Id", "42" }, + { http::field::accept, "text/html" }, + { "aCCEPT", "text/plain" }, + { "X-Empty", "" }, + }; + BOOST_TEST_EQ(f.size(), 5u); + BOOST_TEST_EQ( + f.buffer(), + "Host: example.com\r\n" + "X-Request-Id: 42\r\n" + "Accept: text/html\r\n" + "aCCEPT: text/plain\r\n" + "X-Empty: \r\n" + "\r\n"); + + // string names resolve to constants + BOOST_TEST(f.begin()[0].id == http::field::host); + BOOST_TEST(f.begin()[3].id == http::field::accept); + BOOST_TEST_EQ( + static_cast(f.begin()[1].id), 0); + BOOST_TEST_EQ(f.count(http::field::accept), 2u); + + // a single exact-fit allocation + fields g; + g.reserve(f.buffer().size(), f.size()); + BOOST_TEST_EQ(f.capacity_in_bytes(), g.capacity_in_bytes()); + } + + // assignment through a temporary + { + fields f; + f.append("X-Old", "1"); + f = { { http::field::age, "0" } }; + BOOST_TEST_EQ(f.buffer(), "Age: 0\r\n\r\n"); + } + + // elements copied from another container + { + fields f = { + { http::field::host, "example.com" }, + { "X-Request-Id", "42" }, + }; + fields g = { f.begin()[0], f.begin()[1] }; + BOOST_TEST_EQ(g.buffer(), f.buffer()); + BOOST_TEST(g.begin()[0].id == http::field::host); + BOOST_TEST_EQ( + static_cast(g.begin()[1].id), 0); + } + + // limits are enforced + { + std::string const big_value( + fields::max_value_size + 1, 'b'); + BOOST_TEST_THROWS( + (fields{ { "X-B", big_value } }), + std::length_error); + } + } + + void + testOstream() + { + fields f; + std::ostringstream os; + os << f; + BOOST_TEST_EQ(os.str(), ""); + + f.append(http::field::host, "example.com"); + f.append("X-Custom", "v"); + os << f; + BOOST_TEST_EQ(os.str(), + "Host: example.com\n" + "X-Custom: v\n"); + } + + void + run() + { + testDefault(); + testAppend(); + testAppendList(); + testFindFrom(); + testFindAll(); + testSet(); + testInsert(); + testErase(); + testEraseAll(); + testSpecialFields(); + testAt(); + testSetIterator(); + testFindLast(); + testReverseIterators(); + testCopyMove(); + testSwap(); + testFromFieldsBase(); + testAssignFieldsBase(); + testCapacity(); + testSelfReference(); + testIterators(); + testBig(); + testLimits(); + testInitializerList(); + testOstream(); + } +}; + +TEST_SUITE(fields_test, "boost.burl.fields"); + +} // namespace burl +} // namespace boost diff --git a/test/unit/head_parser.cpp b/test/unit/head_parser.cpp new file mode 100644 index 0000000..b4e5fa6 --- /dev/null +++ b/test/unit/head_parser.cpp @@ -0,0 +1,1569 @@ +// +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +// Test that header file is self-contained. +#include + +#include + +#include + +#include "test_suite.hpp" + +#include +#include +#include +#include +#include + +namespace boost +{ +namespace burl +{ + +class head_parser_test +{ + alignas(4) char buf_[4096]; + + // copy s into the parser's prepare region, + // commit it, and parse + static + system::error_code + feed( + head_parser& pr, + std::string_view s) + { + auto const mb = pr.prepare(); + BOOST_ASSERT(mb.size() >= s.size()); + std::memcpy(mb.data(), s.data(), s.size()); + pr.commit(s.size()); + system::error_code ec; + pr.parse(ec); + return ec; + } + +public: + void + testRequest() + { + std::string_view const msg = + "GET /index.html HTTP/1.1\r\n" + "Host: example.com\r\n" + "User-Agent: burl \r\n" + "X-Empty:\r\n" + "X-Ows: \t \r\n" + "Set-Cookie: a=1\r\n" + "Set-Cookie: b=2\r\n" + "\r\n"; + std::string const body = "BODYBYTES"; + + head_parser pr(true, buf_, sizeof(buf_)); + auto const mb = pr.prepare(); + BOOST_TEST(mb.data() == buf_); + std::memcpy(mb.data(), msg.data(), msg.size()); + std::memcpy( + static_cast(mb.data()) + msg.size(), + body.data(), + body.size()); + pr.commit(msg.size() + body.size()); + system::error_code ec; + pr.parse(ec); + BOOST_TEST(!ec); + + auto const& h = pr.request_head(); + BOOST_TEST(h.method() == http::method::get); + BOOST_TEST_EQ(h.method_text(), "GET"); + BOOST_TEST_EQ(h.target(), "/index.html"); + BOOST_TEST(h.version() == http::version::http_1_1); + + // the header is returned in its original place + BOOST_TEST_EQ(h.buffer(), msg); + BOOST_TEST(h.buffer().data() == buf_); + BOOST_TEST_EQ(h.size(), 6u); + + BOOST_TEST_EQ(h.at(http::field::host), "example.com"); + BOOST_TEST(h.at(http::field::host).data() > buf_); + BOOST_TEST(h.at(http::field::host).data() < buf_ + msg.size()); + + // surrounding whitespace is excluded from values + BOOST_TEST_EQ(h.at(http::field::user_agent), "burl"); + BOOST_TEST_EQ(h.at("X-Empty"), ""); + BOOST_TEST_EQ(h.at("X-Ows"), ""); + + // names as received; lookups case-insensitive + BOOST_TEST_EQ(h.begin()[0].name, "Host"); + BOOST_TEST(h.begin()[0].id == http::field::host); + BOOST_TEST(h.contains("x-empty")); + + std::string all; + for(auto v : h.find_all(http::field::set_cookie)) + { + all.append(v); + all.push_back(';'); + } + BOOST_TEST_EQ(all, "a=1;b=2;"); + + // payload bytes after the header are + // untouched and reported as leftovers + BOOST_TEST_EQ( + std::string_view(buf_ + msg.size(), body.size()), body); + BOOST_TEST(pr.leftovers().data() == buf_ + msg.size()); + BOOST_TEST_EQ(pr.leftovers().size(), body.size()); + + // the table reserve is withheld from the + // writable region, before and after + // completion alike + auto const reserve = message_head_base::table_space( + pr.limits().max_fields); + BOOST_TEST( + pr.prepare().data() == buf_ + msg.size() + body.size()); + BOOST_TEST_EQ( + pr.prepare().size(), + sizeof(buf_) - reserve - msg.size() - body.size()); + + // parsing again is a no-op success + pr.parse(ec); + BOOST_TEST(!ec); + } + + void + testIncremental() + { + std::string_view const msg = + "POST /submit HTTP/1.0\r\n" + "Content-Length: 11\r\n" + "Connection: keep-alive\r\n" + "\r\n"; + + head_parser pr(true, buf_, sizeof(buf_)); + system::error_code ec; + for(char const c : msg) + { + pr.parse(ec); + BOOST_TEST(ec == http::error::need_data); + *static_cast(pr.prepare().data()) = c; + pr.commit(1); + } + pr.parse(ec); + BOOST_TEST(!ec); + + auto const& h = pr.request_head(); + BOOST_TEST(h.method() == http::method::post); + BOOST_TEST_EQ(h.target(), "/submit"); + BOOST_TEST(h.version() == http::version::http_1_0); + BOOST_TEST(h.payload() == http::payload::size); + BOOST_TEST_EQ(h.content_length(), 11u); + BOOST_TEST(h.keep_alive()); + } + + void + testResponse() + { + std::string_view const msg = + "HTTP/1.0 404 Not Found\r\n" + "Content-Length: 5\r\n" + "\r\n"; + + head_parser pr(false, buf_, sizeof(buf_)); + BOOST_TEST(! feed(pr, msg)); + + auto const& h = pr.response_head(); + BOOST_TEST(h.status() == http::status::not_found); + BOOST_TEST_EQ(h.status_int(), 404); + BOOST_TEST_EQ(h.reason(), "Not Found"); + BOOST_TEST(h.version() == http::version::http_1_0); + BOOST_TEST(h.payload() == http::payload::size); + BOOST_TEST_EQ(*h.content_length(), 5u); + BOOST_TEST_EQ(h.buffer(), msg); + + // HTTP/1.0 without keep-alive closes + BOOST_TEST(!h.keep_alive()); + } + + void + testResponseVariants() + { + // no reason-phrase, no space + { + head_parser pr(false, buf_, sizeof(buf_)); + std::string_view const msg = "HTTP/1.1 204\r\n\r\n"; + BOOST_TEST(! feed(pr, msg)); + auto const& h = pr.response_head(); + BOOST_TEST_EQ(h.status_int(), 204); + BOOST_TEST_EQ(h.reason(), ""); + BOOST_TEST(h.payload() == http::payload::none); + } + + // no reason-phrase, trailing space + { + head_parser pr(false, buf_, sizeof(buf_)); + std::string_view const msg = "HTTP/1.1 200 \r\n\r\n"; + BOOST_TEST(! feed(pr, msg)); + auto const& h = pr.response_head(); + BOOST_TEST_EQ(h.status_int(), 200); + BOOST_TEST_EQ(h.reason(), ""); + BOOST_TEST(h.payload() == http::payload::to_eof); + BOOST_TEST(!h.keep_alive()); + } + + // unknown status code + { + head_parser pr(false, buf_, sizeof(buf_)); + std::string_view const msg = "HTTP/1.1 599 Whatever\r\n\r\n"; + BOOST_TEST(! feed(pr, msg)); + BOOST_TEST( + pr.response_head().status() == http::status::unknown); + BOOST_TEST_EQ(pr.response_head().status_int(), 599); + } + + // protocol switch + { + head_parser pr(false, buf_, sizeof(buf_)); + std::string_view const msg = + "HTTP/1.1 101 Switching Protocols\r\n" + "Connection: upgrade\r\n" + "Upgrade: websocket\r\n" + "\r\n"; + BOOST_TEST(! feed(pr, msg)); + auto const& h = pr.response_head(); + BOOST_TEST(h.upgrade()); + BOOST_TEST(h.payload() == http::payload::none); + } + } + + void + testFraming() + { + // Content-Length + { + head_parser pr(true, buf_, sizeof(buf_)); + auto const ec = feed( + pr, + "PUT / HTTP/1.1\r\n" + "Content-Length: 5\r\n" + "\r\n"); + BOOST_TEST(!ec); + BOOST_TEST( + pr.request_head().payload() == http::payload::size); + BOOST_TEST_EQ( + pr.request_head().content_length().value_or(0), 5); + } + + // a coding other than chunked in a response is + // read to EOF + { + head_parser pr(false, buf_, sizeof(buf_)); + auto const ec = feed( + pr, + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: gzip\r\n" + "\r\n"); + BOOST_TEST(!ec); + BOOST_TEST( + pr.response_head().payload() == http::payload::to_eof); + } + + // chunked framing + { + head_parser pr(true, buf_, sizeof(buf_)); + auto const ec = feed( + pr, + "PUT / HTTP/1.1\r\n" + "Transfer-Encoding: gzip, chunked\r\n" + "\r\n"); + BOOST_TEST(!ec); + BOOST_TEST( + pr.request_head().payload() == http::payload::chunked); + } + + // connection close + { + head_parser pr(true, buf_, sizeof(buf_)); + auto const ec = feed( + pr, + "GET / HTTP/1.1\r\n" + "Connection: close\r\n" + "\r\n"); + BOOST_TEST(!ec); + BOOST_TEST(!pr.request_head().keep_alive()); + } + + // an Expect field counts only when its token + // list contains 100-continue + { + head_parser pr(true, buf_, sizeof(buf_)); + auto const ec = feed( + pr, + "PUT / HTTP/1.1\r\n" + "Content-Length: 5\r\n" + "Expect: 100-continue\r\n" + "\r\n"); + BOOST_TEST(!ec); + BOOST_TEST(pr.request_head().expect_100_continue()); + } + { + head_parser pr(true, buf_, sizeof(buf_)); + auto const ec = feed( + pr, + "PUT / HTTP/1.1\r\n" + "Content-Length: 5\r\n" + "Expect: nope\r\n" + "\r\n"); + BOOST_TEST(!ec); + BOOST_TEST(!pr.request_head().expect_100_continue()); + } + } + + void + testObsFold() + { + // each obs-fold is resolved in place: the CRLF + // is overwritten with spaces, leaving the value + // contiguous and the field on one logical line + { + std::string_view const msg = + "GET / HTTP/1.1\r\n" + "X: 1\r\n continued\r\n" + "Y: a\r\n\tb\r\n\tc\r\n" + "Host: example.com\r\n" + "\r\n"; + + head_parser pr(true, buf_, sizeof(buf_)); + BOOST_TEST(! feed(pr, msg)); + auto const& h = pr.request_head(); + BOOST_TEST_EQ(h.size(), 3u); + BOOST_TEST_EQ(h.at("X"), "1 continued"); + BOOST_TEST_EQ(h.at("Y"), "a \tb \tc"); + BOOST_TEST_EQ(h.at(http::field::host), "example.com"); + // the buffer holds the resolved header + BOOST_TEST_EQ( + h.buffer(), + "GET / HTTP/1.1\r\n" + "X: 1 continued\r\n" + "Y: a \tb \tc\r\n" + "Host: example.com\r\n" + "\r\n"); + } + + // a fold is invisible to the cached state + { + std::string_view const msg = + "PUT /u HTTP/1.1\r\n" + "Transfer-Encoding: gzip,\r\n chunked\r\n" + "\r\n"; + + head_parser pr(true, buf_, sizeof(buf_)); + BOOST_TEST(! feed(pr, msg)); + BOOST_TEST(pr.request_head().chunked()); + } + + // the continuation is awaited: a field line is + // complete only once the byte which follows its + // CRLF has arrived + { + std::string_view const msg = + "GET / HTTP/1.1\r\n" + "X: 1\r\n continued\r\n" + "\r\n"; + + head_parser pr(true, buf_, sizeof(buf_)); + system::error_code ec; + // bytes are appended through prepare + // and committed once; those already + // parsed are resolved in place and + // never rewritten + for(char const c : msg) + { + pr.parse(ec); + BOOST_TEST(ec == http::error::need_data); + *static_cast(pr.prepare().data()) = c; + pr.commit(1); + } + pr.parse(ec); + BOOST_TEST(!ec); + BOOST_TEST_EQ(pr.request_head().at("X"), "1 continued"); + } + + // a fold may arrive before any field content; + // the ws it bares is not part of the value + { + std::string_view const msg = + "GET / HTTP/1.1\r\n" + "X:\r\n foo\r\n" + "\r\n"; + + head_parser pr(true, buf_, sizeof(buf_)); + BOOST_TEST(! feed(pr, msg)); + BOOST_TEST_EQ(pr.request_head().at("X"), "foo"); + } + + // a fold which bares only whitespace yields + // an empty value + { + std::string_view const msg = + "GET / HTTP/1.1\r\n" + "X:\r\n \r\n" + "Host: example.com\r\n" + "\r\n"; + + head_parser pr(true, buf_, sizeof(buf_)); + BOOST_TEST(! feed(pr, msg)); + BOOST_TEST_EQ(pr.request_head().at("X"), ""); + } + + // a payload which begins with whitespace does + // not continue the final empty line + { + std::string_view const msg = + "PUT /u HTTP/1.1\r\n" + "Content-Length: 6\r\n" + "\r\n" + " body "; + + head_parser pr(true, buf_, sizeof(buf_)); + BOOST_TEST(! feed(pr, msg)); + auto const& h = pr.request_head(); + BOOST_TEST_EQ(h.size(), 1u); + BOOST_TEST_EQ( + std::string_view(buf_ + h.buffer().size(), 6), " body "); + } + } + + // The parser folds each field into the cached + // state as it arrives, while a header built by + // hand recomputes it from the whole field + // section. For fields the parser accepts, the two + // must arrive at the same state. + void + testFoldMatchesRescan() + { + auto const check = [&]( + std::initializer_list< + std::pair> fields) + { + std::string msg = "PUT /u HTTP/1.1\r\n"; + request_head h(http::method::put, "/u"); + for(auto const& f : fields) + { + msg += http::to_string(f.first); + msg += ": "; + msg += f.second; + msg += "\r\n"; + h.append(f.first, f.second); + } + msg += "\r\n"; + + head_parser pr(true, buf_, sizeof(buf_)); + if(! BOOST_TEST(! feed(pr, msg))) + return; + auto const& p = pr.request_head(); + // every framing contradiction is rejected, + // so a parsed header never reports one + BOOST_TEST(p.payload() != http::payload::error); + BOOST_TEST_EQ(p.buffer(), h.buffer()); + BOOST_TEST(p.payload() == h.payload()); + BOOST_TEST(p.content_length() == h.content_length()); + BOOST_TEST_EQ(p.chunked(), h.chunked()); + BOOST_TEST_EQ(p.keep_alive(), h.keep_alive()); + BOOST_TEST_EQ(p.upgrade(), h.upgrade()); + BOOST_TEST_EQ( + p.expect_100_continue(), h.expect_100_continue()); + }; + + check({ { http::field::content_length, "0" } }); + check({ { http::field::content_length, "5" } }); + check({ { http::field::transfer_encoding, "chunked" } }); + check({ { http::field::transfer_encoding, "gzip, chunked" } }); + check({ { http::field::transfer_encoding, "gzip" }, + { http::field::transfer_encoding, "chunked" } }); + check({ { http::field::transfer_encoding, "chunked" }, + { http::field::connection, "close" } }); + check({ { http::field::connection, "close" } }); + check({ { http::field::expect, "100-continue" }, + { http::field::content_length, "3" } }); + check({ { http::field::connection, "keep-alive" }, + { http::field::connection, "upgrade" }, + { http::field::upgrade, "websocket" } }); + check({ { http::field::expect, "100-continue" } }); + check({ { http::field::host, "example.com" }, + { http::field::content_length, "12" } }); + } + + void + testRejectedHeader() + { + // a header rejected by the final framing rule is + // whole, so the caller can inspect it + { + head_parser pr(true, buf_, sizeof(buf_)); + auto const ec = feed( + pr, + "PUT /u HTTP/1.1\r\n" + "Host: example.com\r\n" + "Transfer-Encoding: gzip\r\n" + "\r\n"); + BOOST_TEST(ec == http::error::bad_transfer_encoding); + auto const& h = pr.request_head(); + BOOST_TEST_EQ( + h.buffer(), + "PUT /u HTTP/1.1\r\n" + "Host: example.com\r\n" + "Transfer-Encoding: gzip\r\n" + "\r\n"); + BOOST_TEST_EQ(h.target(), "/u"); + BOOST_TEST_EQ(h.at(http::field::host), "example.com"); + } + + // a field rejected as it arrives is not part of + // the header at all: the cached state describes + // the fields which preceded it + { + head_parser pr(true, buf_, sizeof(buf_)); + auto const ec = feed( + pr, + "PUT /u HTTP/1.1\r\n" + "Content-Length: 5\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n"); + BOOST_TEST(ec == http::error::bad_payload); + auto const& h = pr.request_head(); + BOOST_TEST_EQ(h.size(), 1u); + BOOST_TEST(! h.contains(http::field::transfer_encoding)); + BOOST_TEST_EQ(h.content_length().value_or(0), 5u); + } + } + + void + testErrors() + { + auto const check = [&]( + bool is_request, + std::string_view msg, + http::error e, + header_limits const& limits = {}) + { + head_parser pr(is_request, buf_, sizeof(buf_), limits); + system::error_code ec = feed(pr, msg); + BOOST_TEST(ec == e); + // errors are sticky + pr.parse(ec); + BOOST_TEST(ec == e); + }; + + // bare LF + check( + true, + "GET / HTTP/1.1\nHost: x\r\n\r\n", + http::error::bad_line_ending); + + // malformed methods + check(true, " / HTTP/1.1\r\n\r\n", http::error::bad_method); + check(true, "GE\x01T / HTTP/1.1\r\n\r\n", http::error::bad_method); + + // malformed targets + check(true, "GET HTTP/1.1\r\n\r\n", http::error::bad_request_target); + check( + true, + "GET /a\x7F" "b HTTP/1.1\r\n\r\n", + http::error::bad_request_target); + + // malformed versions + check(true, "GET / HTTP/2.0\r\n\r\n", http::error::bad_version); + check(true, "GET / HTP/1.1\r\n\r\n", http::error::bad_version); + check(false, "HTP/1.1 200 OK\r\n\r\n", http::error::bad_version); + + // the request line must end in CRLF right + // after the version + check(true, "GET / HTTP/1.1X\r\n\r\n", http::error::bad_line_ending); + check(true, "GET / HTTP/1.1\rX\r\n", http::error::bad_line_ending); + + // an empty target + check(true, "GET HTTP/1.1\r\n\r\n", http::error::bad_request_target); + + // malformed status lines + check(false, "HTTP/1.1 200\r\n", http::error::need_data); + check(false, "HTTP/1.1 x00 OK\r\n\r\n", http::error::bad_status_code); + check(false, "HTTP/1.1 2x0 OK\r\n\r\n", http::error::bad_status_code); + check(false, "HTTP/1.1 2000 OK\r\n\r\n", http::error::bad_status_code); + check(false, "HTTP/1.1 20\r\n\r\n", http::error::bad_status_code); + check(false, "HTTP/1.1200 OK\r\n\r\n", http::error::bad_version); + + // malformed reason phrases + check(false, "HTTP/1.1 200 O\x01K\r\n\r\n", http::error::bad_reason); + check(false, "HTTP/1.1 200 OK\rX\r\n\r\n", http::error::bad_line_ending); + + // malformed fields + check( + true, + "GET / HTTP/1.1\r\nHost : x\r\n\r\n", + http::error::bad_field_name); + check( + true, + "GET / HTTP/1.1\r\n: x\r\n\r\n", + http::error::bad_field_name); + check( + true, + "GET / HTTP/1.1\r\nNoColon\r\n\r\n", + http::error::bad_field_name); + check( + true, + "GET / HTTP/1.1\r\nX: a\x01z\r\n\r\n", + http::error::bad_field_value); + check( + true, + "GET / HTTP/1.1\r\nX: a\x7Fz\r\n\r\n", + http::error::bad_field_value); + + // an obs-fold with no field line to continue + check( + true, + "GET / HTTP/1.1\r\n continued\r\n\r\n", + http::error::bad_field_name); + + // a bare LF ending the continuation of a fold + check( + true, + "GET / HTTP/1.1\r\nX: 1\r\n continued\n\r\n", + http::error::bad_line_ending); + + // a field-section terminator whose CR is not + // followed by LF + check( + true, + "GET / HTTP/1.1\r\nHost: x\r\n\rZ\r\n", + http::error::bad_line_ending); + + // a folded field line is measured whole + check( + true, + "GET / HTTP/1.1\r\nX: 1\r\n continued\r\n\r\n", + http::error::field_size_limit, + { .max_field = 12 }); + + // framing which cannot be trusted is rejected + // as the field which introduces it is parsed, + // in either order + check( + true, + "PUT / HTTP/1.1\r\n" + "Content-Length: 5\r\n" + "Transfer-Encoding: chunked\r\n\r\n", + http::error::bad_payload); + check( + false, + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "Content-Length: 5\r\n\r\n", + http::error::bad_payload); + // a no-payload status does not excuse it + check( + false, + "HTTP/1.1 204 No Content\r\n" + "Content-Length: 5\r\n" + "Transfer-Encoding: chunked\r\n\r\n", + http::error::bad_payload); + + // a Content-Length which is not a single + // decimal number + check( + true, + "PUT / HTTP/1.1\r\nContent-Length: abc\r\n\r\n", + http::error::bad_content_length); + check( + true, + "PUT / HTTP/1.1\r\nContent-Length:\r\n\r\n", + http::error::bad_content_length); + check( + true, + "PUT / HTTP/1.1\r\nContent-Length: 5, 5\r\n\r\n", + http::error::bad_content_length); + // one past the largest representable value + check( + true, + "PUT / HTTP/1.1\r\n" + "Content-Length: 18446744073709551616\r\n\r\n", + http::error::bad_content_length); + // the largest representable value parses + { + head_parser pr(true, buf_, sizeof(buf_)); + BOOST_TEST(! feed( + pr, + "PUT / HTTP/1.1\r\n" + "Content-Length: 18446744073709551615\r\n\r\n")); + BOOST_TEST_EQ( + pr.request_head().content_length().value(), + std::uint64_t(-1)); + } + + // field lines join into a list, so any + // duplicate Content-Length is an error, even + // when the values agree + check( + true, + "PUT / HTTP/1.1\r\n" + "Content-Length: 5\r\nContent-Length: 6\r\n\r\n", + http::error::multiple_content_length); + check( + true, + "PUT / HTTP/1.1\r\n" + "Content-Length: 5\r\nContent-Length: 5\r\n\r\n", + http::error::multiple_content_length); + + // a Transfer-Encoding which is not a token list + check( + true, + "PUT / HTTP/1.1\r\nTransfer-Encoding: @\r\n\r\n", + http::error::bad_transfer_encoding); + + // "chunked" must be applied exactly once, as + // the final coding of the joined value + check( + true, + "PUT / HTTP/1.1\r\n" + "Transfer-Encoding: chunked, chunked\r\n\r\n", + http::error::bad_transfer_encoding); + check( + true, + "PUT / HTTP/1.1\r\n" + "Transfer-Encoding: chunked\r\n" + "Transfer-Encoding: chunked\r\n\r\n", + http::error::bad_transfer_encoding); + check( + true, + "PUT / HTTP/1.1\r\n" + "Transfer-Encoding: chunked\r\n" + "Transfer-Encoding: gzip\r\n\r\n", + http::error::bad_transfer_encoding); + check( + false, + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked, gzip\r\n\r\n", + http::error::bad_transfer_encoding); + + // the length of a request framed by a coding + // other than chunked cannot be determined. + // Whether a later field appends "chunked" is + // unknown until the field section ends, so this + // is rejected there rather than as the field + // arrives + check( + true, + "PUT / HTTP/1.1\r\n" + "Transfer-Encoding: gzip\r\n\r\n", + http::error::bad_transfer_encoding); + check( + true, + "PUT / HTTP/1.1\r\n" + "Transfer-Encoding: gzip\r\n" + "Transfer-Encoding: deflate\r\n\r\n", + http::error::bad_transfer_encoding); + + // HTTP/1.0 has no transfer codings + check( + true, + "PUT / HTTP/1.0\r\nTransfer-Encoding: chunked\r\n\r\n", + http::error::bad_transfer_encoding); + check( + false, + "HTTP/1.0 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n", + http::error::bad_transfer_encoding); + + // limits + check( + true, + "GET /averyveryverylongtarget HTTP/1.1\r\n\r\n", + http::error::start_line_limit, + { .max_start_line = 16 }); + check( + true, + "GET / HTTP/1.1\r\nX-Long: a-value-beyond-the-limit\r\n\r\n", + http::error::field_size_limit, + { .max_field = 16 }); + check( + true, + "GET / HTTP/1.1\r\nA: 1\r\nB: 2\r\n\r\n", + http::error::fields_limit, + { .max_fields = 1 }); + check( + true, + "GET / HTTP/1.1\r\nA: 1\r\nB: 2\r\n\r\n", + http::error::headers_limit, + { .max_size = 20 }); + } + + void + testLimits() + { + // A request whose start line is `sl` bytes and + // which carries `nf` field lines of `fl` bytes + // each, counting delimiters. + auto const msg = []( + std::size_t sl, + std::size_t nf, + std::size_t fl) + { + std::string s = + "GET /" + std::string(sl - 16, 'a') + + " HTTP/1.1\r\n"; + for(std::size_t i = 0; i < nf; ++i) + { + auto const name = "X" + std::to_string(i); + s += name + ": " + + std::string(fl - name.size() - 4, 'v') + + "\r\n"; + } + return s + "\r\n"; + }; + + // Every limit is a maximum, so a header which + // reaches it exactly must be accepted. Each case + // is parsed twice: in one call, and one byte at a + // time. Both must reach the same verdict, which + // is what proves a limit cannot be reported + // before the bytes which violate it have arrived. + auto const check = [&]( + std::string_view s, + http::error e, + header_limits const& limits = {}) + { + { + head_parser pr( + true, buf_, sizeof(buf_), limits); + auto const ec = feed(pr, s); + if(e == http::error::success) + BOOST_TEST(! ec); + else + BOOST_TEST(ec == e); + } + { + head_parser pr( + true, buf_, sizeof(buf_), limits); + system::error_code ec; + for(std::size_t n = 0;; ++n) + { + pr.parse(ec); + if(ec != http::error::need_data || + n == s.size()) + break; + *static_cast( + pr.prepare().data()) = s[n]; + pr.commit(1); + } + if(e == http::error::success) + BOOST_TEST(! ec); + else + BOOST_TEST(ec == e); + } + }; + + // max_size, on a 52-byte header + auto const h52 = msg(20, 3, 10); + BOOST_TEST_EQ(h52.size(), 52u); + check(h52, http::error::headers_limit, { .max_size = 51 }); + check(h52, http::error::success, { .max_size = 52 }); + check(h52, http::error::success, { .max_size = 53 }); + + // max_size also bounds the start line, and the + // remaining budget cannot wrap once it does + auto const h100 = msg(100, 1, 10); + check(h100, http::error::start_line_limit, + { .max_size = 50, .max_start_line = 4096 }); + check(h100, http::error::start_line_limit, + { .max_size = 99, .max_start_line = 4096 }); + // the start line fits exactly, leaving nothing + // for the field section + check(h100, http::error::headers_limit, + { .max_size = 100, .max_start_line = 4096 }); + + // max_start_line binds when it is the smaller of + // the two, and admits a start line which reaches + // it exactly + auto const h40 = msg(40, 1, 10); + check(h40, http::error::start_line_limit, + { .max_start_line = 39 }); + check(h40, http::error::success, { .max_start_line = 40 }); + + // max_field, on 40-byte field lines. A field line + // is complete only once the byte after its CRLF + // has arrived, so the window given to the field + // parser is one byte wider than the limit + auto const f40 = msg(20, 2, 40); + check(f40, http::error::field_size_limit, { .max_field = 39 }); + check(f40, http::error::success, { .max_field = 40 }); + check(f40, http::error::success, { .max_field = 41 }); + + // max_fields, with four present + auto const n4 = msg(20, 4, 10); + check(n4, http::error::fields_limit, { .max_fields = 3 }); + check(n4, http::error::success, { .max_fields = 4 }); + + // a limit still fires when the bytes which + // violate it are followed by unrelated ones + check(h52 + std::string(200, 'Z'), + http::error::headers_limit, { .max_size = 40 }); + + // the same boundary at the default max_field, + // which needs a buffer of its own + { + alignas(4) char big[8192]; + auto const field = [](std::size_t n) + { + return "GET / HTTP/1.1\r\nX: " + + std::string(n - 5, 'v') + "\r\n\r\n"; + }; + auto const at = field(4096); + head_parser pr(true, big, sizeof(big)); + BOOST_TEST(! feed(pr, at)); + BOOST_TEST_EQ( + pr.request_head().at("X").size(), 4096u - 5u); + + auto const over = field(4097); + head_parser pr2(true, big, sizeof(big)); + BOOST_TEST( + feed(pr2, over) == http::error::field_size_limit); + } + } + + void + testTableSpace() + { + // the table reserve leaves no room to + // receive anything: the parser reports + // that it cannot proceed in place + alignas(4) char tiny[64]; + + head_parser pr(true, tiny, sizeof(tiny)); + BOOST_TEST_EQ( + pr.prepare().size(), 0u); // default max_fields = 100 + system::error_code ec; + pr.parse(ec); + BOOST_TEST(ec == http::error::in_place_overflow); + // the error is derived again on request + pr.parse(ec); + BOOST_TEST(ec == http::error::in_place_overflow); + + // with fitting limits the same buffer works + head_parser pr2(true, tiny, sizeof(tiny), { .max_fields = 3 }); + std::string_view const msg2 = "GET / HTTP/1.1\r\nHost: x\r\n\r\n"; + BOOST_TEST(! feed(pr2, msg2)); + BOOST_TEST_EQ(pr2.request_head().at(http::field::host), "x"); + } + + void + testOstream() + { + head_parser pr(true, buf_, sizeof(buf_)); + BOOST_TEST(!feed(pr, + "GET / HTTP/1.1\r\n" + "Host: example.com\r\n" + "User-Agent: burl \r\n" + "\r\n")); + + std::ostringstream os; + os << pr.request_head(); + BOOST_TEST_EQ(os.str(), + "GET / HTTP/1.1\n" + "Host: example.com\n" + "User-Agent: burl\n"); + + // through a fields_base lens only the + // field section is written + os.str({}); + os << static_cast(pr.message_head()); + BOOST_TEST_EQ(os.str(), + "Host: example.com\n" + "User-Agent: burl\n"); + + head_parser ps(false, buf_, sizeof(buf_)); + BOOST_TEST(!feed(ps, + "HTTP/1.1 200 OK\r\n" + "Content-Length: 0\r\n" + "\r\n")); + + os.str({}); + os << ps.response_head(); + BOOST_TEST_EQ(os.str(), + "HTTP/1.1 200 OK\n" + "Content-Length: 0\n"); + + // streamable before parse succeeds; no + // start line has been parsed yet + head_parser pe(true, buf_, sizeof(buf_)); + os.str({}); + os << pe.message_head(); + BOOST_TEST_EQ(os.str(), ""); + } + + void + testMove() + { + alignas(4) char buf[256]; + std::string_view const msg = + "GET /index HTTP/1.1\r\n" + "Host: example.com\r\n" + "\r\n"; + + // move mid-parse: the new object resumes + // where the source stopped, carrying the + // limits with it + header_limits const lim{ .max_fields = 4 }; + head_parser pr(true, buf, sizeof(buf), lim); + system::error_code ec; + std::memcpy(pr.prepare().data(), msg.data(), 21); + pr.commit(21); + pr.parse(ec); + BOOST_TEST(ec == http::error::need_data); + + // the new object continues from the same + // committed bytes + head_parser pr2(std::move(pr)); + BOOST_TEST(pr2.prepare().data() == buf + 21); + BOOST_TEST(! feed(pr2, msg.substr(21))); + auto const& h = pr2.request_head(); + BOOST_TEST_EQ(h.target(), "/index"); + BOOST_TEST_EQ(h.at(http::field::host), "example.com"); + BOOST_TEST_EQ(pr2.message_head().buffer(), msg); + + // move assignment from a fresh parser + // re-arms over the same buffer + pr2 = head_parser(true, buf, sizeof(buf), lim); + std::string_view const msg2 = + "POST /submit HTTP/1.1\r\n\r\n"; + BOOST_TEST(! feed(pr2, msg2)); + BOOST_TEST_EQ( + pr2.request_head().method(), http::method::post); + + // self move-assignment is a no-op + auto& pr2r = pr2; + pr2 = std::move(pr2r); + BOOST_TEST_EQ(pr2.request_head().target(), "/submit"); + + // default construction: zero-size buffer, + // no room to receive anything + head_parser pr3; + BOOST_TEST_EQ(pr3.prepare().size(), 0u); + pr3.parse(ec); + BOOST_TEST(ec == http::error::in_place_overflow); + } + + void + testReset() + { + alignas(4) char buf[256]; + + // parse to completion, then re-arm in place + // and parse a different header over the same + // buffer + head_parser pr(true, buf, sizeof(buf), { .max_fields = 4 }); + std::string_view const msg1 = + "GET /a HTTP/1.1\r\nHost: a\r\n\r\n"; + BOOST_TEST(! feed(pr, msg1)); + BOOST_TEST_EQ(pr.request_head().target(), "/a"); + + pr.reset(); + BOOST_TEST(! pr.got_some()); + BOOST_TEST(pr.prepare().data() == buf); + std::string_view const msg2 = + "POST /bb HTTP/1.1\r\nHost: b\r\nX: y\r\n\r\n"; + BOOST_TEST(! feed(pr, msg2)); + BOOST_TEST(pr.request_head().method() == http::method::post); + BOOST_TEST_EQ(pr.request_head().target(), "/bb"); + BOOST_TEST_EQ(pr.request_head().at(http::field::host), "b"); + + // the limits configured at construction + // survive the reset + BOOST_TEST_EQ(pr.limits().max_fields, 4u); + } + + void + testPipelined() + { + // a second message follows the first into + // the buffer. Between messages a connection + // re-arms the parser, moves the leftovers + // down, and commits them back; they always + // fit within the fresh window + alignas(4) char buf[256]; + std::string_view const msg1 = + "GET /a HTTP/1.1\r\nHost: a\r\n\r\n"; + std::string_view const msg2 = + "POST /b HTTP/1.1\r\n\r\n"; + + head_parser pr(true, buf, sizeof(buf), { .max_fields = 4 }); + BOOST_TEST(! pr.got_some()); + auto const both = + std::string(msg1) + std::string(msg2); + BOOST_TEST(! feed(pr, both)); + BOOST_TEST(pr.got_some()); + auto const lo = pr.leftovers(); + BOOST_TEST(lo.data() == buf + msg1.size()); + BOOST_TEST_EQ( + std::string_view( + static_cast(lo.data()), lo.size()), + msg2); + + std::memmove(buf, lo.data(), lo.size()); + pr.reset(lo.size()); + BOOST_TEST(pr.got_some()); + system::error_code ec; + pr.parse(ec); + BOOST_TEST(! ec); + BOOST_TEST_EQ(pr.request_head().target(), "/b"); + BOOST_TEST_EQ(pr.leftovers().size(), 0u); + } + + void + testBufferGeometry() + { + std::string_view const msg = + "GET / HTTP/1.1\r\nHost: x\r\n\r\n"; + header_limits const lim{ .max_fields = 1 }; + + // any buffer address and size is accepted; + // the end is aligned down for the table + { + alignas(4) char raw[64]; + head_parser pr(true, raw + 1, 41, lim); + // raw + 42 aligns down to raw + 40: + // 39 usable bytes, 12 reserved + BOOST_TEST_EQ(pr.prepare().size(), 27u); + BOOST_TEST(pr.prepare().data() == raw + 1); + BOOST_TEST(! feed(pr, msg)); + BOOST_TEST_EQ( + pr.request_head().at(http::field::host), "x"); + } + + // sizes too small for any storage leave the + // parser empty + { + alignas(4) char raw[16]; + for(std::size_t n = 0; n <= 12; ++n) + { + head_parser pr(true, raw, n, lim); + BOOST_TEST_EQ(pr.prepare().size(), 0u); + system::error_code ec; + pr.parse(ec); + BOOST_TEST(ec == http::error::in_place_overflow); + } + } + + // the header needs more bytes than the + // window can ever receive + { + alignas(4) char raw[36]; + head_parser pr(true, raw, sizeof(raw), lim); + BOOST_TEST_EQ(pr.prepare().size(), 24u); + auto ec = feed(pr, msg.substr(0, 24)); + BOOST_TEST(ec == http::error::in_place_overflow); + // and stays so + pr.parse(ec); + BOOST_TEST(ec == http::error::in_place_overflow); + } + } + + // Feed `s` through prepare()/commit()/parse() in + // chunks of at most `chunk` bytes (0 for as much as + // prepare() offers), stopping once the parser stops + // asking for bytes or the caller has none left to + // give. Reports how much was committed. + static + system::error_code + drive( + head_parser& pr, + std::string_view s, + std::size_t chunk, + std::size_t& fed) + { + system::error_code ec; + fed = 0; + for(;;) + { + pr.parse(ec); + if(ec != http::error::need_data) + break; + auto const mb = pr.prepare(); + if(mb.size() == 0 || fed == s.size()) + break; + auto n = s.size() - fed; + if(chunk != 0 && n > chunk) + n = chunk; + if(n > mb.size()) + n = mb.size(); + std::memcpy(mb.data(), s.data() + fed, n); + pr.commit(n); + fed += n; + } + return ec; + } + + // A request whose `nf` field lines are `fl` bytes + // each, counting delimiters. + static + std::string + fieldsHeader( + std::size_t nf, + std::size_t fl) + { + std::string s = "GET /x HTTP/1.1\r\n"; + for(std::size_t i = 0; i < nf; ++i) + { + auto const name = "X" + std::to_string(i); + s += name + ": " + + std::string(fl - name.size() - 4, 'v') + "\r\n"; + } + return s + "\r\n"; + } + + void + testBufferSweep() + { + // The field table grows down from the end of the + // buffer while received bytes fill it from the + // front, and prepare() withholds the room the + // table needs. The two therefore cannot meet: for + // any buffer address and size, any number of + // fields and any chunking of the input, a header + // which parses must read back byte for byte and + // must leave the payload behind it untouched. A + // failure to hold the reserve shows up here as a + // field that reads back wrong, or as payload + // bytes replaced by a table entry. + alignas(4) char raw[1024]; + std::string const pay(40, 'P'); + + for(std::size_t nf : { 0u, 1u, 3u, 8u }) + for(std::size_t mf : { 0u, 1u, 3u, 8u, 100u }) + for(std::size_t off : { 0u, 1u, 2u, 3u }) + { + auto const h = fieldsHeader(nf, 12); + auto const wire = h + pay; + auto const rsv = fields_base::table_space(mf); + + // buffer sizes bracketing the exact fit, so + // the boundary between "completes" and + // "cannot proceed in place" is crossed + for(std::size_t d = 0; d <= 6; ++d) + { + auto const n = rsv + h.size() + d; + if(off + n > sizeof(raw)) + continue; + + system::error_code first; + for(std::size_t chunk : { 0u, 1u, 7u }) + { + std::memset(raw, 0xCD, sizeof(raw)); + head_parser pr( + true, raw + off, n, { .max_fields = + static_cast(mf) }); + + // the region offered is always inside + // the buffer + auto const mb0 = pr.prepare(); + BOOST_TEST(mb0.size() <= n); + BOOST_TEST( + static_cast(mb0.data()) >= raw + off); + + std::size_t fed = 0; + auto const ec = drive(pr, wire, chunk, fed); + + // how the input is chunked cannot + // change the verdict + if(chunk == 0) + first = ec; + else + BOOST_TEST(ec == first); + + if(ec) + { + BOOST_TEST( + ec == http::error::in_place_overflow || + ec == http::error::fields_limit || + ec == http::error::need_data); + continue; + } + + // the header is exactly the bytes sent + auto const& hd = pr.message_head(); + BOOST_TEST_EQ(hd.buffer(), h); + BOOST_TEST_EQ(hd.size(), nf); + for(std::size_t i = 0; i < nf; ++i) + { + auto const name = "X" + std::to_string(i); + BOOST_TEST_EQ(hd.begin()[i].name, name); + BOOST_TEST_EQ( + hd.begin()[i].value, + std::string(12 - name.size() - 4, 'v')); + } + + // and the payload which followed it in + // is byte for byte where it landed + auto const got = fed - h.size(); + BOOST_TEST_EQ(pr.leftovers().size(), got); + BOOST_TEST( + pr.leftovers().data() == + raw + off + h.size()); + BOOST_TEST_EQ( + std::string_view( + raw + off + h.size(), got), + std::string_view(pay).substr(0, got)); + + // leftovers and the still-writable + // region together stay in the buffer + BOOST_TEST( + static_cast(pr.leftovers().data()) + + pr.leftovers().size() + + pr.prepare().size() <= raw + off + n); + } + } + } + } + + void + testBufferSize() + { + // buffer_size() is the contract between the + // caller's allocation and the limits: a buffer of + // that size must accept the largest header the + // limits permit -- max_size bytes of text *and* + // max_fields entries at the same time, which are + // the two things that compete for the buffer. + auto const maximal = []( + std::size_t ms, + std::size_t nf) + { + // with no field to pad, the target carries + // the length instead + if(nf == 0) + return "GET /" + std::string(ms - 18, 'a') + + " HTTP/1.1\r\n\r\n"; + + // "GET / HTTP/1.1\r\n" + nf fields + "\r\n" + std::string s = "GET / HTTP/1.1\r\n"; + auto rem = ms - 18; + for(std::size_t i = 0; i < nf; ++i) + { + auto const name = "X" + std::to_string(i); + auto len = rem / (nf - i); + if(i + 1 == nf) + len = rem; + s += name + ": " + + std::string(len - name.size() - 4, 'v') + "\r\n"; + rem -= len; + } + return s + "\r\n"; + }; + + for(std::uint32_t mf : { 0u, 1u, 3u, 8u, 100u }) + { + // the smallest max_size which can carry + // mf fields, and a couple of larger ones + auto const least = + static_cast((18 + 8 * mf + 3) & ~3u); + for(std::uint32_t ms : + { least, least + 64u, least + 256u }) + { + header_limits const lim{ + .max_size = ms, .max_fields = mf }; + auto const h = maximal(ms, mf); + BOOST_TEST_EQ(h.size(), ms); + + auto const cap = head_parser::buffer_size(lim); + auto owned = std::unique_ptr( + new char[cap]); + head_parser pr(true, owned.get(), cap, lim); + std::size_t fed = 0; + BOOST_TEST(! drive(pr, h, 0, fed)); + BOOST_TEST_EQ(pr.message_head().buffer(), h); + BOOST_TEST_EQ(pr.message_head().size(), mf); + + // one alignment unit less is not enough: + // the shortfall is reported, not absorbed + // by eating into the table's room + head_parser pr2(true, owned.get(), cap - 4, lim); + std::size_t fed2 = 0; + BOOST_TEST( + drive(pr2, h, 0, fed2) == + http::error::in_place_overflow); + } + } + } + + void + testLimitSweep() + { + // max_size is spent by the start line first and + // the field section gets what is left, as an + // unsigned difference. Should that difference ever + // wrap, the window becomes enormous, the limit + // stops being applied, and the only visible + // symptom is a header longer than max_size being + // accepted -- no error, no crash. Sweep the whole + // small-limit space against headers of several + // shapes and assert the limit is never lost, and + // that a header is accepted only in full. + std::string const hs[] = { + fieldsHeader(0, 0), // 19 + fieldsHeader(1, 10), // 29 + fieldsHeader(3, 10), // 49 + fieldsHeader(2, 40), // 99 + fieldsHeader(8, 8), // 83 + }; + + // generous, so in_place_overflow never stands in + // for a limit verdict + auto const cap = head_parser::buffer_size( + { .max_size = 4096 }, 256); + auto owned = std::unique_ptr(new char[cap]); + + for(auto const& h : hs) + for(std::uint32_t ms = 0; ms <= 120; ++ms) + for(std::uint16_t msl : { 0, 17, 64, 4096 }) + for(std::uint16_t mfd : { 0, 9, 40, 4096 }) + for(std::uint32_t mfl : { 0u, 1u, 3u, 100u }) + { + header_limits const lim{ + .max_size = ms, + .max_fields = mfl, + .max_start_line = msl, + .max_field = mfd }; + + head_parser pr(true, owned.get(), cap, lim); + std::size_t fed = 0; + auto const ec = drive(pr, h, 0, fed); + if(ec) + continue; + // a success must fit the limit it was given, + // and must be the whole header + BOOST_TEST(pr.message_head().buffer().size() <= ms); + BOOST_TEST_EQ(pr.message_head().buffer(), h); + } + } + + void + testFieldLineAtUint16Max() + { + // Field lengths reach the header through a + // uint16_t, which is safe only because max_field + // is one too. The per-field window is max_field+1 + // -- one byte wider than the limit, for the + // obs-fold lookahead -- so this is the case where + // a consumed length could reach 65536 and truncate + // to zero. It must not: the extra byte is looked + // at, never consumed. + header_limits const lim{ + .max_size = 200000, + .max_fields = 8, + .max_field = 65535 }; + auto const cap = head_parser::buffer_size(lim, 16); + auto owned = std::unique_ptr(new char[cap]); + + auto const with = [](std::size_t line) + { + // a field line of exactly `line` bytes + return "GET / HTTP/1.1\r\nX: " + + std::string(line - 5, 'v') + "\r\n\r\n"; + }; + + auto const at = with(65535); + head_parser pr(true, owned.get(), cap, lim); + std::size_t fed = 0; + BOOST_TEST(! drive(pr, at, 0, fed)); + BOOST_TEST_EQ(pr.message_head().buffer().size(), at.size()); + BOOST_TEST_EQ( + pr.request_head().at("X").size(), 65535u - 5u); + + head_parser pr2(true, owned.get(), cap, lim); + std::size_t fed2 = 0; + BOOST_TEST( + drive(pr2, with(65536), 0, fed2) == + http::error::field_size_limit); + } + + void + testPipelineRounds() + { + // Several messages arrive back to back into a + // buffer which holds barely more than one header, + // so every round has leftovers to carry and the + // window that receives them is the one reset() + // just re-armed. + std::string const one = + "GET /a HTTP/1.1\r\nHost: x\r\n\r\n"; + auto const wire = one + one + one + one; + + header_limits const lim{ .max_fields = 2 }; + auto const cap = + head_parser::buffer_size(lim, one.size() / 2); + auto owned = std::unique_ptr(new char[cap]); + + head_parser pr(true, owned.get(), cap, lim); + std::size_t fed = 0; + int done = 0; + for(int round = 0; round < 4; ++round) + { + system::error_code ec; + for(;;) + { + pr.parse(ec); + if(ec != http::error::need_data) + break; + auto const mb = pr.prepare(); + if(mb.size() == 0 || fed == wire.size()) + break; + auto n = wire.size() - fed; + if(n > mb.size()) + n = mb.size(); + std::memcpy(mb.data(), wire.data() + fed, n); + pr.commit(n); + fed += n; + } + if(! BOOST_TEST(! ec)) + break; + BOOST_TEST_EQ(pr.message_head().buffer(), one); + BOOST_TEST_EQ(pr.request_head().at(http::field::host), "x"); + ++done; + + auto const lo = pr.leftovers(); + std::memmove(owned.get(), lo.data(), lo.size()); + pr.reset(lo.size()); + } + BOOST_TEST_EQ(done, 4); + } + + void + run() + { + testRequest(); + testIncremental(); + testResponse(); + testResponseVariants(); + testFraming(); + testObsFold(); + testFoldMatchesRescan(); + testRejectedHeader(); + testErrors(); + testLimits(); + testTableSpace(); + testOstream(); + testMove(); + testReset(); + testPipelined(); + testBufferGeometry(); + testBufferSweep(); + testBufferSize(); + testLimitSweep(); + testFieldLineAtUint16Max(); + testPipelineRounds(); + } +}; + +TEST_SUITE(head_parser_test, "boost.burl.head_parser"); + +} // namespace burl +} // namespace boost diff --git a/test/unit/request_builder.cpp b/test/unit/request_builder.cpp index 3cdf77f..d75e028 100644 --- a/test/unit/request_builder.cpp +++ b/test/unit/request_builder.cpp @@ -107,7 +107,7 @@ class request_builder_test auto req = c.get("http://example.com") .header(http::field::accept_language, "en") .build(); - BOOST_TEST(req.headers.exists(http::field::accept_language)); + BOOST_TEST(req.headers.contains(http::field::accept_language)); BOOST_TEST_EQ(req.headers.at(http::field::accept_language), "en"); // Setting the same field again replaces the previous value. @@ -127,7 +127,7 @@ class request_builder_test auto req = c.get("http://example.com") .header("X-Debug", "1") .build(); - BOOST_TEST(req.headers.exists("X-Debug")); + BOOST_TEST(req.headers.contains("X-Debug")); BOOST_TEST_EQ(req.headers.at("X-Debug"), "1"); // Setting the same name again replaces the previous value. diff --git a/test/unit/request_head.cpp b/test/unit/request_head.cpp new file mode 100644 index 0000000..dc87afe --- /dev/null +++ b/test/unit/request_head.cpp @@ -0,0 +1,1125 @@ +// +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +// Test that header file is self-contained. +#include + +#include + +#include "test_suite.hpp" + +#include +#include +#include +#include +#include +#include + +namespace boost +{ +namespace burl +{ + +static_assert(std::is_nothrow_swappable_v); + +class request_head_test +{ +public: + void + testDefault() + { + request_head h; + BOOST_TEST_EQ(h.buffer(), "GET / HTTP/1.1\r\n\r\n"); + BOOST_TEST(h.method() == http::method::get); + BOOST_TEST(h.version() == http::version::http_1_1); + BOOST_TEST(h.empty()); + + // shrink_to_fit on the shared storage is a + // no-op + h.shrink_to_fit(); + BOOST_TEST_EQ(h.capacity_in_bytes(), 0u); + BOOST_TEST_EQ(h.target(), "/"); + } + + void + testGrow() + { + // append far more than the initial allocation + // can hold, forcing several reallocations + request_head h; + std::string expected = "GET / HTTP/1.1\r\n"; + for(int i = 0; i < 64; ++i) + { + auto const name = "X-Field-" + std::to_string(i); + auto const value = std::string(20, 'a' + (i % 26)); + h.append(name, value); + expected += name + ": " + value + "\r\n"; + } + expected += "\r\n"; + BOOST_TEST_EQ(h.size(), 64u); + BOOST_TEST_EQ(h.buffer(), expected); + // a value survives the reallocations intact + BOOST_TEST_EQ( + h.at("X-Field-0"), std::string(20, 'a')); + BOOST_TEST_EQ( + h.at("X-Field-63"), std::string(20, 'a' + (63 % 26))); + } + + void + testStartLineGrow() + { + // a target far larger than the initial buffer + // forces the start line to grow via realloc + request_head h; + h.append(http::field::host, "example.com"); + auto const target = "/" + std::string(4000, 'p'); + h.set_target(target); + BOOST_TEST_EQ(h.target(), target); + BOOST_TEST_EQ(h.at(http::field::host), "example.com"); + BOOST_TEST_EQ( + h.buffer(), + "GET " + target + " HTTP/1.1\r\n" + "Host: example.com\r\n" + "\r\n"); + } + + // Growing the start line of a head with no fields + // must not land the field capacity exactly on the + // internal non-owning marker: the buffer would be + // disowned and silently leak. + void + testStartLineGrowEmpty() + { + request_head h; + h.set_target("/some/self-contained-target"); + BOOST_TEST(h.capacity_in_bytes() > 0u); + h.append(http::field::host, "example.com"); + BOOST_TEST_EQ( + h.buffer(), + "GET /some/self-contained-target HTTP/1.1\r\n" + "Host: example.com\r\n" + "\r\n"); + } + + // A slow-path start-line grow with a very large + // reserved headroom must not overflow the 16-bit + // prefix capacity; the excess headroom is dropped + // instead. + void + testStartLineHeadroomOverflow() + { + request_head h; + h.reserve(64, 1, 40000); + h.append(http::field::host, "example.com"); + auto const target = "/" + std::string(45000, 'p'); + h.set_target(target); + BOOST_TEST_EQ(h.target(), target); + BOOST_TEST_EQ(h.at(http::field::host), "example.com"); + h.set_target("/short"); + BOOST_TEST_EQ(h.target(), "/short"); + BOOST_TEST_EQ( + h.buffer(), + "GET /short HTTP/1.1\r\n" + "Host: example.com\r\n" + "\r\n"); + } + + // A default-constructed head views shared immutable + // storage, so a start-line change must copy out of it + // before writing. Every path leaves the shared bytes + // pristine, and a rejected change does not allocate. + void + testStartLineDefaultStorage() + { + // a same-size replacement takes the in-place path + { + request_head h; + BOOST_TEST_EQ(h.capacity_in_bytes(), 0u); + h.set_method("PUT"); + BOOST_TEST_EQ(h.buffer(), "PUT / HTTP/1.1\r\n\r\n"); + } + // a shorter start line shrinks in place + { + request_head h; + h.set_start_line("A", "/"); + BOOST_TEST_EQ(h.buffer(), "A / HTTP/1.1\r\n\r\n"); + } + // a longer one reallocates + { + request_head h; + auto const target = "/" + std::string(4000, 'q'); + h.set_target(target); + BOOST_TEST_EQ( + h.buffer(), + "GET " + target + " HTTP/1.1\r\n" + "\r\n"); + } + // a rejected change keeps the head on the shared + // storage instead of leaving a detached buffer + request_head g; + BOOST_TEST_THROWS( + g.set_target(std::string(70000, 'q')), + std::length_error); + BOOST_TEST_EQ(g.capacity_in_bytes(), 0u); + // the shared storage was never written through + BOOST_TEST_EQ(g.buffer(), "GET / HTTP/1.1\r\n\r\n"); + } + + // The start line has its own 16-bit size limit, + // independent of the buffer limit; exceeding it + // throws and leaves the head unchanged. + void + testStartLineLimits() + { + std::string const big( + request_head::max_start_line_size + 1, 'p'); + + request_head h; + h.append(http::field::host, "example.com"); + BOOST_TEST_THROWS(h.set_target(big), std::length_error); + BOOST_TEST_THROWS(h.set_method(big), std::length_error); + BOOST_TEST_THROWS( + h.reserve(0, 0, request_head::max_start_line_size + 1), + std::length_error); + BOOST_TEST_THROWS( + h.set_start_line(http::method::get, big), + std::length_error); + BOOST_TEST_THROWS( + h.set_start_line(big, "/"), std::length_error); + // failed operations leave the head unchanged + BOOST_TEST_EQ( + h.buffer(), + "GET / HTTP/1.1\r\n" + "Host: example.com\r\n" + "\r\n"); + + // the limit covers the whole line, not the target + // alone: "GET " + target + " HTTP/1.1\r\n" spends + // 15 bytes on everything but the target + auto const room = request_head::max_start_line_size - + std::strlen("GET HTTP/1.1\r\n"); + request_head g; + g.set_target("/" + std::string(room - 1, 'p')); + BOOST_TEST_EQ(g.target().size(), room); + // one byte more does not fit + BOOST_TEST_THROWS( + g.set_target("/" + std::string(room, 'p')), + std::length_error); + BOOST_TEST_EQ(g.target().size(), room); + // a shrink is never refused + g.set_target("/"); + BOOST_TEST_EQ( + g.buffer(), "GET / HTTP/1.1\r\n\r\n"); + } + + // Arguments viewing the start line remain valid to + // pass to append and set, including when the call + // reallocates the buffer. + void + testSelfReference() + { + request_head h; + h.append(http::field::host, "example.com"); + h.set_target("/self-referenced-target"); + h.shrink_to_fit(); + // the append reallocates while the value + // views the start line + h.append("X-Original-Uri", h.target()); + BOOST_TEST_EQ( + h.at("X-Original-Uri"), "/self-referenced-target"); + + // from the method text + h.append("X-Method", h.method_text()); + BOOST_TEST_EQ(h.at("X-Method"), "GET"); + + // the replacement reallocates while the new + // value views the start line + h.set(http::field::host, "h"); + h.shrink_to_fit(); + h.set(http::field::host, h.target()); + BOOST_TEST_EQ( + h.at(http::field::host), "/self-referenced-target"); + } + + // Start-line setters accept arguments viewing the + // start line itself, in every splice path. + void + testStartLineSelfReference() + { + // in place, same size + request_head h; + h.append(http::field::host, "example.com"); + h.set_target("/self-aliased-target"); + h.set_target(h.target()); + BOOST_TEST_EQ(h.target(), "/self-aliased-target"); + + // shrink to a suffix of the old target + h.set_target(h.target().substr(6)); + BOOST_TEST_EQ(h.target(), "aliased-target"); + + // the start line grows into the reserved headroom + // while the argument views the old target + h.reserve(0, 0, 64); + h.set_start_line( + "LONGER-METHOD", + h.target(), + http::version::http_1_1); + BOOST_TEST_EQ(h.method_text(), "LONGER-METHOD"); + BOOST_TEST_EQ(h.target(), "aliased-target"); + + // the text before the splice point shifts within + // the headroom while the argument views it + request_head h2; + h2.reserve(0, 0, 64); + h2.set_target(h2.method_text()); + BOOST_TEST_EQ(h2.buffer(), "GET GET HTTP/1.1\r\n\r\n"); + + // the start line grows into the field-region + // slack, shifting the field section + request_head h3; + h3.reserve(1024, 8, 0); + h3.append(http::field::host, "example.com"); + h3.set_target("/mid-path-target"); + h3.set_start_line( + "LONGER-METHOD", + h3.target(), + http::version::http_1_1); + BOOST_TEST_EQ(h3.target(), "/mid-path-target"); + BOOST_TEST_EQ( + h3.buffer(), + "LONGER-METHOD /mid-path-target HTTP/1.1\r\n" + "Host: example.com\r\n" + "\r\n"); + + // the method argument sources from inside the + // target's destination: it must be copied first + request_head h5; + h5.reserve(0, 0, 64); + h5.set_target("/old"); + h5.set_start_line( + h5.method_text(), + "/considerably-longer-target", + http::version::http_1_1); + BOOST_TEST_EQ( + h5.buffer(), + "GET /considerably-longer-target HTTP/1.1\r\n" + "\r\n"); + + // likewise through a reallocation + request_head h6; + h6.set_target("/old"); + h6.shrink_to_fit(); + h6.set_start_line( + h6.method_text(), + "/considerably-longer-target", + http::version::http_1_1); + BOOST_TEST_EQ( + h6.buffer(), + "GET /considerably-longer-target HTTP/1.1\r\n" + "\r\n"); + + // the mirrored direction: the target argument + // shrinks while the method grows + h6.set_start_line( + "DELETE", + h6.target().substr(0, 13), + http::version::http_1_1); + BOOST_TEST_EQ( + h6.buffer(), + "DELETE /considerably HTTP/1.1\r\n" + "\r\n"); + + // shrink to a leading substring of the old target: + // the head move lands on the argument's bytes, + // which are salvaged first + request_head h7; + h7.append(http::field::host, "example.com"); + h7.set_target("/salvaged-target"); + h7.set_target(h7.target().substr(0, 9)); + BOOST_TEST_EQ(h7.target(), "/salvaged"); + // and to an interior substring + h7.set_target(h7.target().substr(1, 4)); + BOOST_TEST_EQ(h7.target(), "salv"); + BOOST_TEST_EQ( + h7.buffer(), + "GET salv HTTP/1.1\r\n" + "Host: example.com\r\n" + "\r\n"); + + // the grow reallocates while the argument views + // the old buffer + request_head h4; + h4.append(http::field::host, "example.com"); + h4.set_target("/slow-path-target"); + h4.shrink_to_fit(); + h4.set_start_line( + "A-MUCH-LONGER-CUSTOM-METHOD", + h4.target(), + http::version::http_1_0); + BOOST_TEST_EQ(h4.target(), "/slow-path-target"); + BOOST_TEST_EQ(h4.at(http::field::host), "example.com"); + BOOST_TEST_EQ( + h4.buffer(), + "A-MUCH-LONGER-CUSTOM-METHOD /slow-path-target HTTP/1.0\r\n" + "Host: example.com\r\n" + "\r\n"); + } + + void + testMove() + { + request_head h1; + h1.set_target("/x"); + h1.append(http::field::host, "example.com"); + auto const before = std::string(h1.buffer()); + + request_head h2(std::move(h1)); + BOOST_TEST_EQ(h2.buffer(), before); + BOOST_TEST_EQ(h2.at(http::field::host), "example.com"); + BOOST_TEST_EQ(h2.method_text(), "GET"); + BOOST_TEST_EQ(h2.target(), "/x"); + + // the moved-from object is left default-constructed + BOOST_TEST_EQ(h1.buffer(), "GET / HTTP/1.1\r\n\r\n"); + BOOST_TEST_EQ(h1.method_text(), "GET"); + BOOST_TEST_EQ(h1.target(), "/"); + + // the moved-from object is destructible and can + // be reassigned + request_head h3; + h3.append(http::field::host, "other"); + h1 = std::move(h3); + BOOST_TEST_EQ(h1.at(http::field::host), "other"); + } + + void + testMoveAssign() + { + request_head h1; + h1.append(http::field::host, "a"); + request_head h2; + h2.append(http::field::host, "b"); + h2.append(http::field::accept, "*/*"); + + h1 = std::move(h2); + BOOST_TEST_EQ( + h1.buffer(), + "GET / HTTP/1.1\r\n" + "Host: b\r\n" + "Accept: */*\r\n" + "\r\n"); + + // self move-assignment is a no-op + auto& hr = h1; + h1 = std::move(hr); + BOOST_TEST_EQ(h1.at(http::field::host), "b"); + } + + void + testSwap() + { + { + request_head h1(http::method::post, "/a"); + h1.append(http::field::host, "a.example"); + h1.set_content_length(7); + request_head h2; + h2.append(http::field::host, "b.example"); + h2.set_chunked(true); + auto const cap1 = h1.capacity_in_bytes(); + auto const cap2 = h2.capacity_in_bytes(); + // a view follows the contents into the + // other header + auto const v1 = h1.at(http::field::host); + + h1.swap(h2); + + // the start line, the fields, and the cached + // metadata all move together + BOOST_TEST_EQ(h1.method_text(), "GET"); + BOOST_TEST_EQ(h1.target(), "/"); + BOOST_TEST_EQ(h1.at(http::field::host), "b.example"); + BOOST_TEST(h1.chunked()); + BOOST_TEST(! h1.content_length().has_value()); + BOOST_TEST_EQ(h1.capacity_in_bytes(), cap2); + + BOOST_TEST(h2.method() == http::method::post); + BOOST_TEST_EQ(h2.target(), "/a"); + BOOST_TEST_EQ(h2.at(http::field::host), "a.example"); + BOOST_TEST(! h2.chunked()); + BOOST_TEST_EQ(h2.content_length().value(), 7u); + BOOST_TEST_EQ(h2.capacity_in_bytes(), cap1); + + BOOST_TEST_EQ(v1, "a.example"); + + // both headers remain usable + h1.set_target("/b"); + h2.append(http::field::accept, "*/*"); + BOOST_TEST_EQ(h1.target(), "/b"); + BOOST_TEST_EQ(h2.at(http::field::accept), "*/*"); + } + + // ADL and std::swap find the hidden friend + { + request_head h1(http::method::put, "/x"); + request_head h2; + swap(h1, h2); + BOOST_TEST_EQ(h1.buffer(), "GET / HTTP/1.1\r\n\r\n"); + BOOST_TEST_EQ(h2.buffer(), "PUT /x HTTP/1.1\r\n\r\n"); + std::swap(h1, h2); + BOOST_TEST_EQ(h1.buffer(), "PUT /x HTTP/1.1\r\n\r\n"); + } + + // swapping with a default header transfers the + // allocation and the shared static buffer + { + request_head h1(http::method::post, "/a"); + request_head h2; + auto const cap1 = h1.capacity_in_bytes(); + h1.swap(h2); + BOOST_TEST_EQ(h1.buffer(), "GET / HTTP/1.1\r\n\r\n"); + BOOST_TEST_EQ(h1.capacity_in_bytes(), 0u); + BOOST_TEST_EQ(h2.buffer(), "POST /a HTTP/1.1\r\n\r\n"); + BOOST_TEST_EQ(h2.capacity_in_bytes(), cap1); + + // the header which received the static buffer + // allocates on its next modification, and the + // start-line headroom moves with it + h1.set_target("/longer-target"); + BOOST_TEST_EQ( + h1.buffer(), "GET /longer-target HTTP/1.1\r\n\r\n"); + } + + // two default headers + { + request_head h1; + request_head h2; + h1.swap(h2); + BOOST_TEST_EQ(h1.buffer(), "GET / HTTP/1.1\r\n\r\n"); + BOOST_TEST_EQ(h1.capacity_in_bytes(), 0u); + BOOST_TEST_EQ(h2.buffer(), "GET / HTTP/1.1\r\n\r\n"); + BOOST_TEST_EQ(h2.capacity_in_bytes(), 0u); + } + + // self-swap has no effect + { + request_head h(http::method::post, "/a"); + h.append(http::field::host, "a.example"); + auto& hr = h; + h.swap(hr); + BOOST_TEST_EQ( + h.buffer(), + "POST /a HTTP/1.1\r\n" + "Host: a.example\r\n" + "\r\n"); + } + } + + void + testCopy() + { + request_head h1; + h1.set_target("/orig"); + h1.append(http::field::host, "example.com"); + + request_head h2(h1); + BOOST_TEST_EQ(h2.buffer(), h1.buffer()); + // independent storage + BOOST_TEST(h2.buffer().data() != h1.buffer().data()); + + // mutating the original leaves the copy intact + h1.set_target("/changed"); + h1.append(http::field::accept, "*/*"); + BOOST_TEST_EQ(h2.target(), "/orig"); + BOOST_TEST_EQ( + h2.buffer(), + "GET /orig HTTP/1.1\r\n" + "Host: example.com\r\n" + "\r\n"); + + // framing metadata is preserved on the copy, and + // mutating the copy's framing leaves the original + // untouched + request_head h3(http::method::post, "/u"); + h3.set_content_length(7); + h3.set_keep_alive(false); + h3.set_expect_100_continue(true); + request_head h4(h3); + BOOST_TEST(h4.content_length().has_value()); + BOOST_TEST_EQ(*h4.content_length(), 7u); + BOOST_TEST(! h4.keep_alive()); + BOOST_TEST(h4.expect_100_continue()); + BOOST_TEST_EQ(h4.buffer(), h3.buffer()); + h4.set_chunked(true); + BOOST_TEST(h4.chunked()); + BOOST_TEST(! h3.chunked()); + BOOST_TEST(h3.content_length().has_value()); + } + + void + testCopyAssign() + { + request_head h1; + h1.append(http::field::host, "a"); + request_head h2; + h2.set_target("/y"); + h2.append(http::field::accept, "*/*"); + + h1 = h2; + BOOST_TEST_EQ(h1.buffer(), h2.buffer()); + BOOST_TEST(h1.buffer().data() != h2.buffer().data()); + + // self copy-assignment is a no-op + auto const& hr = h1; + h1 = hr; + BOOST_TEST_EQ(h1.target(), "/y"); + + // assignment from a default-constructed head when the + // start-line headroom shrank below the default request + // line releases the storage and shares the default + request_head h3; + h3.set_start_line("A", "/", http::version::http_1_0); + h3.append(http::field::host, std::string(100, 'x')); + h3.erase(http::field::host); + h3.shrink_to_fit(); + request_head h4; + h3 = h4; + BOOST_TEST_EQ(h3.buffer(), "GET / HTTP/1.1\r\n\r\n"); + BOOST_TEST(h3.method() == http::method::get); + BOOST_TEST_EQ(h3.capacity_in_bytes(), 0u); + h3.append(http::field::accept, "*/*"); + BOOST_TEST_EQ(h3.at(http::field::accept), "*/*"); + } + + void + testSetChunked() + { + // enabling chunked when a Content-Length exists and + // no Transfer-Encoding is present: the erase must not + // leave a stale iterator dereferenced + request_head h1(http::method::post, "/u"); + h1.set_content_length(7); + h1.set_chunked(true); + BOOST_TEST(! h1.contains(http::field::content_length)); + BOOST_TEST_EQ(h1.count(http::field::transfer_encoding), 1u); + BOOST_TEST_EQ(h1.at(http::field::transfer_encoding), "chunked"); + BOOST_TEST(h1.chunked()); + + // a Content-Length preceding an already-chunked + // Transfer-Encoding: the erase shifts positions, and + // a duplicate "chunked" must not be appended + request_head h2(http::method::post, "/u"); + h2.append(http::field::content_length, "7"); + h2.append(http::field::transfer_encoding, "chunked"); + h2.set_chunked(true); + BOOST_TEST(! h2.contains(http::field::content_length)); + BOOST_TEST_EQ(h2.count(http::field::transfer_encoding), 1u); + BOOST_TEST(h2.chunked()); + + // with a non-final coding, "chunked" is appended + // exactly once + request_head h3(http::method::post, "/u"); + h3.append(http::field::content_length, "7"); + h3.append(http::field::transfer_encoding, "gzip"); + h3.set_chunked(true); + BOOST_TEST(! h3.contains(http::field::content_length)); + BOOST_TEST_EQ(h3.count(http::field::transfer_encoding), 2u); + BOOST_TEST(h3.chunked()); + + // disabling removes a final "chunked" coding + h1.set_chunked(false); + BOOST_TEST(! h1.contains(http::field::transfer_encoding)); + BOOST_TEST(! h1.chunked()); + request_head h5(http::method::post, "/u"); + h5.append(http::field::transfer_encoding, "gzip, chunked"); + h5.set_chunked(false); + BOOST_TEST_EQ(h5.at(http::field::transfer_encoding), "gzip"); + + // whitespace before the removed coding goes + // with it + request_head h6(http::method::post, "/u"); + h6.append(http::field::transfer_encoding, "gzip \t, chunked"); + h6.set_chunked(false); + BOOST_TEST_EQ(h6.at(http::field::transfer_encoding), "gzip"); + + // disabling leaves a non-chunked final coding + // alone, and is a no-op with no + // Transfer-Encoding at all + request_head h7(http::method::post, "/u"); + h7.append(http::field::transfer_encoding, "gzip"); + h7.set_chunked(false); + BOOST_TEST_EQ(h7.at(http::field::transfer_encoding), "gzip"); + h7.erase(http::field::transfer_encoding); + h7.set_chunked(false); + BOOST_TEST(! h7.contains(http::field::transfer_encoding)); + } + + void + testSetKeepAlive() + { + // HTTP/1.1 defaults to keep-alive; requesting + // close adds the token, and requesting + // keep-alive removes the field again + request_head h; + BOOST_TEST(h.keep_alive()); + h.set_keep_alive(false); + BOOST_TEST_EQ(h.at(http::field::connection), "close"); + BOOST_TEST(! h.keep_alive()); + h.set_keep_alive(true); + BOOST_TEST(! h.contains(http::field::connection)); + BOOST_TEST(h.keep_alive()); + + // unrelated tokens survive the rewrite + h.append(http::field::connection, "upgrade"); + h.set_keep_alive(false); + BOOST_TEST_EQ( + h.at(http::field::connection), "upgrade, close"); + h.set_keep_alive(true); + BOOST_TEST_EQ(h.at(http::field::connection), "upgrade"); + + // multiple fields and tokens collapse into one + // field, keeping the unrelated tokens in order + request_head h2; + h2.append(http::field::connection, "keep-alive, x-a"); + h2.append(http::field::connection, "x-b"); + h2.set_keep_alive(false); + BOOST_TEST_EQ(h2.count(http::field::connection), 1u); + BOOST_TEST_EQ( + h2.at(http::field::connection), "x-a, x-b, close"); + + // a value which is not a token list + // contributes nothing + request_head h3; + h3.append(http::field::connection, "@@@"); + h3.set_keep_alive(false); + BOOST_TEST_EQ(h3.at(http::field::connection), "close"); + + // HTTP/1.0 defaults to close; keep-alive must + // be asked for explicitly + request_head h4; + h4.set_version(http::version::http_1_0); + BOOST_TEST(! h4.keep_alive()); + h4.set_keep_alive(true); + BOOST_TEST_EQ( + h4.at(http::field::connection), "keep-alive"); + BOOST_TEST(h4.keep_alive()); + h4.set_keep_alive(false); + BOOST_TEST(! h4.contains(http::field::connection)); + BOOST_TEST(! h4.keep_alive()); + } + + void + testSetVersion() + { + // the version is written in place; nothing + // else moves + request_head h; + h.append(http::field::host, "example.com"); + h.set_version(http::version::http_1_0); + BOOST_TEST(h.version() == http::version::http_1_0); + BOOST_TEST_EQ( + h.buffer(), + "GET / HTTP/1.0\r\n" + "Host: example.com\r\n" + "\r\n"); + h.set_version(http::version::http_1_1); + BOOST_TEST_EQ( + h.buffer(), + "GET / HTTP/1.1\r\n" + "Host: example.com\r\n" + "\r\n"); + + // a default-constructed head detaches from the + // shared storage before writing + request_head g; + BOOST_TEST_EQ(g.capacity_in_bytes(), 0u); + g.set_version(http::version::http_1_0); + BOOST_TEST_EQ(g.buffer(), "GET / HTTP/1.0\r\n\r\n"); + BOOST_TEST(g.capacity_in_bytes() > 0u); + // the shared storage was never written through + request_head fresh; + BOOST_TEST_EQ(fresh.buffer(), "GET / HTTP/1.1\r\n\r\n"); + + // keep-alive follows the version + BOOST_TEST(! g.keep_alive()); + g.set_version(http::version::http_1_1); + BOOST_TEST(g.keep_alive()); + } + + void + testExpect100Continue() + { + request_head h(http::method::post, "/u"); + BOOST_TEST(! h.expect_100_continue()); + h.set_expect_100_continue(true); + BOOST_TEST(h.expect_100_continue()); + BOOST_TEST_EQ(h.at(http::field::expect), "100-continue"); + // setting again does not duplicate the field + h.set_expect_100_continue(true); + BOOST_TEST_EQ(h.count(http::field::expect), 1u); + // disabling removes the field; again is a no-op + h.set_expect_100_continue(false); + BOOST_TEST(! h.expect_100_continue()); + BOOST_TEST(! h.contains(http::field::expect)); + h.set_expect_100_continue(false); + BOOST_TEST(! h.contains(http::field::expect)); + + // the observer tracks the field however it is + // written; other expectations do not count + h.append(http::field::expect, "nope"); + BOOST_TEST(! h.expect_100_continue()); + h.set(http::field::expect, "NOPE, 100-CONTINUE"); + BOOST_TEST(h.expect_100_continue()); + h.erase(http::field::expect); + BOOST_TEST(! h.expect_100_continue()); + } + + void + testUpgrade() + { + // the proposal needs both the Upgrade field + // and the "upgrade" Connection token + request_head h; + BOOST_TEST(! h.upgrade()); + h.append(http::field::upgrade, "h2c"); + BOOST_TEST(! h.upgrade()); + h.append(http::field::connection, "upgrade"); + BOOST_TEST(h.upgrade()); + + // removing either side withdraws it + h.erase(http::field::upgrade); + BOOST_TEST(! h.upgrade()); + h.append(http::field::upgrade, "h2c"); + BOOST_TEST(h.upgrade()); + h.set(http::field::connection, "close"); + BOOST_TEST(! h.upgrade()); + } + + void + testClear() + { + // the request line survives; the fields and + // the state derived from them do not + request_head h(http::method::post, "/upload"); + h.set_content_length(5); + h.set_keep_alive(false); + h.set_expect_100_continue(true); + BOOST_TEST(! h.keep_alive()); + auto const cap = h.capacity_in_bytes(); + + h.clear(); + BOOST_TEST(h.empty()); + BOOST_TEST_EQ(h.buffer(), "POST /upload HTTP/1.1\r\n\r\n"); + BOOST_TEST(h.method() == http::method::post); + BOOST_TEST_EQ(h.capacity_in_bytes(), cap); + BOOST_TEST(! h.content_length().has_value()); + BOOST_TEST(h.payload() == http::payload::none); + BOOST_TEST(h.keep_alive()); + BOOST_TEST(! h.expect_100_continue()); + + // the version bit survives with the request line + request_head g; + g.set_version(http::version::http_1_0); + g.append(http::field::connection, "keep-alive"); + BOOST_TEST(g.keep_alive()); + g.clear(); + BOOST_TEST(g.version() == http::version::http_1_0); + BOOST_TEST(! g.keep_alive()); + } + + // A header built by hand is not checked: the + // framing observers describe whatever the fields + // say, and say so consistently. + void + testFramingByHand() + { + // a Content-Length which is not a single decimal + // number reads as if absent, and is stored as given + { + request_head h(http::method::put, "/u"); + h.append(http::field::content_length, "abc"); + BOOST_TEST(! h.content_length().has_value()); + BOOST_TEST(h.payload() == http::payload::none); + BOOST_TEST_EQ(h.at(http::field::content_length), "abc"); + } + + // a value one past the largest representable + // length reads as if absent; the largest itself + // parses + { + request_head h(http::method::put, "/u"); + h.set(http::field::content_length, + "18446744073709551616"); + BOOST_TEST(! h.content_length().has_value()); + h.set(http::field::content_length, + "18446744073709551615"); + BOOST_TEST_EQ( + h.content_length().value(), std::uint64_t(-1)); + } + + // values which are not token lists: the + // Expect tokens contribute nothing, an empty + // Content-Length reads as if absent, and a + // Transfer-Encoding still counts as one, so + // the request cannot be framed + { + request_head h(http::method::put, "/u"); + h.set(http::field::expect, "@@"); + BOOST_TEST(! h.expect_100_continue()); + h.set(http::field::content_length, ""); + BOOST_TEST(! h.content_length().has_value()); + h.erase(http::field::content_length); + h.append(http::field::transfer_encoding, "@@"); + BOOST_TEST(! h.chunked()); + BOOST_TEST(h.payload() == http::payload::error); + } + + // field lines join into a list, so a duplicate + // leaves no usable length even when the values + // agree; removing it restores one + { + request_head h(http::method::put, "/u"); + h.append(http::field::content_length, "5"); + BOOST_TEST_EQ(h.content_length().value_or(0), 5u); + h.append(http::field::content_length, "5"); + BOOST_TEST(! h.content_length().has_value()); + h.erase(--h.end()); + BOOST_TEST_EQ(h.content_length().value_or(0), 5u); + } + + // Content-Length together with Transfer-Encoding + { + request_head h(http::method::put, "/u"); + h.append(http::field::transfer_encoding, "chunked"); + h.append(http::field::content_length, "5"); + BOOST_TEST(h.payload() == http::payload::error); + BOOST_TEST(! h.chunked()); + h.erase(http::field::content_length); + BOOST_TEST(h.chunked()); + } + + // a request whose final coding is not "chunked" + // cannot be framed + { + request_head h(http::method::put, "/u"); + h.append(http::field::transfer_encoding, "gzip"); + BOOST_TEST(h.payload() == http::payload::error); + } + + // the cached state is a function of the field + // section, not of the mutations which produced + // it: `a` reaches these fields by appending, `b` + // by erasing back down to them + { + request_head a(http::method::put, "/u"); + a.append(http::field::content_length, "5"); + a.append(http::field::content_length, "9"); + + request_head b(http::method::put, "/u"); + b.append(http::field::content_length, "5"); + b.append(http::field::content_length, "9"); + b.append(http::field::content_length, "7"); + b.erase(--b.end()); + + BOOST_TEST_EQ(a.buffer(), b.buffer()); + BOOST_TEST(a.content_length() == b.content_length()); + BOOST_TEST(a.payload() == b.payload()); + } + { + request_head a(http::method::put, "/u"); + a.append(http::field::transfer_encoding, "chunked"); + a.append(http::field::transfer_encoding, "gzip"); + + request_head b(http::method::put, "/u"); + b.append(http::field::transfer_encoding, "chunked"); + b.append(http::field::transfer_encoding, "gzip"); + b.append(http::field::transfer_encoding, "gzip"); + b.erase(--b.end()); + + BOOST_TEST_EQ(a.buffer(), b.buffer()); + BOOST_TEST(a.chunked() == b.chunked()); + BOOST_TEST(a.payload() == b.payload()); + } + } + + void + testReserveShrink() + { + request_head h; + h.reserve(4096, 32, 16); + auto const cap = h.capacity_in_bytes(); + BOOST_TEST(cap >= 4096u); + // reserving within the capacity does not shrink + h.reserve(10, 1, 16); + BOOST_TEST_EQ(h.capacity_in_bytes(), cap); + + h.append(http::field::host, "example.com"); + h.shrink_to_fit(); + // after shrinking, the allocation is small + BOOST_TEST(h.capacity_in_bytes() < 4096u); + BOOST_TEST_EQ(h.at(http::field::host), "example.com"); + BOOST_TEST_EQ( + h.buffer(), + "GET / HTTP/1.1\r\n" + "Host: example.com\r\n" + "\r\n"); + } + + // Adding request-line headroom later must not give back + // the field capacity an earlier reserve obtained. + void + testReserveStartLineLater() + { + request_head h; + h.reserve(4096, 32, 16); + h.append(http::field::host, "example.com"); + auto const cap = h.capacity_in_bytes(); + h.reserve(0, 0, 64); + BOOST_TEST(h.capacity_in_bytes() >= cap); + BOOST_TEST_EQ(h.at(http::field::host), "example.com"); + + // the headroom is usable: a longer request line grows + // into the prefix, leaving the field section in place + auto const* host_data = h.at(http::field::host).data(); + h.set_start_line( + http::method::post, + "/some/longer/target", + http::version::http_1_1); + BOOST_TEST(h.at(http::field::host).data() == host_data); + BOOST_TEST_EQ( + h.buffer(), + "POST /some/longer/target HTTP/1.1\r\n" + "Host: example.com\r\n" + "\r\n"); + + // the field section still holds what the first + // reserve obtained: filling it does not reallocate + h.append(http::field::accept, std::string(3000, 'a')); + BOOST_TEST(h.at(http::field::host).data() == host_data); + } + + // Reserving start-line headroom lets a later, longer + // request line be set without moving the field section. + void + testReserveStartLine() + { + request_head h; + // reserve 64 bytes of request-line headroom + h.reserve(1024, 8, 64); + h.append(http::field::host, "example.com"); + auto const cap = h.capacity_in_bytes(); + auto const* host_data = h.at(http::field::host).data(); + + // a longer request line (35 bytes incl CRLF) still + // fits the reserved headroom + h.set_start_line( + http::method::post, + "/some/longer/target", + http::version::http_1_1); + + // the field section neither moved nor lost capacity: + // the start line grew into the reserved prefix, not + // the field region + BOOST_TEST_EQ(h.capacity_in_bytes(), cap); + BOOST_TEST(h.at(http::field::host).data() == host_data); + BOOST_TEST_EQ( + h.buffer(), + "POST /some/longer/target HTTP/1.1\r\n" + "Host: example.com\r\n" + "\r\n"); + } + + // Snapshot a non-owning request_head_base (as produced by + // head_parser over an external buffer) into an owner. + void + testFromBase() + { + static_assert(std::is_convertible_v< + request_head_base const&, request_head>); + + alignas(4) char buf[4096]; + std::string_view const msg = + "POST /submit HTTP/1.1\r\n" + "Host: example.com\r\n" + "X-A: 1\r\n" + "\r\n"; + auto const parse = + [&](head_parser& pr) + { + std::memcpy( + pr.prepare().data(), msg.data(), msg.size()); + pr.commit(msg.size()); + system::error_code ec; + pr.parse(ec); + BOOST_TEST(!ec); + }; + + head_parser pr(true, buf, sizeof(buf)); + parse(pr); + request_head_base const& base = pr.request_head(); + + // lossless, implicit construction from the base + request_head h = base; + BOOST_TEST_EQ(h.buffer(), msg); + BOOST_TEST(h.method() == http::method::post); + BOOST_TEST_EQ(h.target(), "/submit"); + BOOST_TEST_EQ(h.at(http::field::host), "example.com"); + // independent storage + BOOST_TEST(h.buffer().data() != base.buffer().data()); + + // clobbering the parse buffer leaves the snapshot intact + std::memset(buf, 'Z', msg.size()); + BOOST_TEST_EQ(h.target(), "/submit"); + BOOST_TEST_EQ(h.at(http::field::host), "example.com"); + + // assignment from a base replaces prior contents + request_head g; + g.append(http::field::host, "old"); + head_parser pr2(true, buf, sizeof(buf)); + parse(pr2); + g = pr2.request_head(); + BOOST_TEST_EQ(g.buffer(), msg); + BOOST_TEST(g.buffer().data() != buf); + BOOST_TEST_EQ(g.count(http::field::host), 1u); + } + + void + run() + { + testDefault(); + testGrow(); + testStartLineGrow(); + testStartLineGrowEmpty(); + testStartLineHeadroomOverflow(); + testStartLineDefaultStorage(); + testStartLineLimits(); + testSelfReference(); + testStartLineSelfReference(); + testMove(); + testMoveAssign(); + testSwap(); + testCopy(); + testCopyAssign(); + testSetChunked(); + testSetKeepAlive(); + testSetVersion(); + testExpect100Continue(); + testUpgrade(); + testClear(); + testFramingByHand(); + testReserveShrink(); + testReserveStartLineLater(); + testReserveStartLine(); + testFromBase(); + } +}; + +TEST_SUITE(request_head_test, "boost.burl.request_head"); + +} // namespace burl +} // namespace boost diff --git a/test/unit/response.cpp b/test/unit/response.cpp index 2a914d7..d33f5e6 100644 --- a/test/unit/response.cpp +++ b/test/unit/response.cpp @@ -112,7 +112,8 @@ class response_test void testVersion() { - auto r = test::response_factory({}, http::version::http_1_0) + auto r = test::response_factory( + http::status::ok, http::version::http_1_0) .create(); BOOST_TEST(r.version() == http::version::http_1_0); } diff --git a/test/unit/response_head.cpp b/test/unit/response_head.cpp new file mode 100644 index 0000000..2e54b61 --- /dev/null +++ b/test/unit/response_head.cpp @@ -0,0 +1,605 @@ +// +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +// Test that header file is self-contained. +#include + +#include + +#include "test_suite.hpp" + +#include +#include +#include +#include +#include + +namespace boost +{ +namespace burl +{ + +static_assert(std::is_nothrow_swappable_v); + +class response_head_test +{ +public: + void + testDefault() + { + response_head h; + BOOST_TEST_EQ(h.buffer(), "HTTP/1.1 200 OK\r\n\r\n"); + BOOST_TEST(h.status() == http::status::ok); + BOOST_TEST_EQ(h.status_int(), 200); + BOOST_TEST_EQ(h.reason(), "OK"); + BOOST_TEST(h.empty()); + } + + void + testGrow() + { + response_head h; + std::string expected = "HTTP/1.1 200 OK\r\n"; + for(int i = 0; i < 64; ++i) + { + auto const name = "X-Field-" + std::to_string(i); + auto const value = std::string(20, 'a' + (i % 26)); + h.append(name, value); + expected += name + ": " + value + "\r\n"; + } + expected += "\r\n"; + BOOST_TEST_EQ(h.size(), 64u); + BOOST_TEST_EQ(h.buffer(), expected); + BOOST_TEST_EQ(h.at("X-Field-0"), std::string(20, 'a')); + } + + void + testStartLineGrow() + { + // a long reason phrase forces the status line to + // grow via realloc + response_head h; + h.append(http::field::server, "burl"); + auto const reason = std::string(4000, 'z'); + h.set_start_line(200, reason); + BOOST_TEST_EQ(h.reason(), reason); + BOOST_TEST_EQ(h.at(http::field::server), "burl"); + BOOST_TEST_EQ( + h.buffer(), + "HTTP/1.1 200 " + reason + "\r\n" + "Server: burl\r\n" + "\r\n"); + } + + // A default-constructed head views shared immutable + // storage, so a status-line change must copy out of it + // before writing. Every path leaves the shared bytes + // pristine, and a rejected change does not allocate. + void + testStartLineDefaultStorage() + { + // a shorter status line shrinks in place + { + response_head h; + BOOST_TEST_EQ(h.capacity_in_bytes(), 0u); + h.set_start_line(404, ""); + BOOST_TEST_EQ(h.buffer(), "HTTP/1.1 404 \r\n\r\n"); + } + // a longer one reallocates + { + response_head h; + auto const reason = std::string(4000, 'z'); + h.set_start_line(500, reason); + BOOST_TEST_EQ( + h.buffer(), + "HTTP/1.1 500 " + reason + "\r\n" + "\r\n"); + } + // a rejected change keeps the head on the shared + // storage instead of leaving a detached buffer + response_head g; + BOOST_TEST_THROWS( + g.set_start_line(200, std::string(70000, 'z')), + std::length_error); + BOOST_TEST_EQ(g.capacity_in_bytes(), 0u); + // the shared storage was never written through + BOOST_TEST_EQ(g.buffer(), "HTTP/1.1 200 OK\r\n\r\n"); + } + + // The status line has its own 16-bit size limit, + // independent of the buffer limit; exceeding it + // throws and leaves the head unchanged. + void + testStartLineLimits() + { + response_head h; + h.append(http::field::server, "burl"); + BOOST_TEST_THROWS( + h.set_start_line( + 404, + std::string( + response_head::max_start_line_size, 'z')), + std::length_error); + // the failed operation leaves the head unchanged + BOOST_TEST_EQ(h.status_int(), 200); + BOOST_TEST_EQ( + h.buffer(), + "HTTP/1.1 200 OK\r\n" + "Server: burl\r\n" + "\r\n"); + + // the limit covers the whole line, not the reason + // alone: "HTTP/1.1 200 " + reason + "\r\n" spends + // 15 bytes on everything but the reason + auto const room = response_head::max_start_line_size - + std::strlen("HTTP/1.1 200 \r\n"); + response_head g; + g.set_start_line(404, std::string(room, 'z')); + BOOST_TEST_EQ(g.reason().size(), room); + // one byte more does not fit + BOOST_TEST_THROWS( + g.set_start_line(200, std::string(room + 1, 'z')), + std::length_error); + BOOST_TEST_EQ(g.status_int(), 404); + BOOST_TEST_EQ(g.reason().size(), room); + // a shrink is never refused + g.set_start_line(200, "OK"); + BOOST_TEST_EQ( + g.buffer(), "HTTP/1.1 200 OK\r\n\r\n"); + + // a status code outside [100, 999] is refused + // and leaves the head unchanged + response_head r; + r.append(http::field::server, "burl"); + BOOST_TEST_THROWS( + r.set_start_line(99, "Low"), std::invalid_argument); + BOOST_TEST_THROWS( + r.set_start_line(1000, "High"), std::invalid_argument); + BOOST_TEST_EQ(r.status_int(), 200); + BOOST_TEST_EQ( + r.buffer(), + "HTTP/1.1 200 OK\r\n" + "Server: burl\r\n" + "\r\n"); + } + + // Start-line setters accept arguments viewing the + // start line itself. + void + testStartLineSelfReference() + { + response_head h; + h.append(http::field::server, "burl"); + h.set_start_line(200, "Everything Is Fine"); + h.set_start_line(500, h.reason()); + BOOST_TEST_EQ(h.status_int(), 500); + BOOST_TEST_EQ(h.reason(), "Everything Is Fine"); + + // shrink to a prefix of the old reason + h.set_start_line(404, h.reason().substr(0, 10)); + BOOST_TEST_EQ(h.reason(), "Everything"); + BOOST_TEST_EQ( + h.buffer(), + "HTTP/1.1 404 Everything\r\n" + "Server: burl\r\n" + "\r\n"); + } + + void + testMove() + { + response_head h1; + h1.set_status(http::status::not_found); + h1.append(http::field::server, "burl"); + auto const before = std::string(h1.buffer()); + + response_head h2(std::move(h1)); + BOOST_TEST_EQ(h2.buffer(), before); + BOOST_TEST_EQ(h2.status_int(), 404); + + response_head h3; + h3.append(http::field::server, "other"); + h1 = std::move(h3); + BOOST_TEST_EQ(h1.at(http::field::server), "other"); + } + + void + testSwap() + { + { + response_head h1; + h1.set_status(http::status::not_found); + h1.append(http::field::server, "a"); + h1.set_content_length(7); + response_head h2; + h2.append(http::field::server, "b"); + h2.set_chunked(true); + auto const cap1 = h1.capacity_in_bytes(); + auto const cap2 = h2.capacity_in_bytes(); + // a view follows the contents into the + // other header + auto const v1 = h1.at(http::field::server); + + h1.swap(h2); + + // the status line, the fields, and the cached + // metadata all move together + BOOST_TEST_EQ(h1.status_int(), 200); + BOOST_TEST_EQ(h1.at(http::field::server), "b"); + BOOST_TEST(h1.chunked()); + BOOST_TEST(! h1.content_length().has_value()); + BOOST_TEST_EQ(h1.capacity_in_bytes(), cap2); + + BOOST_TEST_EQ(h2.status_int(), 404); + BOOST_TEST(h2.status() == http::status::not_found); + BOOST_TEST_EQ(h2.at(http::field::server), "a"); + BOOST_TEST(! h2.chunked()); + BOOST_TEST_EQ(h2.content_length().value(), 7u); + BOOST_TEST_EQ(h2.capacity_in_bytes(), cap1); + + BOOST_TEST_EQ(v1, "a"); + + // both headers remain usable + h1.set_status(http::status::accepted); + h2.append(http::field::date, "now"); + BOOST_TEST_EQ(h1.status_int(), 202); + BOOST_TEST_EQ(h2.at(http::field::date), "now"); + } + + // ADL and std::swap find the hidden friend + { + response_head h1(http::status::not_found); + response_head h2; + swap(h1, h2); + BOOST_TEST_EQ(h1.buffer(), "HTTP/1.1 200 OK\r\n\r\n"); + BOOST_TEST_EQ( + h2.buffer(), "HTTP/1.1 404 Not Found\r\n\r\n"); + std::swap(h1, h2); + BOOST_TEST_EQ( + h1.buffer(), "HTTP/1.1 404 Not Found\r\n\r\n"); + } + + // swapping with a default header transfers the + // allocation and the shared static buffer + { + response_head h1(http::status::not_found); + response_head h2; + auto const cap1 = h1.capacity_in_bytes(); + h1.swap(h2); + BOOST_TEST_EQ(h1.buffer(), "HTTP/1.1 200 OK\r\n\r\n"); + BOOST_TEST_EQ(h1.capacity_in_bytes(), 0u); + BOOST_TEST_EQ( + h2.buffer(), "HTTP/1.1 404 Not Found\r\n\r\n"); + BOOST_TEST_EQ(h2.capacity_in_bytes(), cap1); + + // the header which received the static buffer + // allocates on its next modification, and the + // start-line headroom moves with it + h1.set_status(http::status::service_unavailable); + BOOST_TEST_EQ( + h1.buffer(), + "HTTP/1.1 503 Service Unavailable\r\n\r\n"); + } + + // two default headers + { + response_head h1; + response_head h2; + h1.swap(h2); + BOOST_TEST_EQ(h1.buffer(), "HTTP/1.1 200 OK\r\n\r\n"); + BOOST_TEST_EQ(h1.capacity_in_bytes(), 0u); + BOOST_TEST_EQ(h2.buffer(), "HTTP/1.1 200 OK\r\n\r\n"); + BOOST_TEST_EQ(h2.capacity_in_bytes(), 0u); + } + + // self-swap has no effect + { + response_head h(http::status::not_found); + h.append(http::field::server, "a"); + auto& hr = h; + h.swap(hr); + BOOST_TEST_EQ( + h.buffer(), + "HTTP/1.1 404 Not Found\r\n" + "Server: a\r\n" + "\r\n"); + } + } + + void + testCopy() + { + response_head h1; + h1.set_status(http::status::not_found); + h1.append(http::field::server, "burl"); + + response_head h2(h1); + BOOST_TEST_EQ(h2.buffer(), h1.buffer()); + BOOST_TEST(h2.buffer().data() != h1.buffer().data()); + + h1.set_status(http::status::ok); + BOOST_TEST_EQ(h2.status_int(), 404); + BOOST_TEST_EQ( + h2.buffer(), + "HTTP/1.1 404 Not Found\r\n" + "Server: burl\r\n" + "\r\n"); + } + + void + testAssign() + { + response_head h1; + h1.append(http::field::server, "a"); + response_head h2; + h2.set_status(http::status::accepted); + h2.append(http::field::server, "b"); + + h1 = h2; // copy + BOOST_TEST_EQ(h1.buffer(), h2.buffer()); + BOOST_TEST(h1.buffer().data() != h2.buffer().data()); + + // self copy-assignment is a no-op + auto const& hr = h1; + h1 = hr; + BOOST_TEST_EQ(h1.buffer(), h2.buffer()); + + response_head h3; + h3.set_status(http::status::no_content); + h1 = std::move(h3); // move + BOOST_TEST_EQ(h1.status_int(), 204); + + // self move-assignment is a no-op + auto& hm = h1; + h1 = std::move(hm); + BOOST_TEST_EQ(h1.status_int(), 204); + } + + void + testReserveShrink() + { + response_head h; + h.reserve(4096, 32, 17); + BOOST_TEST(h.capacity_in_bytes() >= 4096u); + h.append(http::field::server, "burl"); + h.shrink_to_fit(); + BOOST_TEST(h.capacity_in_bytes() < 4096u); + BOOST_TEST_EQ(h.at(http::field::server), "burl"); + + // the fit is exact up to the allocation rounding, and + // a second call has nothing left to do + BOOST_TEST( + h.capacity_in_bytes() < + h.buffer().size() + + response_head::table_space(h.size()) + 8); + auto const* data = h.buffer().data(); + h.shrink_to_fit(); + BOOST_TEST(h.buffer().data() == data); + BOOST_TEST_EQ( + h.buffer(), + "HTTP/1.1 200 OK\r\n" + "Server: burl\r\n" + "\r\n"); + } + + void + testSetVersion() + { + // the version is written in place; nothing + // else moves + response_head h; + h.set_start_line(404, "Not Found"); + h.append(http::field::server, "burl"); + h.set_version(http::version::http_1_0); + BOOST_TEST(h.version() == http::version::http_1_0); + BOOST_TEST_EQ( + h.buffer(), + "HTTP/1.0 404 Not Found\r\n" + "Server: burl\r\n" + "\r\n"); + h.set_version(http::version::http_1_1); + BOOST_TEST_EQ( + h.buffer(), + "HTTP/1.1 404 Not Found\r\n" + "Server: burl\r\n" + "\r\n"); + + // a default-constructed head detaches from the + // shared storage before writing + response_head g; + BOOST_TEST_EQ(g.capacity_in_bytes(), 0u); + g.set_version(http::version::http_1_0); + BOOST_TEST_EQ(g.buffer(), "HTTP/1.0 200 OK\r\n\r\n"); + BOOST_TEST(g.capacity_in_bytes() > 0u); + // the shared storage was never written through + response_head fresh; + BOOST_TEST_EQ(fresh.buffer(), "HTTP/1.1 200 OK\r\n\r\n"); + + // with a framed payload, keep-alive follows + // the version + g.set_content_length(0); + BOOST_TEST(! g.keep_alive()); + g.set_version(http::version::http_1_1); + BOOST_TEST(g.keep_alive()); + } + + void + testClear() + { + // the status line survives; the fields and + // the state derived from them do not + response_head h; + h.set_start_line( + 404, "Not Found", http::version::http_1_0); + h.set_content_length(5); + h.set_keep_alive(true); + BOOST_TEST(h.keep_alive()); + auto const cap = h.capacity_in_bytes(); + + h.clear(); + BOOST_TEST(h.empty()); + BOOST_TEST_EQ(h.buffer(), "HTTP/1.0 404 Not Found\r\n\r\n"); + BOOST_TEST_EQ(h.status_int(), 404); + BOOST_TEST(h.version() == http::version::http_1_0); + BOOST_TEST_EQ(h.capacity_in_bytes(), cap); + BOOST_TEST(! h.content_length().has_value()); + BOOST_TEST(h.payload() == http::payload::to_eof); + BOOST_TEST(! h.keep_alive()); + } + + // Snapshot a non-owning response_head_base (as produced by + // head_parser over an external buffer) into an owner. + void + testFromBase() + { + static_assert(std::is_convertible_v< + response_head_base const&, response_head>); + + alignas(4) char buf[4096]; + std::string_view const msg = + "HTTP/1.1 404 Not Found\r\n" + "Server: burl\r\n" + "Content-Length: 0\r\n" + "\r\n"; + auto const parse = + [&](head_parser& pr) + { + std::memcpy( + pr.prepare().data(), msg.data(), msg.size()); + pr.commit(msg.size()); + system::error_code ec; + pr.parse(ec); + BOOST_TEST(!ec); + }; + + head_parser pr(false, buf, sizeof(buf)); + parse(pr); + response_head_base const& base = pr.response_head(); + + // lossless, implicit construction from the base + response_head h = base; + BOOST_TEST_EQ(h.buffer(), msg); + BOOST_TEST(h.status() == http::status::not_found); + BOOST_TEST_EQ(h.status_int(), 404); + BOOST_TEST_EQ(h.reason(), "Not Found"); + BOOST_TEST_EQ(h.at(http::field::server), "burl"); + // independent storage + BOOST_TEST(h.buffer().data() != base.buffer().data()); + + // clobbering the parse buffer leaves the snapshot intact + std::memset(buf, 'Z', msg.size()); + BOOST_TEST_EQ(h.reason(), "Not Found"); + BOOST_TEST_EQ(h.at(http::field::server), "burl"); + + // assignment from a base replaces prior contents + response_head g; + g.append(http::field::server, "old"); + head_parser pr2(false, buf, sizeof(buf)); + parse(pr2); + g = pr2.response_head(); + BOOST_TEST_EQ(g.buffer(), msg); + BOOST_TEST(g.buffer().data() != buf); + BOOST_TEST_EQ(g.count(http::field::server), 1u); + } + + // Framing metadata cached in the header must follow the + // content through assignment from a parsed base, on both + // the reallocating and the in-place path. + void + testFromBaseFraming() + { + alignas(4) char buf[4096]; + std::string_view const msg = + "HTTP/1.1 200 Fine and Dandy\r\n" + "Connection: close\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n"; + head_parser pr(false, buf, sizeof(buf)); + std::memcpy(pr.prepare().data(), msg.data(), msg.size()); + pr.commit(msg.size()); + system::error_code ec; + pr.parse(ec); + BOOST_TEST(!ec); + response_head_base const& base = pr.response_head(); + + // small target: the assignment reallocates + response_head h1; + h1 = base; + BOOST_TEST_EQ(h1.buffer(), msg); + BOOST_TEST(h1.status() == http::status::ok); + BOOST_TEST_EQ(h1.status_int(), 200); + BOOST_TEST_EQ(h1.reason(), "Fine and Dandy"); + BOOST_TEST(h1.chunked()); + BOOST_TEST(! h1.keep_alive()); + BOOST_TEST(h1.version() == http::version::http_1_1); + + // large target: the allocation is reused, and the + // cached metadata is replaced all the same + response_head h2; + h2.reserve(512, 16, 64); + h2.set_content_length(42); + BOOST_TEST(h2.content_length().has_value()); + h2 = base; + BOOST_TEST_EQ(h2.buffer(), msg); + BOOST_TEST(h2.capacity_in_bytes() > 400u); + BOOST_TEST(h2.chunked()); + BOOST_TEST(! h2.content_length().has_value()); + BOOST_TEST(! h2.keep_alive()); + BOOST_TEST_EQ(h2.status_int(), 200); + BOOST_TEST_EQ(h2.reason(), "Fine and Dandy"); + } + + // Growing the start line in place must never leave the + // remaining field capacity equal to the internal + // non-owning marker, which would silently leak the + // allocation. + void + testStartLineOwnership() + { + response_head h; + // 17 -> 22 bytes over the minimal allocation + h.set_start_line(http::status::created); + BOOST_TEST_EQ(h.buffer(), "HTTP/1.1 201 Created\r\n\r\n"); + BOOST_TEST(h.capacity_in_bytes() != 0u); + h.append(http::field::age, "1"); + BOOST_TEST_EQ(h.at(http::field::age), "1"); + + // ownership transfers on move + response_head g(std::move(h)); + BOOST_TEST_EQ(g.at(http::field::age), "1"); + BOOST_TEST_EQ(g.status_int(), 201); + BOOST_TEST_EQ(h.buffer(), "HTTP/1.1 200 OK\r\n\r\n"); + } + + void + run() + { + testDefault(); + testGrow(); + testStartLineGrow(); + testStartLineDefaultStorage(); + testStartLineLimits(); + testStartLineOwnership(); + testStartLineSelfReference(); + testMove(); + testSwap(); + testCopy(); + testAssign(); + testReserveShrink(); + testSetVersion(); + testClear(); + testFromBase(); + testFromBaseFraming(); + } +}; + +TEST_SUITE(response_head_test, "boost.burl.response_head"); + +} // namespace burl +} // namespace boost diff --git a/test/unit/static_fields.cpp b/test/unit/static_fields.cpp new file mode 100644 index 0000000..5f77164 --- /dev/null +++ b/test/unit/static_fields.cpp @@ -0,0 +1,197 @@ +// +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +// Test that header file is self-contained. +#include + +#include +#include + +#include "test_suite.hpp" + +#include +#include +#include +#include + +namespace boost +{ +namespace burl +{ + +// The container behavior shared with the owning +// container is covered by the fields tests, and the +// fixed-storage mechanics by the static_request_head +// tests. These tests cover the fields-specific surface. + +static_assert(std::is_nothrow_move_constructible_v); +static_assert(! std::is_copy_constructible_v); + +class static_fields_test +{ +public: + void + testDefault() + { + alignas(4) char buf[64]; + static_fields f(buf, sizeof(buf)); + BOOST_TEST_EQ(f.buffer(), "\r\n"); + BOOST_TEST(f.empty()); + BOOST_TEST_EQ(f.capacity_in_bytes(), 64u); + BOOST_TEST_EQ(f.buffer().data(), &buf[0]); + } + + void + testTooSmall() + { + alignas(4) char buf[8]; + // the storage must hold at least the empty + // field section + BOOST_TEST_THROWS( + static_fields(buf, 1), + std::length_error); + // an exact fit leaves no free capacity + static_fields f(buf + 2, 2); + BOOST_TEST_EQ(f.buffer(), "\r\n"); + BOOST_TEST_EQ(f.capacity_in_bytes(), 2u); + BOOST_TEST_THROWS( + f.append("X", "*"), + std::length_error); + } + + void + testFieldCapacity() + { + // the capacity is fixed: appends beyond it throw + // and leave the contents unchanged + alignas(4) char buf[64]; + static_fields f(buf, sizeof(buf)); + for(int i = 0; i < 3; ++i) + f.append("T", "*"); + auto const before = std::string(f.buffer()); + BOOST_TEST_THROWS( + f.append("T", "*"), + std::length_error); + BOOST_TEST_EQ(f.buffer(), before); + // free space released by an erase is reusable + f.erase(f.begin()); + f.append("T", "*"); + BOOST_TEST_EQ(f.buffer(), before); + + // a bulk append is all-or-nothing: when the + // list does not fit, nothing is appended + f.erase(f.begin()); + BOOST_TEST_THROWS( + f.append({ { "T", "*" }, { "T", "*" } }), + std::length_error); + f.append({ { "T", "*" } }); + BOOST_TEST_EQ(f.buffer(), before); + } + + void + testCopyAssign() + { + fields src = { + { http::field::host, "example.com" }, + { "T", "*" }, + }; + + alignas(4) char buf[128]; + static_fields f(buf, sizeof(buf)); + f = src; + BOOST_TEST_EQ(f.buffer(), src.buffer()); + BOOST_TEST(f.buffer().data() != src.buffer().data()); + BOOST_TEST( + f.buffer().data() >= buf && + f.buffer().data() < buf + sizeof(buf)); + + // assigning a default-constructed container + // copies into the fixed storage instead of + // adopting the shared read-only buffer + f = fields(); + BOOST_TEST_EQ(f.buffer(), "\r\n"); + BOOST_TEST( + f.buffer().data() >= buf && + f.buffer().data() < buf + sizeof(buf)); + f.append("T", "*"); + BOOST_TEST_EQ(f.buffer(), "T: *\r\n\r\n"); + + // assigning a message header copies only the + // field section; the start line is discarded + request_head req; + req.set(http::field::host, "example.com"); + f = req; + BOOST_TEST_EQ(f.buffer(), "Host: example.com\r\n\r\n"); + + // static to static + alignas(4) char buf2[64]; + static_fields f2(buf2, sizeof(buf2)); + f2 = f; + BOOST_TEST_EQ(f2.buffer(), f.buffer()); + BOOST_TEST(f2.buffer().data() != f.buffer().data()); + + // self-assignment + auto const& fr = f2; + f2 = fr; + BOOST_TEST_EQ(f2.buffer(), f.buffer()); + } + + void + testCopyAssignOverflow() + { + // a copy that does not fit throws and leaves the + // contents unchanged + fields big; + big.set("X", std::string(200, 'x')); + + alignas(4) char buf[32]; + static_fields f(buf, sizeof(buf)); + f.append("T", "*"); + BOOST_TEST_THROWS(f = big, std::length_error); + BOOST_TEST_EQ(f.buffer(), "T: *\r\n\r\n"); + } + + void + testMove() + { + alignas(4) char buf[64]; + static_fields f1(buf, sizeof(buf)); + f1.append("T", "*"); + auto const* p = f1.buffer().data(); + static_fields f2(std::move(f1)); + BOOST_TEST_EQ(f2.buffer(), "T: *\r\n\r\n"); + BOOST_TEST_EQ(f2.buffer().data(), p); + BOOST_TEST_EQ(f2.capacity_in_bytes(), 64u); + // the moved-from object no longer refers to the + // storage; modifiers and assignment throw + BOOST_TEST(f1.buffer().data() != p); + BOOST_TEST_EQ(f1.buffer(), "\r\n"); + BOOST_TEST_EQ(f1.capacity_in_bytes(), 0u); + BOOST_TEST_THROWS( + f1.append("T", "*"), + std::length_error); + BOOST_TEST_THROWS(f1 = f2, std::length_error); + } + + void + run() + { + testDefault(); + testTooSmall(); + testFieldCapacity(); + testCopyAssign(); + testCopyAssignOverflow(); + testMove(); + } +}; + +TEST_SUITE(static_fields_test, "boost.burl.static_fields"); + +} // namespace burl +} // namespace boost diff --git a/test/unit/static_request_head.cpp b/test/unit/static_request_head.cpp new file mode 100644 index 0000000..c8758e7 --- /dev/null +++ b/test/unit/static_request_head.cpp @@ -0,0 +1,275 @@ +// +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +// Test that header file is self-contained. +#include + +#include + +#include "test_suite.hpp" + +#include +#include +#include +#include + +namespace boost +{ +namespace burl +{ + +// The container behavior shared with the owning heads — +// fields, start line, framing — is covered by the +// request_head and fields tests. These tests focus on +// the fixed-storage semantics. + +static_assert(std::is_nothrow_move_constructible_v); +static_assert(! std::is_copy_constructible_v); + +class static_request_head_test +{ +public: + void + testDefault() + { + alignas(4) char buf[64]; + static_request_head h(buf, sizeof(buf)); + BOOST_TEST_EQ(h.buffer(), "GET / HTTP/1.1\r\n\r\n"); + BOOST_TEST(h.method() == http::method::get); + BOOST_TEST_EQ(h.target(), "/"); + BOOST_TEST(h.version() == http::version::http_1_1); + BOOST_TEST(h.empty()); + BOOST_TEST_EQ(h.capacity_in_bytes(), 64u); + BOOST_TEST_EQ(h.buffer().data(), &buf[0]); + } + + void + testAlignment() + { + // an unaligned buffer end is aligned down + alignas(4) char buf[64]; + static_request_head h(buf + 1, 62); + BOOST_TEST_EQ(h.capacity_in_bytes(), 59u); + BOOST_TEST_EQ(h.buffer(), "GET / HTTP/1.1\r\n\r\n"); + } + + void + testTooSmall() + { + alignas(4) char buf[20]; + // the storage must hold at least the default head + BOOST_TEST_THROWS( + static_request_head(buf, 17), + std::length_error); + // an exact fit leaves no field capacity + { + static_request_head h(buf + 2, 18); + BOOST_TEST_EQ(h.buffer(), "GET / HTTP/1.1\r\n\r\n"); + BOOST_TEST_EQ(h.capacity_in_bytes(), 18u); + BOOST_TEST_THROWS( + h.append("X", "*"), + std::length_error); + } + static_request_head h(buf, sizeof(buf)); + BOOST_TEST_EQ(h.buffer(), "GET / HTTP/1.1\r\n\r\n"); + BOOST_TEST_THROWS( + h.append("X", "*"), + std::length_error); + h.set_target("/x"); + BOOST_TEST_EQ(h.buffer(), "GET /x HTTP/1.1\r\n\r\n"); + } + + void + testFieldCapacity() + { + // the capacity is fixed: appends beyond it throw + // and leave the contents unchanged + alignas(4) char buf[96]; + static_request_head h(buf, sizeof(buf)); + for(int i = 0; i < 4; ++i) + h.append("T", "*"); + auto const before = std::string(h.buffer()); + BOOST_TEST_THROWS( + h.append("T", "*"), + std::length_error); + BOOST_TEST_EQ(h.buffer(), before); + // free space released by an erase is reusable + h.erase(h.begin()); + h.append("T", "*"); + BOOST_TEST_EQ(h.buffer(), before); + } + + void + testStartLine() + { + // the start line grows into the free field + // capacity, and throws when the storage is + // exhausted + alignas(4) char buf[32]; + static_request_head h(buf, sizeof(buf)); + h.set_target("/abc"); + BOOST_TEST_EQ(h.buffer(), "GET /abc HTTP/1.1\r\n\r\n"); + BOOST_TEST_THROWS( + h.set_target("/" + std::string(40, 'a')), + std::length_error); + BOOST_TEST_EQ(h.buffer(), "GET /abc HTTP/1.1\r\n\r\n"); + } + + void + testStartLineExactFitEmpty() + { + // a start-line grow may fill the storage exactly, + // leaving no free field capacity + alignas(4) char buf[32]; + static_request_head h(buf, sizeof(buf)); + h.set_target("/23456789012345"); + BOOST_TEST_EQ( + h.buffer(), "GET /23456789012345 HTTP/1.1\r\n\r\n"); + BOOST_TEST_THROWS( + h.append("T", "*"), + std::length_error); + BOOST_TEST_EQ( + h.buffer(), "GET /23456789012345 HTTP/1.1\r\n\r\n"); + } + + void + testStartLineExactFit() + { + // with fields present, an exact-fit start-line + // grow succeeds in place + alignas(4) char buf[48]; + static_request_head h(buf, sizeof(buf)); + h.append("T", "*"); + h.set_target("/234567890123"); + BOOST_TEST_EQ( + h.buffer(), + "GET /234567890123 HTTP/1.1\r\n" + "T: *\r\n" + "\r\n"); + BOOST_TEST_THROWS( + h.append("T", "*"), + std::length_error); + } + + void + testCopyAssign() + { + // a copy whose start line is longer than the + // reserved headroom repositions the split within + // the fixed storage + request_head src(http::method::post, "/x"); + src.set(http::field::content_length, "42"); + src.set(http::field::user_agent, "boost"); + + alignas(4) char buf[128]; + static_request_head h(buf, sizeof(buf)); + h = src; + BOOST_TEST_EQ(h.buffer(), src.buffer()); + BOOST_TEST(h.buffer().data() != src.buffer().data()); + BOOST_TEST( + h.buffer().data() >= buf && + h.buffer().data() < buf + sizeof(buf)); + BOOST_TEST(h.method() == http::method::post); + BOOST_TEST(h.content_length() == 42u); + + // assigning a default-constructed head copies + // into the fixed storage instead of adopting the + // shared read-only buffer + h = request_head(); + BOOST_TEST_EQ(h.buffer(), "GET / HTTP/1.1\r\n\r\n"); + BOOST_TEST( + h.buffer().data() >= buf && + h.buffer().data() < buf + sizeof(buf)); + h.append("T", "*"); + BOOST_TEST_EQ(h.buffer(), "GET / HTTP/1.1\r\nT: *\r\n\r\n"); + + // static to static + alignas(4) char buf2[64]; + static_request_head h2(buf2, sizeof(buf2)); + h2 = h; + BOOST_TEST_EQ(h2.buffer(), h.buffer()); + BOOST_TEST(h2.buffer().data() != h.buffer().data()); + + // self-assignment + auto const& hr = h2; + h2 = hr; + BOOST_TEST_EQ(h2.buffer(), h.buffer()); + } + + void + testCopyAssignOverflow() + { + // a copy that does not fit throws and leaves the + // contents unchanged + request_head big; + big.set_target("/" + std::string(200, 'p')); + + alignas(4) char buf[64]; + static_request_head h(buf, sizeof(buf)); + h.append("T", "*"); + BOOST_TEST_THROWS(h = big, std::length_error); + BOOST_TEST_EQ(h.buffer(), "GET / HTTP/1.1\r\nT: *\r\n\r\n"); + + // a copy may fill the storage exactly, with the + // repositioned start line consuming the free + // field capacity + request_head src(http::method::get, "/23456789012345"); + alignas(4) char buf2[32]; + static_request_head h2(buf2, sizeof(buf2)); + h2 = src; + BOOST_TEST_EQ(h2.buffer(), src.buffer()); + BOOST_TEST_THROWS( + h2.append("T", "*"), + std::length_error); + } + + void + testMove() + { + alignas(4) char buf[64]; + static_request_head h1(buf, sizeof(buf)); + h1.set_target("/x"); + auto const* p = h1.buffer().data(); + static_request_head h2(std::move(h1)); + BOOST_TEST_EQ(h2.buffer(), "GET /x HTTP/1.1\r\n\r\n"); + BOOST_TEST_EQ(h2.buffer().data(), p); + BOOST_TEST_EQ(h2.capacity_in_bytes(), 64u); + // the moved-from object no longer refers to the + // storage; modifiers and assignment throw + BOOST_TEST(h1.buffer().data() != p); + BOOST_TEST_EQ(h1.capacity_in_bytes(), 0u); + BOOST_TEST_THROWS( + h1.append("T", "*"), + std::length_error); + BOOST_TEST_THROWS( + h1.set_target("/other"), + std::length_error); + BOOST_TEST_THROWS(h1 = h2, std::length_error); + } + + void + run() + { + testDefault(); + testAlignment(); + testTooSmall(); + testFieldCapacity(); + testStartLine(); + testStartLineExactFitEmpty(); + testStartLineExactFit(); + testCopyAssign(); + testCopyAssignOverflow(); + testMove(); + } +}; + +TEST_SUITE(static_request_head_test, "boost.burl.static_request_head"); + +} // namespace burl +} // namespace boost diff --git a/test/unit/static_response_head.cpp b/test/unit/static_response_head.cpp new file mode 100644 index 0000000..6917fa6 --- /dev/null +++ b/test/unit/static_response_head.cpp @@ -0,0 +1,159 @@ +// +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +// Test that header file is self-contained. +#include + +#include + +#include "test_suite.hpp" + +#include +#include +#include +#include + +namespace boost +{ +namespace burl +{ + +// The container behavior shared with the owning heads is +// covered by the response_head and fields tests, and the +// fixed-storage mechanics by the static_request_head +// tests. These tests cover the response-specific surface. + +static_assert(std::is_nothrow_move_constructible_v); +static_assert(! std::is_copy_constructible_v); + +class static_response_head_test +{ +public: + void + testDefault() + { + alignas(4) char buf[64]; + static_response_head h(buf, sizeof(buf)); + BOOST_TEST_EQ(h.buffer(), "HTTP/1.1 200 OK\r\n\r\n"); + BOOST_TEST(h.status() == http::status::ok); + BOOST_TEST_EQ(h.status_int(), 200u); + BOOST_TEST_EQ(h.reason(), "OK"); + BOOST_TEST(h.version() == http::version::http_1_1); + BOOST_TEST(h.empty()); + BOOST_TEST_EQ(h.capacity_in_bytes(), 64u); + BOOST_TEST_EQ(h.buffer().data(), &buf[0]); + } + + void + testTooSmall() + { + alignas(4) char buf[20]; + // the storage must hold at least the default head + BOOST_TEST_THROWS( + static_response_head(buf, 18), + std::length_error); + // an exact fit leaves no field capacity + { + static_response_head h(buf + 1, 19); + BOOST_TEST_EQ(h.buffer(), "HTTP/1.1 200 OK\r\n\r\n"); + BOOST_TEST_EQ(h.capacity_in_bytes(), 19u); + } + static_response_head h(buf, sizeof(buf)); + BOOST_TEST_EQ(h.buffer(), "HTTP/1.1 200 OK\r\n\r\n"); + BOOST_TEST_THROWS( + h.append("X", "*"), + std::length_error); + } + + void + testCapacity() + { + // the storage is fixed: the start line and fields + // grow within it, and throw when it is exhausted + alignas(4) char buf[64]; + static_response_head h(buf, sizeof(buf)); + h.set_start_line(http::status::not_found); + h.set(http::field::content_type, "text/html"); + BOOST_TEST_EQ( + h.buffer(), + "HTTP/1.1 404 Not Found\r\n" + "Content-Type: text/html\r\n" + "\r\n"); + BOOST_TEST_THROWS( + h.append("X", std::string(40, '*')), + std::length_error); + BOOST_TEST_THROWS( + h.set_start_line( + http::status::internal_server_error, + http::version::http_1_0), + std::length_error); + BOOST_TEST_EQ( + h.buffer(), + "HTTP/1.1 404 Not Found\r\n" + "Content-Type: text/html\r\n" + "\r\n"); + } + + void + testCopyAssign() + { + response_head src(http::status::no_content); + src.set(http::field::server, "boost"); + + alignas(4) char buf[128]; + static_response_head h(buf, sizeof(buf)); + h = src; + BOOST_TEST_EQ(h.buffer(), src.buffer()); + BOOST_TEST(h.buffer().data() != src.buffer().data()); + BOOST_TEST(h.status() == http::status::no_content); + BOOST_TEST(h.payload() == http::payload::none); + + // self-assignment + auto const& hr = h; + h = hr; + BOOST_TEST_EQ(h.buffer(), src.buffer()); + + response_head big; + big.set(http::field::server, std::string(200, 'x')); + BOOST_TEST_THROWS(h = big, std::length_error); + BOOST_TEST_EQ(h.buffer(), src.buffer()); + } + + void + testMove() + { + alignas(4) char buf[64]; + static_response_head h1(buf, sizeof(buf)); + h1.set_status(http::status::accepted); + auto const* p = h1.buffer().data(); + static_response_head h2(std::move(h1)); + BOOST_TEST_EQ(h2.buffer(), "HTTP/1.1 202 Accepted\r\n\r\n"); + BOOST_TEST_EQ(h2.buffer().data(), p); + BOOST_TEST_EQ(h1.capacity_in_bytes(), 0u); + BOOST_TEST_THROWS( + h1.append("X", "*"), + std::length_error); + BOOST_TEST_THROWS(h1 = h2, std::length_error); + } + + void + run() + { + testDefault(); + testTooSmall(); + testCapacity(); + testCopyAssign(); + testMove(); + } +}; + +TEST_SUITE(static_response_head_test, "boost.burl.static_response_head"); + +} // namespace burl +} // namespace boost