From c79b5537215185d3e0fe2cb3c429a3b36adbcdb7 Mon Sep 17 00:00:00 2001 From: Mohammad Nejati Date: Sat, 8 Aug 2026 12:29:37 +0330 Subject: [PATCH 1/2] parser provides access to chunked trailer fields --- include/boost/burl/parser.hpp | 29 ++ src/detail/circular_buffer.cpp | 41 +- src/detail/grammar.hpp | 276 ++++++++++++ src/head_parser.cpp | 245 +---------- src/parser.cpp | 95 ++++- test/unit/detail/circular_buffer.cpp | 70 +++ test/unit/parser.cpp | 611 +++++++++++++++++++++++++++ 7 files changed, 1111 insertions(+), 256 deletions(-) create mode 100644 src/detail/grammar.hpp diff --git a/include/boost/burl/parser.hpp b/include/boost/burl/parser.hpp index 7e3bc56..4395ebb 100644 --- a/include/boost/burl/parser.hpp +++ b/include/boost/burl/parser.hpp @@ -458,6 +458,32 @@ class parser void consume(std::size_t n) noexcept; + /** Copy the trailer fields into a container. + + Appends each field in the trailer section + of a chunked payload to `f`, in the order + received. + + @par Preconditions + `this->got_header() == true` + + @par Exception Safety + Basic guarantee. An exception from the + container leaves the parser unchanged; + fields already appended remain, and the + call may be retried. + + @param f The container to append to. + + @param ec Set to the error, if any + occurred. + */ + BOOST_BURL_DECL + void + parse_trailer( + fields_base& f, + system::error_code& ec); + protected: parser() = default; @@ -508,6 +534,9 @@ class parser std::size_t payload_rem() const noexcept; + std::size_t + trailer_extent() const noexcept; + std::error_code walk_chunks(chunk_fn f, bool dry = false); diff --git a/src/detail/circular_buffer.cpp b/src/detail/circular_buffer.cpp index a237e6f..de05976 100644 --- a/src/detail/circular_buffer.cpp +++ b/src/detail/circular_buffer.cpp @@ -150,21 +150,34 @@ linearize(char* floor) noexcept else if(len != 0) { auto const bufs = data(); - auto const* a = static_cast(bufs[0].data()); - auto an = bufs[0].size(); - auto const* b = static_cast(bufs[1].data()); - auto const bn = bufs[1].size(); - char* base = floor; - do + auto const* a = static_cast(bufs[0].data()); + auto an = bufs[0].size(); + auto const* b = static_cast(bufs[1].data()); + auto const bn = bufs[1].size(); + auto const gap = static_cast(ptr - floor) + (cap - len); + + // leapfrogging to the floor runs ceil(an / gap) rounds + // and re-moves the wrapped range each round; past a + // bound the in-place rotate is cheaper, and it is the + // only option when gap == 0 + if(an <= gap * 16) { - auto* bp = (std::min)(base + an, const_cast(a) - bn); - b = static_cast(std::memmove(bp, b, bn)); - auto chunk_a = static_cast(b - base); - std::memcpy(base, a, chunk_a); - an -= chunk_a; - base += chunk_a; - a += chunk_a; - } while(an); + char* base = floor; + do + { + auto const k = (std::min)(an, gap); + b = static_cast(std::memmove(base + k, b, bn)); + std::memcpy(base, a, k); + an -= k; + base += k; + a += k; + } while(an); + } + else + { + std::rotate(ptr, ptr + pos, ptr + cap); + p = ptr; + } } cap = static_cast((ptr + cap) - p); ptr = p; diff --git a/src/detail/grammar.hpp b/src/detail/grammar.hpp new file mode 100644 index 0000000..daddb1e --- /dev/null +++ b/src/detail/grammar.hpp @@ -0,0 +1,276 @@ +// +// 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_SRC_DETAIL_GRAMMAR_HPP +#define BOOST_BURL_SRC_DETAIL_GRAMMAR_HPP + +#include +#include +#include + +#include +#include + +namespace boost +{ +namespace burl +{ +namespace detail +{ + +template +T +distance( + char const* it, + char const* end) noexcept +{ + return static_cast(end - it); +} + +inline +char const* +trim_front( + char const* it, char const* end) noexcept +{ + while(it != end) + { + if(*it != ' ' && *it != '\t') + break; + ++it; + } + return it; +} + +inline +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; +} + +inline +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)]; +} + +inline +bool +is_print(char c) noexcept +{ + return static_cast(c-32) < 95; +} + +inline +bool +is_target_char(char c) noexcept +{ + auto const u = static_cast(c); + return u >= 0x21 && u <= 0x7e; +} + +inline +bool +is_digit(char c) noexcept +{ + return c >= '0' && c <= '9'; +} + +inline +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 = http::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 = http::error::need_data; + return end; + } + if(*it++ != '\n') + { + ec = http::error::bad_line_ending; + return end; + } + token_end = it - 2; + } + else if(*it == '\n') + { + // bare LF + ec = http::error::bad_line_ending; + return end; + } + else + { + // invalid character + return nullptr; + } + return it; +} + +inline +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 = http::error::need_data; + return; + } + if(it == first || *it != ':') + { + ec = http::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 = http::error::bad_field_value; + return; + } + // Look 1 char past the CRLF to handle obs-fold. + if(it == end) + { + ec = http::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, + http::error limit_err, + system::error_code& ec) noexcept +{ + bool const limited = [&]() + { + if(distance(it, end) >= limit) + { + end = it + limit; + return true; + } + return false; + }(); + parse(it, end, ec); + if(ec == http::error::need_data && limited) + ec = limit_err; +} + +} // namespace detail +} // namespace burl +} // namespace boost + +#endif diff --git a/src/head_parser.cpp b/src/head_parser.cpp index 1bd90fb..c3bd477 100644 --- a/src/head_parser.cpp +++ b/src/head_parser.cpp @@ -10,6 +10,7 @@ #include +#include "detail/grammar.hpp" #include "detail/util.hpp" #include @@ -42,144 +43,16 @@ static_assert( using error = http::error; using version = http::version; -namespace -{ - -template -T -distance( - 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; -} +using detail::distance; +using detail::is_digit; +using detail::is_target_char; +using detail::is_token_char; +using detail::parse_field; +using detail::parse_limited; +using detail::parse_token_to_eol; -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 +namespace { - 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( @@ -337,106 +210,6 @@ parse_reason( 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(distance(it, end) >= limit) - { - end = it + limit; - return true; - } - return false; - }(); - parse(it, end, ec); - if(ec == error::need_data && limited) - ec = limit_err; -} - } // namespace head_parser:: diff --git a/src/parser.cpp b/src/parser.cpp index db1523a..bad4ad8 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -11,6 +11,7 @@ #include +#include "detail/grammar.hpp" #include "detail/util.hpp" #include @@ -33,6 +34,8 @@ namespace burl { using detail::clamp; +using detail::parse_field; +using detail::parse_limited; using http::condition::need_more_input; using http::error::bad_payload; @@ -312,7 +315,8 @@ has_buffered_data() const noexcept switch(payload_) { case payload::chunked: - return in_.size() > chunk_rem_; + return in_.size() > chunk_rem_ + + (fin_chunk_ ? trailer_extent() : 0); case payload::size: return in_.size() > payload_rem(); case payload::to_eof: @@ -375,14 +379,33 @@ payload_rem() const noexcept return clamp(payload_size_ - transferred_); } +std::size_t +parser:: +trailer_extent() const noexcept +{ + BOOST_ASSERT(fin_chunk_); + chained_sequence cs(in_.data()); + cs.advance(clamp(chunk_rem_)); + auto const t0 = cs.size(); + BOOST_VERIFY(! skip_trailer(cs)); + return t0 - cs.size(); +} + void parser:: start(bool head) { BOOST_ASSERT(!started_ || got_body_); - if(payload_sized() && got_body_) + if(payload_sized()) + { in_.consume(payload_rem()); + } + else if(fin_chunk_) + { + in_.consume( + clamp(chunk_rem_) + trailer_extent()); + } hp_.reset( in_.linearize(buf_.get())); @@ -413,6 +436,7 @@ reset() noexcept started_ = false; got_header_ = false; got_body_ = false; + fin_chunk_ = false; eof_ = false; } @@ -437,7 +461,7 @@ parser:: commit_eof() noexcept { eof_ = true; - if(got_header_ && payload_ == payload::to_eof) + if(payload_ == payload::to_eof) got_body_ = true; } @@ -499,13 +523,14 @@ walk_chunks(chunk_fn f, bool dry) // final chunk if(size == 0) { + auto const t0 = cs.size(); // trailer start if(auto ec = skip_trailer(cs); ec) return ec; got_body_ = true; if(!dry) { fin_chunk_ = true; - in_.consume(in_.size() - cs.size()); + in_.consume(in_.size() - t0); } return f({}, true).ec; } @@ -571,11 +596,12 @@ flatten_chunks() if(size == 0) { + auto const t0 = cs.pos(); // trailer start if(auto ec = skip_trailer(cs); ec) return bail(ec); got_body_ = true; fin_chunk_ = true; - keep = cs.pos(); + keep = t0; return bail({}); } @@ -1078,7 +1104,10 @@ pull( if(wec != need_more_input) { if(wec == capy::error::eof) - consume(0); // chunk trailer + { + // finish the framing, retain the trailer + consume(0); + } ec = wec; return {}; } @@ -1153,5 +1182,59 @@ consume(std::size_t n) noexcept } } +void +parser:: +parse_trailer( + fields_base& f, + system::error_code& ec) +{ + ec = {}; + + if(payload_ != payload::chunked) + return; + + if(! fin_chunk_) + { + ec = incomplete; + return; + } + + auto const limit = hp_.limits().max_field + 1u; // 1u for obs lookahead + char const* it = in_.ptr + in_.pos + clamp(chunk_rem_); + char const* end = in_.ptr + clamp(in_.pos + in_.len, in_.cap); + + while(it != end && *it != '\r') + { + auto const it0 = it; + std::string_view name, value; + parse_limited( + [&name, &value](auto& it, auto end, auto& ec) + { + parse_field(it, end, name, value, ec); + }, + it, + end, + limit, + http::error::field_size_limit, + ec); + if(ec) + { + if(ec == need_data) + { + BOOST_ASSERT(in_.wrapped()); + auto const off = static_cast( + it0 - (in_.ptr + in_.pos)); + in_.linearize(in_.ptr); + it = in_.ptr + off; + end = in_.ptr + in_.len; + ec = {}; + continue; + } + return; + } + f.append(name, value); + } +} + } // namespace burl } // namespace boost diff --git a/test/unit/detail/circular_buffer.cpp b/test/unit/detail/circular_buffer.cpp index 6e183e1..00e5a53 100644 --- a/test/unit/detail/circular_buffer.cpp +++ b/test/unit/detail/circular_buffer.cpp @@ -278,6 +278,73 @@ class circular_buffer_test BOOST_TEST(str(cb.data()) == "ghXYZ"); } + void + testLinearizeFullWrapped() + { + // full and wrapped with no room below: the segments + // cannot leapfrog and the contents rotate in place + char store[8]; + circular_buffer cb{ store, sizeof(store) }; + put(cb, "abcdefgh"); + cb.consume(3); + put(cb, "XYZ"); + BOOST_TEST(cb.full()); + BOOST_TEST(cb.wrapped()); + + auto* p = cb.linearize(store); + BOOST_TEST_EQ(p, store); + BOOST_TEST_EQ(cb.ptr, store); + BOOST_TEST_EQ(cb.cap, sizeof(store)); + BOOST_TEST_EQ(cb.pos, 0); + BOOST_TEST(cb.full()); + BOOST_TEST(!cb.wrapped()); + BOOST_TEST(str(cb.data()) == "defghXYZ"); + } + + void + testLinearizeFullWrappedBelowBase() + { + // full and wrapped with room below: the leapfrog lands + // at the floor and the capacity grows + char store[12]; + circular_buffer cb{ store + 4, 8 }; + put(cb, "abcdefgh"); + cb.consume(3); + put(cb, "XYZ"); + BOOST_TEST(cb.full()); + BOOST_TEST(cb.wrapped()); + + auto* p = cb.linearize(store); + BOOST_TEST_EQ(p, store); + BOOST_TEST_EQ(cb.ptr, store); + BOOST_TEST_EQ(cb.cap, sizeof(store)); + BOOST_TEST_EQ(cb.pos, 0); + BOOST_TEST(str(cb.data()) == "defghXYZ"); + } + + void + testLinearizeWrappedManyRounds() + { + // the gap is too small for the leapfrog: the contents + // rotate in place instead, and the room below the + // buffer stays unreclaimed + char store[21]; + circular_buffer cb{ store + 1, 20 }; + put(cb, "abcdefghijklmnopqrst"); + cb.consume(3); + put(cb, "XYZ"); + BOOST_TEST(cb.full()); + BOOST_TEST(cb.wrapped()); + + auto* p = cb.linearize(store); + BOOST_TEST_EQ(p, store + 1); + BOOST_TEST_EQ(cb.ptr, store + 1); + BOOST_TEST_EQ(cb.cap, 20); + BOOST_TEST_EQ(cb.pos, 0); + BOOST_TEST(!cb.wrapped()); + BOOST_TEST(str(cb.data()) == "defghijklmnopqrstXYZ"); + } + void run() { @@ -292,6 +359,9 @@ class circular_buffer_test testLinearizeWrappedOverlap(); testLinearizeWrappedLongFront(); testLinearizeWrappedBelowBase(); + testLinearizeFullWrapped(); + testLinearizeFullWrappedBelowBase(); + testLinearizeWrappedManyRounds(); } }; diff --git a/test/unit/parser.cpp b/test/unit/parser.cpp index 6532303..304032b 100644 --- a/test/unit/parser.cpp +++ b/test/unit/parser.cpp @@ -11,7 +11,9 @@ #include #include +#include #include +#include #include #include @@ -1254,6 +1256,554 @@ class parser_test }()); } + void + testTrailerReadSome() + { + capy::test::read_stream server; + capy::any_read_stream stream(&server); + test_parser pr({}, &stream); + + capy::test::run_blocking()([&]() -> capy::task<> + { + server.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\nhello\r\n" + "0\r\n" + "Expires: never\r\n" + "X-Dup: 1\r\n" + "X-Dup: 2\r\n" + "\r\n" + "NEXT"); + + pr.start(); + char buf[16]; + auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 5); + auto [ec2, n2] = co_await pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec2 == capy::cond::eof); + BOOST_TEST_EQ(n2, 0); + BOOST_TEST(pr.got_body()); + + // the retained trailer does not count as data + // beyond the message; the pipelined octets do + BOOST_TEST(pr.has_buffered_data()); + + fields f; + system::error_code tec; + pr.parse_trailer(f, tec); + BOOST_TEST(!tec); + BOOST_TEST_EQ(f.size(), 3u); + + // insertion order, known-field resolution, and + // duplicates are preserved + BOOST_TEST((*f.begin()).name == "Expires"); + BOOST_TEST(f.at(http::field::expires) == "never"); + auto r = f.find_all("X-Dup"); + auto it = r.begin(); + BOOST_TEST(*it == "1"); + BOOST_TEST(*++it == "2"); + + // each call appends again + pr.parse_trailer(f, tec); + BOOST_TEST(!tec); + BOOST_TEST_EQ(f.size(), 6u); + }()); + } + + void + testTrailerReadBody() + { + capy::test::read_stream server; + capy::any_read_stream stream(&server); + test_parser pr({}, &stream); + + capy::test::run_blocking()([&]() -> capy::task<> + { + server.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\nhello\r\n" + "6\r\n world\r\n" + "0\r\n" + "X-Trailer: v\r\n" + "\r\n" + "HTTP/1.1 200 OK\r\n" + "Content-Length: 3\r\n" + "\r\n" + "bye"); + + pr.start(); + auto [ec, body] = co_await pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(body == "hello world"); + + fields f; + system::error_code tec; + pr.parse_trailer(f, tec); + BOOST_TEST(!tec); + BOOST_TEST_EQ(f.size(), 1u); + BOOST_TEST(f.at("X-Trailer") == "v"); + + // a flattened trailer is extracted in place: the + // view returned by read_body survives + BOOST_TEST(body == "hello world"); + BOOST_TEST(pr.has_buffered_data()); + + // the undelivered view and the trailer are both + // skipped by the restart + pr.start(); + auto [ec2, body2] = co_await pr.read_body(); + BOOST_TEST(!ec2); + BOOST_TEST(body2 == "bye"); + BOOST_TEST(!pr.has_buffered_data()); + }()); + } + + void + testTrailerPullConsume() + { + capy::test::read_stream server; + capy::any_read_stream stream(&server); + test_parser pr({}, &stream); + + capy::test::run_blocking()([&]() -> capy::task<> + { + server.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "3\r\nabc\r\n" + "0\r\n" + "X-T: v\r\n" + "\r\n"); + + pr.start(); + + { + // the dry window: pull has described the whole + // message, but until it reports eof the framing + // is not finalized and the trailer is unavailable + capy::const_buffer arr[2]; + auto [ec, bufs] = co_await pr.pull(arr); + BOOST_TEST(!ec); + BOOST_TEST(pr.got_body()); + fields f; + system::error_code tec; + pr.parse_trailer(f, tec); + BOOST_TEST(tec == http::error::incomplete); + BOOST_TEST_EQ(f.size(), 0u); + } + + std::string got; + for(;;) + { + capy::const_buffer arr[2]; + auto [ec, bufs] = co_await pr.pull(arr); + if(ec) + { + BOOST_TEST(ec == capy::cond::eof); + break; + } + for(auto b : bufs) + got.append( + static_cast(b.data()), b.size()); + pr.consume(capy::buffer_size(bufs)); + } + BOOST_TEST(got == "abc"); + + fields f; + system::error_code tec; + pr.parse_trailer(f, tec); + BOOST_TEST(!tec); + BOOST_TEST_EQ(f.size(), 1u); + BOOST_TEST(f.at("X-T") == "v"); + }()); + } + + void + testTrailerEmptyAndNonChunked() + { + { + // an empty trailer section appends nothing + capy::test::read_stream server; + capy::any_read_stream stream(&server); + test_parser pr({}, &stream); + + capy::test::run_blocking()([&]() -> capy::task<> + { + server.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "0\r\n\r\n"); + + pr.start(); + auto [ec, body] = co_await pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(body == ""); + BOOST_TEST(!pr.has_buffered_data()); + + fields f; + system::error_code tec; + pr.parse_trailer(f, tec); + BOOST_TEST(!tec); + BOOST_TEST(f.empty()); + }()); + } + { + // non-chunked messages have no trailer + capy::test::read_stream server; + capy::any_read_stream stream(&server); + test_parser pr({}, &stream); + + capy::test::run_blocking()([&]() -> capy::task<> + { + server.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"); + + pr.start(); + auto [ec, body] = co_await pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(body == "hello"); + + fields f; + system::error_code tec; + pr.parse_trailer(f, tec); + BOOST_TEST(!tec); + BOOST_TEST(f.empty()); + }()); + } + } + + void + testTrailerObsFold() + { + capy::test::read_stream server; + capy::any_read_stream stream(&server); + test_parser pr({}, &stream); + + capy::test::run_blocking()([&]() -> capy::task<> + { + server.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "0\r\n" + "X-A: 1\r\n" + " fold\r\n" + "X-B: 2\r\n" + "\r\n"); + + pr.start(); + auto [ec, body] = co_await pr.read_body(); + BOOST_TEST(!ec); + + fields f; + system::error_code tec; + pr.parse_trailer(f, tec); + BOOST_TEST(!tec); + BOOST_TEST_EQ(f.size(), 2u); + BOOST_TEST(f.at("X-A") == "1 fold"); + BOOST_TEST(f.at("X-B") == "2"); + + // the in-place unfolding is stable under re-parsing + pr.parse_trailer(f, tec); + BOOST_TEST(!tec); + BOOST_TEST_EQ(f.size(), 4u); + BOOST_TEST(f.at("X-A") == "1 fold"); + }()); + } + + void + testTrailerByteByByte() + { + // one octet per read exercises the rescan of a partially + // received trailer; completion must not double-append + capy::test::read_stream server({}, 1); + capy::any_read_stream stream(&server); + test_parser pr({}, &stream); + + capy::test::run_blocking()([&]() -> capy::task<> + { + server.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\nhello\r\n" + "0\r\n" + "X-A: 1\r\n" + "X-B: 2\r\n" + "\r\n"); + + pr.start(); + auto [ec, body] = co_await pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(body == "hello"); + + fields f; + system::error_code tec; + pr.parse_trailer(f, tec); + BOOST_TEST(!tec); + BOOST_TEST_EQ(f.size(), 2u); + BOOST_TEST(f.at("X-A") == "1"); + BOOST_TEST(f.at("X-B") == "2"); + }()); + } + + void + testTrailerLimit() + { + // a field line of exactly max_field octets is accepted; + // one more is rejected + for(bool over : { false, true }) + { + capy::test::read_stream server; + capy::any_read_stream stream(&server); + parser::config cfg; + cfg.hdr_limits.max_field = 32; + test_parser pr(cfg, &stream); + + capy::test::run_blocking()([&]() -> capy::task<> + { + // "X-Name: " + value + CRLF == 32 octets + std::string const value(over ? 23 : 22, 'a'); + server.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "0\r\n" + "X-Name: " + value + "\r\n" + "\r\n"); + + pr.start(); + auto [ec, body] = co_await pr.read_body(); + BOOST_TEST(!ec); + + fields f; + system::error_code tec; + pr.parse_trailer(f, tec); + if(over) + { + BOOST_TEST(tec == http::error::field_size_limit); + BOOST_TEST(f.empty()); + } + else + { + BOOST_TEST(!tec); + BOOST_TEST(f.at("X-Name") == value); + } + }()); + } + } + + void + testTrailerBadField() + { + { + // a line which is structurally a trailer but not a + // field completes the message; only extraction fails, + // and fields parsed before the error remain + capy::test::read_stream server; + capy::any_read_stream stream(&server); + test_parser pr({}, &stream); + + capy::test::run_blocking()([&]() -> capy::task<> + { + server.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "0\r\n" + "X-A: v\r\n" + "junk\r\n" + "\r\n" + "HTTP/1.1 204 No Content\r\n" + "\r\n"); + + pr.start(); + auto [ec, body] = co_await pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(pr.got_body()); + + fields f; + system::error_code tec; + pr.parse_trailer(f, tec); + BOOST_TEST(tec == http::error::bad_field_name); + BOOST_TEST_EQ(f.size(), 1u); + BOOST_TEST(f.at("X-A") == "v"); + + // the failure does not poison the stream + pr.start(); + auto [hec] = co_await pr.read_header(); + BOOST_TEST(!hec); + BOOST_TEST_EQ(pr.get().status_int(), 204); + }()); + } + { + // a bare LF inside a trailer field survives completion + // and is rejected at extraction + capy::test::read_stream server; + capy::any_read_stream stream(&server); + test_parser pr({}, &stream); + + capy::test::run_blocking()([&]() -> capy::task<> + { + server.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "0\r\n" + "X-B: v\n\r\n" + "\r\n"); + + pr.start(); + auto [ec, body] = co_await pr.read_body(); + BOOST_TEST(!ec); + + fields f; + system::error_code tec; + pr.parse_trailer(f, tec); + BOOST_TEST(tec == http::error::bad_line_ending); + BOOST_TEST(f.empty()); + }()); + } + } + + void + testTrailerStaticFields() + { + capy::test::read_stream server; + capy::any_read_stream stream(&server); + test_parser pr({}, &stream); + + capy::test::run_blocking()([&]() -> capy::task<> + { + server.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "0\r\n" + "X-Trailer: value\r\n" + "\r\n"); + + pr.start(); + auto [ec, body] = co_await pr.read_body(); + BOOST_TEST(!ec); + + // a container too small throws in the caller's frame; + // the trailer remains available for a retry + char small[8]; + static_fields sf(small, sizeof(small)); + system::error_code tec; + BOOST_TEST_THROWS( + pr.parse_trailer(sf, tec), std::length_error); + + fields f; + pr.parse_trailer(f, tec); + BOOST_TEST(!tec); + BOOST_TEST_EQ(f.size(), 1u); + BOOST_TEST(f.at("X-Trailer") == "value"); + }()); + } + + void + testTrailerStraddleWrap() + { + // streaming leaves the retained trailer wherever the final + // chunk landed in the circular buffer. Sweeping the trailer + // length walks the section across the wrap point, so some + // iterations straddle it at every position: mid-name, + // mid-value, at the CRLF, and at the terminal empty line. + // Filling the buffer afterwards makes extraction rearrange + // a completely full buffer in some iterations. + for(std::size_t len = 0; len <= 48; ++len) + { + capy::test::read_stream server({}, 1); + capy::any_read_stream stream(&server); + parser::config cfg; + cfg.hdr_limits.max_size = 96; + cfg.in_buffer = 64; + test_parser pr(cfg, &stream); + + capy::test::run_blocking()([&]() -> capy::task<> + { + std::string const value(len, 'a'); + std::string const chunk(16, 'b'); + std::string wire = + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n"; + for(int i = 0; i != 4; ++i) + wire += "10\r\n" + chunk + "\r\n"; + wire += "0\r\n" + "X-T: " + value + "\r\n" + "\r\n"; + server.provide(wire); + + pr.start(); + auto [hec] = co_await pr.read_header(); + BOOST_TEST(!hec); + + // consume the body as it arrives so the write + // position advances and later octets wrap + std::size_t total = 0; + for(;;) + { + char buf[16]; + auto [ec, n] = co_await pr.read_some( + capy::make_buffer(buf)); + total += n; + if(ec) + { + BOOST_TEST(ec == capy::cond::eof); + break; + } + } + BOOST_TEST_EQ(total, 64); + + // pipelined octets can refill the buffer to the + // brim before extraction; alternate so both a + // full and a partially filled buffer are + // rearranged + if(len % 2 == 0) + { + for(;;) + { + auto const pb = pr.prepare(); + auto const n = capy::buffer_size(pb); + if(n == 0) + break; + for(auto b : pb) + std::memset(b.data(), 'Z', b.size()); + pr.commit(n); + } + } + + fields f; + system::error_code tec; + pr.parse_trailer(f, tec); + BOOST_TEST(!tec); + BOOST_TEST_EQ(f.size(), 1u); + BOOST_TEST(f.at("X-T") == value); + + // the rearrangement preserves the section + pr.parse_trailer(f, tec); + BOOST_TEST(!tec); + BOOST_TEST_EQ(f.size(), 2u); + + BOOST_TEST(pr.has_buffered_data() == + (len % 2 == 0)); + }()); + } + } + void testChunkedPullSmallDest() { @@ -2734,6 +3284,56 @@ class parser_test }()); } + void + testDecoderChunkedTrailer() + { + // the trailer is outside the coded content: extraction + // works alongside an installed decoder + capy::test::read_stream server; + capy::any_read_stream stream(&server); + test_parser pr({}, &stream); + test_decoder dec; + + capy::test::run_blocking()([&]() -> capy::task<> + { + server.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "3\r\nabc\r\n" + "0\r\n" + "X-T: v\r\n" + "\r\n"); + + pr.start(); + auto [hec] = co_await pr.read_header(); + BOOST_TEST(!hec); + pr.set_decoder(&dec); + + std::string got; + for(;;) + { + char buf[8]; + auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); + got.append(buf, n); + if(ec) + { + BOOST_TEST(ec == capy::cond::eof); + break; + } + } + BOOST_TEST(got == decoded("abc")); + BOOST_TEST(pr.got_body()); + + fields f; + system::error_code tec; + pr.parse_trailer(f, tec); + BOOST_TEST(!tec); + BOOST_TEST_EQ(f.size(), 1u); + BOOST_TEST(f.at("X-T") == "v"); + }()); + } + void testDecoderChunkedEarlyEof() { @@ -3812,6 +4412,16 @@ class parser_test testChunkedReadBodySplitMidChunk(); testChunkedReadBodyPipelined(); testChunkedTrailersAndExtensions(); + testTrailerReadSome(); + testTrailerReadBody(); + testTrailerPullConsume(); + testTrailerEmptyAndNonChunked(); + testTrailerObsFold(); + testTrailerByteByByte(); + testTrailerLimit(); + testTrailerBadField(); + testTrailerStaticFields(); + testTrailerStraddleWrap(); testChunkedPullSmallDest(); testChunkedBadFraming(); testChunkedBadFramingWithData(); @@ -3858,6 +4468,7 @@ class parser_test testDecoderReadBodyHardError(); testDecoderPullTwiceWithoutConsume(); testDecoderChunked(); + testDecoderChunkedTrailer(); testDecoderChunkedEarlyEof(); testDecoderChunkedIncomplete(); testDecoderToEof(); From 331021946e004bdcdf2f2feae18a19f1c1a7e4f9 Mon Sep 17 00:00:00 2001 From: Mohammad Nejati Date: Sat, 8 Aug 2026 23:03:00 +0330 Subject: [PATCH 2/2] parser body limit management is simplified --- include/boost/burl/message_reader.hpp | 22 +- include/boost/burl/parser.hpp | 116 +- src/parser.cpp | 419 +- test/unit/parser.cpp | 6185 ++++++++++++------------- test/unit/request_parser.cpp | 64 +- test/unit/response_parser.cpp | 108 +- 6 files changed, 3333 insertions(+), 3581 deletions(-) diff --git a/include/boost/burl/message_reader.hpp b/include/boost/burl/message_reader.hpp index 94ffbfd..4bf265a 100644 --- a/include/boost/burl/message_reader.hpp +++ b/include/boost/burl/message_reader.hpp @@ -70,10 +70,10 @@ namespace burl buffer. Use @ref read_some instead when the octets have to land in memory of your choosing. - Every operation drives @ref parser::parse_header - first, so reading a body without having read the - header explicitly works as expected. This serves - the caller who has no interest in the header: + Every operation parses the header first, so + reading a body without having read the header + explicitly works as expected. This serves the + caller who has no interest in the header: anything decided from it, installing a decoder above all, needs @ref read_header called explicitly, because @ref parser::set_decoder @@ -290,14 +290,10 @@ capy::io_task message_reader:: read_body_(S& stream, parser& pr) { - if(!pr.got_header()) - if(auto [ec] = co_await read_header_(stream, pr); ec) - co_return { ec, {} }; - for(;;) { system::error_code ec; - auto const sv = pr.body(ec); + auto const sv = pr.flatten_body(ec); if(ec != http::error::need_data) co_return { std::error_code(ec), sv }; if(auto [rec] = co_await refill_(stream, pr); rec) @@ -314,10 +310,6 @@ read_some_( parser& pr, MB buffers) { - if(!pr.got_header()) - if(auto [ec] = co_await read_header_(stream, pr); ec) - co_return { ec, 0 }; - capy::buffer_param bp(buffers); for(;;) @@ -380,10 +372,6 @@ pull_( parser& pr, std::span dest) { - if(!pr.got_header()) - if(auto [ec] = co_await read_header_(stream, pr); ec) - co_return { ec, {} }; - for(;;) { system::error_code ec; diff --git a/include/boost/burl/parser.hpp b/include/boost/burl/parser.hpp index 4395ebb..bd36965 100644 --- a/include/boost/burl/parser.hpp +++ b/include/boost/burl/parser.hpp @@ -59,14 +59,21 @@ namespace burl The body can be retrieved three ways, which differ in where the octets end up: - @li @ref body returns the whole body in place, - without copying, + @li @ref flatten_body returns the whole body in + place, without copying, @li @ref read_some copies into caller-supplied memory, or lets an installed decoder write into it directly, and @li @ref pull borrows the parser's own buffers, which @ref consume then releases. + Each parses the header first when it has not + been parsed already, so a caller with no + interest in the header never has to call @ref + parse_header. Installing a decoder does require + it, because @ref set_decoder must run after the + header and before any body octet. + @par Errors Every parsing operation reports through an @@ -317,19 +324,36 @@ class parser /** Return the octets which may be received directly. - Returns the number of octets which may be - read from the stream straight into - caller-supplied memory, bypassing the - parser's buffer entirely, or zero when that - is not permitted. Report octets received - this way with @ref commit_direct. + Body octets may be read from the stream + straight into caller-supplied memory, + bypassing the parser's buffer, when every + one of these holds: + + @li the header has been parsed, + + @li the payload has a known size or is + delimited by the end of the stream, + + @li no @ref decoder is installed, + + @li the buffer holds no payload octets, + + @li @ref commit_eof has not been called, and - This is zero unless the header has been - parsed, no decoder is installed, the payload - has a known size or is delimited by the end - of the stream, the buffer holds no payload - octets, the stream has not ended, and the - body limit has not been reached. + @li the body limit permits more octets: a + known size must fit within what remains of + the limit, and a payload delimited by the + end of the stream must not have reached it. + + Otherwise body octets must be received + through @ref prepare and @ref commit. + + @return The number of octets which may be + received directly, or zero when that is not + permitted. For a payload of known size this + is what remains of the payload; for one + delimited by the end of the stream it is + what remains of the body limit. @see @ref commit_direct. */ @@ -373,36 +397,42 @@ class parser void parse_header(system::error_code& ec); - /** Return the complete body in place. + /** Flatten the body in place and return it. - Reads the remainder of the body into the - parser's own buffer and returns a view of - the whole body, without copying. A chunked - payload is coalesced in place. + Coalesces the buffered body octets into a + contiguous range in the parser's own buffer + and returns a view of them, without copying. + A chunked payload is de-chunked in place. + Parses the header first if @ref got_header + returns false. @par Preconditions - @li `this->got_header() == true` - @li No octet of the body has been retrieved - by @ref read_some or @ref pull. + @ref start has been called. @param ec Set to the error, if any occurred. - Set to @ref http::error::in_place_overflow if - the body does not fit in the buffer. - - @return A view of the body, valid until the - parser is modified. + Set to @ref http::error::need_data until the + complete body is buffered, or to + @ref http::error::in_place_overflow if the + body does not fit in the buffer. + + @return A view of the body octets flattened + so far, valid until the parser is modified. + The body is complete when no error is + reported. */ BOOST_BURL_DECL std::string_view - body(system::error_code& ec); + flatten_body(system::error_code& ec); /** Copy body octets into caller-supplied memory. When a decoder is installed, it writes its - output into `buffers` directly. + output into `buffers` directly. Parses the + header first if @ref got_header returns + false. @par Preconditions - `this->got_header() == true` + @ref start has been called. @param buffers The destination. @@ -422,10 +452,11 @@ class parser Fills `dest` with descriptors referring to the parser's own buffers. Release them with - @ref consume. + @ref consume. Parses the header first if + @ref got_header returns false. @par Preconditions - `this->got_header() == true` + @ref start has been called. @param dest The descriptors to fill. @@ -490,7 +521,7 @@ class parser BOOST_BURL_DECL parser( config const& cfg, - bool is_request); + bool is_req); parser(parser&& other) noexcept = default; @@ -522,18 +553,6 @@ class parser std::error_code need_more() const noexcept; - std::size_t - raw_limit_rem() const noexcept; - - std::size_t - dec_limit_rem() const noexcept; - - bool - payload_sized() const noexcept; - - std::size_t - payload_rem() const noexcept; - std::size_t trailer_extent() const noexcept; @@ -553,14 +572,11 @@ class parser decoder * dec_ = nullptr; detail::circular_buffer in_; detail::circular_buffer out_; - std::uint64_t chunk_rem_ = 0; - std::uint64_t transferred_ = 0; - std::uint64_t decoded_ = 0; + std::uint64_t rem_ = 0; std::uint64_t body_limit_ = 0; - std::uint64_t payload_size_ = 0; + std::uint64_t limit_rem_ = 0; std::error_code dec_err_; http::payload payload_ = http::payload::none; - bool is_req_ : 1 = true; bool head_ : 1 = false; bool started_ : 1 = false; bool got_header_ : 1 = false; diff --git a/src/parser.cpp b/src/parser.cpp index bad4ad8..2403f50 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -17,7 +17,6 @@ #include #include #include -#include #include #include #include @@ -36,6 +35,7 @@ namespace burl using detail::clamp; using detail::parse_field; using detail::parse_limited; +using detail::distance; using http::condition::need_more_input; using http::error::bad_payload; @@ -44,6 +44,7 @@ using http::error::end_of_stream; using http::error::in_place_overflow; using http::error::incomplete; using http::error::need_data; +using http::error::field_size_limit; using payload = http::payload; @@ -242,13 +243,22 @@ collect( } auto -prefix( - auto buf, - std::size_t n) noexcept -> decltype(buf) +prefix(auto buf, std::size_t n) noexcept + -> decltype(buf) { return { buf.data(), clamp(buf.size(), n) }; }; +auto +first(auto const& bs) noexcept + -> decltype(*begin(bs)) +{ + for(auto b : bs) + if(b.size() != 0) + return b; + return {}; +} + } // namespace struct parser::chunk_fn @@ -280,15 +290,14 @@ struct parser::chunk_fn parser:: parser( config const& cfg, - bool is_request) + bool is_req) : body_limit_(cfg.body_limit) - , is_req_(is_request) { auto const h_cap = head_parser::bytes_needed( 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 }; + hp_ = { is_req, buf_.get(), h_cap, cfg.hdr_limits }; in_ = { buf_.get(), static_cast( hp_.ceiling() - buf_.get()) }; out_ = { buf_.get() + h_cap, cfg.dec_buffer }; @@ -312,18 +321,7 @@ bool parser:: has_buffered_data() const noexcept { - switch(payload_) - { - case payload::chunked: - return in_.size() > chunk_rem_ + - (fin_chunk_ ? trailer_extent() : 0); - case payload::size: - return in_.size() > payload_rem(); - case payload::to_eof: - return false; - default: - return !in_.empty(); - } + return in_.size() > rem_ + trailer_extent(); } std::array @@ -344,48 +342,14 @@ need_more() const noexcept return need_data; } -std::size_t -parser:: -raw_limit_rem() const noexcept -{ - BOOST_ASSERT(!dec_); - if(body_limit_ <= transferred_) - return 0; - return clamp(body_limit_ - transferred_); -} - -std::size_t -parser:: -dec_limit_rem() const noexcept -{ - BOOST_ASSERT(dec_); - if(body_limit_ <= decoded_) - return 0; - return clamp(body_limit_ - decoded_); -} - -bool -parser:: -payload_sized() const noexcept -{ - return payload_ == payload::size; -} - -std::size_t -parser:: -payload_rem() const noexcept -{ - BOOST_ASSERT(payload_sized()); - return clamp(payload_size_ - transferred_); -} - std::size_t parser:: trailer_extent() const noexcept { - BOOST_ASSERT(fin_chunk_); + if(!fin_chunk_) + return 0; chained_sequence cs(in_.data()); - cs.advance(clamp(chunk_rem_)); + cs.advance(clamp(rem_)); auto const t0 = cs.size(); BOOST_VERIFY(! skip_trailer(cs)); return t0 - cs.size(); @@ -397,32 +361,22 @@ start(bool head) { BOOST_ASSERT(!started_ || got_body_); - if(payload_sized()) - { - in_.consume(payload_rem()); - } - else if(fin_chunk_) - { - in_.consume( - clamp(chunk_rem_) + trailer_extent()); - } - + in_.consume( + clamp(rem_) + trailer_extent()); hp_.reset( in_.linearize(buf_.get())); - 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; + dec_ = nullptr; + rem_ = 0; + limit_rem_ = body_limit_; + dec_err_ = {}; + payload_ = payload::none; + head_ = head; + started_ = true; + got_header_ = false; + got_body_ = false; + mid_chunk_ = false; + fin_chunk_ = false; } void @@ -432,6 +386,7 @@ reset() noexcept hp_.reset(buf_.get()); in_.reset(buf_.get()); + rem_ = 0; payload_ = payload::none; started_ = false; got_header_ = false; @@ -452,7 +407,7 @@ parser:: commit(std::size_t n) noexcept { in_.commit(n); - if(payload_sized() && payload_rem() <= in_.size()) + if(payload_ == payload::size && rem_ <= in_.size()) got_body_ = true; } @@ -469,15 +424,17 @@ std::size_t parser:: direct_capacity() const noexcept { - if(!got_header_ || dec_ || eof_ || !in_.empty()) + if(dec_ || eof_ || !in_.empty()) return 0; switch(payload_) { case payload::size: - return clamp(payload_rem(), raw_limit_rem()); + if(rem_ > limit_rem_) + return 0; + return clamp(rem_); case payload::to_eof: - return raw_limit_rem(); + return clamp(limit_rem_); default: return 0; } @@ -487,8 +444,9 @@ void parser:: commit_direct(std::size_t n) noexcept { - transferred_ += n; - if(payload_sized() && payload_rem() == 0) + rem_ -= n; + limit_rem_ -= n; + if(payload_ == payload::size && rem_ == 0) got_body_ = true; } @@ -497,18 +455,19 @@ parser:: walk_chunks(chunk_fn f, bool dry) { chained_sequence cs = in_.data(); - std::uint64_t size = chunk_rem_; + std::uint64_t size = rem_; if(fin_chunk_) { // from flatten_chunks - auto const b = in_.first(clamp(chunk_rem_)); + auto const b = in_.first(clamp(rem_)); auto const [ec, n] = f(b, true); if(!dry) { in_.consume(n); - chunk_rem_ -= n; - transferred_ += n; + rem_ -= n; + if(!dec_) + limit_rem_ -= n; } return ec; } @@ -546,9 +505,10 @@ walk_chunks(chunk_fn f, bool dry) if(!dry) { in_.consume(in_.size() - cs.size()); - chunk_rem_ = size; - transferred_ += n; - mid_chunk_ = true; + rem_ = size; + mid_chunk_ = true; + if(!dec_) + limit_rem_ -= n; } if(ec || n < b.size()) return ec; @@ -569,7 +529,7 @@ flatten_chunks() BOOST_ASSERT(in_.pos == 0); - std::size_t flat = clamp(chunk_rem_, in_.len); + std::size_t flat = clamp(rem_, in_.len); char const* keep = in_.ptr + flat; chained_sequence cs(keep, in_.len - flat); @@ -608,7 +568,7 @@ flatten_chunks() if(size > in_.cap - flat) return bail(in_place_overflow); - chunk_rem_ = flat + size; + rem_ = flat + size; mid_chunk_ = true; auto const n = clamp(size, cs.size()); @@ -662,7 +622,6 @@ parse_header(system::error_code& ec) auto const& h = hp_.message_head(); got_header_ = true; payload_ = head_ ? payload::none : h.payload(); - payload_size_ = h.content_length().value_or(0); auto const head_size = h.buffer().size(); @@ -675,12 +634,14 @@ parse_header(system::error_code& ec) got_body_ = true; break; case payload::size: - if(payload_rem() <= in_.size() - head_size) + rem_ = h.content_length().value_or(0); + if(rem_ <= in_.size() - head_size) got_body_ = true; break; case payload::chunked: break; case payload::to_eof: + rem_ = std::uint64_t(-1); if(eof_) got_body_ = true; break; @@ -699,7 +660,6 @@ void parser:: set_decoder(decoder* dec) noexcept { - BOOST_ASSERT(transferred_ == 0); dec_ = dec; } @@ -707,108 +667,86 @@ void parser:: set_body_limit(std::uint64_t n) noexcept { + auto const transferred = body_limit_ - limit_rem_; + limit_rem_ = n > transferred ? n - transferred : 0; body_limit_ = n; } std::string_view parser:: -body(system::error_code& ec) +flatten_body(system::error_code& ec) { - BOOST_ASSERT(got_header_); - - ec = {}; + parse_header(ec); + if(ec) + return {}; if(dec_) { - if(decoded_ != out_.size()) - { - ec = incomplete; - return {}; - } for(;;) { if(out_.full()) { ec = in_place_overflow; - return {}; + break; } auto pb = out_.prepare(); auto const n = decode_some(pb, ec); out_.commit(n); - if(ec) + if(ec == capy::cond::eof) { - if(ec == capy::cond::eof) - { - ec = {}; - return { out_.ptr, out_.len }; - } - return {}; + ec = {}; + break; } + if(ec) + break; } - } - - if(transferred_ != 0) - { - ec = incomplete; - return {}; + return { out_.linearize(out_.ptr), out_.len }; } switch(payload_) { - case payload::error: - case payload::none: - { - return {}; - } case payload::chunked: { + in_.linearize(in_.ptr); for(;;) { - if(chunk_rem_ > raw_limit_rem()) + if(rem_ > limit_rem_) { ec = body_too_large; - return {}; + break; } if(fin_chunk_) - return { in_.ptr, clamp(chunk_rem_) }; + break; if(auto fec = flatten_chunks(); fec) { if(fec != need_more_input) ec = fec; else ec = need_more(); - return {}; + break; } } + return { in_.ptr, clamp(rem_, in_.len) }; } case payload::size: { - auto const rem = payload_rem(); - if(rem > raw_limit_rem()) - { + if(rem_ > limit_rem_) ec = body_too_large; - return {}; - } - if(got_body_) - return { in_.ptr, clamp(in_.len, rem) }; - ec = need_more(); - return {}; + else if(!got_body_) + ec = need_more(); + return { in_.linearize(in_.ptr), clamp(rem_, in_.len) }; } case payload::to_eof: { - if(in_.size() > raw_limit_rem()) - { + if(in_.size() > limit_rem_) ec = body_too_large; - return {}; - } - if(got_body_) - return { in_.ptr, in_.len }; - ec = need_more(); - return {}; + else if(!got_body_) + ec = need_more(); + return { in_.linearize(in_.ptr), in_.len }; } + default: + return {}; } - - return {}; } burl::response_head_base const& @@ -831,8 +769,6 @@ decode_some( std::span buffers, system::error_code& ec) { - ec = {}; - if(capy::buffer_empty(buffers)) return 0; @@ -854,19 +790,16 @@ decode_some( std::size_t cons = 0; for(;;) { - auto const out = capy::front(outbufs.data()); + auto const out = first(outbufs.data()); if(out.size() == 0) return { {}, cons }; - auto const lim = dec_limit_rem(); - if(lim == 0) - return { body_too_large, cons }; + auto const lim = clamp(limit_rem_); auto const r = dec_->process( prefix(out, lim), in, last); in += r.consumed; cons += r.consumed; - transferred_ += r.consumed; prod += r.produced; - decoded_ += r.produced; + limit_rem_ -= r.produced; outbufs.consume(r.produced); if(r.ec) { @@ -875,6 +808,8 @@ decode_some( } if(r.produced == 0 && r.consumed == 0) { + if(lim == 0) + return { body_too_large, cons }; dec_err_ = error::decode_error; return { {}, cons }; } @@ -885,12 +820,6 @@ decode_some( switch(payload_) { - case payload::error: - case payload::none: - { - ec = capy::error::eof; - return 0; - } case payload::chunked: { auto const wec = walk_chunks(decode); @@ -907,15 +836,16 @@ decode_some( { for(;;) { - auto const rem = payload_sized() ? payload_rem() : in_.size(); - auto const in = in_.first(rem); + auto const in = in_.first(clamp(rem_)); if(in.size() == 0 && !got_body_) { ec = need_more(); return 0; } - auto [dec_ec, cons] = decode(in, got_body_ && in.size() == rem); + auto [dec_ec, cons] = decode( + in, got_body_ && in.size() == clamp(rem_, in_.size())); in_.consume(cons); + rem_ -= cons; if(prod != 0) return prod; if(dec_ec) @@ -925,9 +855,12 @@ decode_some( } } } + default: + { + ec = capy::error::eof; + return 0; + } } - - return 0; } std::size_t @@ -936,15 +869,16 @@ read_some( std::span buffers, system::error_code& ec) { - BOOST_ASSERT(got_header_); - - ec = {}; + parse_header(ec); + if(ec) + return 0; if(dec_) { if(!out_.empty()) { - auto const n = capy::buffer_copy(buffers, out_.data()); + auto const n = capy::buffer_copy( + buffers, out_.data()); out_.consume(n); return n; } @@ -956,22 +890,17 @@ read_some( auto const n = capy::buffer_copy( buffers, in_.data(), at_most); in_.consume(n); - transferred_ += n; + rem_ -= n; + limit_rem_ -= n; return n; }; switch(payload_) { - case payload::error: - case payload::none: - { - ec = capy::error::eof; - return 0; - } case payload::chunked: { std::size_t read = 0; - std::size_t lim = raw_limit_rem(); + std::size_t lim = clamp(limit_rem_); auto outbufs = capy::consuming_buffers(buffers); auto const wec = walk_chunks( [&](capy::const_buffer b, bool) @@ -979,13 +908,13 @@ read_some( { auto const take = clamp(b.size(), lim); lim -= take; - auto const n = capy::buffer_copy(outbufs.data(), b, take); + auto const n = capy::buffer_copy( + outbufs.data(), b, take); read += n; outbufs.consume(n); if(take < b.size()) return { body_too_large, n }; return { {}, n }; - }); if(read != 0) return read; @@ -1005,34 +934,36 @@ read_some( } case payload::size: { - auto const rem = payload_rem(); - if(rem == 0) + if(rem_ != 0) { - ec = capy::error::eof; - return 0; + if(rem_ > limit_rem_) + { + ec = body_too_large; + return 0; + } + if(!in_.empty()) + return copy(clamp(rem_)); } - auto const lim = raw_limit_rem(); - if(lim == 0) + if(got_body_) { - ec = body_too_large; + ec = capy::error::eof; return 0; } - if(!in_.empty()) - return copy(clamp(rem, lim)); ec = need_more(); return 0; } case payload::to_eof: { - auto const lim = raw_limit_rem(); - if(lim == 0) + if(!in_.empty()) { - ec = body_too_large; - return 0; + if(limit_rem_ == 0) + { + ec = body_too_large; + return 0; + } + return copy(clamp(limit_rem_)); } - if(!in_.empty()) - return copy(lim); - if(eof_) + if(got_body_) { ec = capy::error::eof; return 0; @@ -1040,9 +971,12 @@ read_some( ec = need_more(); return 0; } + default: + { + ec = capy::error::eof; + return 0; + } } - - return 0; } std::span @@ -1051,9 +985,9 @@ pull( std::span dest, system::error_code& ec) { - BOOST_ASSERT(got_header_); - - ec = {}; + parse_header(ec); + if(ec) + return {}; if(dec_) { @@ -1070,30 +1004,21 @@ pull( switch(payload_) { - case payload::error: - case payload::none: - { - ec = capy::error::eof; - return {}; - } case payload::chunked: { std::size_t n = 0; - std::size_t lim = raw_limit_rem(); - if(lim == 0) - { - ec = body_too_large; - return {}; - } + std::size_t lim = clamp(limit_rem_); auto const wec = walk_chunks( - [&](capy::const_buffer b, bool last) + [&](capy::const_buffer b, bool) -> capy::io_result { - if(last && b.size() == 0) + if(b.size() == 0) return { capy::error::eof, 0 }; - auto const take = clamp(b.size(), lim); - if(take == 0 || n == dest.size()) + if(n == dest.size()) return { {}, 0 }; + if(lim == 0) + return { body_too_large, 0 }; + auto const take = clamp(b.size(), lim); lim -= take; dest[n++] = { b.data(), take }; return { {}, take }; @@ -1116,34 +1041,36 @@ pull( } case payload::size: { - auto const rem = payload_rem(); - auto const lim = raw_limit_rem(); - if(rem == 0) + if(rem_ != 0) { - ec = capy::error::eof; - return {}; + if(rem_ > limit_rem_) + { + ec = body_too_large; + return {}; + } + if(!in_.empty()) + return collect(dest, in_.data(), clamp(rem_)); } - if(lim == 0) + if(got_body_) { - ec = body_too_large; + ec = capy::error::eof; return {}; } - if(!in_.empty()) - return collect(dest, in_.data(), clamp(rem, lim)); ec = need_more(); return {}; } case payload::to_eof: { - auto const lim = raw_limit_rem(); - if(lim == 0) + if(!in_.empty()) { - ec = body_too_large; - return {}; + if(limit_rem_ == 0) + { + ec = body_too_large; + return {}; + } + return collect(dest, in_.data(), clamp(limit_rem_)); } - if(!in_.empty()) - return collect(dest, in_.data(), lim); - if(eof_) + if(got_body_) { ec = capy::error::eof; return {}; @@ -1151,9 +1078,12 @@ pull( ec = need_more(); return {}; } + default: + { + ec = capy::error::eof; + return {}; + } } - - return {}; } void @@ -1177,7 +1107,8 @@ consume(std::size_t n) noexcept return; default: in_.consume(n); - transferred_ += n; + rem_ -= n; + limit_rem_ -= n; return; } } @@ -1193,15 +1124,14 @@ parse_trailer( if(payload_ != payload::chunked) return; - if(! fin_chunk_) + if(!fin_chunk_) { ec = incomplete; return; } - auto const limit = hp_.limits().max_field + 1u; // 1u for obs lookahead - char const* it = in_.ptr + in_.pos + clamp(chunk_rem_); - char const* end = in_.ptr + clamp(in_.pos + in_.len, in_.cap); + char const* it = in_.ptr + in_.pos + clamp(rem_); + char const* end = in_.ptr + clamp(in_.pos + in_.len, in_.cap); while(it != end && *it != '\r') { @@ -1214,16 +1144,15 @@ parse_trailer( }, it, end, - limit, - http::error::field_size_limit, + hp_.limits().max_field + 1u, // 1u for obs lookahead, + field_size_limit, ec); if(ec) { if(ec == need_data) { BOOST_ASSERT(in_.wrapped()); - auto const off = static_cast( - it0 - (in_.ptr + in_.pos)); + auto const off = distance(it0, in_.ptr + in_.pos); in_.linearize(in_.ptr); it = in_.ptr + off; end = in_.ptr + in_.len; diff --git a/test/unit/parser.cpp b/test/unit/parser.cpp index 304032b..bb0cb24 100644 --- a/test/unit/parser.cpp +++ b/test/unit/parser.cpp @@ -12,18 +12,25 @@ #include #include -#include #include +#include +#include +#include +#include +#include +#include +#include #include -#include -#include #include +#include +#include #include #include #include #include +#include #include #include "test_suite.hpp" @@ -34,20 +41,72 @@ namespace burl { // parser has protected members (it is a base for the request/response -// parsers) and performs no I/O. This shim exposes the protected members -// and binds a stream, so that the sans-io base can be exercised through -// the same spellings a @ref message_reader offers. +// parsers) and performs no I/O: octets go in through prepare()/commit() +// and come back out through the sans-io operations. This shim exposes +// the protected members and adds a scripted wire, so that a test can say +// exactly what arrives and when, with no stream and no coroutine. +// +// The read_* members are the synchronous equivalents of what a @ref +// message_reader does over a socket: drive the parser, and whenever it +// asks for input hand it the next slice of the wire. An exhausted wire +// is a closed connection. struct test_parser : parser { test_parser( - config const& cfg, - capy::any_read_stream* stream = nullptr, + config const& cfg = {}, bool is_request = false) : parser(cfg, is_request) - , stream_(stream) { } + //-------------------------------------------- + // + // the wire + // + //-------------------------------------------- + + // Append octets for later refills. + void + provide(std::string_view s) + { + wire_.append(s); + } + + // Cap what a single refill delivers, the way a transport's read + // size does. + void + max_read(std::size_t n) noexcept + { + max_read_ = n; + } + + // Report the close together with the last octets rather than on a + // following empty refill. Real transports do this. + void + eager_eof() noexcept + { + eager_eof_ = true; + } + + // Hand the parser the next slice of the wire, or close. + void + refill() + { + if(pos_ == wire_.size()) + return commit_eof(); + auto const n = capy::buffer_copy(prepare(), next_()); + pos_ += n; + commit(n); + if(eager_eof_ && pos_ == wire_.size()) + commit_eof(); + } + + //-------------------------------------------- + // + // protected surface + // + //-------------------------------------------- + void start(bool head = false) { @@ -60,58 +119,171 @@ struct test_parser : parser return get_response(); } - auto + // Discard the parsing state and the scripted wire alike. + void + reset() noexcept + { + parser::reset(); + wire_.clear(); + pos_ = 0; + } + + using parser::consume; + + //-------------------------------------------- + // + // driving + // + //-------------------------------------------- + + std::error_code read_header() { - return reader().read_header(); + for(;;) + { + system::error_code ec; + parse_header(ec); + if(ec != http::error::need_data) + return ec; + refill(); + } } - auto + std::pair read_body() { - return reader().read_body(); + if(! got_header()) + if(auto ec = read_header(); ec) + return { ec, {} }; + + for(;;) + { + system::error_code ec; + auto const sv = flatten_body(ec); + if(ec != http::error::need_data) + return { ec, sv }; + refill(); + } } template - auto + std::pair read_some(MB buffers) { - return reader().read_some(std::move(buffers)); + if(! got_header()) + if(auto ec = read_header(); ec) + return { ec, 0 }; + + capy::buffer_param bp(buffers); + + for(;;) + { + system::error_code ec; + auto const n = parser::read_some(bp.data(), ec); + if(ec != http::error::need_data) + return { ec, n }; + + if(auto const lim = direct_capacity(); lim != 0) + { + auto const mbs = bp.data(); + auto const rn = direct_read( + capy::buffer_slice(mbs, 0, lim)); + if(rn != 0) + return { {}, rn }; + continue; + } + + refill(); + } + } + + // hand the parser the span exactly as given, with no + // buffer_param in between to drop the empty buffers + std::pair + read_some_raw(std::span buffers) + { + if(! got_header()) + if(auto ec = read_header(); ec) + return { ec, 0 }; + + for(;;) + { + system::error_code ec; + auto const n = parser::read_some(buffers, ec); + if(ec != http::error::need_data) + return { ec, n }; + refill(); + } } template - auto + std::pair read(MB buffers) { - return reader().read(std::move(buffers)); + auto const total_size = capy::buffer_size(buffers); + capy::consuming_buffers dest(buffers); + std::size_t total = 0; + + while(total < total_size) + { + auto [ec, n] = read_some(dest.data()); + dest.consume(n); + total += n; + if(ec && total < total_size) + return { ec, total }; + } + + return { {}, total }; } - auto + std::pair> pull(std::span dest) { - return reader().pull(dest); + if(! got_header()) + if(auto ec = read_header(); ec) + return { ec, {} }; + + for(;;) + { + system::error_code ec; + auto const bufs = parser::pull(dest, ec); + if(ec != http::error::need_data) + return { ec, bufs }; + refill(); + } } - // The stream is bound at the reader now, so rebinding is the shim's - // job rather than the parser's. - void - reset(capy::any_read_stream* stream) noexcept +private: + // The pass-through read: octets travel from the wire straight into + // the caller's memory, never entering the parser's buffer. + template + std::size_t + direct_read(MB buffers) { - parser::reset(); - stream_ = stream; + if(pos_ == wire_.size()) + { + commit_eof(); + return 0; + } + auto const n = capy::buffer_copy(buffers, next_()); + pos_ += n; + commit_direct(n); + if(eager_eof_ && pos_ == wire_.size()) + commit_eof(); + return n; } - using parser::consume; - using parser::reset; - -private: - message_reader - reader() noexcept + capy::const_buffer + next_() const noexcept { - return { stream_, this }; + return { wire_.data() + pos_, + (std::min)(wire_.size() - pos_, max_read_) }; } - capy::any_read_stream* stream_; + std::string wire_; + std::size_t pos_ = 0; + std::size_t max_read_ = std::size_t(-1); + bool eager_eof_ = false; }; class parser_test @@ -193,129 +365,6 @@ class parser_test std::size_t trailer_pos_ = 0; }; - // A stream that reports eof alongside the octets which complete the - // transfer, rather than on a following empty read. Real transports do - // this; capy::test::read_stream never does, so the case that a read - // delivers data and a contingency at once needs its own double. - class eager_eof_stream - { - std::string data_; - std::size_t pos_ = 0; - std::size_t max_read_size_; - - public: - explicit - eager_eof_stream( - std::string data, - std::size_t max_read_size = std::size_t(-1)) - : data_(std::move(data)) - , max_read_size_(max_read_size) - { - } - - template - auto - read_some(MB buffers) - { - struct awaitable - { - eager_eof_stream* self_; - MB buffers_; - - bool - await_ready() const noexcept - { - return true; - } - - void - await_suspend( - std::coroutine_handle<>, - capy::io_env const*) const noexcept - { - } - - capy::io_result - await_resume() - { - auto avail = self_->data_.size() - self_->pos_; - if(avail == 0) - return { capy::error::eof, 0 }; - if(avail > self_->max_read_size_) - avail = self_->max_read_size_; - auto const n = capy::buffer_copy( - buffers_, - capy::make_buffer( - self_->data_.data() + self_->pos_, avail)); - self_->pos_ += n; - // eof accompanies the last octets - if(self_->pos_ == self_->data_.size()) - return { capy::error::eof, n }; - return { {}, n }; - } - }; - return awaitable{ this, buffers }; - } - }; - - // A stream that reports a scripted error a number of times before - // delegating to the underlying test stream; models a timed-out or - // hard-failed read at the transport. - class flaky_stream - { - capy::test::read_stream& inner_; - std::error_code ec_; - int fails_; - - public: - flaky_stream( - capy::test::read_stream& inner, - std::error_code ec, - int fails = 1) - : inner_(inner) - , ec_(ec) - , fails_(fails) - { - } - - template - auto - read_some(MB buffers) - { - struct awaitable - { - flaky_stream* self_; - MB buffers_; - - bool - await_ready() const noexcept - { - return true; - } - - void - await_suspend( - std::coroutine_handle<>, - capy::io_env const*) const noexcept - { - } - - capy::io_result - await_resume() - { - if(self_->fails_ > 0) - { - --self_->fails_; - return { self_->ec_, 0 }; - } - return self_->inner_.read_some( - buffers_).await_resume(); - } - }; - return awaitable{ this, buffers }; - } - }; - static std::string make_body(std::size_t n) @@ -347,178 +396,169 @@ class parser_test void testHeader() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n"); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n"); - pr.start(); - auto [ec] = co_await pr.read_header(); - BOOST_TEST(!ec); - BOOST_TEST(pr.got_header()); - BOOST_TEST_EQ(pr.get().status_int(), 200); + pr.start(); + auto ec = pr.read_header(); + BOOST_TEST(!ec); + BOOST_TEST(pr.got_header()); + BOOST_TEST_EQ(pr.get().status_int(), 200); - // no body octet has arrived yet - BOOST_TEST(!pr.got_body()); + // no body octet has arrived yet + BOOST_TEST(!pr.got_body()); - // repeated calls are no-ops - auto [ec2] = co_await pr.read_header(); - BOOST_TEST(!ec2); - }()); + // repeated calls are no-ops + auto ec2 = pr.read_header(); + BOOST_TEST(!ec2); } void testHeaderEagerComplete() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); - - pr.start(); - auto [ec] = co_await pr.read_header(); - BOOST_TEST(!ec); - - // the whole body arrived with the header: complete - // (arrival) but not drained (undelivered) - BOOST_TEST(pr.got_body()); - BOOST_TEST(!pr.has_buffered_data()); - }()); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"); + + pr.start(); + auto ec = pr.read_header(); + BOOST_TEST(!ec); + + // the whole body arrived with the header: complete + // (arrival) but not drained (undelivered) + BOOST_TEST(pr.got_body()); + BOOST_TEST(!pr.has_buffered_data()); } void testHeaderSyntaxError() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide("BOGUS NONSENSE\r\n\r\n"); - - pr.start(); - auto [ec] = co_await pr.read_header(); - BOOST_TEST(ec); - BOOST_TEST(!pr.got_header()); - BOOST_TEST(!pr.got_body()); - - // terminal: the same error repeats deterministically - auto [ec2] = co_await pr.read_header(); - BOOST_TEST(ec2 == ec); - char buf[4]; - auto [ec3, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec3 == ec); - BOOST_TEST_EQ(n, 0); - }()); + test_parser pr; + pr.provide("BOGUS NONSENSE\r\n\r\n"); + + pr.start(); + auto ec = pr.read_header(); + BOOST_TEST(ec); + BOOST_TEST(!pr.got_header()); + BOOST_TEST(!pr.got_body()); + + // terminal: the same error repeats deterministically + auto ec2 = pr.read_header(); + BOOST_TEST(ec2 == ec); + char buf[4]; + auto [ec3, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec3 == ec); + BOOST_TEST_EQ(n, 0); } void testHeaderPayloadError() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - // 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" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "hello"); + test_parser pr; - pr.start(); - auto [ec] = co_await pr.read_header(); - BOOST_TEST(ec == http::error::bad_payload); - BOOST_TEST(!pr.got_header()); - BOOST_TEST(!pr.got_body()); - }()); + // 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 + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "hello"); + + pr.start(); + auto ec = pr.read_header(); + BOOST_TEST(ec == http::error::bad_payload); + BOOST_TEST(!pr.got_header()); + BOOST_TEST(!pr.got_body()); } void testRequestBadTransferEncoding() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream, true); - - capy::test::run_blocking()([&]() -> capy::task<> - { - // a request whose Transfer-Encoding does not end in - // chunked has no defined length; unlike a response it - // has no to-eof fallback, so the header is rejected - server.provide( - "PUT / HTTP/1.1\r\n" - "Transfer-Encoding: gzip\r\n" - "\r\n"); - - pr.start(); - auto [ec] = co_await pr.read_header(); - BOOST_TEST(ec == http::error::bad_transfer_encoding); - BOOST_TEST(!pr.got_header()); - BOOST_TEST(!pr.got_body()); - }()); + test_parser pr({}, true); + + // a request whose Transfer-Encoding does not end in + // chunked has no defined length; unlike a response it + // has no to-eof fallback, so the header is rejected + pr.provide( + "PUT / HTTP/1.1\r\n" + "Transfer-Encoding: gzip\r\n" + "\r\n"); + + pr.start(); + auto ec = pr.read_header(); + BOOST_TEST(ec == http::error::bad_transfer_encoding); + BOOST_TEST(!pr.got_header()); + BOOST_TEST(!pr.got_body()); } void testEndOfStream() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + // a clean close before any octet of the message: the + // retryable stale-connection signal + test_parser pr; - capy::test::run_blocking()([&]() -> capy::task<> - { - // a clean close before any octet of the message: the - // retryable stale-connection signal - pr.start(); - auto [ec] = co_await pr.read_header(); - BOOST_TEST(ec == http::error::end_of_stream); - BOOST_TEST(!pr.got_header()); + pr.start(); + auto ec = pr.read_header(); + BOOST_TEST(ec == http::error::end_of_stream); + BOOST_TEST(!pr.got_header()); - auto [ec2] = co_await pr.read_header(); - BOOST_TEST(ec2 == http::error::end_of_stream); - }()); + auto ec2 = pr.read_header(); + BOOST_TEST(ec2 == http::error::end_of_stream); } void testIncompleteHeader() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; + pr.provide("HTTP/1.1 200 OK\r\nContent-"); - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide("HTTP/1.1 200 OK\r\nContent-"); + pr.start(); + auto ec = pr.read_header(); + BOOST_TEST(ec == http::error::incomplete); + BOOST_TEST(!pr.got_header()); - pr.start(); - auto [ec] = co_await pr.read_header(); - BOOST_TEST(ec == http::error::incomplete); + auto ec2 = pr.read_header(); + BOOST_TEST(ec2 == http::error::incomplete); + } + + void + testHeaderResumes() + { + // a header split across refills is resumed, not restarted: + // each partial arrival asks for more input and leaves the + // parser usable. The start line is 17 octets, so five + // refills of four deliver it without reaching the close. + test_parser pr; + pr.provide("HTTP/1.1 200 OK\r\n"); + pr.max_read(4); + + pr.start(); + for(int i = 0; i != 5; ++i) + { + system::error_code ec; + pr.parse_header(ec); + BOOST_TEST(ec == http::error::need_data); BOOST_TEST(!pr.got_header()); + pr.refill(); + } - auto [ec2] = co_await pr.read_header(); - BOOST_TEST(ec2 == http::error::incomplete); - }()); + pr.provide( + "Content-Length: 5\r\n" + "\r\n" + "hello"); + auto [ec, body] = pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(body == "hello"); } void @@ -529,25 +569,20 @@ class parser_test parser::config cfg; cfg.hdr_limits.max_fields = 1; cfg.hdr_limits.max_size = 64; - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr(cfg, &stream); + test_parser pr(cfg); - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "X-Filler: " + std::string(100, 'x') + "\r\n" - "\r\n"); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "X-Filler: " + std::string(100, 'x') + "\r\n" + "\r\n"); - pr.start(); - auto [ec] = co_await pr.read_header(); - BOOST_TEST(ec == http::error::headers_limit); + pr.start(); + auto ec = pr.read_header(); + BOOST_TEST(ec == http::error::headers_limit); - // exceeding a header limit is terminal - auto [ec2] = co_await pr.read_header(); - BOOST_TEST(ec2 == http::error::headers_limit); - }()); + // exceeding a header limit is terminal + auto ec2 = pr.read_header(); + BOOST_TEST(ec2 == http::error::headers_limit); } //-------------------------------------------- @@ -559,272 +594,231 @@ class parser_test void testSizedReadSome() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"); - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); + pr.start(); + auto ec = pr.read_header(); + BOOST_TEST(!ec); - pr.start(); - auto [ec] = co_await pr.read_header(); + char buf[3]; + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); BOOST_TEST(!ec); - - char buf[3]; - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 3); - BOOST_TEST(std::string_view(buf, n) == "hel"); - } - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 2); - BOOST_TEST(std::string_view(buf, n) == "lo"); - } - BOOST_TEST(pr.got_body()); - BOOST_TEST(!pr.has_buffered_data()); - // terminal success repeats deterministically - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec == capy::cond::eof); - BOOST_TEST_EQ(n, 0); - } - { - auto [ec, n] = co_await pr.read(capy::make_buffer(buf)); - BOOST_TEST(ec == capy::cond::eof); - BOOST_TEST_EQ(n, 0); - } - }()); + BOOST_TEST_EQ(n, 3); + BOOST_TEST(std::string_view(buf, n) == "hel"); + } + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 2); + BOOST_TEST(std::string_view(buf, n) == "lo"); + } + BOOST_TEST(pr.got_body()); + BOOST_TEST(!pr.has_buffered_data()); + // terminal success repeats deterministically + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(n, 0); + } + { + auto [ec, n] = pr.read(capy::make_buffer(buf)); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(n, 0); + } } void testSizedPullConsume() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"); - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); + pr.start(); + auto ec = pr.read_header(); + BOOST_TEST(!ec); - pr.start(); - auto [ec] = co_await pr.read_header(); + capy::const_buffer arr[2]; + { + auto [ec, bufs] = pr.pull(arr); BOOST_TEST(!ec); - - capy::const_buffer arr[2]; - { - auto [ec, bufs] = co_await pr.pull(arr); - BOOST_TEST(!ec); - BOOST_TEST_EQ(bufs.size(), 1); - BOOST_TEST_EQ(bufs[0].size(), 5); - } - pr.consume(3); - { - // unconsumed data is re-returned - auto [ec, bufs] = co_await pr.pull(arr); - BOOST_TEST(!ec); - BOOST_TEST_EQ(bufs.size(), 1); - BOOST_TEST_EQ(bufs[0].size(), 2); - } - pr.consume(2); - BOOST_TEST(pr.got_body()); - { - auto [ec, bufs] = co_await pr.pull(arr); - BOOST_TEST(ec == capy::cond::eof); - BOOST_TEST_EQ(bufs.size(), 0); - } - { - auto [ec, bufs] = co_await pr.pull(arr); - BOOST_TEST(ec == capy::cond::eof); - BOOST_TEST_EQ(bufs.size(), 0); - } - }()); + BOOST_TEST_EQ(bufs.size(), 1); + BOOST_TEST_EQ(bufs[0].size(), 5); + } + pr.consume(3); + { + // unconsumed data is re-returned + auto [ec, bufs] = pr.pull(arr); + BOOST_TEST(!ec); + BOOST_TEST_EQ(bufs.size(), 1); + BOOST_TEST_EQ(bufs[0].size(), 2); + } + pr.consume(2); + BOOST_TEST(pr.got_body()); + { + auto [ec, bufs] = pr.pull(arr); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(bufs.size(), 0); + } + { + auto [ec, bufs] = pr.pull(arr); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(bufs.size(), 0); + } } void testSizedReadBody() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"); - capy::test::run_blocking()([&]() -> capy::task<> + pr.start(); { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); + auto [ec, body] = pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(body == "hello"); + } + BOOST_TEST(pr.got_body()); + BOOST_TEST(!pr.has_buffered_data()); - pr.start(); - { - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(!ec); - BOOST_TEST(body == "hello"); - } - BOOST_TEST(pr.got_body()); - BOOST_TEST(!pr.has_buffered_data()); - - // read_body is non-destructive: the view is stable and - // the parser is not drained - { - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(!ec); - BOOST_TEST(body == "hello"); - } - - // a streaming read serves the same bytes - char buf[8]; - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); + // read_body is non-destructive: the view is stable and + // the parser is not drained + { + auto [ec, body] = pr.read_body(); BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 5); - BOOST_TEST(std::string_view(buf, n) == "hello"); - auto [ec2, n2] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec2 == capy::cond::eof); - BOOST_TEST_EQ(n2, 0); - }()); + BOOST_TEST(body == "hello"); + } + + // a streaming read serves the same bytes + char buf[8]; + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 5); + BOOST_TEST(std::string_view(buf, n) == "hello"); + auto [ec2, n2] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec2 == capy::cond::eof); + BOOST_TEST_EQ(n2, 0); } void testSizedTrailingJunk() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "helloJUNK"); - - pr.start(); - char buf[16]; - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 5); - BOOST_TEST(std::string_view(buf, n) == "hello"); - auto [ec2, n2] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec2 == capy::cond::eof); - BOOST_TEST_EQ(n2, 0); - BOOST_TEST(pr.got_body()); - BOOST_TEST(pr.has_buffered_data()); - }()); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "helloJUNK"); + + pr.start(); + char buf[16]; + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 5); + BOOST_TEST(std::string_view(buf, n) == "hello"); + auto [ec2, n2] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec2 == capy::cond::eof); + BOOST_TEST_EQ(n2, 0); + BOOST_TEST(pr.got_body()); + BOOST_TEST(pr.has_buffered_data()); } void testSizedZeroLength() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 0\r\n" + "\r\n"); - capy::test::run_blocking()([&]() -> capy::task<> + pr.start(); + auto ec = pr.read_header(); + BOOST_TEST(!ec); + BOOST_TEST(pr.got_body()); { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 0\r\n" - "\r\n"); - - pr.start(); - auto [ec] = co_await pr.read_header(); + auto [ec, body] = pr.read_body(); BOOST_TEST(!ec); - BOOST_TEST(pr.got_body()); - { - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(!ec); - BOOST_TEST(body.empty()); - } - char buf[4]; - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec == capy::cond::eof); - BOOST_TEST_EQ(n, 0); - } - capy::const_buffer arr[2]; - { - auto [ec, bufs] = co_await pr.pull(arr); - BOOST_TEST(ec == capy::cond::eof); - BOOST_TEST_EQ(bufs.size(), 0); - } - }()); + BOOST_TEST(body.empty()); + } + char buf[4]; + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(n, 0); + } + capy::const_buffer arr[2]; + { + auto [ec, bufs] = pr.pull(arr); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(bufs.size(), 0); + } } void testSizedIncomplete() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hel"); - capy::test::run_blocking()([&]() -> capy::task<> + pr.start(); + char buf[8]; { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hel"); - - pr.start(); - char buf[8]; - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 3); - } - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec == http::error::incomplete); - BOOST_TEST_EQ(n, 0); - } - BOOST_TEST(!pr.got_body()); - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec == http::error::incomplete); - BOOST_TEST_EQ(n, 0); - } - }()); + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 3); + } + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == http::error::incomplete); + BOOST_TEST_EQ(n, 0); + } + BOOST_TEST(!pr.got_body()); + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == http::error::incomplete); + BOOST_TEST_EQ(n, 0); + } } void testSizedReadBodyIncomplete() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hel"); - - pr.start(); - // read_body yields an empty view on any error - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(ec == http::error::incomplete); - BOOST_TEST(body.empty()); - - auto [ec2, body2] = co_await pr.read_body(); - BOOST_TEST(ec2 == http::error::incomplete); - BOOST_TEST(body2.empty()); - }()); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hel"); + + pr.start(); + // the octets which did arrive are still flattened and + // viewable; the error says the body is not all there + auto [ec, body] = pr.read_body(); + BOOST_TEST(ec == http::error::incomplete); + BOOST_TEST(body == "hel"); + + auto [ec2, body2] = pr.read_body(); + BOOST_TEST(ec2 == http::error::incomplete); + BOOST_TEST(body2 == "hel"); } void @@ -832,33 +826,24 @@ class parser_test { parser::config cfg; cfg.body_limit = 4; - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr(cfg, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); - - pr.start(); - auto [ec] = co_await pr.read_header(); - BOOST_TEST(!ec); - BOOST_TEST(pr.got_header()); - - char buf[16]; - // the in-limit prefix is delivered first - auto [ec2, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec2); - BOOST_TEST_EQ(n, 4); - - auto [ec3, n2] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec3 == http::error::body_too_large); - BOOST_TEST_EQ(n2, 0); - }()); + test_parser pr(cfg); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"); + + pr.start(); + auto ec = pr.read_header(); + BOOST_TEST(!ec); + BOOST_TEST(pr.got_header()); + + char buf[16]; + // the declared length alone settles it: no + // octet is delivered + auto [ec2, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec2 == http::error::body_too_large); + BOOST_TEST_EQ(n, 0); } void @@ -866,25 +851,18 @@ class parser_test { parser::config cfg; cfg.body_limit = 4; - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr(cfg, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); - - pr.start(); - // the body can never be returned whole; read_body yields an - // empty view alongside the error - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(ec == http::error::body_too_large); - BOOST_TEST(body.empty()); - }()); + test_parser pr(cfg); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"); + + pr.start(); + // the declared length alone settles it: the body can never + // be returned whole, and no refill will change that + auto [ec, body] = pr.read_body(); + BOOST_TEST(ec == http::error::body_too_large); } void @@ -893,56 +871,44 @@ class parser_test // a zero budget fails the pull before any delivery parser::config cfg; cfg.body_limit = 0; - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr(cfg, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); - - pr.start(); - capy::const_buffer arr[2]; - auto [ec, bufs] = co_await pr.pull(arr); - BOOST_TEST(ec == http::error::body_too_large); - BOOST_TEST_EQ(bufs.size(), 0); - }()); + test_parser pr(cfg); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"); + + pr.start(); + capy::const_buffer arr[2]; + auto [ec, bufs] = pr.pull(arr); + BOOST_TEST(ec == http::error::body_too_large); + BOOST_TEST_EQ(bufs.size(), 0); } void testSizedMixedStreamThenView() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"); - capy::test::run_blocking()([&]() -> capy::task<> + pr.start(); + char buf[2]; { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); - - pr.start(); - char buf[2]; - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 2); - } - // read_body serves the whole body as one view and cannot - // reconstruct a body whose leading octets were already - // streamed, so it fails rather than returning a remainder - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(ec == http::error::incomplete); - BOOST_TEST(body.empty()); - BOOST_TEST(pr.got_body()); - }()); + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 2); + } + // the octets already streamed out are gone, so flattening + // what is left yields the remainder rather than the whole + // body + auto [ec, body] = pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(body == "llo"); + BOOST_TEST(pr.got_body()); } void @@ -954,43 +920,40 @@ class parser_test "\r\n"; // in_ is sized so the header fits but the body does not: - // an exact header window leaves ~25 octets for the body + // an exact header window leaves 25 octets for the body parser::config cfg; cfg.hdr_limits.max_fields = 1; cfg.hdr_limits.max_size = hdr.size(); cfg.in_buffer = 25; - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr(cfg, &stream); + test_parser pr(cfg); - capy::test::run_blocking()([&]() -> capy::task<> - { - auto const body = make_body(40); - server.provide(std::string(hdr) + body); + auto const body = make_body(40); + pr.provide(std::string(hdr) + body); - pr.start(); - { - auto [ec, part] = co_await pr.read_body(); - BOOST_TEST(ec == http::error::in_place_overflow); - BOOST_TEST(part.empty()); - } - // delivery-side overflow is transient: streaming reads - // drain the whole message (read_body did not consume) - std::string got; - for(;;) + pr.start(); + { + // the buffer cannot hold the whole body; what fits is + // still flattened and viewable + auto [ec, part] = pr.read_body(); + BOOST_TEST(ec == http::error::in_place_overflow); + BOOST_TEST(part == std::string_view(body).substr(0, 25)); + } + // delivery-side overflow is transient: streaming reads + // drain the whole message (read_body did not consume) + std::string got; + for(;;) + { + char buf[16]; + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + got.append(buf, n); + if(ec) { - char buf[16]; - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - got.append(buf, n); - if(ec) - { - BOOST_TEST(ec == capy::cond::eof); - break; - } + BOOST_TEST(ec == capy::cond::eof); + break; } - BOOST_TEST(got == body); - BOOST_TEST(pr.got_body()); - }()); + } + BOOST_TEST(got == body); + BOOST_TEST(pr.got_body()); } //-------------------------------------------- @@ -1002,715 +965,611 @@ class parser_test void testChunkedReadSome() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\nhello\r\n" + "0\r\n\r\n"); - capy::test::run_blocking()([&]() -> capy::task<> + pr.start(); + char buf[3]; { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "5\r\nhello\r\n" - "0\r\n\r\n"); - - pr.start(); - char buf[3]; - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 3); - BOOST_TEST(std::string_view(buf, n) == "hel"); - } - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 2); - BOOST_TEST(std::string_view(buf, n) == "lo"); - } - BOOST_TEST(pr.got_body()); + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 3); + BOOST_TEST(std::string_view(buf, n) == "hel"); + } + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 2); + BOOST_TEST(std::string_view(buf, n) == "lo"); + } + BOOST_TEST(pr.got_body()); - // the terminal framing was consumed: the connection - // holds nothing beyond the message - BOOST_TEST(!pr.has_buffered_data()); - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec == capy::cond::eof); - BOOST_TEST_EQ(n, 0); - } - }()); + // the terminal framing was consumed: the connection + // holds nothing beyond the message + BOOST_TEST(!pr.has_buffered_data()); + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(n, 0); + } } void testChunkedPullConsume() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\nhello\r\n" + "8\r\nuniverse\r\n" + "0\r\n\r\n"); - capy::test::run_blocking()([&]() -> capy::task<> + pr.start(); + capy::const_buffer arr[2]; { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "5\r\nhello\r\n" - "8\r\nuniverse\r\n" - "0\r\n\r\n"); - - pr.start(); - capy::const_buffer arr[2]; - { - auto [ec, bufs] = co_await pr.pull(arr); - BOOST_TEST(!ec); - BOOST_TEST_EQ(bufs.size(), 2); - BOOST_TEST_EQ(bufs[0].size(), 5); - BOOST_TEST_EQ(bufs[1].size(), 8); - } - pr.consume(3); - { - auto [ec, bufs] = co_await pr.pull(arr); - BOOST_TEST(!ec); - BOOST_TEST_EQ(bufs.size(), 2); - BOOST_TEST_EQ(bufs[0].size(), 2); - BOOST_TEST_EQ(bufs[1].size(), 8); - } - pr.consume(10); - BOOST_TEST(pr.got_body()); - BOOST_TEST(!pr.has_buffered_data()); - { - auto [ec, bufs] = co_await pr.pull(arr); - BOOST_TEST(ec == capy::cond::eof); - BOOST_TEST_EQ(bufs.size(), 0); - } - }()); + auto [ec, bufs] = pr.pull(arr); + BOOST_TEST(!ec); + BOOST_TEST_EQ(bufs.size(), 2); + BOOST_TEST_EQ(bufs[0].size(), 5); + BOOST_TEST_EQ(bufs[1].size(), 8); + } + pr.consume(3); + { + auto [ec, bufs] = pr.pull(arr); + BOOST_TEST(!ec); + BOOST_TEST_EQ(bufs.size(), 2); + BOOST_TEST_EQ(bufs[0].size(), 2); + BOOST_TEST_EQ(bufs[1].size(), 8); + } + pr.consume(10); + BOOST_TEST(pr.got_body()); + BOOST_TEST(!pr.has_buffered_data()); + { + auto [ec, bufs] = pr.pull(arr); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(bufs.size(), 0); + } } void testChunkedReadBody() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\nhello\r\n" + "6\r\n world\r\n" + "0\r\n\r\n"); - capy::test::run_blocking()([&]() -> capy::task<> + pr.start(); { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "5\r\nhello\r\n" - "6\r\n world\r\n" - "0\r\n\r\n"); - - pr.start(); - { - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(!ec); - BOOST_TEST(body == "hello world"); - } - BOOST_TEST(pr.got_body()); - BOOST_TEST(!pr.has_buffered_data()); - { - // stable repeated view - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(!ec); - BOOST_TEST(body == "hello world"); - } - // a streaming switch drains the parked dechunked bytes - char buf[16]; - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); + auto [ec, body] = pr.read_body(); BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 11); - BOOST_TEST(std::string_view(buf, n) == "hello world"); - auto [ec2, n2] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec2 == capy::cond::eof); - BOOST_TEST_EQ(n2, 0); - }()); + BOOST_TEST(body == "hello world"); + } + BOOST_TEST(pr.got_body()); + BOOST_TEST(!pr.has_buffered_data()); + { + // stable repeated view + auto [ec, body] = pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(body == "hello world"); + } + // a streaming switch drains the parked dechunked bytes + char buf[16]; + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 11); + BOOST_TEST(std::string_view(buf, n) == "hello world"); + auto [ec2, n2] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec2 == capy::cond::eof); + BOOST_TEST_EQ(n2, 0); } void testChunkedReadBodyByteByByte() { - // one octet per read exercises every resume point of the + // one octet per refill exercises every resume point of the // in-place flattening walk: split chunk-size line, split // extension, split chunk data, split closing CRLF, and // split trailers - capy::test::read_stream server({}, 1); - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "5;ext=1\r\nhello\r\n" - "6\r\n world\r\n" - "0\r\n" - "X-Trailer: v\r\n" - "\r\n"); - - pr.start(); - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(!ec); - BOOST_TEST(body == "hello world"); - BOOST_TEST(pr.got_body()); - BOOST_TEST(!pr.has_buffered_data()); - }()); - } + test_parser pr; + pr.max_read(1); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5;ext=1\r\nhello\r\n" + "6\r\n world\r\n" + "0\r\n" + "X-Trailer: v\r\n" + "\r\n"); + + pr.start(); + auto [ec, body] = pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(body == "hello world"); + BOOST_TEST(pr.got_body()); + BOOST_TEST(!pr.has_buffered_data()); + } void testChunkedReadBodySplitMidChunk() { - // a chunk larger than the transport's read size needs several - // refills; the missing bytes must land in position without + // a chunk larger than a single refill needs several of + // them; the missing bytes must land in position without // corrupting the already-flattened prefix - capy::test::read_stream server({}, 4); - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "1a\r\nabcdefghijklmnopqrstuvwxyz\r\n" - "0\r\n\r\n"); - - pr.start(); - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(!ec); - BOOST_TEST(body == "abcdefghijklmnopqrstuvwxyz"); - BOOST_TEST(pr.got_body()); - BOOST_TEST(!pr.has_buffered_data()); - }()); + test_parser pr; + pr.max_read(4); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "1a\r\nabcdefghijklmnopqrstuvwxyz\r\n" + "0\r\n\r\n"); + + pr.start(); + auto [ec, body] = pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(body == "abcdefghijklmnopqrstuvwxyz"); + BOOST_TEST(pr.got_body()); + BOOST_TEST(!pr.has_buffered_data()); } void testChunkedReadBodyPipelined() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "5\r\nhello\r\n" - "6\r\n world\r\n" - "0\r\n\r\n" - "NEXT"); - - pr.start(); - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(!ec); - BOOST_TEST(body == "hello world"); - BOOST_TEST(pr.got_body()); - - // the pipelined octets survive the compaction and sit - // right past the body view - BOOST_TEST(pr.has_buffered_data()); - }()); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\nhello\r\n" + "6\r\n world\r\n" + "0\r\n\r\n" + "NEXT"); + + pr.start(); + auto [ec, body] = pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(body == "hello world"); + BOOST_TEST(pr.got_body()); + + // the pipelined octets survive the compaction and sit + // right past the body view + BOOST_TEST(pr.has_buffered_data()); } void testChunkedTrailersAndExtensions() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "5;ext=value\r\nhello\r\n" - "0\r\n" - "X-Trailer: yes\r\n" - "X-More: sure\r\n" - "\r\n" - "NEXT"); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5;ext=value\r\nhello\r\n" + "0\r\n" + "X-Trailer: yes\r\n" + "X-More: sure\r\n" + "\r\n" + "NEXT"); + + pr.start(); + char buf[16]; + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 5); + auto [ec2, n2] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec2 == capy::cond::eof); + BOOST_TEST_EQ(n2, 0); + BOOST_TEST(pr.got_body()); + + // trailers consumed with the message; the pipelined + // octets past it are detected + BOOST_TEST(pr.has_buffered_data()); + } - pr.start(); - char buf[16]; - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 5); - auto [ec2, n2] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec2 == capy::cond::eof); - BOOST_TEST_EQ(n2, 0); - BOOST_TEST(pr.got_body()); + void + testTrailerReadSome() + { + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\nhello\r\n" + "0\r\n" + "Expires: never\r\n" + "X-Dup: 1\r\n" + "X-Dup: 2\r\n" + "\r\n" + "NEXT"); + + pr.start(); + char buf[16]; + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 5); + auto [ec2, n2] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec2 == capy::cond::eof); + BOOST_TEST_EQ(n2, 0); + BOOST_TEST(pr.got_body()); + + // the retained trailer does not count as data + // beyond the message; the pipelined octets do + BOOST_TEST(pr.has_buffered_data()); + + fields f; + system::error_code tec; + pr.parse_trailer(f, tec); + BOOST_TEST(!tec); + BOOST_TEST_EQ(f.size(), 3u); + + // insertion order, known-field resolution, and + // duplicates are preserved + BOOST_TEST((*f.begin()).name == "Expires"); + BOOST_TEST(f.at(http::field::expires) == "never"); + auto r = f.find_all("X-Dup"); + auto it = r.begin(); + BOOST_TEST(*it == "1"); + BOOST_TEST(*++it == "2"); + + // each call appends again + pr.parse_trailer(f, tec); + BOOST_TEST(!tec); + BOOST_TEST_EQ(f.size(), 6u); + } - // trailers consumed with the message; the pipelined - // octets past it are detected - BOOST_TEST(pr.has_buffered_data()); - }()); + void + testTrailerReadBody() + { + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\nhello\r\n" + "6\r\n world\r\n" + "0\r\n" + "X-Trailer: v\r\n" + "\r\n" + "HTTP/1.1 200 OK\r\n" + "Content-Length: 3\r\n" + "\r\n" + "bye"); + + pr.start(); + auto [ec, body] = pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(body == "hello world"); + + fields f; + system::error_code tec; + pr.parse_trailer(f, tec); + BOOST_TEST(!tec); + BOOST_TEST_EQ(f.size(), 1u); + BOOST_TEST(f.at("X-Trailer") == "v"); + + // a flattened trailer is extracted in place: the + // view returned by read_body survives + BOOST_TEST(body == "hello world"); + BOOST_TEST(pr.has_buffered_data()); + + // the undelivered view and the trailer are both + // skipped by the restart + pr.start(); + auto [ec2, body2] = pr.read_body(); + BOOST_TEST(!ec2); + BOOST_TEST(body2 == "bye"); + BOOST_TEST(!pr.has_buffered_data()); } void - testTrailerReadSome() + testTrailerPullConsume() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "3\r\nabc\r\n" + "0\r\n" + "X-T: v\r\n" + "\r\n"); - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "5\r\nhello\r\n" - "0\r\n" - "Expires: never\r\n" - "X-Dup: 1\r\n" - "X-Dup: 2\r\n" - "\r\n" - "NEXT"); + pr.start(); - pr.start(); - char buf[16]; - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); + { + // the dry window: pull has described the whole + // message, but until it reports eof the framing + // is not finalized and the trailer is unavailable + capy::const_buffer arr[2]; + auto [ec, bufs] = pr.pull(arr); BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 5); - auto [ec2, n2] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec2 == capy::cond::eof); - BOOST_TEST_EQ(n2, 0); BOOST_TEST(pr.got_body()); - - // the retained trailer does not count as data - // beyond the message; the pipelined octets do - BOOST_TEST(pr.has_buffered_data()); - fields f; system::error_code tec; pr.parse_trailer(f, tec); - BOOST_TEST(!tec); - BOOST_TEST_EQ(f.size(), 3u); - - // insertion order, known-field resolution, and - // duplicates are preserved - BOOST_TEST((*f.begin()).name == "Expires"); - BOOST_TEST(f.at(http::field::expires) == "never"); - auto r = f.find_all("X-Dup"); - auto it = r.begin(); - BOOST_TEST(*it == "1"); - BOOST_TEST(*++it == "2"); - - // each call appends again - pr.parse_trailer(f, tec); - BOOST_TEST(!tec); - BOOST_TEST_EQ(f.size(), 6u); - }()); + BOOST_TEST(tec == http::error::incomplete); + BOOST_TEST_EQ(f.size(), 0u); + } + + std::string got; + for(;;) + { + capy::const_buffer arr[2]; + auto [ec, bufs] = pr.pull(arr); + if(ec) + { + BOOST_TEST(ec == capy::cond::eof); + break; + } + for(auto b : bufs) + got.append( + static_cast(b.data()), b.size()); + pr.consume(capy::buffer_size(bufs)); + } + BOOST_TEST(got == "abc"); + + fields f; + system::error_code tec; + pr.parse_trailer(f, tec); + BOOST_TEST(!tec); + BOOST_TEST_EQ(f.size(), 1u); + BOOST_TEST(f.at("X-T") == "v"); } void - testTrailerReadBody() + testTrailerEmptyAndNonChunked() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> { - server.provide( + // an empty trailer section appends nothing + test_parser pr; + pr.provide( "HTTP/1.1 200 OK\r\n" "Transfer-Encoding: chunked\r\n" "\r\n" - "5\r\nhello\r\n" - "6\r\n world\r\n" - "0\r\n" - "X-Trailer: v\r\n" - "\r\n" - "HTTP/1.1 200 OK\r\n" - "Content-Length: 3\r\n" - "\r\n" - "bye"); + "0\r\n\r\n"); pr.start(); - auto [ec, body] = co_await pr.read_body(); + auto [ec, body] = pr.read_body(); BOOST_TEST(!ec); - BOOST_TEST(body == "hello world"); + BOOST_TEST(body == ""); + BOOST_TEST(!pr.has_buffered_data()); fields f; system::error_code tec; pr.parse_trailer(f, tec); BOOST_TEST(!tec); - BOOST_TEST_EQ(f.size(), 1u); - BOOST_TEST(f.at("X-Trailer") == "v"); - - // a flattened trailer is extracted in place: the - // view returned by read_body survives - BOOST_TEST(body == "hello world"); - BOOST_TEST(pr.has_buffered_data()); - - // the undelivered view and the trailer are both - // skipped by the restart - pr.start(); - auto [ec2, body2] = co_await pr.read_body(); - BOOST_TEST(!ec2); - BOOST_TEST(body2 == "bye"); - BOOST_TEST(!pr.has_buffered_data()); - }()); - } - - void - testTrailerPullConsume() - { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> + BOOST_TEST(f.empty()); + } { - server.provide( + // non-chunked messages have no trailer + test_parser pr; + pr.provide( "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" + "Content-Length: 5\r\n" "\r\n" - "3\r\nabc\r\n" - "0\r\n" - "X-T: v\r\n" - "\r\n"); + "hello"); pr.start(); - - { - // the dry window: pull has described the whole - // message, but until it reports eof the framing - // is not finalized and the trailer is unavailable - capy::const_buffer arr[2]; - auto [ec, bufs] = co_await pr.pull(arr); - BOOST_TEST(!ec); - BOOST_TEST(pr.got_body()); - fields f; - system::error_code tec; - pr.parse_trailer(f, tec); - BOOST_TEST(tec == http::error::incomplete); - BOOST_TEST_EQ(f.size(), 0u); - } - - std::string got; - for(;;) - { - capy::const_buffer arr[2]; - auto [ec, bufs] = co_await pr.pull(arr); - if(ec) - { - BOOST_TEST(ec == capy::cond::eof); - break; - } - for(auto b : bufs) - got.append( - static_cast(b.data()), b.size()); - pr.consume(capy::buffer_size(bufs)); - } - BOOST_TEST(got == "abc"); + auto [ec, body] = pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(body == "hello"); fields f; system::error_code tec; pr.parse_trailer(f, tec); BOOST_TEST(!tec); - BOOST_TEST_EQ(f.size(), 1u); - BOOST_TEST(f.at("X-T") == "v"); - }()); + BOOST_TEST(f.empty()); + } } void - testTrailerEmptyAndNonChunked() + testTrailerObsFold() { - { - // an empty trailer section appends nothing - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "0\r\n\r\n"); - - pr.start(); - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(!ec); - BOOST_TEST(body == ""); - BOOST_TEST(!pr.has_buffered_data()); - - fields f; - system::error_code tec; - pr.parse_trailer(f, tec); - BOOST_TEST(!tec); - BOOST_TEST(f.empty()); - }()); - } - { - // non-chunked messages have no trailer - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); - - pr.start(); - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(!ec); - BOOST_TEST(body == "hello"); - - fields f; - system::error_code tec; - pr.parse_trailer(f, tec); - BOOST_TEST(!tec); - BOOST_TEST(f.empty()); - }()); - } + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "0\r\n" + "X-A: 1\r\n" + " fold\r\n" + "X-B: 2\r\n" + "\r\n"); + + pr.start(); + auto [ec, body] = pr.read_body(); + BOOST_TEST(!ec); + + fields f; + system::error_code tec; + pr.parse_trailer(f, tec); + BOOST_TEST(!tec); + BOOST_TEST_EQ(f.size(), 2u); + BOOST_TEST(f.at("X-A") == "1 fold"); + BOOST_TEST(f.at("X-B") == "2"); + + // the in-place unfolding is stable under re-parsing + pr.parse_trailer(f, tec); + BOOST_TEST(!tec); + BOOST_TEST_EQ(f.size(), 4u); + BOOST_TEST(f.at("X-A") == "1 fold"); } void - testTrailerObsFold() + testTrailerByteByByte() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + // one octet per refill exercises the rescan of a partially + // received trailer; completion must not double-append + test_parser pr; + pr.max_read(1); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\nhello\r\n" + "0\r\n" + "X-A: 1\r\n" + "X-B: 2\r\n" + "\r\n"); + + pr.start(); + auto [ec, body] = pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(body == "hello"); + + fields f; + system::error_code tec; + pr.parse_trailer(f, tec); + BOOST_TEST(!tec); + BOOST_TEST_EQ(f.size(), 2u); + BOOST_TEST(f.at("X-A") == "1"); + BOOST_TEST(f.at("X-B") == "2"); + } - capy::test::run_blocking()([&]() -> capy::task<> + void + testTrailerLimit() + { + // a field line of exactly max_field octets is accepted; + // one more is rejected + for(bool over : { false, true }) { - server.provide( + parser::config cfg; + cfg.hdr_limits.max_field = 32; + test_parser pr(cfg); + + // "X-Name: " + value + CRLF == 32 octets + std::string const value(over ? 23 : 22, 'a'); + pr.provide( "HTTP/1.1 200 OK\r\n" "Transfer-Encoding: chunked\r\n" "\r\n" "0\r\n" - "X-A: 1\r\n" - " fold\r\n" - "X-B: 2\r\n" + "X-Name: " + value + "\r\n" "\r\n"); pr.start(); - auto [ec, body] = co_await pr.read_body(); + auto [ec, body] = pr.read_body(); BOOST_TEST(!ec); fields f; system::error_code tec; pr.parse_trailer(f, tec); - BOOST_TEST(!tec); - BOOST_TEST_EQ(f.size(), 2u); - BOOST_TEST(f.at("X-A") == "1 fold"); - BOOST_TEST(f.at("X-B") == "2"); - - // the in-place unfolding is stable under re-parsing - pr.parse_trailer(f, tec); - BOOST_TEST(!tec); - BOOST_TEST_EQ(f.size(), 4u); - BOOST_TEST(f.at("X-A") == "1 fold"); - }()); + if(over) + { + BOOST_TEST(tec == http::error::field_size_limit); + BOOST_TEST(f.empty()); + } + else + { + BOOST_TEST(!tec); + BOOST_TEST(f.at("X-Name") == value); + } + } } void - testTrailerByteByByte() + testTrailerBadField() { - // one octet per read exercises the rescan of a partially - // received trailer; completion must not double-append - capy::test::read_stream server({}, 1); - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> { - server.provide( + // a line which is structurally a trailer but not a + // field completes the message; only extraction fails, + // and fields parsed before the error remain + test_parser pr; + pr.provide( "HTTP/1.1 200 OK\r\n" "Transfer-Encoding: chunked\r\n" "\r\n" - "5\r\nhello\r\n" "0\r\n" - "X-A: 1\r\n" - "X-B: 2\r\n" + "X-A: v\r\n" + "junk\r\n" + "\r\n" + "HTTP/1.1 204 No Content\r\n" "\r\n"); pr.start(); - auto [ec, body] = co_await pr.read_body(); + auto [ec, body] = pr.read_body(); BOOST_TEST(!ec); - BOOST_TEST(body == "hello"); + BOOST_TEST(pr.got_body()); fields f; system::error_code tec; pr.parse_trailer(f, tec); - BOOST_TEST(!tec); - BOOST_TEST_EQ(f.size(), 2u); - BOOST_TEST(f.at("X-A") == "1"); - BOOST_TEST(f.at("X-B") == "2"); - }()); - } - - void - testTrailerLimit() - { - // a field line of exactly max_field octets is accepted; - // one more is rejected - for(bool over : { false, true }) - { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - parser::config cfg; - cfg.hdr_limits.max_field = 32; - test_parser pr(cfg, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - // "X-Name: " + value + CRLF == 32 octets - std::string const value(over ? 23 : 22, 'a'); - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "0\r\n" - "X-Name: " + value + "\r\n" - "\r\n"); - - pr.start(); - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(!ec); - - fields f; - system::error_code tec; - pr.parse_trailer(f, tec); - if(over) - { - BOOST_TEST(tec == http::error::field_size_limit); - BOOST_TEST(f.empty()); - } - else - { - BOOST_TEST(!tec); - BOOST_TEST(f.at("X-Name") == value); - } - }()); - } - } - - void - testTrailerBadField() - { - { - // a line which is structurally a trailer but not a - // field completes the message; only extraction fails, - // and fields parsed before the error remain - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + BOOST_TEST(tec == http::error::bad_field_name); + BOOST_TEST_EQ(f.size(), 1u); + BOOST_TEST(f.at("X-A") == "v"); - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "0\r\n" - "X-A: v\r\n" - "junk\r\n" - "\r\n" - "HTTP/1.1 204 No Content\r\n" - "\r\n"); - - pr.start(); - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(!ec); - BOOST_TEST(pr.got_body()); - - fields f; - system::error_code tec; - pr.parse_trailer(f, tec); - BOOST_TEST(tec == http::error::bad_field_name); - BOOST_TEST_EQ(f.size(), 1u); - BOOST_TEST(f.at("X-A") == "v"); - - // the failure does not poison the stream - pr.start(); - auto [hec] = co_await pr.read_header(); - BOOST_TEST(!hec); - BOOST_TEST_EQ(pr.get().status_int(), 204); - }()); + // the failure does not poison the stream + pr.start(); + auto hec = pr.read_header(); + BOOST_TEST(!hec); + BOOST_TEST_EQ(pr.get().status_int(), 204); } { // a bare LF inside a trailer field survives completion // and is rejected at extraction - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "0\r\n" - "X-B: v\n\r\n" - "\r\n"); - - pr.start(); - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(!ec); - - fields f; - system::error_code tec; - pr.parse_trailer(f, tec); - BOOST_TEST(tec == http::error::bad_line_ending); - BOOST_TEST(f.empty()); - }()); - } - } - - void - testTrailerStaticFields() - { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( + test_parser pr; + pr.provide( "HTTP/1.1 200 OK\r\n" "Transfer-Encoding: chunked\r\n" "\r\n" "0\r\n" - "X-Trailer: value\r\n" + "X-B: v\n\r\n" "\r\n"); pr.start(); - auto [ec, body] = co_await pr.read_body(); + auto [ec, body] = pr.read_body(); BOOST_TEST(!ec); - // a container too small throws in the caller's frame; - // the trailer remains available for a retry - char small[8]; - static_fields sf(small, sizeof(small)); - system::error_code tec; - BOOST_TEST_THROWS( - pr.parse_trailer(sf, tec), std::length_error); - fields f; + system::error_code tec; pr.parse_trailer(f, tec); - BOOST_TEST(!tec); - BOOST_TEST_EQ(f.size(), 1u); - BOOST_TEST(f.at("X-Trailer") == "value"); - }()); + BOOST_TEST(tec == http::error::bad_line_ending); + BOOST_TEST(f.empty()); + } + } + + void + testTrailerStaticFields() + { + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "0\r\n" + "X-Trailer: value\r\n" + "\r\n"); + + pr.start(); + auto [ec, body] = pr.read_body(); + BOOST_TEST(!ec); + + // a container too small throws in the caller's frame; + // the trailer remains available for a retry + char small[8]; + static_fields sf(small, sizeof(small)); + system::error_code tec; + BOOST_TEST_THROWS( + pr.parse_trailer(sf, tec), std::length_error); + + fields f; + pr.parse_trailer(f, tec); + BOOST_TEST(!tec); + BOOST_TEST_EQ(f.size(), 1u); + BOOST_TEST(f.at("X-Trailer") == "value"); } void @@ -1725,238 +1584,204 @@ class parser_test // a completely full buffer in some iterations. for(std::size_t len = 0; len <= 48; ++len) { - capy::test::read_stream server({}, 1); - capy::any_read_stream stream(&server); parser::config cfg; cfg.hdr_limits.max_size = 96; cfg.in_buffer = 64; - test_parser pr(cfg, &stream); + test_parser pr(cfg); + pr.max_read(1); - capy::test::run_blocking()([&]() -> capy::task<> - { - std::string const value(len, 'a'); - std::string const chunk(16, 'b'); - std::string wire = - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n"; - for(int i = 0; i != 4; ++i) - wire += "10\r\n" + chunk + "\r\n"; - wire += "0\r\n" - "X-T: " + value + "\r\n" - "\r\n"; - server.provide(wire); - - pr.start(); - auto [hec] = co_await pr.read_header(); - BOOST_TEST(!hec); - - // consume the body as it arrives so the write - // position advances and later octets wrap - std::size_t total = 0; - for(;;) + std::string const value(len, 'a'); + std::string const chunk(16, 'b'); + std::string wire = + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n"; + for(int i = 0; i != 4; ++i) + wire += "10\r\n" + chunk + "\r\n"; + wire += "0\r\n" + "X-T: " + value + "\r\n" + "\r\n"; + pr.provide(wire); + + pr.start(); + auto hec = pr.read_header(); + BOOST_TEST(!hec); + + // consume the body as it arrives so the write + // position advances and later octets wrap + std::size_t total = 0; + for(;;) + { + char buf[16]; + auto [ec, n] = pr.read_some( + capy::make_buffer(buf)); + total += n; + if(ec) { - char buf[16]; - auto [ec, n] = co_await pr.read_some( - capy::make_buffer(buf)); - total += n; - if(ec) - { - BOOST_TEST(ec == capy::cond::eof); - break; - } + BOOST_TEST(ec == capy::cond::eof); + break; } - BOOST_TEST_EQ(total, 64); + } + BOOST_TEST_EQ(total, 64); - // pipelined octets can refill the buffer to the - // brim before extraction; alternate so both a - // full and a partially filled buffer are - // rearranged - if(len % 2 == 0) + // pipelined octets can refill the buffer to the + // brim before extraction; alternate so both a + // full and a partially filled buffer are + // rearranged + if(len % 2 == 0) + { + for(;;) { - for(;;) - { - auto const pb = pr.prepare(); - auto const n = capy::buffer_size(pb); - if(n == 0) - break; - for(auto b : pb) - std::memset(b.data(), 'Z', b.size()); - pr.commit(n); - } + auto const pb = pr.prepare(); + auto const n = capy::buffer_size(pb); + if(n == 0) + break; + for(auto b : pb) + std::memset(b.data(), 'Z', b.size()); + pr.commit(n); } + } - fields f; - system::error_code tec; - pr.parse_trailer(f, tec); - BOOST_TEST(!tec); - BOOST_TEST_EQ(f.size(), 1u); - BOOST_TEST(f.at("X-T") == value); + fields f; + system::error_code tec; + pr.parse_trailer(f, tec); + BOOST_TEST(!tec); + BOOST_TEST_EQ(f.size(), 1u); + BOOST_TEST(f.at("X-T") == value); - // the rearrangement preserves the section - pr.parse_trailer(f, tec); - BOOST_TEST(!tec); - BOOST_TEST_EQ(f.size(), 2u); + // the rearrangement preserves the section + pr.parse_trailer(f, tec); + BOOST_TEST(!tec); + BOOST_TEST_EQ(f.size(), 2u); - BOOST_TEST(pr.has_buffered_data() == - (len % 2 == 0)); - }()); + BOOST_TEST(pr.has_buffered_data() == + (len % 2 == 0)); } } void testChunkedPullSmallDest() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "3\r\nabc\r\n" + "3\r\ndef\r\n" + "0\r\n\r\n"); + + pr.start(); + // a single descriptor cannot span chunks: delivery + // stops at the boundary and resumes on the next pull + capy::const_buffer arr[1]; { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "3\r\nabc\r\n" - "3\r\ndef\r\n" - "0\r\n\r\n"); - - pr.start(); - // a single descriptor cannot span chunks: delivery - // stops at the boundary and resumes on the next pull - capy::const_buffer arr[1]; - { - auto [ec, bufs] = co_await pr.pull(arr); - BOOST_TEST(!ec); - BOOST_TEST_EQ(bufs.size(), 1); - BOOST_TEST_EQ(bufs[0].size(), 3); - } - pr.consume(3); - { - auto [ec, bufs] = co_await pr.pull(arr); - BOOST_TEST(!ec); - BOOST_TEST_EQ(bufs.size(), 1); - BOOST_TEST_EQ(bufs[0].size(), 3); - } - pr.consume(3); - { - auto [ec, bufs] = co_await pr.pull(arr); - BOOST_TEST(ec == capy::cond::eof); - BOOST_TEST_EQ(bufs.size(), 0); - } - BOOST_TEST(pr.got_body()); - }()); + auto [ec, bufs] = pr.pull(arr); + BOOST_TEST(!ec); + BOOST_TEST_EQ(bufs.size(), 1); + BOOST_TEST_EQ(bufs[0].size(), 3); + } + pr.consume(3); + { + auto [ec, bufs] = pr.pull(arr); + BOOST_TEST(!ec); + BOOST_TEST_EQ(bufs.size(), 1); + BOOST_TEST_EQ(bufs[0].size(), 3); + } + pr.consume(3); + { + auto [ec, bufs] = pr.pull(arr); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(bufs.size(), 0); + } + BOOST_TEST(pr.got_body()); } void testChunkedBadFraming() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "ZZZ\r\nhello\r\n"); - capy::test::run_blocking()([&]() -> capy::task<> + pr.start(); + char buf[16]; { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "ZZZ\r\nhello\r\n"); - - pr.start(); - char buf[16]; - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec == http::error::bad_payload); - BOOST_TEST_EQ(n, 0); - } - BOOST_TEST(!pr.got_body()); - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec == http::error::bad_payload); - BOOST_TEST_EQ(n, 0); - } - }()); + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == http::error::bad_payload); + BOOST_TEST_EQ(n, 0); + } + BOOST_TEST(!pr.got_body()); + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == http::error::bad_payload); + BOOST_TEST_EQ(n, 0); + } } void testChunkedBadFramingWithData() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "5\r\nhello\r\n" - "ZZZ"); - - pr.start(); - char buf[16]; - // the bytes that decoded cleanly before the error are - // delivered first; the error reports on the next call - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 5); - BOOST_TEST(std::string_view(buf, n) == "hello"); - auto [ec2, n2] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec2 == http::error::bad_payload); - BOOST_TEST_EQ(n2, 0); - }()); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\nhello\r\n" + "ZZZ"); + + pr.start(); + char buf[16]; + // the bytes that decoded cleanly before the error are + // delivered first; the error reports on the next call + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 5); + BOOST_TEST(std::string_view(buf, n) == "hello"); + auto [ec2, n2] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec2 == http::error::bad_payload); + BOOST_TEST_EQ(n2, 0); } void testChunkedBadChunkExtension() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - // a lone CR inside a chunk extension - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "5;ext\rZ\r\nhello\r\n" - "0\r\n\r\n"); - - pr.start(); - char buf[16]; - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec == http::error::bad_payload); - BOOST_TEST_EQ(n, 0); - }()); + test_parser pr; + // a lone CR inside a chunk extension + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5;ext\rZ\r\nhello\r\n" + "0\r\n\r\n"); + + pr.start(); + char buf[16]; + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == http::error::bad_payload); + BOOST_TEST_EQ(n, 0); } void testChunkedSizeOverflow() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - // a chunk size that does not fit in 64 bits - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "FFFFFFFFFFFFFFFFF\r\nhello\r\n" - "0\r\n\r\n"); - - pr.start(); - char buf[16]; - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec == http::error::bad_payload); - BOOST_TEST_EQ(n, 0); - }()); + test_parser pr; + // a chunk size that does not fit in 64 bits + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "FFFFFFFFFFFFFFFFF\r\nhello\r\n" + "0\r\n\r\n"); + + pr.start(); + char buf[16]; + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == http::error::bad_payload); + BOOST_TEST_EQ(n, 0); } void @@ -1966,131 +1791,107 @@ class parser_test // ways: no CR at all, and a CR followed by the wrong octet for(std::string_view tail : { "XY", "\rX" }) { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "5\r\nhello" + std::string(tail)); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\nhello" + std::string(tail)); - pr.start(); - char buf[16]; - // the valid chunk data is delivered first - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 5); + pr.start(); + char buf[16]; + // the valid chunk data is delivered first + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 5); - auto [ec2, n2] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec2 == http::error::bad_payload); - BOOST_TEST_EQ(n2, 0); - }()); + auto [ec2, n2] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec2 == http::error::bad_payload); + BOOST_TEST_EQ(n2, 0); } } void testChunkedBadTrailer() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - // a lone CR inside the trailer section - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "0\r\n\rX"); - - pr.start(); - char buf[16]; - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec == http::error::bad_payload); - BOOST_TEST_EQ(n, 0); - BOOST_TEST(!pr.got_body()); - }()); + test_parser pr; + // a lone CR inside the trailer section + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "0\r\n\rX"); + + pr.start(); + char buf[16]; + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == http::error::bad_payload); + BOOST_TEST_EQ(n, 0); + BOOST_TEST(!pr.got_body()); } void testChunkedPullDrainThenError() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\nhello\r\n" + "ZZZ"); - capy::test::run_blocking()([&]() -> capy::task<> + pr.start(); + capy::const_buffer arr[4]; { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "5\r\nhello\r\n" - "ZZZ"); - - pr.start(); - capy::const_buffer arr[4]; - { - // pull returns error XOR data: the valid chunk is - // delivered with no error... - auto [ec, bufs] = co_await pr.pull(arr); - BOOST_TEST(!ec); - BOOST_TEST_EQ(bufs.size(), 1); - BOOST_TEST_EQ(bufs[0].size(), 5); - } - pr.consume(5); - { - // ...and the sticky error surfaces once drained - auto [ec, bufs] = co_await pr.pull(arr); - BOOST_TEST(ec == http::error::bad_payload); - BOOST_TEST_EQ(bufs.size(), 0); - } - { - auto [ec, bufs] = co_await pr.pull(arr); - BOOST_TEST(ec == http::error::bad_payload); - BOOST_TEST_EQ(bufs.size(), 0); - } - }()); + // pull returns error XOR data: the valid chunk is + // delivered with no error... + auto [ec, bufs] = pr.pull(arr); + BOOST_TEST(!ec); + BOOST_TEST_EQ(bufs.size(), 1); + BOOST_TEST_EQ(bufs[0].size(), 5); + } + pr.consume(5); + { + // ...and the sticky error surfaces once drained + auto [ec, bufs] = pr.pull(arr); + BOOST_TEST(ec == http::error::bad_payload); + BOOST_TEST_EQ(bufs.size(), 0); + } + { + auto [ec, bufs] = pr.pull(arr); + BOOST_TEST(ec == http::error::bad_payload); + BOOST_TEST_EQ(bufs.size(), 0); + } } void testChunkedIncomplete() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\nhel"); - capy::test::run_blocking()([&]() -> capy::task<> + pr.start(); + char buf[16]; { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "5\r\nhel"); - - pr.start(); - char buf[16]; - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 3); - } - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec == http::error::incomplete); - BOOST_TEST_EQ(n, 0); - } - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec == http::error::incomplete); - BOOST_TEST_EQ(n, 0); - } - }()); + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 3); + } + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == http::error::incomplete); + BOOST_TEST_EQ(n, 0); + } + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == http::error::incomplete); + BOOST_TEST_EQ(n, 0); + } } void @@ -2105,190 +1906,309 @@ class parser_test cfg.hdr_limits.max_fields = 1; cfg.hdr_limits.max_size = hdr.size(); cfg.in_buffer = 49; - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr(cfg, &stream); + test_parser pr(cfg); - capy::test::run_blocking()([&]() -> capy::task<> - { - // a chunk extension longer than what remains of in_ - server.provide(std::string(hdr) + - "5;e=" + std::string(80, 'x') + "\r\nhello\r\n0\r\n\r\n"); + // a chunk extension longer than what remains of in_ + pr.provide(std::string(hdr) + + "5;e=" + std::string(80, 'x') + "\r\nhello\r\n0\r\n\r\n"); - pr.start(); - char buf[16]; - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec == http::error::in_place_overflow); - BOOST_TEST_EQ(n, 0); - } - // framing overflow is terminal, unlike delivery overflow - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec == http::error::in_place_overflow); - BOOST_TEST_EQ(n, 0); - } - }()); + pr.start(); + char buf[16]; + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == http::error::in_place_overflow); + BOOST_TEST_EQ(n, 0); + } + // framing overflow is terminal, unlike delivery overflow + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == http::error::in_place_overflow); + BOOST_TEST_EQ(n, 0); + } } void testChunkedReadBodyChunkLargerThanBuffer() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; + // the chunk declares more octets than in_ can ever + // hold: assembling a contiguous body cannot succeed + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "FFFFFF\r\nhello"); - capy::test::run_blocking()([&]() -> capy::task<> - { - // the chunk declares more octets than in_ can ever - // hold: assembling a contiguous body cannot succeed - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "FFFFFF\r\nhello"); + pr.start(); + auto [ec, body] = pr.read_body(); + BOOST_TEST(ec == http::error::in_place_overflow); - pr.start(); - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(ec == http::error::in_place_overflow); - BOOST_TEST(body.empty()); - }()); + // the walk never cleared the chunk header, so not one + // octet of the chunk was flattened + BOOST_TEST(body.empty()); } void - testChunkedBodyLimitViaReadBody() + testChunkedBodyLimit() { parser::config cfg; cfg.body_limit = 4; - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr(cfg, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "a\r\n0123456789\r\n" - "0\r\n\r\n"); + test_parser pr(cfg); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "a\r\n0123456789\r\n" + "0\r\n\r\n"); + + pr.start(); + char buf[16]; + // the in-limit prefix is delivered first + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 4); + + auto [ec2, n2] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec2 == http::error::body_too_large); + BOOST_TEST_EQ(n2, 0); + } - pr.start(); - // the body can never be returned whole; read_body - // yields an empty view alongside the error - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(ec == http::error::body_too_large); - BOOST_TEST(body.empty()); - }()); + void + testChunkedBodyLimitViaReadBody() + { + parser::config cfg; + cfg.body_limit = 4; + test_parser pr(cfg); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "a\r\n0123456789\r\n" + "0\r\n\r\n"); + + pr.start(); + // the flattened body outgrows the budget: it can never be + // returned whole + auto [ec, body] = pr.read_body(); + BOOST_TEST(ec == http::error::body_too_large); } void - testChunkedBodyLimit() + testChunkedBodyLimitViaConsume() { parser::config cfg; cfg.body_limit = 4; - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr(cfg, &stream); + test_parser pr(cfg); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "a\r\n0123456789\r\n" + "0\r\n\r\n"); + + pr.start(); + capy::const_buffer arr[2]; + // delivery is clamped at the limit: the caller never + // observes body octets past it + auto [ec, bufs] = pr.pull(arr); + BOOST_TEST(!ec); + BOOST_TEST_EQ(capy::buffer_size(bufs), 4); + + pr.consume(4); + auto [ec2, bufs2] = pr.pull(arr); + BOOST_TEST(ec2 == http::error::body_too_large); + BOOST_TEST_EQ(bufs2.size(), 0); + + // the walk can never reach the terminal framing: the + // message stays incomplete and the failure is sticky + pr.consume(0); + BOOST_TEST(!pr.got_body()); + auto [ec3, bufs3] = pr.pull(arr); + BOOST_TEST(ec3 == http::error::body_too_large); + } - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "a\r\n0123456789\r\n" - "0\r\n\r\n"); + void + testChunkedExactLimitPull() + { + // a body that ends exactly on the budget: the walk must + // still reach the terminal chunk. only body octets past the + // budget make the payload too large + parser::config cfg; + cfg.body_limit = 5; + test_parser pr(cfg); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\nhello\r\n" + "0\r\n\r\n"); - pr.start(); - char buf[16]; - // the in-limit prefix is delivered first - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); + pr.start(); + capy::const_buffer arr[2]; + { + auto [ec, bufs] = pr.pull(arr); BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 4); - - auto [ec2, n2] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec2 == http::error::body_too_large); - BOOST_TEST_EQ(n2, 0); - }()); + BOOST_TEST_EQ(capy::buffer_size(bufs), 5); + pr.consume(5); + } + { + auto [ec, bufs] = pr.pull(arr); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(bufs.size(), 0); + } + BOOST_TEST(pr.got_body()); } void - testChunkedBodyLimitViaConsume() + testChunkedExactLimitDecoder() { parser::config cfg; - cfg.body_limit = 4; - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr(cfg, &stream); + cfg.body_limit = 5; + test_parser pr(cfg); + test_decoder dec; - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "a\r\n0123456789\r\n" - "0\r\n\r\n"); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\nhello\r\n" + "0\r\n\r\n"); - pr.start(); - capy::const_buffer arr[2]; - // delivery is clamped at the limit: the caller never - // observes body octets past it - auto [ec, bufs] = co_await pr.pull(arr); + pr.start(); + auto hec = pr.read_header(); + BOOST_TEST(!hec); + pr.set_decoder(&dec); + + char buf[16]; + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); BOOST_TEST(!ec); - BOOST_TEST_EQ(capy::buffer_size(bufs), 4); + BOOST_TEST_EQ(n, 5); + BOOST_TEST(std::string_view(buf, n) == decoded("hello")); + } + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(n, 0); + } + BOOST_TEST(pr.got_body()); + } - pr.consume(4); - auto [ec2, bufs2] = co_await pr.pull(arr); - BOOST_TEST(ec2 == http::error::body_too_large); - BOOST_TEST_EQ(bufs2.size(), 0); + void + testChunkedExactLimitDecoderViaReadBody() + { + parser::config cfg; + cfg.body_limit = 5; + test_parser pr(cfg); + test_decoder dec; - // the walk can never reach the terminal framing: the - // message stays incomplete and the failure is sticky - pr.consume(0); - BOOST_TEST(!pr.got_body()); - auto [ec3, bufs3] = co_await pr.pull(arr); - BOOST_TEST(ec3 == http::error::body_too_large); - }()); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\nhello\r\n" + "0\r\n\r\n"); + + pr.start(); + auto hec = pr.read_header(); + BOOST_TEST(!hec); + pr.set_decoder(&dec); + + auto [ec, body] = pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(body == decoded("hello")); + BOOST_TEST(pr.got_body()); } void - testChunkedByteByByte() + testChunkedEmptyBodyZeroLimit() + { + // no body octets, so nothing can exceed the budget + parser::config cfg; + cfg.body_limit = 0; + test_parser pr(cfg); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "0\r\n\r\n"); + + pr.start(); + capy::const_buffer arr[2]; + auto [ec, bufs] = pr.pull(arr); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(bufs.size(), 0); + BOOST_TEST(pr.got_body()); + } + + void + testChunkedBodyLimitDecoder() { - capy::test::read_stream server({}, 1); - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + // the budget still bites on the decode path: the decoder + // has output left to give and no room to give it in + parser::config cfg; + cfg.body_limit = 4; + test_parser pr(cfg); + test_decoder dec; + + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "a\r\n0123456789\r\n" + "0\r\n\r\n"); - capy::test::run_blocking()([&]() -> capy::task<> + pr.start(); + auto hec = pr.read_header(); + BOOST_TEST(!hec); + pr.set_decoder(&dec); + + char buf[16]; { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "5;ext=1\r\nhello\r\n" - "6\r\n world\r\n" - "0\r\n" - "X-Trailer: v\r\n" - "\r\n"); + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 4); + BOOST_TEST(std::string_view(buf, n) == decoded("0123")); + } + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == http::error::body_too_large); + BOOST_TEST_EQ(n, 0); + } + BOOST_TEST(!pr.got_body()); + } - pr.start(); - std::string got; - for(;;) + void + testChunkedByteByByte() + { + test_parser pr; + pr.max_read(1); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5;ext=1\r\nhello\r\n" + "6\r\n world\r\n" + "0\r\n" + "X-Trailer: v\r\n" + "\r\n"); + + pr.start(); + std::string got; + for(;;) + { + char buf[4]; + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + got.append(buf, n); + if(ec) { - char buf[4]; - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - got.append(buf, n); - if(ec) - { - BOOST_TEST(ec == capy::cond::eof); - break; - } + BOOST_TEST(ec == capy::cond::eof); + break; } - BOOST_TEST(got == "hello world"); - BOOST_TEST(pr.got_body()); - BOOST_TEST(!pr.has_buffered_data()); - }()); + } + BOOST_TEST(got == "hello world"); + BOOST_TEST(pr.got_body()); + BOOST_TEST(!pr.has_buffered_data()); } void @@ -2308,44 +2228,41 @@ class parser_test cfg.hdr_limits.max_size = hdr.size(); 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); + test_parser pr(cfg); - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide(hdr); - server.provide( - "5\r\nhello\r\n" - "6\r\n world\r\n" - "0\r\n\r\n"); + pr.provide(hdr); + pr.provide( + "5\r\nhello\r\n" + "6\r\n world\r\n" + "0\r\n\r\n"); - pr.start(); - std::string got; - { - auto [ec, part] = co_await pr.read_body(); - BOOST_TEST(ec == http::error::in_place_overflow); - BOOST_TEST(part.empty()); - } - // the switch to streaming first drains the bytes parked - // in the decode buffer (the same bytes the partial view - // exposed — read_body does not consume), then continues - // with the wire - for(;;) + pr.start(); + std::string got; + { + // the chunks flattened before the overflow are + // viewable; the rest of the body is not + auto [ec, part] = pr.read_body(); + BOOST_TEST(ec == http::error::in_place_overflow); + BOOST_TEST(part == "hello"); + } + // the switch to streaming first drains the bytes parked + // in the decode buffer (the same bytes the partial view + // exposed — read_body does not consume), then continues + // with the wire + for(;;) + { + char buf[4]; + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + got.append(buf, n); + if(ec) { - char buf[4]; - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - got.append(buf, n); - if(ec) - { - BOOST_TEST(ec == capy::cond::eof); - break; - } + BOOST_TEST(ec == capy::cond::eof); + break; } - BOOST_TEST(got == "hello world"); - BOOST_TEST(pr.got_body()); - BOOST_TEST(!pr.has_buffered_data()); - }()); + } + BOOST_TEST(got == "hello world"); + BOOST_TEST(pr.got_body()); + BOOST_TEST(!pr.has_buffered_data()); } //-------------------------------------------- @@ -2357,284 +2274,393 @@ class parser_test void testToEofReadSome() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "\r\n" + "hello"); - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "\r\n" - "hello"); + pr.start(); + auto ec = pr.read_header(); + BOOST_TEST(!ec); + BOOST_TEST(pr.get().payload() == + http::payload::to_eof); + BOOST_TEST(!pr.got_body()); - pr.start(); - auto [ec] = co_await pr.read_header(); + char buf[16]; + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); BOOST_TEST(!ec); - BOOST_TEST(pr.get().payload() == - http::payload::to_eof); - BOOST_TEST(!pr.got_body()); - - char buf[16]; - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 5); - BOOST_TEST(std::string_view(buf, n) == "hello"); - } - BOOST_TEST(!pr.got_body()); - { - // stream EOF completes the message; it is not an error - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec == capy::cond::eof); - BOOST_TEST_EQ(n, 0); - } - BOOST_TEST(pr.got_body()); - BOOST_TEST(!pr.has_buffered_data()); - }()); + BOOST_TEST_EQ(n, 5); + BOOST_TEST(std::string_view(buf, n) == "hello"); + } + BOOST_TEST(!pr.got_body()); + { + // stream EOF completes the message; it is not an error + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(n, 0); + } + BOOST_TEST(pr.got_body()); + BOOST_TEST(!pr.has_buffered_data()); } void testToEofReadBody() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "\r\n" + "hello world"); + + pr.start(); + auto [ec, body] = pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(body == "hello world"); + BOOST_TEST(pr.got_body()); + + auto [ec2, body2] = pr.read_body(); + BOOST_TEST(!ec2); + BOOST_TEST(body2 == "hello world"); + } - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "\r\n" - "hello world"); + void + testToEofPull() + { + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "\r\n" + "hello"); - pr.start(); - auto [ec, body] = co_await pr.read_body(); + pr.start(); + capy::const_buffer arr[2]; + { + auto [ec, bufs] = pr.pull(arr); BOOST_TEST(!ec); - BOOST_TEST(body == "hello world"); - BOOST_TEST(pr.got_body()); + BOOST_TEST_EQ(capy::buffer_size(bufs), 5); + } + pr.consume(5); + { + auto [ec, bufs] = pr.pull(arr); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(bufs.size(), 0); + } + BOOST_TEST(pr.got_body()); + } - auto [ec2, body2] = co_await pr.read_body(); - BOOST_TEST(!ec2); - BOOST_TEST(body2 == "hello world"); - }()); + void + testToEofEmptyBody() + { + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "\r\n"); + + pr.start(); + auto [ec, body] = pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(body.empty()); + BOOST_TEST(pr.got_body()); + } + + void + testToEofBodyLimit() + { + parser::config cfg; + cfg.body_limit = 4; + test_parser pr(cfg); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "\r\n" + "0123456789"); + + pr.start(); + char buf[16]; + // the in-limit prefix is delivered first + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 4); + + auto [ec2, n2] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec2 == http::error::body_too_large); + BOOST_TEST_EQ(n2, 0); + } + + + void + testToEofBodyLimitViaReadBody() + { + parser::config cfg; + cfg.body_limit = 4; + test_parser pr(cfg); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "\r\n" + "hello"); + + pr.start(); + // a close-delimited body has no declared length, so the + // budget is tested against what has arrived + auto [ec, body] = pr.read_body(); + BOOST_TEST(ec == http::error::body_too_large); + } + + void + testToEofPullBodyLimitZero() + { + // a zero budget fails the pull before any delivery + parser::config cfg; + cfg.body_limit = 0; + test_parser pr(cfg); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "\r\n" + "hello"); + + pr.start(); + capy::const_buffer arr[2]; + auto [ec, bufs] = pr.pull(arr); + BOOST_TEST(ec == http::error::body_too_large); + BOOST_TEST_EQ(bufs.size(), 0); + } + + void + testToEofExactLimit() + { + // a close-delimited body has no declared length, so a spent + // budget alone proves nothing: the body is too large only + // once an octet past it arrives. here none does, and the + // close lands on a refill of its own + parser::config cfg; + cfg.body_limit = 5; + test_parser pr(cfg); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "\r\n" + "hello"); + + pr.start(); + char buf[16]; + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 5); + BOOST_TEST(std::string_view(buf, n) == "hello"); + } + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(n, 0); + } + BOOST_TEST(pr.got_body()); } void - testToEofPull() + testToEofExactLimitEagerEof() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + // the same body, with the close reported together with the + // last octets: the framing is already complete when the + // budget runs out + parser::config cfg; + cfg.body_limit = 5; + test_parser pr(cfg); + pr.eager_eof(); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "\r\n" + "hello"); - capy::test::run_blocking()([&]() -> capy::task<> + pr.start(); + char buf[16]; { - server.provide( - "HTTP/1.1 200 OK\r\n" - "\r\n" - "hello"); - - pr.start(); - capy::const_buffer arr[2]; - { - auto [ec, bufs] = co_await pr.pull(arr); - BOOST_TEST(!ec); - BOOST_TEST_EQ(capy::buffer_size(bufs), 5); - } - pr.consume(5); - { - auto [ec, bufs] = co_await pr.pull(arr); - BOOST_TEST(ec == capy::cond::eof); - BOOST_TEST_EQ(bufs.size(), 0); - } - BOOST_TEST(pr.got_body()); - }()); + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 5); + } + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(n, 0); + } + BOOST_TEST(pr.got_body()); } void - testToEofEmptyBody() + testToEofExactLimitPull() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + parser::config cfg; + cfg.body_limit = 5; + test_parser pr(cfg); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "\r\n" + "hello"); - capy::test::run_blocking()([&]() -> capy::task<> + pr.start(); + capy::const_buffer arr[2]; { - server.provide( - "HTTP/1.1 200 OK\r\n" - "\r\n"); - - pr.start(); - auto [ec, body] = co_await pr.read_body(); + auto [ec, bufs] = pr.pull(arr); BOOST_TEST(!ec); - BOOST_TEST(body.empty()); - BOOST_TEST(pr.got_body()); - }()); + BOOST_TEST_EQ(capy::buffer_size(bufs), 5); + pr.consume(5); + } + { + auto [ec, bufs] = pr.pull(arr); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(bufs.size(), 0); + } + BOOST_TEST(pr.got_body()); } void - testToEofBodyLimit() + testToEofExactLimitDecoder() { + // the decoder announces its end of stream on a final call + // that produces nothing. a spent budget must not stop that + // call from happening parser::config cfg; - cfg.body_limit = 4; - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr(cfg, &stream); + cfg.body_limit = 5; + test_parser pr(cfg); + test_decoder dec; - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "\r\n" - "0123456789"); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "\r\n" + "hello"); - pr.start(); - char buf[16]; - // the in-limit prefix is delivered first - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 4); + pr.start(); + auto hec = pr.read_header(); + BOOST_TEST(!hec); + pr.set_decoder(&dec); - auto [ec2, n2] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec2 == http::error::body_too_large); - BOOST_TEST_EQ(n2, 0); - }()); + char buf[16]; + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 5); + BOOST_TEST(std::string_view(buf, n) == decoded("hello")); + } + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(n, 0); + } + BOOST_TEST(pr.got_body()); } - //-------------------------------------------- - // - // set_body_limit - // - //-------------------------------------------- - void - testToEofBodyLimitViaReadBody() + testToEofExactLimitDecoderViaReadBody() { parser::config cfg; - cfg.body_limit = 4; - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr(cfg, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "\r\n" - "hello"); + cfg.body_limit = 5; + test_parser pr(cfg); + test_decoder dec; - pr.start(); - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(ec == http::error::body_too_large); - BOOST_TEST(body.empty()); - }()); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "\r\n" + "hello"); + + pr.start(); + auto hec = pr.read_header(); + BOOST_TEST(!hec); + pr.set_decoder(&dec); + + auto [ec, body] = pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(body == decoded("hello")); + BOOST_TEST(pr.got_body()); } void - testToEofPullBodyLimitZero() + testToEofEmptyBodyZeroLimit() { - // a zero budget fails the pull before any delivery + // an empty close-delimited body is not too large, whatever + // the budget parser::config cfg; cfg.body_limit = 0; - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr(cfg, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "\r\n" - "hello"); - - pr.start(); - capy::const_buffer arr[2]; - auto [ec, bufs] = co_await pr.pull(arr); - BOOST_TEST(ec == http::error::body_too_large); - BOOST_TEST_EQ(bufs.size(), 0); - }()); + test_parser pr(cfg); + pr.provide("HTTP/1.1 200 OK\r\n\r\n"); + + pr.start(); + char buf[16]; + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(n, 0); + BOOST_TEST(pr.got_body()); } + //-------------------------------------------- + // + // set_body_limit + // + //-------------------------------------------- + void testSetBodyLimitEnforced() { // a limit installed via the setter (not the config) is // enforced on the next read - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); // default config: no limit - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); - - pr.start(); - auto [ec] = co_await pr.read_header(); - BOOST_TEST(!ec); - - pr.set_body_limit(4); - - char buf[16]; - // the in-limit prefix is delivered first - auto [ec2, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec2); - BOOST_TEST_EQ(n, 4); - - auto [ec3, n2] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec3 == http::error::body_too_large); - BOOST_TEST_EQ(n2, 0); - }()); + test_parser pr; // default config: no limit + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"); + + pr.start(); + auto ec = pr.read_header(); + BOOST_TEST(!ec); + + pr.set_body_limit(4); + + char buf[16]; + // the declared length alone settles it: no + // octet is delivered + auto [ec2, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec2 == http::error::body_too_large); + BOOST_TEST_EQ(n, 0); } void testSetBodyLimitRaiseUnblocks() { - // body_too_large is not terminal: raising the limit mid-body - // resumes delivery of the remaining octets without re-reading + // body_too_large is not terminal: raising the limit + // resumes delivery without re-reading parser::config cfg; cfg.body_limit = 4; - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr(cfg, &stream); + test_parser pr(cfg); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 10\r\n" + "\r\n" + "0123456789"); - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 10\r\n" - "\r\n" - "0123456789"); + pr.start(); + auto ec = pr.read_header(); + BOOST_TEST(!ec); - pr.start(); - auto [ec] = co_await pr.read_header(); + char buf[16]; + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == http::error::body_too_large); + BOOST_TEST_EQ(n, 0); + } + pr.set_body_limit(10); + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); BOOST_TEST(!ec); - - char buf[16]; - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 4); - BOOST_TEST(std::string_view(buf, n) == "0123"); - } - // at the limit: the next read would fail - pr.set_body_limit(10); - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 6); - BOOST_TEST(std::string_view(buf, n) == "456789"); - } - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec == capy::cond::eof); - BOOST_TEST_EQ(n, 0); - } - BOOST_TEST(pr.got_body()); - }()); + BOOST_TEST_EQ(n, 10); + BOOST_TEST(std::string_view(buf, n) == "0123456789"); + } + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(n, 0); + } + BOOST_TEST(pr.got_body()); } void @@ -2643,35 +2669,29 @@ class parser_test // lowering the limit below what has already been transferred // must clamp the remaining budget to zero (no unsigned // underflow), so the next read fails as too large - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 10\r\n" + "\r\n" + "0123456789"); - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 10\r\n" - "\r\n" - "0123456789"); + pr.start(); + auto ec = pr.read_header(); + BOOST_TEST(!ec); - pr.start(); - auto [ec] = co_await pr.read_header(); + char buf[3]; + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); BOOST_TEST(!ec); - - char buf[3]; - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 3); // transferred == 3 - } - pr.set_body_limit(2); // below the 3 already delivered - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec == http::error::body_too_large); - BOOST_TEST_EQ(n, 0); - } - }()); + BOOST_TEST_EQ(n, 3); // transferred == 3 + } + pr.set_body_limit(2); // below the 3 already delivered + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == http::error::body_too_large); + BOOST_TEST_EQ(n, 0); + } } void @@ -2681,45 +2701,40 @@ class parser_test // and a raise unblocks it just as it does the raw path parser::config cfg; cfg.body_limit = 2; - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr(cfg, &stream); + test_parser pr(cfg); test_decoder dec; // finishes on the eof kick - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"); - pr.start(); - auto [hec] = co_await pr.read_header(); - BOOST_TEST(!hec); - pr.set_decoder(&dec); + pr.start(); + auto hec = pr.read_header(); + BOOST_TEST(!hec); + pr.set_decoder(&dec); - char buf[16]; - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 2); // decoded output clamped to the limit - BOOST_TEST(std::string_view(buf, n) == decoded("he")); - } - pr.set_body_limit(100); - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 3); - BOOST_TEST(std::string_view(buf, n) == decoded("llo")); - } - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec == capy::cond::eof); - BOOST_TEST_EQ(n, 0); - } - BOOST_TEST(pr.got_body()); - }()); + char buf[16]; + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 2); // decoded output clamped to the limit + BOOST_TEST(std::string_view(buf, n) == decoded("he")); + } + pr.set_body_limit(100); + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 3); + BOOST_TEST(std::string_view(buf, n) == decoded("llo")); + } + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(n, 0); + } + BOOST_TEST(pr.got_body()); } //-------------------------------------------- @@ -2731,66 +2746,54 @@ class parser_test void testHeadResponse() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; + // framing fields are present but no body follows + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 100\r\n" + "\r\n"); - capy::test::run_blocking()([&]() -> capy::task<> - { - // framing fields are present but no body follows - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 100\r\n" - "\r\n"); + pr.start(true); + auto ec = pr.read_header(); + BOOST_TEST(!ec); + BOOST_TEST(pr.got_body()); + BOOST_TEST(!pr.has_buffered_data()); - pr.start(true); - auto [ec] = co_await pr.read_header(); + // streaming reads must not touch the wire + char buf[8]; + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(n, 0); + } + { + auto [ec, body] = pr.read_body(); BOOST_TEST(!ec); - BOOST_TEST(pr.got_body()); - BOOST_TEST(!pr.has_buffered_data()); - - // streaming reads must not touch the stream - char buf[8]; - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec == capy::cond::eof); - BOOST_TEST_EQ(n, 0); - } - { - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(!ec); - BOOST_TEST(body.empty()); - } - capy::const_buffer arr[2]; - { - auto [ec, bufs] = co_await pr.pull(arr); - BOOST_TEST(ec == capy::cond::eof); - BOOST_TEST_EQ(bufs.size(), 0); - } - }()); + BOOST_TEST(body.empty()); + } + capy::const_buffer arr[2]; + { + auto [ec, bufs] = pr.pull(arr); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(bufs.size(), 0); + } } void testHeadResponseWithJunk() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 100\r\n" - "\r\n" - "JUNK"); - - pr.start(true); - auto [ec] = co_await pr.read_header(); - BOOST_TEST(!ec); - BOOST_TEST(pr.got_body()); - BOOST_TEST(pr.has_buffered_data()); - }()); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 100\r\n" + "\r\n" + "JUNK"); + + pr.start(true); + auto ec = pr.read_header(); + BOOST_TEST(!ec); + BOOST_TEST(pr.got_body()); + BOOST_TEST(pr.has_buffered_data()); } //-------------------------------------------- @@ -2802,486 +2805,477 @@ class parser_test void testDecoderSized() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; test_decoder dec; dec.eof_at = 5; // ends exactly with the last consumed octet - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"); - pr.start(); - auto [hec] = co_await pr.read_header(); - BOOST_TEST(!hec); - pr.set_decoder(&dec); + pr.start(); + auto hec = pr.read_header(); + BOOST_TEST(!hec); + pr.set_decoder(&dec); - char buf[16]; - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 5); - BOOST_TEST(std::string_view(buf, n) == decoded("hello")); - } - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec == capy::cond::eof); - BOOST_TEST_EQ(n, 0); - } - BOOST_TEST(pr.got_body()); - BOOST_TEST(!pr.has_buffered_data()); - }()); + char buf[16]; + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 5); + BOOST_TEST(std::string_view(buf, n) == decoded("hello")); + } + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(n, 0); + } + BOOST_TEST(pr.got_body()); + BOOST_TEST(!pr.has_buffered_data()); } void testDecoderSizedFinishOnKick() - { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - test_decoder dec; // finishes only when told the input ended - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); + { + test_parser pr; + test_decoder dec; // finishes only when told the input ended - pr.start(); - auto [hec] = co_await pr.read_header(); - BOOST_TEST(!hec); - pr.set_decoder(&dec); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"); - char buf[16]; - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 5); - } - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec == capy::cond::eof); - BOOST_TEST_EQ(n, 0); - } - BOOST_TEST(dec.finished); - }()); + pr.start(); + auto hec = pr.read_header(); + BOOST_TEST(!hec); + pr.set_decoder(&dec); + + char buf[16]; + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 5); + } + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(n, 0); + } + BOOST_TEST(dec.finished); } void testDecoderTrailer() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; test_decoder dec; dec.trailer = "XY"; // flush output produced by the eof kick - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"); - pr.start(); - auto [hec] = co_await pr.read_header(); - BOOST_TEST(!hec); - pr.set_decoder(&dec); + pr.start(); + auto hec = pr.read_header(); + BOOST_TEST(!hec); + pr.set_decoder(&dec); - std::string got; - for(;;) + std::string got; + for(;;) + { + char buf[4]; + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + got.append(buf, n); + if(ec) { - char buf[4]; - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - got.append(buf, n); - if(ec) - { - BOOST_TEST(ec == capy::cond::eof); - break; - } + BOOST_TEST(ec == capy::cond::eof); + break; } - BOOST_TEST(got == decoded("hello") + "XY"); - }()); + } + BOOST_TEST(got == decoded("hello") + "XY"); } void testDecoderTruncatedStream() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; test_decoder dec; dec.ignore_eof = true; // never finishes: a truncated stream - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"); - pr.start(); - auto [hec] = co_await pr.read_header(); - BOOST_TEST(!hec); - pr.set_decoder(&dec); + pr.start(); + auto hec = pr.read_header(); + BOOST_TEST(!hec); + pr.set_decoder(&dec); - char buf[16]; - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 5); - } - { - // the raw side ended and the decoder stalls: this - // must be an error, not a hang on the socket - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec == error::decode_error); - BOOST_TEST_EQ(n, 0); - } - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec == error::decode_error); - BOOST_TEST_EQ(n, 0); - } - }()); + char buf[16]; + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 5); + } + { + // the raw side ended and the decoder stalls: this + // must be an error, not a wait for more input + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == error::decode_error); + BOOST_TEST_EQ(n, 0); + } + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == error::decode_error); + BOOST_TEST_EQ(n, 0); + } } void testDecoderEarlyEof() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; test_decoder dec; dec.eof_at = 3; // ends inside the declared payload - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); - - pr.start(); - auto [hec] = co_await pr.read_header(); - BOOST_TEST(!hec); - pr.set_decoder(&dec); - - char buf[16]; - // the cleanly decoded bytes are delivered first - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 3); - - auto [ec2, n2] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec2 == http::error::bad_payload); - BOOST_TEST_EQ(n2, 0); - }()); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"); + + pr.start(); + auto hec = pr.read_header(); + BOOST_TEST(!hec); + pr.set_decoder(&dec); + + char buf[16]; + // the cleanly decoded bytes are delivered first + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 3); + + auto [ec2, n2] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec2 == http::error::bad_payload); + BOOST_TEST_EQ(n2, 0); } void testDecoderHardError() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; test_decoder dec; dec.fail_ec = capy::error::test_failure; dec.fail_at = 3; - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); - - pr.start(); - auto [hec] = co_await pr.read_header(); - BOOST_TEST(!hec); - pr.set_decoder(&dec); - - char buf[16]; - // the bytes decoded before the failure are delivered first - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 3); - - auto [ec2, n2] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec2 == capy::error::test_failure); - BOOST_TEST_EQ(n2, 0); - }()); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"); + + pr.start(); + auto hec = pr.read_header(); + BOOST_TEST(!hec); + pr.set_decoder(&dec); + + char buf[16]; + // the bytes decoded before the failure are delivered first + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 3); + + auto [ec2, n2] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec2 == capy::error::test_failure); + BOOST_TEST_EQ(n2, 0); } void testDecoderPullServesDataBeforeError() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; test_decoder dec; dec.fail_ec = capy::error::test_failure; dec.fail_at = 3; - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"); - pr.start(); - auto [hec] = co_await pr.read_header(); - BOOST_TEST(!hec); - pr.set_decoder(&dec); + pr.start(); + auto hec = pr.read_header(); + BOOST_TEST(!hec); + pr.set_decoder(&dec); - capy::const_buffer arr[2]; - { - // decoded bytes produced before the failure are - // served with no error (error XOR data)... - auto [ec, bufs] = co_await pr.pull(arr); - BOOST_TEST(!ec); - BOOST_TEST_EQ(capy::buffer_size(bufs), 3); - } - pr.consume(3); - { - // ...and the stored failure surfaces once drained - auto [ec, bufs] = co_await pr.pull(arr); - BOOST_TEST(ec == capy::error::test_failure); - BOOST_TEST_EQ(bufs.size(), 0); - } - }()); + capy::const_buffer arr[2]; + { + // decoded bytes produced before the failure are + // served with no error (error XOR data)... + auto [ec, bufs] = pr.pull(arr); + BOOST_TEST(!ec); + BOOST_TEST_EQ(capy::buffer_size(bufs), 3); + } + pr.consume(3); + { + // ...and the stored failure surfaces once drained + auto [ec, bufs] = pr.pull(arr); + BOOST_TEST(ec == capy::error::test_failure); + BOOST_TEST_EQ(bufs.size(), 0); + } } void testDecoderNoPayload() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; test_decoder dec; - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 0\r\n" - "\r\n"); - - pr.start(); - auto [hec] = co_await pr.read_header(); - BOOST_TEST(!hec); - pr.set_decoder(&dec); - - // a bodiless message ends the decode path without - // ever invoking the decoder - char buf[8]; - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec == capy::cond::eof); - BOOST_TEST_EQ(n, 0); - BOOST_TEST(!dec.finished); - }()); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 0\r\n" + "\r\n"); + + pr.start(); + auto hec = pr.read_header(); + BOOST_TEST(!hec); + pr.set_decoder(&dec); + + // a bodiless message ends the decode path without + // ever invoking the decoder + char buf[8]; + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(n, 0); + BOOST_TEST(!dec.finished); } void testDecoderReadSomeEmptyBuffer() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; test_decoder dec; - capy::test::run_blocking()([&]() -> capy::task<> + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"); + + pr.start(); + auto hec = pr.read_header(); + BOOST_TEST(!hec); + pr.set_decoder(&dec); + + // an empty destination transfers nothing and is + // not an error + { + auto [ec, n] = pr.read_some(capy::mutable_buffer{}); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 0); + } + char buf[16]; + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 5); + BOOST_TEST(std::string_view(buf, n) == decoded("hello")); + } + } + + void + testDecoderReadSomeLeadingEmptyBuffer() + { + // a destination whose first buffer is empty still has + // room in it: the decoder must skip to the buffer that + // does, rather than read the empty front as "full" + char buf[16]; + auto dest = [&] + { + return std::array{ + capy::mutable_buffer{}, + capy::mutable_buffer{ buf, sizeof(buf) } }; + }; { - server.provide( + test_parser pr; + test_decoder dec; + + pr.provide( "HTTP/1.1 200 OK\r\n" "Content-Length: 5\r\n" "\r\n" "hello"); pr.start(); - auto [hec] = co_await pr.read_header(); + auto hec = pr.read_header(); BOOST_TEST(!hec); pr.set_decoder(&dec); - // an empty destination transfers nothing and is - // not an error - { - auto [ec, n] = co_await pr.read_some( - capy::mutable_buffer{}); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 0); - } - char buf[16]; - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 5); - BOOST_TEST(std::string_view(buf, n) == decoded("hello")); - } - }()); + auto [ec, n] = pr.read_some_raw(dest()); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 5); + BOOST_TEST(std::string_view(buf, n) == decoded("hello")); + } + { + test_parser pr; + test_decoder dec; + + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\nhello\r\n" + "0\r\n\r\n"); + + pr.start(); + auto hec = pr.read_header(); + BOOST_TEST(!hec); + pr.set_decoder(&dec); + + auto [ec, n] = pr.read_some_raw(dest()); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 5); + BOOST_TEST(std::string_view(buf, n) == decoded("hello")); + } } void testDecoderBodyAfterStreaming() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; test_decoder dec; - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"); - pr.start(); - auto [hec] = co_await pr.read_header(); - BOOST_TEST(!hec); - pr.set_decoder(&dec); + pr.start(); + auto hec = pr.read_header(); + BOOST_TEST(!hec); + pr.set_decoder(&dec); - char buf[3]; - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 3); - } - // read_body cannot reconstruct a body whose leading - // octets were already streamed out - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(ec == http::error::incomplete); - BOOST_TEST(body.empty()); - }()); + char buf[3]; + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 3); + } + // the decoded octets already streamed out are gone, so + // what is flattened is the remainder of the output + auto [ec, body] = pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(body == decoded("lo")); } void testDecoderReadBodyHardError() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; test_decoder dec; dec.fail_ec = capy::error::test_failure; dec.fail_at = 3; - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); - - pr.start(); - auto [hec] = co_await pr.read_header(); - BOOST_TEST(!hec); - pr.set_decoder(&dec); - - // the in-place body cannot be completed - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(ec == capy::error::test_failure); - BOOST_TEST(body.empty()); - }()); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"); + + pr.start(); + auto hec = pr.read_header(); + BOOST_TEST(!hec); + pr.set_decoder(&dec); + + // the in-place body cannot be completed; the output + // produced before the failure is still viewable + auto [ec, body] = pr.read_body(); + BOOST_TEST(ec == capy::error::test_failure); + BOOST_TEST(body == decoded("hel")); } void testDecoderPullTwiceWithoutConsume() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; test_decoder dec; - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"); - pr.start(); - auto [hec] = co_await pr.read_header(); - BOOST_TEST(!hec); - pr.set_decoder(&dec); + pr.start(); + auto hec = pr.read_header(); + BOOST_TEST(!hec); + pr.set_decoder(&dec); - capy::const_buffer arr[2]; - { - auto [ec, bufs] = co_await pr.pull(arr); - BOOST_TEST(!ec); - BOOST_TEST_EQ(capy::buffer_size(bufs), 5); - } - // without a consume the same octets are served again, - // straight from the decoded buffer - { - auto [ec, bufs] = co_await pr.pull(arr); - BOOST_TEST(!ec); - BOOST_TEST_EQ(capy::buffer_size(bufs), 5); - } - pr.consume(5); - { - auto [ec, bufs] = co_await pr.pull(arr); - BOOST_TEST(ec == capy::cond::eof); - BOOST_TEST_EQ(bufs.size(), 0); - } - }()); + capy::const_buffer arr[2]; + { + auto [ec, bufs] = pr.pull(arr); + BOOST_TEST(!ec); + BOOST_TEST_EQ(capy::buffer_size(bufs), 5); + } + // without a consume the same octets are served again, + // straight from the decoded buffer + { + auto [ec, bufs] = pr.pull(arr); + BOOST_TEST(!ec); + BOOST_TEST_EQ(capy::buffer_size(bufs), 5); + } + pr.consume(5); + { + auto [ec, bufs] = pr.pull(arr); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(bufs.size(), 0); + } } void testDecoderChunked() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; test_decoder dec; dec.trailer = "!"; - capy::test::run_blocking()([&]() -> capy::task<> + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\nhello\r\n" + "6\r\n world\r\n" + "0\r\n\r\n"); + + pr.start(); + auto hec = pr.read_header(); + BOOST_TEST(!hec); + pr.set_decoder(&dec); + + std::string got; + for(;;) { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "5\r\nhello\r\n" - "6\r\n world\r\n" - "0\r\n\r\n"); - - pr.start(); - auto [hec] = co_await pr.read_header(); - BOOST_TEST(!hec); - pr.set_decoder(&dec); - - std::string got; - for(;;) - { - char buf[8]; - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - got.append(buf, n); - if(ec) - { - BOOST_TEST(ec == capy::cond::eof); - break; - } + char buf[8]; + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + got.append(buf, n); + if(ec) + { + BOOST_TEST(ec == capy::cond::eof); + break; } - BOOST_TEST(got == decoded("hello world") + "!"); - BOOST_TEST(pr.got_body()); - BOOST_TEST(!pr.has_buffered_data()); - }()); + } + BOOST_TEST(got == decoded("hello world") + "!"); + BOOST_TEST(pr.got_body()); + BOOST_TEST(!pr.has_buffered_data()); } void @@ -3289,164 +3283,144 @@ class parser_test { // the trailer is outside the coded content: extraction // works alongside an installed decoder - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; test_decoder dec; - capy::test::run_blocking()([&]() -> capy::task<> + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "3\r\nabc\r\n" + "0\r\n" + "X-T: v\r\n" + "\r\n"); + + pr.start(); + auto hec = pr.read_header(); + BOOST_TEST(!hec); + pr.set_decoder(&dec); + + std::string got; + for(;;) { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "3\r\nabc\r\n" - "0\r\n" - "X-T: v\r\n" - "\r\n"); - - pr.start(); - auto [hec] = co_await pr.read_header(); - BOOST_TEST(!hec); - pr.set_decoder(&dec); - - std::string got; - for(;;) + char buf[8]; + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + got.append(buf, n); + if(ec) { - char buf[8]; - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - got.append(buf, n); - if(ec) - { - BOOST_TEST(ec == capy::cond::eof); - break; - } + BOOST_TEST(ec == capy::cond::eof); + break; } - BOOST_TEST(got == decoded("abc")); - BOOST_TEST(pr.got_body()); - - fields f; - system::error_code tec; - pr.parse_trailer(f, tec); - BOOST_TEST(!tec); - BOOST_TEST_EQ(f.size(), 1u); - BOOST_TEST(f.at("X-T") == "v"); - }()); + } + BOOST_TEST(got == decoded("abc")); + BOOST_TEST(pr.got_body()); + + fields f; + system::error_code tec; + pr.parse_trailer(f, tec); + BOOST_TEST(!tec); + BOOST_TEST_EQ(f.size(), 1u); + BOOST_TEST(f.at("X-T") == "v"); } void testDecoderChunkedEarlyEof() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; test_decoder dec; dec.eof_at = 3; // ends before the final chunk - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "5\r\nhello\r\n" - "0\r\n\r\n"); - - pr.start(); - auto [hec] = co_await pr.read_header(); - BOOST_TEST(!hec); - pr.set_decoder(&dec); - - char buf[16]; - // the cleanly decoded bytes are delivered first - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 3); - - auto [ec2, n2] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec2 == http::error::bad_payload); - BOOST_TEST_EQ(n2, 0); - }()); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\nhello\r\n" + "0\r\n\r\n"); + + pr.start(); + auto hec = pr.read_header(); + BOOST_TEST(!hec); + pr.set_decoder(&dec); + + char buf[16]; + // the cleanly decoded bytes are delivered first + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 3); + + auto [ec2, n2] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec2 == http::error::bad_payload); + BOOST_TEST_EQ(n2, 0); } void testDecoderChunkedIncomplete() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; test_decoder dec; - capy::test::run_blocking()([&]() -> capy::task<> - { - // the stream ends mid-chunk - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "5\r\nab"); - - pr.start(); - auto [hec] = co_await pr.read_header(); - BOOST_TEST(!hec); - pr.set_decoder(&dec); - - char buf[16]; - // the cleanly decoded bytes are delivered first - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 2); - BOOST_TEST(std::string_view(buf, n) == decoded("ab")); - - auto [ec2, n2] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec2 == http::error::incomplete); - BOOST_TEST_EQ(n2, 0); - }()); + // the stream ends mid-chunk + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\nab"); + + pr.start(); + auto hec = pr.read_header(); + BOOST_TEST(!hec); + pr.set_decoder(&dec); + + char buf[16]; + // the cleanly decoded bytes are delivered first + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 2); + BOOST_TEST(std::string_view(buf, n) == decoded("ab")); + + auto [ec2, n2] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec2 == http::error::incomplete); + BOOST_TEST_EQ(n2, 0); } void testDecoderToEof() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; test_decoder dec; dec.eof_at = 5; - capy::test::run_blocking()([&]() -> capy::task<> - { - // a close-delimited (to_eof) body whose decoder reaches - // its own end-of-stream while octets remain unconsumed and - // before the peer closes: the parser ends the body at the - // decoder's eof and rejects the trailing octets - server.provide( - "HTTP/1.1 200 OK\r\n" - "\r\n" - "helloJUNK"); + // a close-delimited (to_eof) body whose decoder reaches + // its own end-of-stream while octets remain unconsumed and + // before the peer closes: the parser ends the body at the + // decoder's eof and rejects the trailing octets + pr.provide( + "HTTP/1.1 200 OK\r\n" + "\r\n" + "helloJUNK"); - pr.start(); - auto [hec] = co_await pr.read_header(); - BOOST_TEST(!hec); - pr.set_decoder(&dec); + pr.start(); + auto hec = pr.read_header(); + BOOST_TEST(!hec); + pr.set_decoder(&dec); - char buf[16]; - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 5); - BOOST_TEST(std::string_view(buf, n) == decoded("hello")); - } - { - // the decoder is done but octets remain and the - // connection has not closed, so the payload is rejected - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec == http::error::bad_payload); - BOOST_TEST_EQ(n, 0); - } - // the message never completes: to_eof completion requires - // an observed stream EOF, which never arrives - BOOST_TEST(!pr.got_body()); - }()); + char buf[16]; + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 5); + BOOST_TEST(std::string_view(buf, n) == decoded("hello")); + } + { + // the decoder is done but octets remain and the + // connection has not closed, so the payload is rejected + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == http::error::bad_payload); + BOOST_TEST_EQ(n, 0); + } + // the message never completes: to_eof completion requires + // an observed stream EOF, which never arrives + BOOST_TEST(!pr.got_body()); } // decoder that echoes 5 body bytes (+1), then treats any further @@ -3479,74 +3453,64 @@ class parser_test void testDecoderToEofTrailerSeparateRead() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; trailer_consuming_decoder dec; - capy::test::run_blocking()([&]() -> capy::task<> + // a to_eof body whose decoder finishes by consuming a + // trailer that produces no output; the trailer arrives in a + // refill separate from the body. the decoder consumes it + // cleanly, leaving nothing buffered, and the subsequent + // refill observes the peer's close, so the message completes + pr.provide("HTTP/1.1 200 OK\r\n\r\nhello"); + pr.start(); + auto hec = pr.read_header(); + BOOST_TEST(!hec); + pr.set_decoder(&dec); + + char buf[16]; { - // a to_eof body whose decoder finishes by consuming a - // trailer that produces no output; the trailer arrives in a - // read separate from the body. the decoder consumes it - // cleanly, leaving nothing buffered, and the subsequent read - // observes the peer's close, so the message completes - server.provide("HTTP/1.1 200 OK\r\n\r\nhello"); - pr.start(); - auto [hec] = co_await pr.read_header(); - BOOST_TEST(!hec); - pr.set_decoder(&dec); - - char buf[16]; - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 5); - BOOST_TEST(std::string_view(buf, n) == decoded("hello")); - } - // the non-producing trailer arrives on its own - server.provide("TRL"); - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec == capy::cond::eof); - BOOST_TEST_EQ(n, 0); - } - // the trailing read reached the peer's close, so to_eof - // framing is complete - BOOST_TEST(pr.got_body()); - }()); + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 5); + BOOST_TEST(std::string_view(buf, n) == decoded("hello")); + } + // the non-producing trailer arrives on its own + pr.provide("TRL"); + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(n, 0); + } + // the trailing refill reached the peer's close, so to_eof + // framing is complete + BOOST_TEST(pr.got_body()); } void testDecoderReadBody() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; test_decoder dec; dec.eof_at = 5; - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); - - pr.start(); - auto [hec] = co_await pr.read_header(); - BOOST_TEST(!hec); - pr.set_decoder(&dec); - - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(!ec); - BOOST_TEST(body == decoded("hello")); - - auto [ec2, body2] = co_await pr.read_body(); - BOOST_TEST(!ec2); - BOOST_TEST(body2 == decoded("hello")); - }()); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"); + + pr.start(); + auto hec = pr.read_header(); + BOOST_TEST(!hec); + pr.set_decoder(&dec); + + auto [ec, body] = pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(body == decoded("hello")); + + auto [ec2, body2] = pr.read_body(); + BOOST_TEST(!ec2); + BOOST_TEST(body2 == decoded("hello")); } void @@ -3554,465 +3518,354 @@ class parser_test { parser::config cfg; cfg.dec_buffer = 4; // decoded output exceeds the buffer - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr(cfg, &stream); + test_parser pr(cfg); test_decoder dec; dec.eof_at = 5; - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"); - pr.start(); - auto [hec] = co_await pr.read_header(); - BOOST_TEST(!hec); - pr.set_decoder(&dec); + pr.start(); + auto hec = pr.read_header(); + BOOST_TEST(!hec); + pr.set_decoder(&dec); - std::string got; - { - auto [ec, part] = co_await pr.read_body(); - BOOST_TEST(ec == http::error::in_place_overflow); - BOOST_TEST(part.empty()); - } - for(;;) + std::string got; + { + // what the decode buffer holds is viewable; the rest + // of the output has nowhere to go + auto [ec, part] = pr.read_body(); + BOOST_TEST(ec == http::error::in_place_overflow); + BOOST_TEST(part == decoded("hell")); + } + for(;;) + { + char buf[4]; + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + got.append(buf, n); + if(ec) { - char buf[4]; - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - got.append(buf, n); - if(ec) - { - BOOST_TEST(ec == capy::cond::eof); - break; - } + BOOST_TEST(ec == capy::cond::eof); + break; } - BOOST_TEST(got == decoded("hello")); - }()); + } + BOOST_TEST(got == decoded("hello")); } //-------------------------------------------- // - // transport errors, lifecycle + // lifecycle // //-------------------------------------------- - void - testTransientStreamError() - { - capy::test::read_stream server; - flaky_stream flaky(server, capy::error::canceled, 1); - capy::any_read_stream stream(&flaky); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); - - pr.start(); - { - // a canceled read (e.g. a timeout wrapper fired) is - // transient: it must not poison the parser - auto [ec] = co_await pr.read_header(); - BOOST_TEST(ec == capy::cond::canceled); - BOOST_TEST(!pr.got_header()); - } - { - auto [ec] = co_await pr.read_header(); - BOOST_TEST(!ec); - BOOST_TEST(pr.got_header()); - } - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(!ec); - BOOST_TEST(body == "hello"); - }()); - } - - void - testTerminalStreamError() - { - capy::test::read_stream server; - flaky_stream flaky(server, capy::error::test_failure, 1); - capy::any_read_stream stream(&flaky); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); - - pr.start(); - auto [ec] = co_await pr.read_header(); - BOOST_TEST(ec == capy::error::test_failure); - }()); - } - void testResetAfterFailure() { - capy::test::read_stream server1; - capy::any_read_stream stream1(&server1); - test_parser pr({}, &stream1); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "ZZZ"); - capy::test::run_blocking()([&]() -> capy::task<> + pr.start(); + char buf[8]; { - server1.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "ZZZ"); - - pr.start(); - char buf[8]; - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec == http::error::bad_payload); - } + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == http::error::bad_payload); + } - // reset is the universal escape: failure is per-message - capy::test::read_stream server2; - capy::any_read_stream stream2(&server2); - pr.reset(&stream2); - BOOST_TEST(!pr.got_header()); + // reset is the universal escape: failure is per-message + pr.reset(); + BOOST_TEST(!pr.got_header()); - server2.provide( - "HTTP/1.1 404 Not Found\r\n" - "Content-Length: 3\r\n" - "\r\n" - "bye"); + pr.provide( + "HTTP/1.1 404 Not Found\r\n" + "Content-Length: 3\r\n" + "\r\n" + "bye"); - pr.start(); - { - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(!ec); - BOOST_TEST(body == "bye"); - } - BOOST_TEST_EQ(pr.get().status_int(), 404); - }()); + pr.start(); + { + auto [ec, body] = pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(body == "bye"); + } + BOOST_TEST_EQ(pr.get().status_int(), 404); } void testPullEmptyDest() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "5\r\nhello\r\n" - "0\r\n\r\n"); - - pr.start(); - auto [ec, bufs] = co_await pr.pull({}); - BOOST_TEST(!ec); - BOOST_TEST_EQ(bufs.size(), 0); - }()); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "5\r\nhello\r\n" + "0\r\n\r\n"); + + pr.start(); + auto [ec, bufs] = pr.pull({}); + BOOST_TEST(!ec); + BOOST_TEST_EQ(bufs.size(), 0); } void testMoveCtor() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"); - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); + pr.start(); + auto ec = pr.read_header(); + BOOST_TEST(!ec); - pr.start(); - auto [ec] = co_await pr.read_header(); + char buf[3]; + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 3); + } - char buf[3]; - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 3); - } - - // move-construct mid-body: pr2 must adopt the in-progress state - // (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()); - { - auto [ec, n] = co_await pr2.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 2); - } - { - auto [ec, n] = co_await pr2.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec == capy::cond::eof); - BOOST_TEST_EQ(n, 0); - } - }()); + // move-construct mid-body: pr2 must adopt the in-progress state + // (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()); + { + auto [ec, n] = pr2.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 2); + } + { + auto [ec, n] = pr2.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(n, 0); + } } void testMoveAssign() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - test_parser pr2({}, &stream); // owns its own allocation - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); + test_parser pr; + test_parser pr2; // owns its own allocation - pr.start(); - auto [ec] = co_await pr.read_header(); - BOOST_TEST(!ec); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"); - char buf[3]; - { - auto [ec, n] = co_await pr.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 3); - } + pr.start(); + auto ec = pr.read_header(); + BOOST_TEST(!ec); - // move-assign: pr2's original allocation must be released (no - // leak, no double free) and pr2 adopts pr's in-progress state. - pr2 = std::move(pr); - BOOST_TEST(pr2.got_header()); - { - auto [ec, n] = co_await pr2.read_some(capy::make_buffer(buf)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 2); - } - { - auto [ec, n] = co_await pr2.read_some(capy::make_buffer(buf)); - BOOST_TEST(ec == capy::cond::eof); - BOOST_TEST_EQ(n, 0); - } - }()); + char buf[3]; + { + auto [ec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 3); + } + + // move-assign: pr2's original allocation must be released (no + // leak, no double free) and pr2 adopts pr's in-progress state. + pr2 = std::move(pr); + BOOST_TEST(pr2.got_header()); + { + auto [ec, n] = pr2.read_some(capy::make_buffer(buf)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 2); + } + { + auto [ec, n] = pr2.read_some(capy::make_buffer(buf)); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(n, 0); + } } void testReset() { - capy::test::read_stream server1; - capy::any_read_stream stream1(&server1); - test_parser pr({}, &stream1); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"); - capy::test::run_blocking()([&]() -> capy::task<> + pr.start(); { - server1.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); - - pr.start(); - { - auto [ec] = co_await pr.read_header(); - BOOST_TEST(!ec); - BOOST_TEST(pr.got_header()); - } - char buf[8]; - { - auto [ec, n] = co_await pr.read(capy::make_buffer(buf)); - BOOST_TEST(ec == capy::cond::eof); - BOOST_TEST_EQ(n, 5); - BOOST_TEST(std::string_view(buf, n) == "hello"); - } + auto ec = pr.read_header(); + BOOST_TEST(!ec); + BOOST_TEST(pr.got_header()); + } + char buf[8]; + { + auto [ec, n] = pr.read(capy::make_buffer(buf)); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(n, 5); + BOOST_TEST(std::string_view(buf, n) == "hello"); + } - // reset onto a fresh stream must return the parser to a - // construction-like state: buffers restored, flags cleared, and a - // second, differently-framed message parses correctly. - capy::test::read_stream server2; - capy::any_read_stream stream2(&server2); - pr.reset(&stream2); - BOOST_TEST(!pr.got_header()); + // reset must return the parser to a construction-like state: + // buffers restored, flags cleared, and a second, differently + // framed message parses correctly. + pr.reset(); + BOOST_TEST(!pr.got_header()); - server2.provide( - "HTTP/1.1 404 Not Found\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n" - "3\r\nbye\r\n" - "0\r\n\r\n"); + pr.provide( + "HTTP/1.1 404 Not Found\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "3\r\nbye\r\n" + "0\r\n\r\n"); - pr.start(); - { - auto [ec] = co_await pr.read_header(); - BOOST_TEST(!ec); - BOOST_TEST(pr.got_header()); - } - { - auto [ec, n] = co_await pr.read(capy::make_buffer(buf)); - BOOST_TEST(ec == capy::cond::eof); - BOOST_TEST_EQ(n, 3); - BOOST_TEST(std::string_view(buf, n) == "bye"); - } - BOOST_TEST(!pr.has_buffered_data()); - }()); + pr.start(); + { + auto ec = pr.read_header(); + BOOST_TEST(!ec); + BOOST_TEST(pr.got_header()); + } + { + auto [ec, n] = pr.read(capy::make_buffer(buf)); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(n, 3); + BOOST_TEST(std::string_view(buf, n) == "bye"); + } + BOOST_TEST(!pr.has_buffered_data()); } void testStartPipelined() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello" + "HTTP/1.1 200 OK\r\n" + "Content-Length: 3\r\n" + "\r\n" + "bye"); - capy::test::run_blocking()([&]() -> capy::task<> + pr.start(); { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello" - "HTTP/1.1 200 OK\r\n" - "Content-Length: 3\r\n" - "\r\n" - "bye"); - - pr.start(); - { - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(!ec); - BOOST_TEST(body == "hello"); - } - BOOST_TEST(pr.got_body()); - BOOST_TEST(pr.has_buffered_data()); - pr.consume(5); - - // start() re-arms for the next message on the same - // stream, carrying the pipelined octets over - pr.start(); - BOOST_TEST(!pr.got_header()); - { - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(!ec); - BOOST_TEST(body == "bye"); - } - BOOST_TEST(pr.got_body()); - BOOST_TEST(!pr.has_buffered_data()); - - // nothing follows the second message; the undelivered - // "bye" view is skipped by the restart - pr.start(); - auto [ec] = co_await pr.read_header(); - BOOST_TEST(ec == http::error::end_of_stream); - }()); + auto [ec, body] = pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(body == "hello"); + } + BOOST_TEST(pr.got_body()); + BOOST_TEST(pr.has_buffered_data()); + pr.consume(5); + + // start() re-arms for the next message on the same + // stream, carrying the pipelined octets over + pr.start(); + BOOST_TEST(!pr.got_header()); + { + auto [ec, body] = pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(body == "bye"); + } + BOOST_TEST(pr.got_body()); + BOOST_TEST(!pr.has_buffered_data()); + + // nothing follows the second message; the undelivered + // "bye" view is skipped by the restart + pr.start(); + auto ec = pr.read_header(); + BOOST_TEST(ec == http::error::end_of_stream); } void testStartParksCompleteMessage() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello" + "HTTP/1.1 200 OK\r\n" + "Content-Length: 3\r\n" + "\r\n" + "bye"); - capy::test::run_blocking()([&]() -> capy::task<> + pr.start(); + char const* first = nullptr; { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello" - "HTTP/1.1 200 OK\r\n" - "Content-Length: 3\r\n" - "\r\n" - "bye"); + auto [ec, body] = pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(body == "hello"); + first = body.data(); + } + pr.consume(5); - pr.start(); - char const* first = nullptr; - { - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(!ec); - BOOST_TEST(body == "hello"); - first = body.data(); - } - pr.consume(5); + pr.start(); - pr.start(); + // the octets carried over are reported even though + // the header has not completed yet + BOOST_TEST(pr.has_buffered_data()); + { + auto [ec, body] = pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(body == "bye"); - // the octets carried over are reported even though - // the header has not completed yet - BOOST_TEST(pr.has_buffered_data()); - { - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(!ec); - BOOST_TEST(body == "bye"); - - // the whole message was already buffered, so the - // header was parsed where its octets lay; nothing - // was moved back to the front of the buffer - BOOST_TEST(body.data() > first + 5); - } - }()); + // the whole message was already buffered, so the + // header was parsed where its octets lay; nothing + // was moved back to the front of the buffer + BOOST_TEST(body.data() > first + 5); + } } void testStartCompactsIncompleteMessage() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello" + "HTTP/1.1 200 OK\r\n" + "Content-Length: 3\r\n" + "\r\n"); - capy::test::run_blocking()([&]() -> capy::task<> + pr.start(); + char const* first = nullptr; { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello" - "HTTP/1.1 200 OK\r\n" - "Content-Length: 3\r\n" - "\r\n"); + auto [ec, body] = pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(body == "hello"); + first = body.data(); + } + pr.consume(5); - pr.start(); - char const* first = nullptr; - { - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(!ec); - BOOST_TEST(body == "hello"); - first = body.data(); - } - pr.consume(5); + pr.start(); + { + auto ec = pr.read_header(); + BOOST_TEST(!ec); + } + pr.provide("bye"); + { + auto [ec, body] = pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(body == "bye"); - pr.start(); - { - auto [ec] = co_await pr.read_header(); - BOOST_TEST(!ec); - } - server.provide("bye"); - { - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(!ec); - BOOST_TEST(body == "bye"); - - // the body had not arrived, so the header was - // moved back to the front to recover the window; - // both headers are the same length - BOOST_TEST(body.data() == first); - } - }()); + // the body had not arrived, so the header was + // moved back to the front to recover the window; + // both headers are the same length + BOOST_TEST(body.data() == first); + } } void @@ -4026,74 +3879,62 @@ class parser_test cfg.hdr_limits.max_fields = 1; cfg.hdr_limits.max_size = 39; cfg.in_buffer = 36; - - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr(cfg, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> + test_parser pr(cfg); + + pr.provide( + "HTTP/1.1 200 OK\r\n" // 39 octets + "Content-Length: 20\r\n" + "\r\n" + "01234567890123456789" // 20 octets + "HTTP/1.1 200 OK\r\n" // 38 octets + "Content-Length: 3\r\n" + "\r\n" + "bye"); + + pr.start(); { - server.provide( - "HTTP/1.1 200 OK\r\n" // 39 octets - "Content-Length: 20\r\n" - "\r\n" - "01234567890123456789" // 20 octets - "HTTP/1.1 200 OK\r\n" // 38 octets - "Content-Length: 3\r\n" - "\r\n" - "bye"); - - pr.start(); - { - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(!ec); - BOOST_TEST(body == "01234567890123456789"); - } - pr.consume(20); + auto [ec, body] = pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(body == "01234567890123456789"); + } + pr.consume(20); - pr.start(); - { - auto [ec, body] = co_await pr.read_body(); - BOOST_TEST(!ec); - BOOST_TEST(body == "bye"); - } - }()); + pr.start(); + { + auto [ec, body] = pr.read_body(); + BOOST_TEST(!ec); + BOOST_TEST(body == "bye"); + } } void testStartSkipsUndeliveredRemainder() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello" + "HTTP/1.1 204 No Content\r\n" + "\r\n"); - capy::test::run_blocking()([&]() -> capy::task<> + pr.start(); { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello" - "HTTP/1.1 204 No Content\r\n" - "\r\n"); - - pr.start(); - { - auto [ec] = co_await pr.read_header(); - BOOST_TEST(!ec); - } - // the whole sized body arrived with the header; the - // caller moves on without ever delivering it - BOOST_TEST(pr.got_body()); - pr.start(); - { - auto [ec] = co_await pr.read_header(); - BOOST_TEST(!ec); - BOOST_TEST_EQ(pr.get().status_int(), 204); - } - BOOST_TEST(pr.got_body()); - BOOST_TEST(!pr.has_buffered_data()); - }()); + auto ec = pr.read_header(); + BOOST_TEST(!ec); + } + // the whole sized body arrived with the header; the + // caller moves on without ever delivering it + BOOST_TEST(pr.got_body()); + pr.start(); + { + auto ec = pr.read_header(); + BOOST_TEST(!ec); + BOOST_TEST_EQ(pr.get().status_int(), 204); + } + BOOST_TEST(pr.got_body()); + BOOST_TEST(!pr.has_buffered_data()); } //-------------------------------------------- @@ -4108,97 +3949,72 @@ class parser_test // The pass-through budget is what lets a body be received straight // into caller memory. It must be offered exactly when that is safe. { - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n"); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n"); - pr.start(); - auto [ec] = co_await pr.read_header(); - BOOST_TEST(!ec); - // nothing buffered, so the whole payload may go direct - BOOST_TEST_EQ(pr.direct_capacity(), 5u); + pr.start(); + auto ec = pr.read_header(); + BOOST_TEST(!ec); + // nothing buffered, so the whole payload may go direct + BOOST_TEST_EQ(pr.direct_capacity(), 5u); - // a lowered limit clamps the budget - pr.set_body_limit(3); - BOOST_TEST_EQ(pr.direct_capacity(), 3u); - pr.set_body_limit(std::uint64_t(-1)); - }()); + // a limit below the declared size withholds + // the budget: the message can never complete + pr.set_body_limit(3); + BOOST_TEST_EQ(pr.direct_capacity(), 0u); } { // octets which arrived with the header must be drained first - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "he"); - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "he"); - - pr.start(); - auto [ec] = co_await pr.read_header(); - BOOST_TEST(!ec); - BOOST_TEST_EQ(pr.direct_capacity(), 0u); - - char buf[8]; - auto [rec, n] = co_await pr.read_some( - capy::make_buffer(buf)); - BOOST_TEST(!rec); - BOOST_TEST_EQ(n, 2); + pr.start(); + auto ec = pr.read_header(); + BOOST_TEST(!ec); + BOOST_TEST_EQ(pr.direct_capacity(), 0u); + + char buf[8]; + auto [rec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!rec); + BOOST_TEST_EQ(n, 2); - // buffer drained: the remainder may go direct - BOOST_TEST_EQ(pr.direct_capacity(), 3u); - }()); + // buffer drained: the remainder may go direct + BOOST_TEST_EQ(pr.direct_capacity(), 3u); } { // a decoder has to see the octets, so nothing may bypass it - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; test_decoder dec; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n"); - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n"); - - pr.start(); - auto [ec] = co_await pr.read_header(); - BOOST_TEST(!ec); - pr.set_decoder(&dec); - BOOST_TEST_EQ(pr.direct_capacity(), 0u); - }()); + pr.start(); + auto ec = pr.read_header(); + BOOST_TEST(!ec); + pr.set_decoder(&dec); + BOOST_TEST_EQ(pr.direct_capacity(), 0u); } { // chunked framing has to be walked, so it cannot go direct - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Transfer-Encoding: chunked\r\n" - "\r\n"); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n"); - pr.start(); - auto [ec] = co_await pr.read_header(); - BOOST_TEST(!ec); - BOOST_TEST_EQ(pr.direct_capacity(), 0u); - }()); + pr.start(); + auto ec = pr.read_header(); + BOOST_TEST(!ec); + BOOST_TEST_EQ(pr.direct_capacity(), 0u); } } @@ -4208,39 +4024,33 @@ class parser_test // With a buffer far smaller than the body, a single read_some can // only deliver more than the buffer holds by reading straight into // the caller's memory. - capy::test::read_stream server; - capy::any_read_stream stream(&server); auto const body = make_body(4096); test_parser pr( { .hdr_limits = { .max_size = 128, .max_fields = 4 }, .in_buffer = 64 - }, - &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 4096\r\n" - "\r\n"); - - pr.start(); - auto [ec] = co_await pr.read_header(); - BOOST_TEST(!ec); + }); - // the body arrives only after the header, so none of it is - // buffered and all of it is eligible for the direct path - server.provide(body); - - std::string dest(4096, '\0'); - auto [rec, n] = co_await pr.read_some( - capy::mutable_buffer(dest.data(), dest.size())); - BOOST_TEST(!rec); - BOOST_TEST_EQ(n, 4096); - BOOST_TEST(std::string_view(dest.data(), n) == body); - BOOST_TEST(pr.got_body()); - }()); + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 4096\r\n" + "\r\n"); + + pr.start(); + auto ec = pr.read_header(); + BOOST_TEST(!ec); + + // the body arrives only after the header, so none of it is + // buffered and all of it is eligible for the direct path + pr.provide(body); + + std::string dest(4096, '\0'); + auto [rec, n] = pr.read_some( + capy::mutable_buffer(dest.data(), dest.size())); + BOOST_TEST(!rec); + BOOST_TEST_EQ(n, 4096); + BOOST_TEST(std::string_view(dest.data(), n) == body); + BOOST_TEST(pr.got_body()); } void @@ -4248,37 +4058,30 @@ class parser_test { { // pipelined: the next message is visible once this one is done - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); + test_parser pr; + pr.provide( + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello" + "HTTP/1.1 204 No Content\r\n" + "\r\n"); - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello" - "HTTP/1.1 204 No Content\r\n" - "\r\n"); - - pr.start(); - auto [ec] = co_await pr.read_header(); - BOOST_TEST(!ec); - - char buf[8]; - auto [rec, n] = co_await pr.read_some( - capy::make_buffer(buf)); - BOOST_TEST(!rec); - BOOST_TEST_EQ(n, 5); + pr.start(); + auto ec = pr.read_header(); + BOOST_TEST(!ec); + + char buf[8]; + auto [rec, n] = pr.read_some(capy::make_buffer(buf)); + BOOST_TEST(!rec); + BOOST_TEST_EQ(n, 5); - BOOST_TEST(pr.has_buffered_data()); - auto const bufs = pr.buffered_data(); - BOOST_TEST_EQ(capy::buffer_size(bufs), 27u); - BOOST_TEST(std::string_view( - static_cast(bufs[0].data()), - bufs[0].size()).starts_with("HTTP/1.1 204")); - }()); + BOOST_TEST(pr.has_buffered_data()); + auto const bufs = pr.buffered_data(); + BOOST_TEST_EQ(capy::buffer_size(bufs), 27u); + BOOST_TEST(std::string_view( + static_cast(bufs[0].data()), + bufs[0].size()).starts_with("HTTP/1.1 204")); } { // A response whose framing runs to the end of the stream hides @@ -4286,97 +4089,82 @@ class parser_test // 200 answer to CONNECT: the caller knows there is no body even // though the response alone cannot say so, and buffered_data is // the only way to recover the octets which follow. - capy::test::read_stream server; - capy::any_read_stream stream(&server); - test_parser pr({}, &stream); - - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 Connection Established\r\n" - "\r\n" - "\x16\x03\x01"); + test_parser pr; + pr.provide( + "HTTP/1.1 200 Connection Established\r\n" + "\r\n" + "\x16\x03\x01"); - pr.start(); - auto [ec] = co_await pr.read_header(); - BOOST_TEST(!ec); - BOOST_TEST(pr.get().payload() == - http::payload::to_eof); + pr.start(); + auto ec = pr.read_header(); + BOOST_TEST(!ec); + BOOST_TEST(pr.get().payload() == + http::payload::to_eof); - BOOST_TEST(!pr.has_buffered_data()); - auto const bufs = pr.buffered_data(); - BOOST_TEST_EQ(capy::buffer_size(bufs), 3u); - BOOST_TEST(std::string_view( - static_cast(bufs[0].data()), - bufs[0].size()) == "\x16\x03\x01"); - }()); + BOOST_TEST(!pr.has_buffered_data()); + auto const bufs = pr.buffered_data(); + BOOST_TEST_EQ(capy::buffer_size(bufs), 3u); + BOOST_TEST(std::string_view( + static_cast(bufs[0].data()), + bufs[0].size()) == "\x16\x03\x01"); } } void testEofWithOctets() { - // A read which delivers the last octets together with eof must not - // lose them: they are committed before the contingency is acted on, - // and reported before eof is. + // A refill which delivers the last octets together with the close + // must not lose them: they are committed before the contingency is + // acted on, and reported before eof is. { - eager_eof_stream server( + test_parser pr; + pr.eager_eof(); + pr.provide( "HTTP/1.1 200 OK\r\n" "\r\n" "hello"); - test_parser pr({}, nullptr); - capy::any_read_stream stream(&server); - pr.reset(&stream); - capy::test::run_blocking()([&]() -> capy::task<> - { - pr.start(); - auto [ec] = co_await pr.read_header(); - BOOST_TEST(!ec); + pr.start(); + auto ec = pr.read_header(); + BOOST_TEST(!ec); - std::string got; - char buf[16]; - for(;;) + std::string got; + char buf[16]; + for(;;) + { + auto [rec, n] = pr.read_some(capy::make_buffer(buf)); + got.append(buf, n); + if(rec) { - auto [rec, n] = co_await pr.read_some( - capy::make_buffer(buf)); - got.append(buf, n); - if(rec) - { - // any contingency ends the read; only eof is - // expected here - BOOST_TEST(rec == capy::cond::eof); - break; - } + // any contingency ends the read; only eof is + // expected here + BOOST_TEST(rec == capy::cond::eof); + break; } - BOOST_TEST(got == "hello"); - BOOST_TEST(pr.got_body()); - }()); + } + BOOST_TEST(got == "hello"); + BOOST_TEST(pr.got_body()); } { // same, through a decoder, which forces every octet to travel // via the parser's buffer rather than the pass-through path - eager_eof_stream server( + test_parser pr; + test_decoder dec; + pr.eager_eof(); + pr.provide( "HTTP/1.1 200 OK\r\n" "Content-Length: 5\r\n" "\r\n" "hello"); - test_parser pr({}, nullptr); - capy::any_read_stream stream(&server); - pr.reset(&stream); - test_decoder dec; - capy::test::run_blocking()([&]() -> capy::task<> - { - pr.start(); - auto [ec] = co_await pr.read_header(); - BOOST_TEST(!ec); - pr.set_decoder(&dec); + pr.start(); + auto ec = pr.read_header(); + BOOST_TEST(!ec); + pr.set_decoder(&dec); - auto [bec, body] = co_await pr.read_body(); - BOOST_TEST(!bec); - BOOST_TEST(body == decoded("hello")); - }()); + auto [bec, body] = pr.read_body(); + BOOST_TEST(!bec); + BOOST_TEST(body == decoded("hello")); } } @@ -4390,6 +4178,7 @@ class parser_test testRequestBadTransferEncoding(); testEndOfStream(); testIncompleteHeader(); + testHeaderResumes(); testHeaderLargerThanBuffer(); testSizedReadSome(); @@ -4436,6 +4225,11 @@ class parser_test testChunkedBodyLimit(); testChunkedBodyLimitViaReadBody(); testChunkedBodyLimitViaConsume(); + testChunkedBodyLimitDecoder(); + testChunkedExactLimitPull(); + testChunkedExactLimitDecoder(); + testChunkedExactLimitDecoderViaReadBody(); + testChunkedEmptyBodyZeroLimit(); testChunkedByteByByte(); testChunkedReadBodyOverflowThenStream(); @@ -4446,6 +4240,12 @@ class parser_test testToEofBodyLimit(); testToEofBodyLimitViaReadBody(); testToEofPullBodyLimitZero(); + testToEofExactLimit(); + testToEofExactLimitEagerEof(); + testToEofExactLimitPull(); + testToEofExactLimitDecoder(); + testToEofExactLimitDecoderViaReadBody(); + testToEofEmptyBodyZeroLimit(); testSetBodyLimitEnforced(); testSetBodyLimitRaiseUnblocks(); @@ -4464,6 +4264,7 @@ class parser_test testDecoderPullServesDataBeforeError(); testDecoderNoPayload(); testDecoderReadSomeEmptyBuffer(); + testDecoderReadSomeLeadingEmptyBuffer(); testDecoderBodyAfterStreaming(); testDecoderReadBodyHardError(); testDecoderPullTwiceWithoutConsume(); @@ -4476,8 +4277,6 @@ class parser_test testDecoderReadBody(); testDecoderReadBodyOverflowThenStream(); - testTransientStreamError(); - testTerminalStreamError(); testResetAfterFailure(); testPullEmptyDest(); testMoveCtor(); diff --git a/test/unit/request_parser.cpp b/test/unit/request_parser.cpp index bcbfee0..08c2dd7 100644 --- a/test/unit/request_parser.cpp +++ b/test/unit/request_parser.cpp @@ -11,13 +11,13 @@ #include #include -#include -#include -#include +#include +#include #include "test_suite.hpp" +#include #include namespace boost @@ -26,38 +26,50 @@ namespace burl { class request_parser_test { + // The parser does no I/O: octets go in through prepare()/commit(). + static + void + feed(parser& pr, std::string_view s) + { + auto const n = capy::buffer_copy( + pr.prepare(), + capy::const_buffer(s.data(), s.size())); + BOOST_TEST_EQ(n, s.size()); + pr.commit(n); + } + public: void test_header() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); request_parser pr(request_parser::config{}); - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "GET /index.html HTTP/1.1\r\n" - "Host: example.com\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); + pr.start(); + feed(pr, + "GET /index.html HTTP/1.1\r\n" + "Host: example.com\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"); + + system::error_code ec; + pr.parse_header(ec); + BOOST_TEST(!ec); + BOOST_TEST(pr.got_header()); - pr.start(); - auto [ec] = co_await message_reader{ &stream, &pr }.read_header(); - BOOST_TEST(!ec); - BOOST_TEST(pr.got_header()); + BOOST_TEST(pr.get().method() == http::method::get); + BOOST_TEST(pr.get().target() == "/index.html"); - BOOST_TEST(pr.get().method() == http::method::get); - BOOST_TEST(pr.get().target() == "/index.html"); + char buf[8]; + capy::mutable_buffer mb(buf, sizeof(buf)); + auto n = pr.read_some({ &mb, 1 }, ec); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 5); + BOOST_TEST(std::string_view(buf, n) == "hello"); - char buf[8]; - auto [bec, n] = co_await message_reader{ &stream, &pr } - .read(capy::make_buffer(buf)); - BOOST_TEST(bec == capy::cond::eof); - BOOST_TEST_EQ(n, 5); - BOOST_TEST(std::string_view(buf, n) == "hello"); - }()); + n = pr.read_some({ &mb, 1 }, ec); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(n, 0); } void diff --git a/test/unit/response_parser.cpp b/test/unit/response_parser.cpp index ddc6d7f..8f046ed 100644 --- a/test/unit/response_parser.cpp +++ b/test/unit/response_parser.cpp @@ -11,13 +11,13 @@ #include #include -#include -#include -#include +#include +#include #include "test_suite.hpp" +#include #include namespace boost @@ -26,66 +26,74 @@ namespace burl { class response_parser_test { + // The parser does no I/O: octets go in through prepare()/commit(). + static + void + feed(parser& pr, std::string_view s) + { + auto const n = capy::buffer_copy( + pr.prepare(), + capy::const_buffer(s.data(), s.size())); + BOOST_TEST_EQ(n, s.size()); + pr.commit(n); + } + public: void test_header() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); response_parser pr(response_parser::config{}); - capy::test::run_blocking()([&]() -> capy::task<> - { - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n" - "hello"); - - pr.start(); - auto [ec] = co_await message_reader{ &stream, &pr }.read_header(); - BOOST_TEST(!ec); - BOOST_TEST(pr.got_header()); - // the whole body arrived with the header, so the message - // is already complete (arrival semantics) - BOOST_TEST(pr.got_body()); - - BOOST_TEST(pr.get().status() == http::status::ok); - BOOST_TEST_EQ(pr.get().status_int(), 200); - BOOST_TEST(pr.get().reason() == "OK"); - - char buf[8]; - auto [bec, n] = co_await message_reader{ &stream, &pr } - .read(capy::make_buffer(buf)); - BOOST_TEST(bec == capy::cond::eof); - BOOST_TEST_EQ(n, 5); - BOOST_TEST(std::string_view(buf, n) == "hello"); - }()); + pr.start(); + feed(pr, + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n" + "hello"); + + system::error_code ec; + pr.parse_header(ec); + BOOST_TEST(!ec); + BOOST_TEST(pr.got_header()); + // the whole body arrived with the header, so the message + // is already complete (arrival semantics) + BOOST_TEST(pr.got_body()); + + BOOST_TEST(pr.get().status() == http::status::ok); + BOOST_TEST_EQ(pr.get().status_int(), 200); + BOOST_TEST(pr.get().reason() == "OK"); + + char buf[8]; + capy::mutable_buffer mb(buf, sizeof(buf)); + auto n = pr.read_some({ &mb, 1 }, ec); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 5); + BOOST_TEST(std::string_view(buf, n) == "hello"); + + n = pr.read_some({ &mb, 1 }, ec); + BOOST_TEST(ec == capy::cond::eof); + BOOST_TEST_EQ(n, 0); } void test_head_response() { - capy::test::read_stream server; - capy::any_read_stream stream(&server); response_parser pr(response_parser::config{}); - capy::test::run_blocking()([&]() -> capy::task<> - { - // Response to a HEAD request: framing fields are present but no - // body follows. start(true) tells the parser not to read one. - server.provide( - "HTTP/1.1 200 OK\r\n" - "Content-Length: 5\r\n" - "\r\n"); - - pr.start(true); - auto [ec] = co_await message_reader{ &stream, &pr }.read_header(); - BOOST_TEST(!ec); - BOOST_TEST(pr.got_header()); - BOOST_TEST(pr.got_body()); - BOOST_TEST_EQ(pr.get().status_int(), 200); - }()); + // Response to a HEAD request: framing fields are present but no + // body follows. start(true) tells the parser not to read one. + pr.start(true); + feed(pr, + "HTTP/1.1 200 OK\r\n" + "Content-Length: 5\r\n" + "\r\n"); + + system::error_code ec; + pr.parse_header(ec); + BOOST_TEST(!ec); + BOOST_TEST(pr.got_header()); + BOOST_TEST(pr.got_body()); + BOOST_TEST_EQ(pr.get().status_int(), 200); } void