From 5cb0e7df1f10b1b71dce25cb9f4ffd16a02d2b16 Mon Sep 17 00:00:00 2001 From: Mohammad Nejati Date: Mon, 17 Aug 2026 12:55:51 +0330 Subject: [PATCH 1/3] fix MSVC C4244 in head_parser tests --- test/unit/head_parser.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/unit/head_parser.cpp b/test/unit/head_parser.cpp index e4bfc7c..1f38082 100644 --- a/test/unit/head_parser.cpp +++ b/test/unit/head_parser.cpp @@ -1393,7 +1393,8 @@ class head_parser_test return s + "\r\n"; }; - for(std::uint16_t mf : { 0u, 1u, 3u, 8u, 100u }) + for(std::uint16_t mf : std::initializer_list< + std::uint16_t>{ 0, 1, 3, 8, 100 }) { // the smallest max_size which can carry // mf fields, and a couple of larger ones @@ -1512,9 +1513,12 @@ class head_parser_test for(auto const& h : hs) for(std::uint32_t ms = 0; ms <= 120; ++ms) - for(std::uint16_t msl : { 0, 17, 64, 4096 }) - for(std::uint16_t mfd : { 0, 9, 40, 4096 }) - for(std::uint16_t mfl : { 0, 1, 3, 100 }) + for(std::uint16_t msl : std::initializer_list< + std::uint16_t>{ 0, 17, 64, 4096 }) + for(std::uint16_t mfd : std::initializer_list< + std::uint16_t>{ 0, 9, 40, 4096 }) + for(std::uint16_t mfl : std::initializer_list< + std::uint16_t>{ 0, 1, 3, 100 }) { header_limits const lim{ .max_size = ms, From 0b4c7b2c0bc79579ea45958ca4da52b1f6b7ae00 Mon Sep 17 00:00:00 2001 From: Mohammad Nejati Date: Tue, 18 Aug 2026 20:06:16 +0330 Subject: [PATCH 2/3] sync capy's io_result tuple alias --- doc/modules/ROOT/pages/testing.adoc | 2 +- example/nlohmann_json.cpp | 2 +- include/boost/burl/client.hpp | 2 +- include/boost/burl/conversion.hpp | 4 +- include/boost/burl/message_reader.hpp | 4 +- .../burl/test/detail/buffer_connection.hpp | 4 +- src/client.cpp | 32 +++++---- src/detail/connection_pool.cpp | 70 ++++++++++--------- src/detail/drain_body.hpp | 4 +- src/detail/http_tunnel.cpp | 13 ++-- src/detail/serializer.cpp | 6 +- src/detail/serializer.hpp | 2 +- src/detail/socks5_tunnel.cpp | 41 ++++++----- src/json.cpp | 8 +-- src/parser.cpp | 18 ++--- test/unit/body_test.hpp | 2 +- test/unit/client.cpp | 8 +-- test/unit/detail/connection_pool.cpp | 8 +-- test/unit/detail/http_tunnel.cpp | 2 +- test/unit/detail/send_file.cpp | 2 +- test/unit/detail/socks5_tunnel.cpp | 2 +- test/unit/file.cpp | 6 +- test/unit/parser.cpp | 4 +- test/unit/scripted_net.hpp | 2 +- 24 files changed, 129 insertions(+), 119 deletions(-) diff --git a/doc/modules/ROOT/pages/testing.adoc b/doc/modules/ROOT/pages/testing.adoc index b548574..44c6138 100644 --- a/doc/modules/ROOT/pages/testing.adoc +++ b/doc/modules/ROOT/pages/testing.adoc @@ -83,7 +83,7 @@ cfg.connect_handler = { auto [a, b] = capy::test::make_stream_pair(); // drive b from the test as the "server"; hand a to the client - co_return { {}, capy::any_stream(std::move(a)) }; + co_return { std::error_code(), capy::any_stream(std::move(a)) }; }; burl::client client(co_await capy::this_coro::executor, tls_ctx, cfg); diff --git a/example/nlohmann_json.cpp b/example/nlohmann_json.cpp index 253a4a6..6eef516 100644 --- a/example/nlohmann_json.cpp +++ b/example/nlohmann_json.cpp @@ -84,7 +84,7 @@ tag_invoke(burl::body_to_tag, burl::response& resp) auto doc = nlohmann::json::parse(sv, nullptr, false); if(doc.is_discarded()) co_return { make_error_code(std::errc::bad_message), {} }; - co_return { {}, std::move(doc) }; + co_return { std::error_code(), std::move(doc) }; } // end::body_to[] diff --git a/include/boost/burl/client.hpp b/include/boost/burl/client.hpp index 52d4202..7143e30 100644 --- a/include/boost/burl/client.hpp +++ b/include/boost/burl/client.hpp @@ -302,7 +302,7 @@ class client { auto [a, b] = capy::test::make_stream_pair(); // drive b from the test; hand a to the client - co_return { {}, capy::any_stream(std::move(a)) }; + co_return { std::error_code(), capy::any_stream(std::move(a)) }; }; @endcode */ diff --git a/include/boost/burl/conversion.hpp b/include/boost/burl/conversion.hpp index df76094..afb393f 100644 --- a/include/boost/burl/conversion.hpp +++ b/include/boost/burl/conversion.hpp @@ -75,8 +75,8 @@ struct body_from_tag { auto [ec, sv] = co_await resp.try_as_view(); if(ec) - co_return { ec, {} }; - co_return { {}, my_type{ sv } }; + co_return { ec, my_type{} }; + co_return { std::error_code(), my_type{ sv } }; } @endcode diff --git a/include/boost/burl/message_reader.hpp b/include/boost/burl/message_reader.hpp index 4bf265a..2d71a12 100644 --- a/include/boost/burl/message_reader.hpp +++ b/include/boost/burl/message_reader.hpp @@ -330,7 +330,7 @@ read_some_( else if(rec) co_return { rec, rn }; if(rn != 0) - co_return { {}, rn }; + co_return { std::error_code(), rn }; continue; } @@ -361,7 +361,7 @@ read_( co_return { ec, total }; } - co_return { {}, total }; + co_return { std::error_code(), total }; } template diff --git a/include/boost/burl/test/detail/buffer_connection.hpp b/include/boost/burl/test/detail/buffer_connection.hpp index 445e6df..36bfc86 100644 --- a/include/boost/burl/test/detail/buffer_connection.hpp +++ b/include/boost/burl/test/detail/buffer_connection.hpp @@ -74,7 +74,7 @@ class buffer_connection final : public burl::detail::connection auto const n = capy::buffer_copy( bufs, capy::buffer_slice(b, head_pos_)); head_pos_ += n; - co_return { {}, n }; + co_return { std::error_code(), n }; } if(auto ec = fuse_.maybe_fail()) @@ -92,7 +92,7 @@ class buffer_connection final : public burl::detail::connection ++idx_; pos_ = 0; } - co_return { {}, n }; + co_return { std::error_code(), n }; } capy::io_task diff --git a/src/client.cpp b/src/client.cpp index a682560..5aed1c9 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -255,9 +255,9 @@ client::execute_impl( head.set(field::cookie, cookies); } - auto [cec, conn] = co_await pool_->acquire(url); - if(cec) - co_return { cec, {} }; + auto [ec, conn] = co_await pool_->acquire(url); + if(ec) + co_return { ec, response{} }; // TODO: expect100timeout @@ -266,22 +266,24 @@ client::execute_impl( if(request.body.has_value()) { http::any_buffer_sink sink(&sr); - if(auto [wec] = co_await request.body.write(sink); wec) - co_return { wec, {} }; + std::tie(ec) = co_await request.body.write(sink); + if(ec) + co_return { ec, response{} }; } if(!sr.is_done()) { - if(auto [wec] = co_await sr.write_eof(); wec) - co_return { wec, {} }; + std::tie(ec) = co_await sr.write_eof(); + if(ec) + co_return { ec, response{} }; } parser.reset(); parser.start(is_head); - auto [rec] = co_await message_reader{ + std::tie(ec) = co_await message_reader{ &conn, &parser }.read_header(); - if(rec) - co_return { rec, {} }; + if(ec) + co_return { ec, response{} }; // extract cookies if(config_.cookies) @@ -299,10 +301,10 @@ client::execute_impl( if(!is_redirect || !followlocation) { - auto ec = std::error_code{}; auto status_int = parser.get().status_int(); - if(status_int >= 400) - ec = std::error_code(status_int, burl_category()); + ec = status_int >= 400 + ? std::error_code(status_int, burl_category()) + : std::error_code(); std::unique_ptr dec; if(auto_decode && !is_head) @@ -327,7 +329,7 @@ client::execute_impl( conn.return_to_pool(); if(maxredirs-- == 0) - co_return { error::too_many_redirects, {} }; + co_return { error::too_many_redirects, response{} }; // Set the Referer header to the URL we are leaving. if(config_.autoreferer) @@ -341,7 +343,7 @@ client::execute_impl( // Prepare the next request to follow the redirect url = detail::resolve_location(parser.get(), url); if(url.empty()) - co_return { error::bad_redirect_response, {} }; + co_return { error::bad_redirect_response, response{} }; // Change the method according to RFC 9110, Section 15.4.4. if(need_method_change && head.method() != http::method::head) diff --git a/src/detail/connection_pool.cpp b/src/detail/connection_pool.cpp index 8e4eddb..8857c26 100644 --- a/src/detail/connection_pool.cpp +++ b/src/detail/connection_pool.cpp @@ -59,13 +59,14 @@ connect_tcp( urls::url_view url) { corosio::resolver resolver(exec); - auto [rec, eps] = co_await resolver.resolve( + auto [ec, eps] = co_await resolver.resolve( url.encoded_host_address(), effective_port(url)); - if(rec) - co_return rec; + if(ec) + co_return ec; - if(auto [cec, ep] = co_await corosio::connect(socket, eps); cec) - co_return cec; + std::tie(ec, std::ignore) = co_await corosio::connect(socket, eps); + if(ec) + co_return ec; if(cfg.tcp_nodelay) socket.set_option(corosio::socket_option::no_delay(true)); @@ -254,10 +255,10 @@ connection_pool::acquire(urls::url_view url) entry.conn->set_io_timeout(config_.io_timeout); co_return { - {}, - { std::move(entry.conn), - weak_from_this(), - std::move(key) } + std::error_code(), + pooled_connection{ std::move(entry.conn), + weak_from_this(), + std::move(key) } }; } @@ -265,14 +266,14 @@ connection_pool::acquire(urls::url_view url) co_await corosio::timeout( connect(url), config_.connect_timeout); if(ec) - co_return { ec, {} }; + co_return { ec, pooled_connection{} }; conn->set_io_timeout(config_.io_timeout); co_return { - {}, - { std::move(conn), - weak_from_this(), - std::move(key) } + std::error_code(), + pooled_connection{ std::move(conn), + weak_from_this(), + std::move(key) } }; } @@ -296,15 +297,15 @@ connection_pool::connect(urls::url_view url) const using urls::scheme; if(url.scheme_id() != scheme::http && url.scheme_id() != scheme::https) - co_return { error::unsupported_url_scheme, {} }; + co_return { error::unsupported_url_scheme, nullptr }; if(config_.connect_handler) { auto [ec, stream] = co_await config_.connect_handler(url); if(ec) - co_return { ec, {} }; - co_return { - {}, std::make_unique(std::move(stream)) }; + co_return { ec, nullptr }; + co_return { std::error_code(), + std::make_unique(std::move(stream)) }; } corosio::tcp_socket socket(exec_); @@ -313,27 +314,27 @@ connection_pool::connect(urls::url_view url) const { auto const& proxy = *config_.proxy; if(effective_port(proxy).empty()) - co_return { error::unsupported_proxy_scheme, {} }; + co_return { error::unsupported_proxy_scheme, nullptr }; if(auto [ec] = co_await connect_tcp(socket, exec_, config_, proxy); ec) - co_return { ec, {} }; + co_return { ec, nullptr }; if(proxy.scheme() == "http") { auto [ec] = co_await open_http_tunnel( capy::any_stream(&socket), url, proxy); if(ec) - co_return { ec, {} }; + co_return { ec, nullptr }; } else if(proxy.scheme() == "socks5") { urls::url resolved; corosio::resolver resolver(exec_); - auto [rec, eps] = co_await resolver.resolve( + auto [ec, eps] = co_await resolver.resolve( url.encoded_host_address(), effective_port(url)); - if(rec) - co_return { rec, {} }; + if(ec) + co_return { ec, nullptr }; auto const& ep = eps.front().get_endpoint(); resolved.set_port_number(ep.port()); @@ -344,27 +345,27 @@ connection_pool::connect(urls::url_view url) const resolved.set_host_ipv6( urls::ipv6_address(ep.v6_address().to_bytes())); - auto [ec] = co_await open_socks5_tunnel( + std::tie(ec) = co_await open_socks5_tunnel( capy::any_stream(&socket), resolved, proxy); if(ec) - co_return { ec, {} }; + co_return { ec, nullptr }; } else if(proxy.scheme() == "socks5h") { auto [ec] = co_await open_socks5_tunnel( capy::any_stream(&socket), url, proxy); if(ec) - co_return { ec, {} }; + co_return { ec, nullptr }; } else { - co_return { error::unsupported_proxy_scheme, {} }; + co_return { error::unsupported_proxy_scheme, nullptr }; } } else { if(auto [ec] = co_await connect_tcp(socket, exec_, config_, url); ec) - co_return { ec, {} }; + co_return { ec, nullptr }; } if(url.scheme_id() == scheme::https) @@ -372,14 +373,15 @@ connection_pool::connect(urls::url_view url) const auto conn = std::make_unique(std::move(socket), tls_ctx_); conn->set_hostname(url.encoded_host()); - auto [hec] = co_await conn->handshake(); - if(hec) - co_return { hec, {} }; + auto [ec] = co_await conn->handshake(); + if(ec) + co_return { ec, nullptr }; - co_return { {}, std::move(conn) }; + co_return { std::error_code(), std::move(conn) }; } - co_return { {}, std::make_unique(std::move(socket)) }; + co_return { std::error_code(), + std::make_unique(std::move(socket)) }; } void diff --git a/src/detail/drain_body.hpp b/src/detail/drain_body.hpp index 89ae0ca..538a4e8 100644 --- a/src/detail/drain_body.hpp +++ b/src/detail/drain_body.hpp @@ -39,7 +39,7 @@ drain_body( while(!parser.got_body()) { if(attempts-- == 0) - co_return { {}, false }; + co_return { std::error_code(), false }; capy::const_buffer arr[8]; auto [ec, bufs] = co_await reader.pull(arr); @@ -51,7 +51,7 @@ drain_body( } parser.consume(capy::buffer_size(bufs)); } - co_return { {}, true }; + co_return { std::error_code(), true }; } } // namespace detail diff --git a/src/detail/http_tunnel.cpp b/src/detail/http_tunnel.cpp index 47fb151..1839037 100644 --- a/src/detail/http_tunnel.cpp +++ b/src/detail/http_tunnel.cpp @@ -50,15 +50,18 @@ open_http_tunnel( req.set(http::field::proxy_authorization, value); } - if(auto [ec, n] = - co_await capy::write(stream, capy::make_buffer(req.buffer())); - ec) + std::error_code ec; + + std::tie(ec, std::ignore) = co_await capy::write( + stream, capy::make_buffer(req.buffer())); + if(ec) co_return ec; response_parser parser(response_parser::config{}); parser.start(); - if(auto [ec] = co_await message_reader{ - &stream, &parser }.read_header(); ec) + std::tie(ec) = co_await message_reader{ + &stream, &parser }.read_header(); + if(ec) co_return { error::proxy_connect_failed }; auto status = parser.get().status(); diff --git a/src/detail/serializer.cpp b/src/detail/serializer.cpp index 74fd5fa..67d3503 100644 --- a/src/detail/serializer.cpp +++ b/src/detail/serializer.cpp @@ -239,7 +239,7 @@ encode( if(n != tail.size()) in = tail[n++]; else if(!eof) - co_return { {}, consumed }; + co_return { std::error_code(), consumed }; } if(out_len_ == out_cap_) @@ -276,7 +276,7 @@ encode( } if(!eof && consumed != 0) - co_return { {}, consumed }; + co_return { std::error_code(), consumed }; } } @@ -365,7 +365,7 @@ flush( len = 0; hdr_sent_ = true; done_ = eof; - co_return { {}, consumed }; + co_return { std::error_code(), consumed }; } } // namespace detail diff --git a/src/detail/serializer.hpp b/src/detail/serializer.hpp index 65f8e31..81a6db3 100644 --- a/src/detail/serializer.hpp +++ b/src/detail/serializer.hpp @@ -195,7 +195,7 @@ write_some(Buffers buffers) if(should_coalesce(avail)) { do_commit(capy::buffer_copy(do_prepare(), buffers)); - co_return { {}, avail }; + co_return { std::error_code(), avail }; } co_return co_await process( capy::buffer_param(buffers).data(), false); diff --git a/src/detail/socks5_tunnel.cpp b/src/detail/socks5_tunnel.cpp index 339bdaa..4492620 100644 --- a/src/detail/socks5_tunnel.cpp +++ b/src/detail/socks5_tunnel.cpp @@ -36,11 +36,13 @@ open_socks5_tunnel( urls::url_view target, urls::url_view proxy) { + std::error_code ec; + // Greeting: offer username/password auth only when credentials are present. if(proxy.has_userinfo()) { std::uint8_t greeting[4] = { 0x05, 0x02, 0x00, 0x02 }; - auto [ec, n] = + std::tie(ec, std::ignore) = co_await capy::write(stream, capy::make_buffer(greeting)); if(ec) co_return ec; @@ -48,16 +50,16 @@ open_socks5_tunnel( else { std::uint8_t greeting[3] = { 0x05, 0x01, 0x00 }; - auto [ec, n] = + std::tie(ec, std::ignore) = co_await capy::write(stream, capy::make_buffer(greeting)); if(ec) co_return ec; } std::uint8_t greeting_resp[2]; - if(auto [ec, n] = - co_await capy::read(stream, capy::make_buffer(greeting_resp)); - ec) + std::tie(ec, std::ignore) = + co_await capy::read(stream, capy::make_buffer(greeting_resp)); + if(ec) co_return ec; if(greeting_resp[0] != 0x05) @@ -80,15 +82,15 @@ open_socks5_tunnel( auth_req.push_back(static_cast(pass.decoded_size())); pass.decode({}, urls::string_token::append_to(auth_req)); - if(auto [ec, n] = - co_await capy::write(stream, capy::make_buffer(auth_req)); - ec) + std::tie(ec, std::ignore) = + co_await capy::write(stream, capy::make_buffer(auth_req)); + if(ec) co_return ec; std::uint8_t auth_resp[2]; - if(auto [ec, n] = - co_await capy::read(stream, capy::make_buffer(auth_resp)); - ec) + std::tie(ec, std::ignore) = + co_await capy::read(stream, capy::make_buffer(auth_resp)); + if(ec) co_return ec; if(auth_resp[1] != 0x00) @@ -141,15 +143,16 @@ open_socks5_tunnel( conn_req.push_back(static_cast((port >> 8) & 0xFF)); conn_req.push_back(static_cast(port & 0xFF)); - if(auto [ec, n] = co_await capy::write(stream, capy::make_buffer(conn_req)); - ec) + std::tie(ec, std::ignore) = + co_await capy::write(stream, capy::make_buffer(conn_req)); + if(ec) co_return ec; // connection response std::uint8_t reply_head[5]; - if(auto [ec, n] = - co_await capy::read(stream, capy::make_buffer(reply_head)); - ec) + std::tie(ec, std::ignore) = + co_await capy::read(stream, capy::make_buffer(reply_head)); + if(ec) co_return ec; if(reply_head[1] != 0x00) @@ -173,9 +176,9 @@ open_socks5_tunnel( std::string reply_tail; reply_tail.resize(tail); - if(auto [ec, n] = - co_await capy::read(stream, capy::make_buffer(reply_tail)); - ec) + std::tie(ec, std::ignore) = + co_await capy::read(stream, capy::make_buffer(reply_tail)); + if(ec) co_return ec; co_return {}; diff --git a/src/json.cpp b/src/json.cpp index 91b445a..eeb7bc2 100644 --- a/src/json.cpp +++ b/src/json.cpp @@ -107,7 +107,7 @@ tag_invoke(body_to_tag, response& resp) parser.finish(ec); if(ec) co_return { ec, {} }; - co_return { {}, parser.release() }; + co_return { std::error_code(), parser.release() }; } co_return { ec, {} }; } @@ -138,7 +138,7 @@ tag_invoke(body_to_tag, response& resp) auto r = jv.try_as_object(); if(r.has_error()) co_return { r.error(), {} }; - co_return { {}, std::move(*r) }; + co_return { std::error_code(), std::move(*r) }; } any_request_body @@ -156,7 +156,7 @@ tag_invoke(body_to_tag, response& resp) auto r = jv.try_as_array(); if(r.has_error()) co_return { r.error(), {} }; - co_return { {}, std::move(*r) }; + co_return { std::error_code(), std::move(*r) }; } any_request_body @@ -174,7 +174,7 @@ tag_invoke(body_to_tag, response& resp) auto r = jv.try_as_string(); if(r.has_error()) co_return { r.error(), {} }; - co_return { {}, std::move(*r) }; + co_return { std::error_code(), std::move(*r) }; } } // namespace burl diff --git a/src/parser.cpp b/src/parser.cpp index 2403f50..6d65dc5 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -491,7 +491,7 @@ walk_chunks(chunk_fn f, bool dry) fin_chunk_ = true; in_.consume(in_.size() - t0); } - return f({}, true).ec; + return std::get<0>(f({}, true)); } invoke: @@ -792,7 +792,7 @@ decode_some( { auto const out = first(outbufs.data()); if(out.size() == 0) - return { {}, cons }; + return { std::error_code(), cons }; auto const lim = clamp(limit_rem_); auto const r = dec_->process( prefix(out, lim), in, last); @@ -804,17 +804,17 @@ decode_some( if(r.ec) { dec_err_ = r.ec; - return { {}, cons }; + return { std::error_code(), cons }; } if(r.produced == 0 && r.consumed == 0) { if(lim == 0) return { body_too_large, cons }; dec_err_ = error::decode_error; - return { {}, cons }; + return { std::error_code(), cons }; } if(in.size() == 0) - return { {}, cons }; + return { std::error_code(), cons }; } }; @@ -914,7 +914,7 @@ read_some( outbufs.consume(n); if(take < b.size()) return { body_too_large, n }; - return { {}, n }; + return { std::error_code(), n }; }); if(read != 0) return read; @@ -1015,13 +1015,13 @@ pull( if(b.size() == 0) return { capy::error::eof, 0 }; if(n == dest.size()) - return { {}, 0 }; + return { std::error_code(), 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 }; + return { std::error_code(), take }; }, true); if(n != 0) @@ -1102,7 +1102,7 @@ consume(std::size_t n) noexcept { auto const take = clamp(b.size(), n); n -= take; - return { {}, take }; + return { std::error_code(), take }; }); return; default: diff --git a/test/unit/body_test.hpp b/test/unit/body_test.hpp index 0945f4a..01f0f33 100644 --- a/test/unit/body_test.hpp +++ b/test/unit/body_test.hpp @@ -61,7 +61,7 @@ check_io_body( http::any_buffer_sink sink(&bs); capy::run_async( ioc.get_executor(), - [&](capy::io_result<> res) {ec = res.ec; }) + [&](capy::io_result<> res) {ec = std::get<0>(res); }) (body.write(sink)); ioc.run(); diff --git a/test/unit/client.cpp b/test/unit/client.cpp index 94c9d7c..ce03c98 100644 --- a/test/unit/client.cpp +++ b/test/unit/client.cpp @@ -52,7 +52,7 @@ class client_test auto n = capy::buffer_copy( mb, capy::make_buffer(rd_buf)); rd_buf.clear(); - co_return { {}, n }; + co_return { std::error_code(), n }; } auto [ec] = co_await corosio::delay(10s); @@ -62,7 +62,7 @@ class client_test capy::io_task write_some(auto cb) { - co_return { {}, capy::buffer_size(cb) }; + co_return { std::error_code(), capy::buffer_size(cb) }; } }; @@ -837,7 +837,7 @@ class client_test cfg.connect_handler = [](urls::url_view) -> capy::io_task { - co_return { {}, + co_return { std::error_code(), capy::any_stream{ slow_stream{ "HTTP/1.1 200 OK\r\n" "Cont" } } }; @@ -864,7 +864,7 @@ class client_test cfg.timeout = 10ms; cfg.connect_handler = [](urls::url_view) -> capy::io_task { - co_return { {}, capy::any_stream{ + co_return { std::error_code(), capy::any_stream{ slow_stream{ "HTTP/1.1 200 OK\r\n" "Content-Length: 5\r\n" diff --git a/test/unit/detail/connection_pool.cpp b/test/unit/detail/connection_pool.cpp index fb16290..7d91a32 100644 --- a/test/unit/detail/connection_pool.cpp +++ b/test/unit/detail/connection_pool.cpp @@ -125,7 +125,7 @@ class connection_pool_test for(;;) { if(auto pos = buf.find(delim); pos != std::string::npos) - co_return { {}, pos + delim.size() }; + co_return { std::error_code(), pos + delim.size() }; char tmp[256]; auto [ec, n] = co_await s.read_some(capy::make_buffer(tmp)); buf.append(tmp, n); @@ -350,11 +350,11 @@ class connection_pool_test if(auto [ec] = co_await corosio::delay(1s); ec) { BOOST_TEST_EQ(ec, capy::error::canceled); - co_return { ec, {} }; + co_return { ec, capy::any_stream{} }; } BOOST_TEST_FAIL(); auto [cli, srv] = capy::test::make_stream_pair(); - co_return { {}, capy::any_stream(std::move(cli)) }; + co_return { std::error_code(), capy::any_stream(std::move(cli)) }; }; auto client_task = [&]() -> capy::task<> @@ -380,7 +380,7 @@ class connection_pool_test cfg.io_timeout = 10ms; cfg.connect_handler = [](urls::url_view) -> capy::io_task { - co_return { {}, capy::any_stream{ slow_stream{} } }; + co_return { std::error_code(), capy::any_stream{ slow_stream{} } }; }; auto client_task = [&]() -> capy::task<> diff --git a/test/unit/detail/http_tunnel.cpp b/test/unit/detail/http_tunnel.cpp index 82f3ec0..cfa020a 100644 --- a/test/unit/detail/http_tunnel.cpp +++ b/test/unit/detail/http_tunnel.cpp @@ -35,7 +35,7 @@ class http_tunnel_test { std::error_code ret; capy::test::run_blocking( - [&](capy::io_result<> rs){ ret = rs.ec;}) + [&](capy::io_result<> rs){ ret = std::get<0>(rs);}) (open_http_tunnel(&client, target, proxy)); return ret; } diff --git a/test/unit/detail/send_file.cpp b/test/unit/detail/send_file.cpp index 3272e68..5977313 100644 --- a/test/unit/detail/send_file.cpp +++ b/test/unit/detail/send_file.cpp @@ -39,7 +39,7 @@ class send_file_test http::any_buffer_sink sink(&bs); capy::run_async( ioc.get_executor(), - [&](capy::io_result<> r) { ret = r.ec; }) + [&](capy::io_result<> r) { ret = std::get<0>(r); }) (send_file(sink, path, size)); ioc.run(); return ret; diff --git a/test/unit/detail/socks5_tunnel.cpp b/test/unit/detail/socks5_tunnel.cpp index c78fe38..66d9bb6 100644 --- a/test/unit/detail/socks5_tunnel.cpp +++ b/test/unit/detail/socks5_tunnel.cpp @@ -48,7 +48,7 @@ class socks5_tunnel_test { std::error_code ret; capy::test::run_blocking( - [&](capy::io_result<> rs){ ret = rs.ec;}) + [&](capy::io_result<> rs){ ret = std::get<0>(rs);}) (open_socks5_tunnel(&client, target, proxy)); return ret; } diff --git a/test/unit/file.cpp b/test/unit/file.cpp index a938bb2..63518f1 100644 --- a/test/unit/file.cpp +++ b/test/unit/file.cpp @@ -106,7 +106,7 @@ class file_test ioc.get_executor(), [&](capy::io_result res) { - BOOST_TEST(!res.ec); + BOOST_TEST(!get<0>(res)); BOOST_TEST_EQ(get<1>(res), dest.path); BOOST_TEST_EQ(read_file(dest.path), "frag1frag2frag3"); })(r.try_as(dest.path)); @@ -127,7 +127,7 @@ class file_test ioc.get_executor(), [&](capy::io_result res) { - BOOST_TEST(res.ec); + BOOST_TEST(get<0>(res)); BOOST_TEST_EQ(get<1>(res), ""); BOOST_TEST_EQ(read_file(dest.path), "original contents"); })(r.try_as(dest.path)); @@ -150,7 +150,7 @@ class file_test ioc.get_executor(), [&](capy::io_result res) { - BOOST_TEST_EQ(res.ec, http::error::incomplete); + BOOST_TEST_EQ(get<0>(res), http::error::incomplete); BOOST_TEST_EQ(get<1>(res), dest.path); BOOST_TEST_EQ(read_file(dest.path), "Payl"); })(r.try_as(dest.path)); diff --git a/test/unit/parser.cpp b/test/unit/parser.cpp index bb0cb24..365d789 100644 --- a/test/unit/parser.cpp +++ b/test/unit/parser.cpp @@ -189,7 +189,7 @@ struct test_parser : parser auto const rn = direct_read( capy::buffer_slice(mbs, 0, lim)); if(rn != 0) - return { {}, rn }; + return { std::error_code(), rn }; continue; } @@ -233,7 +233,7 @@ struct test_parser : parser return { ec, total }; } - return { {}, total }; + return { std::error_code(), total }; } std::pair> diff --git a/test/unit/scripted_net.hpp b/test/unit/scripted_net.hpp index 198a0f8..fda5ff2 100644 --- a/test/unit/scripted_net.hpp +++ b/test/unit/scripted_net.hpp @@ -65,7 +65,7 @@ struct scripted_net if(n < close_after.size() && close_after[n]) srv.close(); servers.push_back(std::move(srv)); - co_return { {}, capy::any_stream(std::move(cli)) }; + co_return { std::error_code(), capy::any_stream(std::move(cli)) }; }; return cfg; } From 8eab1b35ef180979eff9d58125638146ebfc0940 Mon Sep 17 00:00:00 2001 From: Mohammad Nejati Date: Sun, 16 Aug 2026 20:45:08 +0330 Subject: [PATCH 3/3] serializer is sans-io --- include/boost/burl.hpp | 9 + include/boost/burl/detail/flat_buffer.hpp | 60 + include/boost/burl/error.hpp | 8 +- include/boost/burl/message_writer.hpp | 324 ++ include/boost/burl/parser.hpp | 4 +- include/boost/burl/serializer.hpp | 666 +++++ src/client.cpp | 15 +- src/detail/decoders.cpp | 4 +- src/detail/flat_buffer.cpp | 85 + src/detail/serializer.cpp | 373 --- src/detail/serializer.hpp | 235 -- src/error.cpp | 2 +- src/parser.cpp | 2 +- src/serializer.cpp | 605 ++++ test/unit/detail/decoders.cpp | 10 +- test/unit/detail/flat_buffer.cpp | 188 ++ test/unit/detail/serializer.cpp | 2254 -------------- test/unit/error.cpp | 2 +- test/unit/message_writer.cpp | 745 +++++ test/unit/parser.cpp | 4 +- test/unit/serializer.cpp | 3271 +++++++++++++++++++++ test/unit/temp_file.hpp | 3 +- 22 files changed, 5980 insertions(+), 2889 deletions(-) create mode 100644 include/boost/burl/detail/flat_buffer.hpp create mode 100644 include/boost/burl/message_writer.hpp create mode 100644 include/boost/burl/serializer.hpp create mode 100644 src/detail/flat_buffer.cpp delete mode 100644 src/detail/serializer.cpp delete mode 100644 src/detail/serializer.hpp create mode 100644 src/serializer.cpp create mode 100644 test/unit/detail/flat_buffer.cpp delete mode 100644 test/unit/detail/serializer.cpp create mode 100644 test/unit/message_writer.cpp create mode 100644 test/unit/serializer.cpp diff --git a/include/boost/burl.hpp b/include/boost/burl.hpp index 6674682..f9a3373 100644 --- a/include/boost/burl.hpp +++ b/include/boost/burl.hpp @@ -13,6 +13,8 @@ #include #include #include +#include +#include #include #include #include @@ -20,14 +22,21 @@ #include #include #include +#include +#include #include +#include #include #include +#include #include #include +#include #include #include #include +#include +#include #include #include #include diff --git a/include/boost/burl/detail/flat_buffer.hpp b/include/boost/burl/detail/flat_buffer.hpp new file mode 100644 index 0000000..ec87057 --- /dev/null +++ b/include/boost/burl/detail/flat_buffer.hpp @@ -0,0 +1,60 @@ +// +// 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_DETAIL_FLAT_BUFFER_HPP +#define BOOST_BURL_DETAIL_FLAT_BUFFER_HPP + +#include + +#include + +namespace boost +{ +namespace burl +{ +namespace detail +{ + +struct flat_buffer +{ + char* ptr = nullptr; + std::size_t cap = 0; + std::size_t pos = 0; + std::size_t len = 0; + + bool + empty() const noexcept; + + std::size_t + size() const noexcept; + + std::size_t + capacity() const noexcept; + + capy::const_buffer + data() const noexcept; + + capy::mutable_buffer + prepare() const noexcept; + + void + commit(std::size_t n) noexcept; + + void + consume(std::size_t n) noexcept; + + void + clear() noexcept; +}; + +} // namespace detail +} // namespace burl +} // namespace boost + +#endif diff --git a/include/boost/burl/error.hpp b/include/boost/burl/error.hpp index 7c8eb57..a1b511f 100644 --- a/include/boost/burl/error.hpp +++ b/include/boost/burl/error.hpp @@ -65,11 +65,11 @@ enum class error */ proxy_unsupported_version, - /** The request body size did not match its content length. + /** The message body size did not match its content length. - The number of bytes produced by the request body - differed from the `Content-Length` declared for the - request. + The number of bytes produced by the message body + differed from the `Content-Length` declared for it, + or a body was supplied for a message that has none. */ body_size_mismatch, diff --git a/include/boost/burl/message_writer.hpp b/include/boost/burl/message_writer.hpp new file mode 100644 index 0000000..2054aef --- /dev/null +++ b/include/boost/burl/message_writer.hpp @@ -0,0 +1,324 @@ +// +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +#ifndef BOOST_BURL_MESSAGE_WRITER_HPP +#define BOOST_BURL_MESSAGE_WRITER_HPP + +#include + +#include +#include +#include +#include + +#include +#include +#include + +namespace boost +{ +namespace burl +{ + +/** Drives a @ref serializer over a stream. + + A writer binds a stream to a serializer. It + holds only pointers to both, which must outlive + it. + + @par Example + @code + serializer sr( cfg ); + message_writer writer( &sock, &sr ); + + sr.start( &head ); + + auto [ec, n] = co_await writer.write_eof( + capy::make_buffer( body )); + @endcode + + Every operation writes the header first if it + has not gone out yet. Small writes coalesce in + the serializer's staging buffer; large ones are + spliced into the same gathered write as the + framing, without copying. + + If an operation is cancelled mid-write, the + serializer's accounting stays true to the wire: + the completion counts already reported cover + exactly the consumed octets, and re-issuing a + write of the unconsumed remainder resumes the + message. + + This type satisfies @ref capy::WriteStream, + @ref http::WriteSink, and @ref http::BufferSink, + all over the octets of the message body. + + @tparam S A type satisfying @ref capy::WriteStream. + + @see @ref serializer. +*/ +template +class message_writer +{ + S* s_; + serializer* sr_; + +public: + /** Constructor. + + @par Preconditions + Neither pointer is null, and both objects + outlive the writer. + + @param stream The stream to write to. + + @param sr The serializer to drive. + */ + message_writer(S* stream, serializer* sr) noexcept + : s_(stream) + , sr_(sr) + { + BOOST_ASSERT(s_ != nullptr); + BOOST_ASSERT(sr_ != nullptr); + } + + /** Return writable staging memory. + + @param dest The descriptors to fill. + + @see @ref serializer::prepare. + */ + std::span + prepare(std::span dest) + { + return sr_->prepare(dest); + } + + /** Asynchronously commit staged octets. + + Reports octets written into memory obtained + from @ref prepare, flushing the staging + buffer when it runs low. + + @param n The number of octets written. + + @return An awaitable yielding `(error_code)`. + */ + capy::io_task<> + commit(std::size_t n) + { + return commit_(*s_, *sr_, n); + } + + /** Asynchronously commit final octets and end the body. + + @param n The number of octets written. + + @return An awaitable yielding `(error_code)`. + */ + capy::io_task<> + commit_eof(std::size_t n) + { + return commit_eof_(*s_, *sr_, n); + } + + /** Asynchronously end the body with no more octets. + + @return An awaitable yielding `(error_code)`. + */ + capy::io_task<> + write_eof() + { + return commit_eof_(*s_, *sr_, 0); + } + + /** Asynchronously write the message header. + + Flushes pending output without ending the + body. This sends the header, if needed, and + any staged data after it. + + Mainly used for `Expect: 100-continue`, + where the server must receive the header + before the body is generated. + + Once the header is sent, the framing and + encoding are fixed. Small bodies can no + longer switch to `Content-Length` or + identity encoding. + + @return An awaitable yielding + `(error_code)`. + */ + capy::io_task<> + write_header() + { + return drain_(*s_, *sr_); + } + + /** Asynchronously write body octets. + + Writes at least one octet of `buffers` + unless it is empty; small inputs coalesce + without I/O. May consume the input only + partially. + + @param buffers The octets to write. + + @return An awaitable yielding + `(error_code,std::size_t)`. + */ + template + capy::io_task + write_some(CB buffers) + { + return drive_(*s_, *sr_, std::move(buffers), true); + } + + /** Asynchronously write a whole buffer sequence. + + Writes until `buffers` is fully consumed or + an error occurs. + + @param buffers The octets to write. + + @return An awaitable yielding + `(error_code,std::size_t)`. + */ + template + capy::io_task + write(CB buffers) + { + return drive_(*s_, *sr_, std::move(buffers), true); + } + + /** Asynchronously write final octets and end the body. + + Writes all of `buffers` and completes the + message. The whole body is supplied up + front, which is what lets a small body of + undeclared size go out with + `Content-Length` framing. + + @param buffers The final octets to write. + + @return An awaitable yielding + `(error_code,std::size_t)`. + */ + template + capy::io_task + write_eof(CB buffers) + { + return drive_(*s_, *sr_, std::move(buffers), false); + } + +private: + template + static capy::io_task + drive_( + S& stream, + serializer& sr, + CB buffers, + bool more); + + static capy::io_task<> + drain_(S& stream, serializer& sr); + + static capy::io_task<> + commit_( + S& stream, + serializer& sr, + std::size_t n); + + static capy::io_task<> + commit_eof_( + S& stream, + serializer& sr, + std::size_t n); +}; + +//------------------------------------------------ + +template +template +capy::io_task +message_writer:: +drive_( + S& stream, + serializer& sr, + CB buffers, + bool more) +{ + capy::const_buffer_param bp(buffers); + capy::const_buffer dest[16]; + std::size_t total = 0; + for(;;) + { + system::error_code ec; + auto const body = bp.data(); + auto const bufs = sr.frame( + dest, body, more || bp.more(), ec); + auto [wec, n] = co_await stream.write_some(bufs); + auto const k = sr.consume(n); + bp.consume(k); + total += k; + if(ec) + co_return { ec, total }; + if(wec) + co_return { wec, total }; + if(bufs.empty() && !bp.more()) + co_return { std::error_code(), total }; + } +} + +template +capy::io_task<> +message_writer:: +drain_(S& stream, serializer& sr) +{ + auto [ec, n] = co_await drive_( + stream, sr, capy::const_buffer{}, true); + (void)n; + co_return { ec }; +} + +template +capy::io_task<> +message_writer:: +commit_( + S& stream, + serializer& sr, + std::size_t n) +{ + sr.commit(n); + if(!sr.should_drain()) + co_return {}; + co_return co_await drain_(stream, sr); +} + +template +capy::io_task<> +message_writer:: +commit_eof_( + S& stream, + serializer& sr, + std::size_t n) +{ + sr.commit(n); + auto [ec, k] = co_await drive_( + stream, sr, capy::const_buffer{}, false); + (void)k; + co_return { ec }; +} + +} // namespace burl +} // namespace boost + +#endif diff --git a/include/boost/burl/parser.hpp b/include/boost/burl/parser.hpp index bd36965..7bca09f 100644 --- a/include/boost/burl/parser.hpp +++ b/include/boost/burl/parser.hpp @@ -137,7 +137,7 @@ class parser @param in The octets to decode. - @param eof True when `in` ends the + @param more False when `in` ends the payload. @return The octets consumed and @@ -147,7 +147,7 @@ class parser process( capy::mutable_buffer out, capy::const_buffer in, - bool eof) = 0; + bool more) = 0; }; /// Settings which apply for the life of the parser. diff --git a/include/boost/burl/serializer.hpp b/include/boost/burl/serializer.hpp new file mode 100644 index 0000000..4ca29d1 --- /dev/null +++ b/include/boost/burl/serializer.hpp @@ -0,0 +1,666 @@ +// +// 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_SERIALIZER_HPP +#define BOOST_BURL_SERIALIZER_HPP + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace boost +{ +namespace burl +{ + +/** A serializer for HTTP/1.1 messages. + + Objects of this type incrementally produce the + wire representation of a message. The serializer + performs no I/O; the caller obtains buffers + describing the octets to transfer next and + reports the amount actually transferred. + + @par The body + Body octets may be provided in two ways, which + can be mixed: + + @li supplied directly to @ref frame as a buffer + sequence, or + @li written into the staging buffer obtained + from @ref prepare and made part of the body + with @ref commit. + + Small amounts of data are accumulated internally + and framed together; a call to @ref frame may + therefore return no buffers, or reference fewer + octets than were supplied. Data supplied to + @ref frame whose total size is at least + `config::min_direct` is framed by reference, + without copying; supplied memory must remain + valid and unchanged until consumed. + + @par Framing + The framing of the body is selected from the + payload of the message passed to @ref start: no + body, a body of exactly Content-Length octets, + the chunked transfer coding, or a body delimited + by the end of the stream. + + Until the first octet of the header is consumed, + the serializer may alter the framing-related + fields of the message; for example, when the + total size of the body becomes known before the + header is transferred, chunked or + stream-delimited framing may be replaced by an + explicit Content-Length. Once transfer of the + header has begun, the message is not modified. + + @par Encoding + An @ref encoder passed to @ref start applies a + content coding to the body; the encoder's output + is framed in its place. Setting the + Content-Encoding field is the caller's + responsibility, but when the complete body is + smaller than `config::enc_threshold` the + serializer may skip encoding, in which case it + removes the Content-Encoding field and the body + is serialized unencoded. + + @par Errors + When a call to @ref frame reports an error, the + message is failed and serialization cannot + proceed; the only valid operations on the + serializer are @ref start and destruction. +*/ +class serializer +{ +public: + /** An interface for encoding body content. + */ + struct encoder + { + /// The result of a call to @ref process. + struct result + { + /// The number of input octets consumed + std::size_t consumed; + + /// The number of output octets produced + std::size_t produced; + + /** The status of the operation. + + An empty value indicates success, + `capy::cond::eof` indicates the + encoded stream is complete, and any + other value is an error which fails + serialization. + */ + std::error_code ec; + }; + + virtual ~encoder() = default; + + /** Encode body octets. + + This function is called repeatedly by + the serializer. + + The value of `more` is `false` when + `in` contains the final input octets, + possibly none; once `false`, it is + `false` in every subsequent call. After + the final input is consumed the function + is called, with empty input, until it + reports completion by returning + `capy::cond::eof`. + + Requirements on implementations: + + @li `result::consumed` does not exceed + `in.size()`, and `result::produced` + does not exceed `out.size()`. + @li Every call with non-empty `out` + makes progress: it consumes input, + produces output, or returns + completion or an error. + @li `capy::cond::eof` is returned only + after all input has been consumed + and all remaining output has been + produced. + + After this function returns + `capy::cond::eof` or an error, it is + never called again. + + @param out The destination for encoded + octets. + + @param in The input octets, which may + be empty. + + @param more `false` if `in` completes + the input. + + @return The amounts consumed and + produced, and the status. + */ + virtual result + process( + capy::mutable_buffer out, + capy::const_buffer in, + bool more) = 0; + }; + + /** Serializer configuration settings. */ + struct config + { + /** The size of the staging buffer. + + The staging buffer holds body octets + which are ready for transfer: data + committed through @ref prepare and + @ref commit or copied from small + supplied buffers when no encoder is + used, or the output of the encoder + otherwise. + */ + std::size_t stage_buffer = 64 * 1024; + + /** The minimum capacity before draining. + + When the free capacity of the buffer + written by @ref prepare falls below + this value, @ref should_drain returns + `true` and staged octets become + eligible for transfer on the next call + to @ref frame. + */ + std::size_t min_prepare = 4 * 1024; + + /** The zero-copy threshold. + + Without an encoder, body data supplied + to @ref frame whose total size is at + least this value is framed by + reference, without copying. Smaller + amounts are copied into the staging + buffer, to be coalesced with subsequent + data. + */ + std::size_t min_direct = 2 * 1024; + + /** The size of the encoder input stage. + + When an encoder is used, this buffer + accumulates body data before it is + passed to the encoder. It is unused + otherwise. + */ + std::size_t enc_buffer = 8 * 1024; + + /** The encoding threshold. + + When an encoder is used, body data is + accumulated until at least this many + octets are available before the encoder + is first invoked. If the complete body + is smaller than this value the + serializer may skip encoding entirely. + */ + std::size_t enc_threshold = 4 * 1024; + }; + + /** Constructor. + + The serializer allocates a single internal + buffer whose size is derived from `cfg`; no + further allocations are performed + afterwards. + + @param cfg The configuration settings to + use. + + @throws std::bad_alloc Allocation of the + internal buffer failed. + */ + BOOST_BURL_DECL + explicit serializer(config const& cfg); + + /** Constructor. + + The state of `other`, is transferred to the new + object. Buffers previously returned by + @ref prepare or @ref frame remain valid and + refer to the new object. Afterwards, the + moved-from serializer may only be destroyed + or assigned to. + + @param other The serializer to move from. + */ + serializer(serializer&& other) noexcept = default; + + /** Assignment. + + The state of `other` is transferred; any + message being serialized by `*this` is + abandoned. Buffers previously returned by + `other` remain valid and refer to `*this`. + Afterwards, the moved-from serializer may + only be destroyed or assigned to. + + @param other The serializer to move from. + */ + serializer& + operator=(serializer&& other) noexcept = default; + + serializer(serializer const&) = delete; + + serializer& + operator=(serializer const&) = delete; + + /** Return `true` if the message is finished. + + The message is finished when every + serialized octet, including any trailer, + has been consumed. + This function may also return `true` after + the message has failed. In either case, + the serializer may be reused by calling + @ref start. + */ + bool + is_done() const noexcept + { + return done_; + } + + /** Return `true` if the header was transferred. + + Returns `true` when every octet of the + serialized header has been consumed. + */ + bool + is_header_done() const noexcept + { + return msg_ != nullptr && + msg_->buffer().size() == header_offset_; + } + + /** Return `true` if staged octets should drain. + + Returns `true` when the free capacity of + the buffer written by @ref prepare is below + @ref config::min_prepare. + */ + bool + should_drain() const noexcept + { + return stage_.capacity() < min_prepare_; + } + + /** Start serializing a message. + + Any message currently being serialized is + abandoned and the serializer is reset. The + framing of the body is selected from + `msg->payload()`. Until the first octet of + the header is consumed, the serializer may + modify the framing-related fields of `msg`. + The caller must not modify `msg` while the + message is being serialized. + + When `head` is `true`, the message is + serialized as the response to a HEAD + request: only the header is emitted, + exactly as stored in `msg`, and the body + is omitted. + + @par Preconditions + `msg` is not null. + + @param msg The message to serialize. + Ownership is not transferred; the object + must remain valid until the message + completes or is abandoned by another call + to `start` or by destroying the serializer. + + @param enc The encoder to apply to the + body, or `nullptr`. A fresh encoder object + is required for each message. Ownership is + not transferred; the object must remain + valid until the message completes or is + abandoned. + + @param head `true` to serialize the + response to a HEAD request. + */ + BOOST_BURL_DECL + void + start( + message_head_base* msg, + encoder* enc = nullptr, + bool head = false) noexcept; + + /** Set the trailer fields. + + The referenced fields are serialized after + the final chunk of a body which uses the + chunked transfer coding; with any other + framing the trailer is ignored. Passing + `nullptr` removes a previously set trailer. + + Ownership is not transferred; the object + must remain valid until the message + completes or is abandoned. + + @par Preconditions + A message is being serialized. The end of + the body has not been declared. + + @param t The trailer fields, or `nullptr`. + */ + void + set_trailer(fields_base const* t) noexcept + { + BOOST_ASSERT(!sealed_); + trailer_ = t; + } + + /** Obtain a buffer for writing body octets. + + The result is empty when the staging buffer + is full; in that case, transfer staged octets + by calling @ref frame and @ref consume. + @ref should_drain indicates when draining + is advisable. + + @par Preconditions + A message is being serialized. The end of + the body has not been declared. + + @param dest The span receiving the buffer + descriptor. + + @return The prefix of `dest` which was + filled. + */ + BOOST_BURL_DECL + std::span + prepare(std::span dest); + + /** Make written octets part of the body. + + @par Preconditions + A message is being serialized. The end of + the body has not been declared. `n` does + not exceed the size of the buffer returned + by @ref prepare. + + @param n The number of octets written. + */ + BOOST_BURL_DECL + void + commit(std::size_t n) noexcept; + + /** Obtain buffers for the next serialized octets. + + This function combines pending header and + staged body octets with body octets supplied + by the caller, and returns descriptors for + the octets to transfer next. The returned + buffers may reference the serializer, message, + trailer, or supplied buffers; they remain valid + until the matching call to @ref consume. + + Passing `more == false` declares the end + of the body. Afterwards, only octets which + were supplied but not yet consumed may be + supplied again. + + The result might be empty, which indicates + that either the supplied octets were + absorbed for later framing or that nothing + is ready for transfer. `@ref consume` must + still be called before the next call to query + octets of `buffers` that were absorbed. + + @par Preconditions + A message is being serialized. + + @param dest Storage for the returned + descriptors. + + @param buffers The body octets not yet + consumed. + + @param more `true` if further body octets + will be supplied. + + @param ec Set to the error, if any. + + @return The prefix of `dest` containing + the descriptors, which may be empty. + */ + template + std::span + frame( + std::span dest, + CB const& buffers, + bool more, + system::error_code& ec) + { + source_of src(buffers); + return frame_(dest, src, more, ec); + } + + /** Obtain buffers for the next serialized octets. + + Equivalent to calling @ref frame with an empty + buffer sequence. + + @par Preconditions + A message is being serialized. + + @param dest Storage for the returned + descriptors. + + @param more `true` if body octets will be + supplied later. + + @param ec Set to the error, if any. + + @return The prefix of `dest` containing the + descriptors, which may be empty. + */ + std::span + frame( + std::span dest, + bool more, + system::error_code& ec) + { + source src; + return frame_(dest, src, more, ec); + } + + /** Release transferred octets. + + This function informs the serializer that + `n` octets from the front of the buffers + most recently returned by @ref frame were + transferred. Buffers previously returned by + @ref frame are invalidated. + + The return value is the number of octets of + the caller-supplied body consumed by this + call: octets which were transferred, or + which were captured by the serializer, by + copy or by encoding, in the preceding call + to @ref frame. The caller advances its + remaining body input by this amount. + + @par Preconditions + A message is being serialized. `n` does not + exceed the total size of the buffers + returned by the preceding call to @ref frame. + + @param n The number of octets transferred. + + @return The number of supplied body octets + consumed. + */ + BOOST_BURL_DECL + std::size_t + consume(std::size_t n) noexcept; + +private: + static constexpr std::size_t margin = 24; + + struct source + { + std::size_t remain = 0; + + virtual + capy::const_buffer + next() noexcept + { + return {}; + } + }; + + template + struct source_of : source + { + explicit + source_of(CB const& bs) noexcept + : it_(capy::begin(bs)) + , end_(capy::end(bs)) + { + remain = capy::buffer_size(bs); + } + + capy::const_buffer + next() noexcept override + { + while(it_ != end_) + { + capy::const_buffer const b(*it_++); + if(b.size() != 0) + { + remain -= b.size(); + return b; + } + } + return {}; + } + + private: + decltype(capy::begin( + std::declval())) it_; + decltype(capy::end( + std::declval())) end_; + }; + + BOOST_BURL_DECL + std::span + frame_( + std::span dest, + source& src, + bool more, + system::error_code& ec); + + bool + chunked_() const noexcept; + + bool + to_eof_() const noexcept; + + bool + should_coalesce_( + std::size_t avail) const noexcept; + + detail::flat_buffer& + buffered_() noexcept; + + capy::const_buffer + epilogue_buf_() const noexcept; + + capy::const_buffer + trailer_buf_() const noexcept; + + bool + settled_() const noexcept; + + void + open_chunk_(std::uint64_t s) noexcept; + + void + decide_framing_( + std::uint64_t total) noexcept; + + void + encode_( + source& src, + system::error_code& ec); + + bool + ingest_( + source& src, + bool more, + system::error_code& ec); + + std::span + gather_( + std::span dest, + source& src, + bool flush_body, + bool flush_header) noexcept; + + std::unique_ptr buf_; + std::size_t min_prepare_; + std::size_t min_direct_; + std::size_t enc_threshold_; + + detail::flat_buffer stage_; + detail::flat_buffer enc_out_; + + message_head_base* msg_ = nullptr; + encoder* enc_ = nullptr; + fields_base const* trailer_ = nullptr; + + std::uint32_t header_offset_ = 0; + std::uint32_t tail_offset_ = 0; + std::uint64_t owed_ = 0; + std::size_t input_framed_ = 0; + std::size_t input_digested_ = 0; + std::uint8_t prefix_rem_ = 0; + http::payload payload_ = http::payload::none; + bool crlf_owed_ = false; + bool enc_started_ = false; + bool sealed_ = false; + bool done_ = false; +}; + +} // namespace burl +} // namespace boost + +#endif diff --git a/src/client.cpp b/src/client.cpp index 5aed1c9..e6c7890 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -18,15 +18,14 @@ #include "detail/decoders.hpp" #include "detail/drain_body.hpp" #include "detail/redirect.hpp" -#include "detail/serializer.hpp" #include #include -#include -#include #include #include +#include #include +#include #include #include @@ -229,7 +228,7 @@ client::execute_impl( .dec_buffer = config_.response_inplace_buffer, .body_limit = config_.response_body_limit }); - detail::serializer sr({}); + serializer sr({}); auto url = request.url; auto trusted = true; @@ -261,18 +260,18 @@ client::execute_impl( // TODO: expect100timeout - capy::any_write_stream stream(&conn); - sr.reset(&stream, &head); + sr.start(&head); + message_writer writer(&conn, &sr); if(request.body.has_value()) { - http::any_buffer_sink sink(&sr); + http::any_buffer_sink sink(&writer); std::tie(ec) = co_await request.body.write(sink); if(ec) co_return { ec, response{} }; } if(!sr.is_done()) { - std::tie(ec) = co_await sr.write_eof(); + std::tie(ec) = co_await writer.write_eof(); if(ec) co_return { ec, response{} }; } diff --git a/src/detail/decoders.cpp b/src/detail/decoders.cpp index 7b14c2a..0b04587 100644 --- a/src/detail/decoders.cpp +++ b/src/detail/decoders.cpp @@ -64,7 +64,7 @@ class zlib_decoder final process( capy::mutable_buffer out, capy::const_buffer in, - bool eof) override + bool more) override { strm_.next_in = static_cast( const_cast(in.data())); @@ -74,7 +74,7 @@ class zlib_decoder final strm_.avail_out = saturate(out.size()); auto const rs = ::inflate( - &strm_, eof ? Z_FINISH : Z_NO_FLUSH); + &strm_, more ? Z_NO_FLUSH : Z_FINISH); auto const ec = [&]() -> std::error_code { diff --git a/src/detail/flat_buffer.cpp b/src/detail/flat_buffer.cpp new file mode 100644 index 0000000..144d91b --- /dev/null +++ b/src/detail/flat_buffer.cpp @@ -0,0 +1,85 @@ +// +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +#include + +#include + +namespace boost +{ +namespace burl +{ +namespace detail +{ + +bool +flat_buffer:: +empty() const noexcept +{ + return len == 0; +} + +std::size_t +flat_buffer:: +size() const noexcept +{ + return len; +} + +std::size_t +flat_buffer:: +capacity() const noexcept +{ + return cap - pos - len; +} + +capy::const_buffer +flat_buffer:: +data() const noexcept +{ + return { ptr + pos, len }; +} + +capy::mutable_buffer +flat_buffer:: +prepare() const noexcept +{ + return { ptr + pos + len, cap - pos - len }; +} + +void +flat_buffer:: +commit(std::size_t n) noexcept +{ + BOOST_ASSERT(n <= capacity()); + len += n; +} + +void +flat_buffer:: +consume(std::size_t n) noexcept +{ + BOOST_ASSERT(n <= len); + pos += n; + len -= n; + if(len == 0) + pos = 0; +} + +void +flat_buffer:: +clear() noexcept +{ + pos = 0; + len = 0; +} + +} // namespace detail +} // namespace burl +} // namespace boost diff --git a/src/detail/serializer.cpp b/src/detail/serializer.cpp deleted file mode 100644 index 67d3503..0000000 --- a/src/detail/serializer.cpp +++ /dev/null @@ -1,373 +0,0 @@ -// -// Copyright (c) 2026 Mohammad Nejati -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/burl -// - -#include "serializer.hpp" - -#include -#include -#include -#include - -#include -#include - -namespace boost -{ -namespace burl -{ -namespace detail -{ - -serializer:: -serializer(config const& cfg, capy::any_write_stream* stream) - : stream_(stream) - , min_prepare_(cfg.min_prepare) - , direct_thr_(cfg.direct_thr) - , enc_thr_(cfg.enc_thr) - , out_(new unsigned char[ - cfg.out_buffer + cfg.enc_buffer + margin] + margin) - , out_cap_(cfg.enc_buffer) - , in_(out_) - , in_cap_(cfg.out_buffer) -{ -} - -serializer:: -serializer(serializer&& other) noexcept - : stream_(other.stream_) - , msg_(other.msg_) - , enc_(other.enc_) - , min_prepare_(other.min_prepare_) - , direct_thr_(other.direct_thr_) - , enc_thr_(other.enc_thr_) - , out_(other.out_) - , out_cap_(other.out_cap_) - , out_len_(other.out_len_) - , in_(other.in_) - , in_cap_(other.in_cap_) - , in_len_(other.in_len_) - , total_body_(other.total_body_) - , head_(other.head_) - , enc_started_(other.enc_started_) - , hdr_sent_(other.hdr_sent_) - , done_(other.done_) -{ - other.out_ = nullptr; -} - -serializer& -serializer:: -operator=(serializer&& other) noexcept -{ - if(this == &other) - return *this; - this->~serializer(); - return *new(this) serializer(std::move(other)); -} - -serializer:: -~serializer() -{ - if(out_) - delete[](out_ - margin); -} - -void -serializer:: -reset( - capy::any_write_stream* stream, - message_head_base* msg, - encoder* enc, - bool head) noexcept -{ - if(head) - enc = nullptr; - - if(!enc == (in_ != out_)) - { - std::swap(in_cap_, out_cap_); - in_ = enc ? out_ + out_cap_ : out_; - } - - stream_ = stream; - msg_ = msg; - enc_ = enc; - in_len_ = 0; - out_len_ = 0; - total_body_ = 0; - head_ = head; - enc_started_ = false; - hdr_sent_ = false; - done_ = false; -} - -capy::io_task<> -serializer:: -write_eof() -{ - return commit_eof(0); -} - -std::span -serializer:: -prepare(std::span dest) -{ - if(dest.empty() || capacity() == 0) - return dest.first(0); - dest[0] = do_prepare(); - return dest.first(1); -} - -capy::io_task<> -serializer:: -commit(std::size_t n) -{ - BOOST_ASSERT(n <= capacity()); - do_commit(n); - if(capacity() >= min_prepare_) - co_return {}; - auto [ec, _] = co_await process({}, false); - co_return { ec }; -} - -capy::io_task<> -serializer:: -commit_eof(std::size_t n) -{ - BOOST_ASSERT(n <= capacity()); - do_commit(n); - finalize(0); - auto [ec, _] = co_await process({}, true); - co_return { ec }; -} - -std::size_t -serializer:: -capacity() const noexcept -{ - return in_cap_ - in_len_; -} - -capy::mutable_buffer -serializer:: -do_prepare() noexcept -{ - return { in_ + in_len_, in_cap_ - in_len_ }; -} - -void -serializer:: -do_commit(std::size_t n) noexcept -{ - in_len_ += n; -} - -bool -serializer:: -should_coalesce(std::size_t avail) const noexcept -{ - if(avail > capacity()) - return false; - if(enc_) - return !enc_started_ && avail + in_len_ < enc_thr_; - return avail < direct_thr_; -} - -void -serializer:: -finalize(std::size_t remaining) noexcept -{ - if(head_) - return; - - if(enc_) - { - if(enc_started_) - return; - - if(remaining + in_len_ >= enc_thr_) - return; - - msg_->erase(http::field::content_encoding); - enc_ = nullptr; - } - decide_framing(remaining); -} - -void -serializer:: -decide_framing(std::size_t remaining) noexcept -{ - if(!msg_->chunked() || hdr_sent_) - return; - - BOOST_ASSERT(total_body_ == 0); - msg_->erase(http::field::transfer_encoding); - msg_->set_content_length((enc_ ? out_len_ : in_len_) + remaining); -} - -capy::io_task -serializer:: -process( - std::span tail, - bool eof) -{ - if(enc_) - return encode(tail, eof); - return flush(tail, eof); -} - -capy::io_task -serializer:: -encode( - std::span tail, - bool eof) -{ - std::size_t n = 0; - std::size_t consumed = 0; - capy::const_buffer in = { in_, in_len_ }; - for(;;) - { - if(in.size() == 0) - { - if(n != tail.size()) - in = tail[n++]; - else if(!eof) - co_return { std::error_code(), consumed }; - } - - if(out_len_ == out_cap_) - { - if(auto [ec, _] = co_await flush({}, false); ec) - co_return { ec, consumed }; - } - - auto r = enc_->process( - { out_ + out_len_, out_cap_ - out_len_ }, - in, - eof && n == tail.size()); - - enc_started_ = true; - out_len_ += r.produced; - in += r.consumed; - if(in_len_ != 0) - in_len_ -= r.consumed; - else - consumed += r.consumed; - - if(r.ec) - { - if(r.ec == capy::cond::eof) - { - decide_framing(0); - auto [ec, _] = co_await flush({}, true); - co_return { ec, consumed }; - } - else - { - co_return { r.ec, consumed }; - } - } - - if(!eof && consumed != 0) - co_return { std::error_code(), consumed }; - } -} - -capy::io_task -serializer:: -flush( - std::span tail, - bool eof) -{ - auto const buf = enc_ ? out_ : in_; - auto& len = enc_ ? out_len_ : in_len_; - auto const tail_len = capy::buffer_size(tail); - auto const chunked = msg_->chunked() && !head_; - BOOST_ASSERT(eof || len + tail_len != 0); - BOOST_ASSERT(tail.size() <= capy::detail::max_iovec_); - - capy::const_buffer vec[capy::detail::max_iovec_ + 3]; - std::size_t n = 0; - std::size_t sum = 0; - auto const append = [&](capy::const_buffer b) - { - vec[n++] = b; - sum += b.size(); - }; - - if(!hdr_sent_) - append(capy::make_buffer(msg_->buffer())); - - if(chunked) - { - static constexpr char hex[] = "0123456789abcdef"; - auto s = len + tail_len; - auto p = buf; - - *--p = '\n'; - *--p = '\r'; - - do - { - *--p = hex[s & 0xF]; - s >>= 4; - } while(s != 0); - - // prev chunk's CRLF - if(total_body_) - { - *--p = '\n'; - *--p = '\r'; - } - - append({ p, static_cast(buf - p) + len }); - } - else - { - auto const decl = [&]()-> std::uint64_t - { - if(head_) - return 0; - return msg_->content_length().value_or(0); - }(); - auto const prod = total_body_ + len + tail_len; - - if(prod > decl || (eof && prod != decl)) - co_return { error::body_size_mismatch, 0 }; - - if(len != 0) - append({ buf, len }); - } - - auto const owned = sum; - - for(auto& b : tail) - append(b); - - if(chunked && eof) - append({ "\r\n0\r\n\r\n", len || tail_len ? 7u : 2u }); - - auto const need = chunked || eof ? sum : owned + !!tail_len; - auto [ec, written] = co_await capy::write_at_least( - *stream_, std::span{ vec, n }, need); - if(ec) - co_return { ec, 0 }; - - auto const consumed = (std::min)(written - owned, tail_len); - total_body_ += len + consumed; - len = 0; - hdr_sent_ = true; - done_ = eof; - co_return { std::error_code(), consumed }; -} - -} // namespace detail -} // namespace burl -} // namespace boost diff --git a/src/detail/serializer.hpp b/src/detail/serializer.hpp deleted file mode 100644 index 81a6db3..0000000 --- a/src/detail/serializer.hpp +++ /dev/null @@ -1,235 +0,0 @@ -// -// 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_SERIALIZER_HPP -#define BOOST_BURL_SRC_DETAIL_SERIALIZER_HPP - -#include -#include - -#include -#include -#include -#include -#include - -namespace boost -{ -namespace burl -{ -namespace detail -{ - -class serializer -{ -public: - struct encoder - { - struct result - { - std::size_t consumed; - std::size_t produced; - std::error_code ec; - }; - - virtual ~encoder() = default; - - virtual result - process( - capy::mutable_buffer out, - capy::const_buffer in, - bool eof) = 0; - }; - - struct config - { - std::size_t out_buffer = 64 * 1024; - std::size_t min_prepare = 4 * 1024; - std::size_t direct_thr = 2 * 1024; - std::size_t enc_buffer = 8 * 1024; - std::size_t enc_thr = 4 * 1024; - }; - - serializer( - config const& cfg, - capy::any_write_stream* stream = nullptr); - - serializer(serializer&& other) noexcept; - - serializer& - operator=(serializer&& other) noexcept; - - serializer(const serializer&) = delete; - - serializer& - operator=(const serializer&) = delete; - - ~serializer(); - - bool - is_done() const noexcept - { - return done_; - } - - capy::any_write_stream* - stream() const noexcept - { - return stream_; - } - - message_head_base* - message() const noexcept - { - return msg_; - } - - void - reset( - capy::any_write_stream* stream, - message_head_base* msg, - encoder* enc = nullptr, - bool head = false) noexcept; - - void - reset( - message_head_base* msg, - encoder* enc = nullptr, - bool head = false) noexcept - { - BOOST_ASSERT(stream_); - reset(stream_, msg, enc, head); - } - - template - capy::io_task - write_some(Buffers buffers); - - template - capy::io_task - write(Buffers buffers); - - template - capy::io_task - write_eof(Buffers buffers); - - capy::io_task<> - write_eof(); - - std::span - prepare(std::span dest); - - capy::io_task<> - commit(std::size_t n); - - capy::io_task<> - commit_eof(std::size_t n); - -private: - std::size_t - capacity() const noexcept; - - capy::mutable_buffer - do_prepare() noexcept; - - void - do_commit(std::size_t n) noexcept; - - bool - should_coalesce(std::size_t avail) const noexcept; - - void - finalize(std::size_t remaining) noexcept; - - void - decide_framing(std::size_t remaining) noexcept; - - capy::io_task - process( - std::span tail, - bool eof); - - capy::io_task - encode( - std::span tail, - bool eof); - - capy::io_task - flush( - std::span tail, - bool eof); - - static constexpr std::size_t margin = 24; - - capy::any_write_stream* stream_; - message_head_base* msg_ = nullptr; - encoder* enc_ = nullptr; - std::size_t min_prepare_; - std::size_t direct_thr_; - std::size_t enc_thr_; - unsigned char* out_; - std::size_t out_cap_; - std::size_t out_len_ = 0; - unsigned char* in_; - std::size_t in_cap_; - std::size_t in_len_ = 0; - std::uint64_t total_body_ = 0; - bool head_ = false; - bool enc_started_ = false; - bool hdr_sent_ = false; - bool done_ = false; -}; - -template -capy::io_task -serializer:: -write_some(Buffers buffers) -{ - auto const avail = capy::buffer_size(buffers); - if(should_coalesce(avail)) - { - do_commit(capy::buffer_copy(do_prepare(), buffers)); - co_return { std::error_code(), avail }; - } - co_return co_await process( - capy::buffer_param(buffers).data(), false); -} - -template -capy::io_task -serializer:: -write(Buffers buffers) -{ - return capy::write(*this, std::move(buffers)); -} - -template -capy::io_task -serializer:: -write_eof(Buffers buffers) -{ - finalize(capy::buffer_size(buffers)); - capy::buffer_param bp(buffers); - std::size_t sum = 0; - for(;;) - { - bool eof = !bp.more(); - auto [ec, n] = co_await process(bp.data(), eof); - sum += n; - bp.consume(n); - if(ec || eof) - co_return { ec, sum }; - } -} - -} // namespace detail -} // namespace burl -} // namespace boost - -#endif diff --git a/src/error.cpp b/src/error.cpp index ccad005..ae39109 100644 --- a/src/error.cpp +++ b/src/error.cpp @@ -51,7 +51,7 @@ error_category::message(int ev) const case error::proxy_unsupported_version: return "unsupported proxy protocol version"; case error::body_size_mismatch: - return "request body size did not match content length"; + return "message body size did not match content length"; case error::decode_error: return "response body could not be decoded"; default: diff --git a/src/parser.cpp b/src/parser.cpp index 6d65dc5..c289a55 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -795,7 +795,7 @@ decode_some( return { std::error_code(), cons }; auto const lim = clamp(limit_rem_); auto const r = dec_->process( - prefix(out, lim), in, last); + prefix(out, lim), in, !last); in += r.consumed; cons += r.consumed; prod += r.produced; diff --git a/src/serializer.cpp b/src/serializer.cpp new file mode 100644 index 0000000..24c8014 --- /dev/null +++ b/src/serializer.cpp @@ -0,0 +1,605 @@ +// +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +#include +#include + +#include "detail/util.hpp" + +#include +#include +#include +#include + +#include +#include + +namespace boost +{ +namespace burl +{ + +// assert relying facts +static_assert( + fields_base::max_buffer_size <= + (std::numeric_limits::max)()); + +using http::payload; +using detail::clamp; + +namespace +{ + +capy::const_buffer +suffix(capy::const_buffer buf, std::size_t n) noexcept +{ + return { + static_cast(buf.data()) + n, + buf.size() - n + }; +} + +} // namespace + +serializer:: +serializer(config const& cfg) + : buf_(new char[margin + cfg.stage_buffer + cfg.enc_buffer]) + , min_prepare_(cfg.min_prepare) + , min_direct_(cfg.min_direct) + , enc_threshold_(cfg.enc_threshold) + , stage_{ buf_.get() + margin, cfg.stage_buffer } + , enc_out_{ buf_.get() + margin, cfg.enc_buffer } +{ +} + +void +serializer:: +start( + message_head_base* msg, + encoder* enc, + bool head) noexcept +{ + BOOST_ASSERT(msg != nullptr); + + if(head) + enc = nullptr; + + msg_ = msg; + enc_ = enc; + trailer_ = nullptr; + + if((enc != nullptr) != (stage_.ptr != enc_out_.ptr)) + { + std::swap(stage_.cap, enc_out_.cap); + stage_.ptr = enc_out_.ptr + (enc ? enc_out_.cap : 0); + } + + stage_.clear(); + enc_out_.clear(); + + header_offset_ = 0; + tail_offset_ = 0; + input_framed_ = 0; + input_digested_ = 0; + prefix_rem_ = 0; + payload_ = head ? payload::none : msg->payload(); + crlf_owed_ = false; + enc_started_ = false; + sealed_ = false; + done_ = false; + owed_ = [&]() -> std::uint64_t + { + switch(payload_) + { + case payload::none: + case payload::chunked: + return 0; + case payload::to_eof: + return std::uint64_t(-1); + default: + return msg->content_length().value_or(0); + } + }(); +} + +std::span +serializer:: +prepare(std::span dest) +{ + BOOST_ASSERT(msg_ != nullptr); + BOOST_ASSERT(!sealed_); + if(dest.empty() || stage_.capacity() == 0) + return dest.first(0); + dest[0] = stage_.prepare(); + return dest.first(1); +} + +void +serializer:: +commit(std::size_t n) noexcept +{ + BOOST_ASSERT(msg_ != nullptr); + BOOST_ASSERT(!sealed_); + stage_.commit(n); +} + +bool +serializer:: +chunked_() const noexcept +{ + return payload_ == payload::chunked; +} + +bool +serializer:: +to_eof_() const noexcept +{ + return payload_ == payload::to_eof; +} + +bool +serializer:: +should_coalesce_(std::size_t avail) const noexcept +{ + if(avail > stage_.capacity()) + return false; + if(enc_) + return !enc_started_ && + avail + stage_.len < enc_threshold_; + return avail < min_direct_; +} + +detail::flat_buffer& +serializer:: +buffered_() noexcept +{ + return enc_out_.empty() ? stage_ : enc_out_; +} + +capy::const_buffer +serializer:: +epilogue_buf_() const noexcept +{ + static constexpr char s[] = + "\r\n" "0\r\n" "\r\n"; + std::size_t const skip = crlf_owed_ ? 0 : 2; + std::size_t const drop = trailer_ ? 2 : 0; + return { s + skip, sizeof(s) - 1 - skip - drop }; +} + +capy::const_buffer +serializer:: +trailer_buf_() const noexcept +{ + if(trailer_) + return capy::make_buffer(trailer_->buffer()); + return {}; +} + +bool +serializer:: +settled_() const noexcept +{ + if(msg_->buffer().size() != header_offset_) + return false; + + if(input_framed_ != 0) + return false; + + if(!stage_.empty() || !enc_out_.empty()) + return false; + + if( chunked_() && + tail_offset_ != + epilogue_buf_().size() + + trailer_buf_().size()) + return false; + + return owed_ == 0; +} + +void +serializer:: +open_chunk_(std::uint64_t s) noexcept +{ + BOOST_ASSERT(prefix_rem_ == 0); + BOOST_ASSERT(owed_ == 0); + BOOST_ASSERT(s != 0); + + auto const p0 = buffered_().ptr + buffered_().pos; + auto p = p0; + + *--p = '\n'; + *--p = '\r'; + + static constexpr char hex[] = "0123456789abcdef"; + auto v = s; + do + { + *--p = hex[v & 0xF]; + v >>= 4; + } while(v != 0); + + // prev chunk's CRLF + if(crlf_owed_) + { + *--p = '\n'; + *--p = '\r'; + } + crlf_owed_ = true; + + prefix_rem_ = static_cast(p0 - p); + owed_ = s; +} + +void +serializer:: +decide_framing_(std::uint64_t total) noexcept +{ + if(header_offset_ != 0 || trailer_) + return; + if(payload_ == payload::chunked || payload_ == payload::to_eof) + { + msg_->erase(http::field::transfer_encoding); + msg_->set_content_length(total); + payload_ = payload::size; + owed_ = total; + } +} + +void +serializer:: +encode_(source& src, system::error_code& ec) +{ + auto feed = [&]( + capy::const_buffer in, bool more) + { + auto const r = enc_->process( + enc_out_.prepare(), in, more); + enc_started_ = true; + enc_out_.commit(r.produced); + if(r.ec == capy::cond::eof) + { + decide_framing_(enc_out_.size()); + if(to_eof_()) + owed_ = enc_out_.size(); + enc_ = nullptr; + } + else if(r.ec) + { + ec = r.ec; + } + return r.consumed; + }; + + // staged input drains first + while(!stage_.empty()) + { + if(enc_out_.capacity() == 0) + return; + auto const more = !sealed_ || src.remain != 0; + stage_.consume(feed(stage_.data(), more)); + if(ec.failed() || !enc_) + return; + } + + // the supplied buffers + for(auto cur = src.next(); cur.size() != 0;) + { + if(enc_out_.capacity() == 0) + return; + auto const n = feed( + cur, !sealed_ || src.remain != 0); + cur += n; + input_digested_ += n; + if(ec.failed() || !enc_) + return; + if(cur.size() == 0) + cur = src.next(); + } + + // finish the stream + while(sealed_) + { + if(enc_out_.capacity() == 0) + return; + feed({}, false); + if(ec.failed() || !enc_) + return; + } +} + +bool +serializer:: +ingest_( + source& src, + bool more, + system::error_code& ec) +{ + auto const sealing = !more && !sealed_; + + if(chunked_()) + { + if(sealed_ && !enc_ && src.remain != 0) + { + ec = make_error_code( + std::errc::invalid_argument); + return false; + } + } + else if(!enc_) + { + auto const have = std::uint64_t(src.remain) + + stage_.size() + enc_out_.size(); + if( have > owed_ || + (!to_eof_() && sealing && have < owed_)) + { + if(to_eof_()) + ec = make_error_code( + std::errc::invalid_argument); + else + ec = error::body_size_mismatch; + return false; + } + } + + if(sealing) + sealed_ = true; + + if(sealed_) + { + auto const total = stage_.size() + src.remain; + if( enc_ && !enc_started_ && header_offset_ == 0 && + total < enc_threshold_) + { + msg_->erase(http::field::content_encoding); + enc_ = nullptr; + } + if(!enc_) + { + decide_framing_(total); + if(to_eof_() && sealing) + owed_ = total; + } + } + + // small inputs are coalesced into the stage + if( !sealed_ && src.remain != 0 && + should_coalesce_(src.remain)) + { + input_digested_ = src.remain; + for(auto b = src.next(); b.size() != 0; + b = src.next()) + stage_.commit(capy::buffer_copy( + stage_.prepare(), b)); + if(enc_) + return false; + } + else if(enc_) + { + encode_(src, ec); + if(ec) + { + enc_ = nullptr; + done_ = true; + return false; + } + + auto const finished = (enc_ == nullptr); + auto const encoded = enc_out_.size(); + if(!chunked_() && !to_eof_()) + { + if(encoded > owed_ || (finished && encoded != owed_)) + { + done_ = true; + ec = error::body_size_mismatch; + return false; + } + } + auto const flush = finished || enc_out_.capacity() == 0; + if(flush && encoded != 0 && chunked_()) + open_chunk_(encoded); + return flush; + } + + auto const flush = src.remain != 0 || sealed_ || + should_drain() || stage_.pos != 0; + if(flush && chunked_()) + { + auto const s = buffered_().size() + src.remain; + if(s != 0) + open_chunk_(s); + } + return flush; +} + +std::span +serializer:: +gather_( + std::span dest, + source& src, + bool flush_body, + bool flush_header) noexcept +{ + std::size_t n = 0; + + // returns false when dest is exhausted + auto const push = [&](capy::const_buffer b) + { + if(b.size() == 0) + return true; + if(n == dest.size()) + return false; + dest[n++] = b; + return true; + }; + + // [header] + if(flush_header) + push(suffix(capy::make_buffer( + msg_->buffer()), header_offset_)); + + // [CRLF + prefix][buffered] + auto const& buffered = buffered_(); + auto buffered_take = std::uint64_t(buffered.size()); + if(chunked_()) + buffered_take = (std::min)(buffered_take, owed_); + else if(!flush_body) + buffered_take = 0; + push({ buffered.ptr + buffered.pos - prefix_rem_, + prefix_rem_ + std::size_t(buffered_take) }); + + // [supplied body] + auto const quota = owed_ - buffered_take; + auto const supplied = src.remain; + std::size_t placed = 0; + if(!enc_) + { + while(placed != quota) + { + auto const b = src.next(); + if(b.size() == 0) + break; + auto const k = clamp(quota - placed, b.size()); + if(!push({ b.data(), k })) + break; + placed += k; + } + } + input_framed_ = placed; + + // [epilogue][trailer] + if( sealed_ && !enc_ && chunked_() && + buffered.size() + supplied == owed_ && + placed == supplied) + { + auto const eb = epilogue_buf_(); + auto const tb = trailer_buf_(); + auto const k = clamp(tail_offset_, eb.size()); + push(suffix(eb, k)); + push(suffix(tb, tail_offset_ - k)); + } + + return { dest.data(), n }; +} + +std::span +serializer:: +frame_( + std::span dest, + source& src, + bool more, + system::error_code& ec) +{ + BOOST_ASSERT(msg_ != nullptr); + + ec = {}; + + if(done_ && !settled_()) + { + ec = make_error_code( + std::errc::state_not_recoverable); + return {}; + } + + BOOST_ASSERT(input_digested_ == 0); + + auto const drain_call = (src.remain == 0); + + bool flush_body = true; + if(owed_ == 0 || !chunked_()) + { + flush_body = ingest_(src, more, ec); + if(ec) + return {}; + } + + auto const out = gather_( + dest, src, flush_body, flush_body || drain_call); + + if(out.empty() && !dest.empty() && !done_) + { + if(!more && owed_ != 0) + { + if(to_eof_() || chunked_()) + ec = make_error_code( + std::errc::invalid_argument); + else + ec = error::body_size_mismatch; + } + else if(sealed_) + { + done_ = settled_(); + } + } + + return out; +} + +std::size_t +serializer:: +consume(std::size_t n) noexcept +{ + // [header] + { + auto const k = clamp( + n, msg_->buffer().size() - header_offset_); + header_offset_ += static_cast(k); + n -= k; + } + + // [CRLF + prefix] + { + auto const k = clamp(n, prefix_rem_); + prefix_rem_ -= static_cast(k); + n -= k; + } + + // [buffered] + { + auto& buffered = buffered_(); + auto const rem = clamp(owed_, buffered.size()); + auto const k = clamp(n, rem); + buffered.consume(k); + owed_ -= k; + n -= k; + } + + // [supplied body] + std::size_t input_taken; + { + auto const k = clamp(n, input_framed_); + input_framed_ -= k; + owed_ -= k; + input_taken = k; + n -= k; + } + + // [epilogue][trailer] + if(sealed_ && chunked_()) + { + auto const total = epilogue_buf_().size() + + trailer_buf_().size(); + auto const k = clamp(n, total - tail_offset_); + tail_offset_ += static_cast(k); + n -= k; + } + + BOOST_ASSERT(n == 0); + + if(sealed_ && !done_) + done_ = settled_(); + + auto const r = input_taken + input_digested_; + input_digested_ = 0; + return r; +} + +} // namespace burl +} // namespace boost diff --git a/test/unit/detail/decoders.cpp b/test/unit/detail/decoders.cpp index 4ee6a29..7845145 100644 --- a/test/unit/detail/decoders.cpp +++ b/test/unit/detail/decoders.cpp @@ -64,11 +64,11 @@ class decoders_test { auto const n = input.size() < in_step ? input.size() : in_step; - auto const eof = n == input.size(); + auto const more = n != input.size(); auto const res = dec.process( capy::mutable_buffer(out.data(), out.size()), capy::const_buffer(input.data(), n), - eof); + more); r.body.append(out.data(), res.produced); input.remove_prefix(res.consumed); if(res.ec) @@ -80,7 +80,7 @@ class decoders_test } // no forward progress at the end of input // would repeat forever - if(eof && res.consumed == 0 && res.produced == 0) + if(!more && res.consumed == 0 && res.produced == 0) break; } r.leftover = input.size(); @@ -301,7 +301,7 @@ class decoders_test auto const res = dec->process( capy::mutable_buffer(buf, sizeof(buf)), capy::const_buffer("HTTP/1.1 200 OK", 15), - true); + false); BOOST_TEST(res.ec == capy::cond::eof); BOOST_TEST_EQ(res.consumed, 0u); BOOST_TEST_EQ(res.produced, 0u); @@ -393,7 +393,7 @@ class decoders_test auto const res = dec->process( capy::mutable_buffer(buf, sizeof(buf)), capy::const_buffer("HTTP/1.1 200 OK", 15), - true); + false); BOOST_TEST(res.ec == capy::cond::eof); BOOST_TEST_EQ(res.consumed, 0u); BOOST_TEST_EQ(res.produced, 0u); diff --git a/test/unit/detail/flat_buffer.cpp b/test/unit/detail/flat_buffer.cpp new file mode 100644 index 0000000..68228ce --- /dev/null +++ b/test/unit/detail/flat_buffer.cpp @@ -0,0 +1,188 @@ +// +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +// Test that header file is self-contained. +#include + +#include +#include + +#include +#include +#include + +#include "test_suite.hpp" + +namespace boost +{ +namespace burl +{ +namespace detail +{ + +class flat_buffer_test +{ + static + std::string + str(capy::const_buffer b) + { + return { static_cast(b.data()), b.size() }; + } + + // Writes through prepare/commit, like a stream read would. + static + void + put(flat_buffer& fb, std::string_view s) + { + auto const n = capy::buffer_copy( + fb.prepare(), capy::make_buffer(s.data(), s.size())); + BOOST_TEST_EQ(n, s.size()); + fb.commit(n); + } + +public: + void + testEmpty() + { + char store[8]; + flat_buffer fb{ store, sizeof(store) }; + + BOOST_TEST(fb.empty()); + BOOST_TEST_EQ(fb.size(), 0); + BOOST_TEST_EQ(fb.capacity(), sizeof(store)); + BOOST_TEST(str(fb.data()).empty()); + + // an empty run still reports its base, which is what a + // caller writing below the read cursor needs + BOOST_TEST_EQ(fb.data().data(), store); + BOOST_TEST_EQ(fb.prepare().data(), store); + BOOST_TEST_EQ(fb.prepare().size(), sizeof(store)); + } + + void + testFillAndDrain() + { + char store[8]; + flat_buffer fb{ store, sizeof(store) }; + + put(fb, "abcd"); + BOOST_TEST(!fb.empty()); + BOOST_TEST_EQ(fb.size(), 4); + BOOST_TEST_EQ(fb.capacity(), 4); + BOOST_TEST_EQ(str(fb.data()), "abcd"); + + fb.consume(1); + BOOST_TEST_EQ(str(fb.data()), "bcd"); + BOOST_TEST_EQ(fb.pos, 1); + + fb.consume(3); + BOOST_TEST(fb.empty()); + BOOST_TEST_EQ(fb.pos, 0); // rewound + BOOST_TEST_EQ(fb.capacity(), sizeof(store)); + } + + void + testNoCompaction() + { + // Consumed octets are not reclaimed until the rewind: + // capacity shrinks while the run is alive, so the space + // below the read cursor stays where the caller left it. + char store[8]; + flat_buffer fb{ store, sizeof(store) }; + + put(fb, "abcdef"); + fb.consume(4); + BOOST_TEST_EQ(fb.size(), 2); + BOOST_TEST_EQ(fb.capacity(), 2); // not 6 + BOOST_TEST_EQ(fb.data().data(), store + 4); + BOOST_TEST_EQ(fb.prepare().data(), store + 6); + + // the four consumed octets are still addressable below + // the cursor, and still hold what was written + BOOST_TEST_EQ( + std::string(store, 4), "abcd"); + + put(fb, "gh"); + BOOST_TEST_EQ(str(fb.data()), "efgh"); + BOOST_TEST_EQ(fb.capacity(), 0); + + fb.consume(4); + BOOST_TEST_EQ(fb.capacity(), sizeof(store)); + } + + void + testPartialCommit() + { + char store[4]; + flat_buffer fb{ store, sizeof(store) }; + + auto const n = capy::buffer_copy( + fb.prepare(), capy::make_buffer("abcdef", 6)); + BOOST_TEST_EQ(n, 4); // capped by capacity + fb.commit(n); + BOOST_TEST_EQ(str(fb.data()), "abcd"); + BOOST_TEST_EQ(fb.capacity(), 0); + BOOST_TEST_EQ(fb.prepare().size(), 0); + } + + void + testClear() + { + char store[8]; + flat_buffer fb{ store, sizeof(store) }; + + put(fb, "abcd"); + fb.consume(2); + + // the region survives, the contents do not + fb.clear(); + BOOST_TEST(fb.empty()); + BOOST_TEST_EQ(fb.pos, 0); + BOOST_TEST_EQ(fb.capacity(), sizeof(store)); + BOOST_TEST_EQ(fb.data().data(), store); + } + + void + testSwap() + { + // Two regions exchanging roles keep their own storage, + // which is how a caller can tell them apart afterwards. + char a[8]; + char b[4]; + flat_buffer x{ a, sizeof(a) }; + flat_buffer y{ b, sizeof(b) }; + + put(x, "abcd"); + std::swap(x, y); + + BOOST_TEST_EQ(x.ptr, b); + BOOST_TEST_EQ(x.cap, sizeof(b)); + BOOST_TEST(x.empty()); + BOOST_TEST_EQ(str(y.data()), "abcd"); + } + + void + run() + { + testEmpty(); + testFillAndDrain(); + testNoCompaction(); + testPartialCommit(); + testClear(); + testSwap(); + } +}; + +TEST_SUITE( + flat_buffer_test, + "boost.burl.detail.flat_buffer"); + +} // namespace detail +} // namespace burl +} // namespace boost diff --git a/test/unit/detail/serializer.cpp b/test/unit/detail/serializer.cpp deleted file mode 100644 index e6db226..0000000 --- a/test/unit/detail/serializer.cpp +++ /dev/null @@ -1,2254 +0,0 @@ -// -// Copyright (c) 2026 Mohammad Nejati -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/burl -// - -// Test that header file is self-contained. -#include "src/detail/serializer.hpp" - -#include -#include - -#include -#include -#include -#include -#include - -#include - -#include "test_suite.hpp" - -namespace boost -{ -namespace burl -{ -namespace detail -{ - -class serializer_test -{ - // A scaled-down config so that framing and encoder - // thresholds (staged vs. direct writes, commit-triggered - // flushes, encoder drop) are crossed with tiny bodies, and - // so tests keep exercising the same paths if the default - // config values change. - static constexpr serializer::config cfg{ - .out_buffer = 64, - .min_prepare = 32, - .direct_thr = 16, - .enc_buffer = 32, - .enc_thr = 8 }; - - static request_head - make_request() - { - request_head req; - req.set_chunked(true); - return req; - } - - static request_head - make_request(std::size_t cl) - { - request_head req; - req.set_content_length(cl); - return req; - } - - // An encoder that increments every body byte by one, so - // encoded output is distinguishable from identity output, - // and appends an optional trailer once the input ends. It - // consumes and produces as much as the given buffers allow. - struct test_encoder : serializer::encoder - { - std::string trailer; - std::error_code fail; - std::size_t calls = 0; - bool finished = false; - - explicit test_encoder(std::string trl = {}) - : trailer(std::move(trl)) - { - } - - result - process( - capy::mutable_buffer out, - capy::const_buffer in, - bool eof) override - { - ++calls; - if(fail) - return { 0, 0, fail }; - - auto* dst = static_cast(out.data()); - auto* src = static_cast(in.data()); - auto const n = (std::min)(out.size(), in.size()); - for(std::size_t i = 0; i != n; ++i) - dst[i] = static_cast(src[i] + 1); - - result r{ n, n, {} }; - if(eof && n == in.size()) - { - auto const t = (std::min)( - out.size() - n, trailer.size() - trailer_pos_); - std::memcpy( - dst + n, trailer.data() + trailer_pos_, t); - trailer_pos_ += t; - r.produced += t; - if(trailer_pos_ == trailer.size()) - { - finished = true; - r.ec = capy::error::eof; - } - } - return r; - } - - private: - std::size_t trailer_pos_ = 0; - }; - - // Returns a body with a varied byte pattern so reordered - // or duplicated regions cannot go unnoticed. - static std::string - make_body(std::size_t n) - { - std::string s(n, '\0'); - for(std::size_t i = 0; i != n; ++i) - s[i] = static_cast('0' + i % 64); - return s; - } - - // Returns the mock-encoded form of `s`. - static std::string - encoded(std::string_view s) - { - std::string r(s); - for(auto& c : r) - c = static_cast(c + 1); - return r; - } - -public: - void - testContentLengthSmallBody() - { - auto req = make_request(5); - auto [client, server] = capy::test::make_stream_pair(); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req); - BOOST_TEST(!sr.is_done()); - - auto [ec1, n] = co_await sr.write( - capy::const_buffer("hello", 5)); - BOOST_TEST(!ec1); - BOOST_TEST_EQ(n, 5u); - BOOST_TEST(!sr.is_done()); - - auto [ec2] = co_await sr.write_eof(); - BOOST_TEST(!ec2); - BOOST_TEST(sr.is_done()); - }()); - - BOOST_TEST_EQ( - server.data(), - std::string(req.buffer()) + "hello"); - } - - void - testContentLengthLargeBody() - { - // A body at the direct-write threshold bypasses staging. - std::string const body(cfg.direct_thr, 'x'); - - auto req = make_request(body.size()); - auto [client, server] = capy::test::make_stream_pair(); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req); - - auto [ec1, n] = co_await sr.write( - capy::make_buffer(body)); - BOOST_TEST(!ec1); - BOOST_TEST_EQ(n, body.size()); - - auto [ec2] = co_await sr.write_eof(); - BOOST_TEST(!ec2); - }()); - - BOOST_TEST_EQ( - server.data(), - std::string(req.buffer()) + body); - } - - void - testContentLengthMultipleBuffers() - { - std::string const b1(4, 'a'); - std::string const b2(4, 'b'); - - auto req = make_request(b1.size() + b2.size()); - auto [client, server] = capy::test::make_stream_pair(); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req); - - std::array bufs{ - capy::make_buffer(b1), - capy::make_buffer(b2) }; - - auto [ec, n] = co_await sr.write_eof(bufs); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, b1.size() + b2.size()); - BOOST_TEST(sr.is_done()); - }()); - - BOOST_TEST_EQ( - server.data(), - std::string(req.buffer()) + b1 + b2); - } - - void - testBodySizeMismatch() - { - // fewer bytes than Content-Length - { - auto req = make_request(10); - auto [client, server] = capy::test::make_stream_pair(); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req); - - auto [ec1, n] = co_await sr.write( - capy::const_buffer("hello", 5)); - BOOST_TEST(!ec1); - BOOST_TEST_EQ(n, 5u); - - auto [ec2] = co_await sr.write_eof(); - BOOST_TEST(ec2 == error::body_size_mismatch); - BOOST_TEST(!sr.is_done()); - }()); - - // Nothing reaches the wire when the mismatch is - // detected before the header is flushed. - BOOST_TEST(server.data().empty()); - } - - // more bytes than Content-Length - { - auto req = make_request(3); - auto [client, server] = capy::test::make_stream_pair(); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req); - - auto [ec, n] = co_await sr.write_eof( - capy::const_buffer("hello", 5)); - BOOST_TEST(ec == error::body_size_mismatch); - BOOST_TEST(!sr.is_done()); - }()); - - BOOST_TEST(server.data().empty()); - } - } - - void - testChunkedSmallBodyConvertsToContentLength() - { - auto req = make_request(); - auto [client, server] = capy::test::make_stream_pair(); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req); - - // The body stays below direct_thr, so it is fully - // buffered before the header goes out. - auto [ec1, n] = co_await sr.write( - capy::const_buffer("hello world", 11)); - BOOST_TEST(!ec1); - BOOST_TEST_EQ(n, 11u); - - auto [ec2] = co_await sr.write_eof(); - BOOST_TEST(!ec2); - }()); - - // The entire body was buffered before the header was - // flushed, so chunked encoding is replaced with - // Content-Length and the body is sent unframed. - BOOST_TEST(!req.chunked()); - BOOST_TEST_EQ(req.content_length().value(), 11u); - BOOST_TEST( - server.data().find("Transfer-Encoding") == - std::string_view::npos); - BOOST_TEST_EQ( - server.data(), - std::string(req.buffer()) + "hello world"); - } - - void - testChunkedWriteEofWithBody() - { - auto req = make_request(); - auto [client, server] = capy::test::make_stream_pair(); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req); - - auto [ec, n] = co_await sr.write_eof( - capy::const_buffer("hello", 5)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 5u); - }()); - - BOOST_TEST(!req.chunked()); - BOOST_TEST_EQ(req.content_length().value(), 5u); - BOOST_TEST_EQ( - server.data(), - std::string(req.buffer()) + "hello"); - } - - void - testChunkedLargeBody() - { - std::string const body(cfg.direct_thr, 'x'); - - auto req = make_request(); - auto [client, server] = capy::test::make_stream_pair(); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req); - - auto [ec1, n1] = co_await sr.write( - capy::make_buffer(body)); - BOOST_TEST(!ec1); - BOOST_TEST_EQ(n1, body.size()); - - auto [ec2, n2] = co_await sr.write( - capy::const_buffer("hello", 5)); - BOOST_TEST(!ec2); - BOOST_TEST_EQ(n2, 5u); - BOOST_TEST(!sr.is_done()); - - auto [ec3] = co_await sr.write_eof(); - BOOST_TEST(!ec3); - BOOST_TEST(sr.is_done()); - }()); - - // The large write is gathered with the header and sent - // as its own chunk; the small write is buffered and - // flushed at eof as a chunk. Chunk sizes are always - // minimal-width; 16 == 0x10. - std::string expected(req.buffer()); - expected += "10\r\n" + body + "\r\n"; - expected += "5\r\nhello\r\n"; - expected += "0\r\n\r\n"; - BOOST_TEST(req.chunked()); - BOOST_TEST_EQ(server.data(), expected); - } - - void - testChunkedWriteEofWithTail() - { - // Once the header is on the wire, write_eof() with - // caller buffers must frame the staged bytes and the - // caller's bytes as the final chunk and terminate the - // body with the last-chunk in the same gather. - std::string const body(cfg.direct_thr, 'x'); - - auto req = make_request(); - auto [client, server] = capy::test::make_stream_pair(); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req); - - auto [ec1, n1] = co_await sr.write( - capy::make_buffer(body)); - BOOST_TEST(!ec1); - BOOST_TEST_EQ(n1, body.size()); - - auto [ec2, n2] = co_await sr.write( - capy::const_buffer("abc", 3)); - BOOST_TEST(!ec2); - BOOST_TEST_EQ(n2, 3u); - - auto [ec3, n3] = co_await sr.write_eof( - capy::const_buffer("hello", 5)); - BOOST_TEST(!ec3); - BOOST_TEST_EQ(n3, 5u); - }()); - - std::string expected(req.buffer()); - expected += "10\r\n" + body + "\r\n"; - expected += "8\r\nabchello\r\n"; - expected += "0\r\n\r\n"; - BOOST_TEST(req.chunked()); - BOOST_TEST_EQ(server.data(), expected); - } - - void - testChunkedWriteEofTailOnly() - { - // write_eof() with caller buffers while no bytes are - // staged must still terminate the body with the - // last-chunk after the final chunk. - std::string const body(cfg.direct_thr, 'x'); - - auto req = make_request(); - auto [client, server] = capy::test::make_stream_pair(); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req); - - auto [ec1, n1] = co_await sr.write( - capy::make_buffer(body)); - BOOST_TEST(!ec1); - BOOST_TEST_EQ(n1, body.size()); - - auto [ec2, n2] = co_await sr.write_eof( - capy::const_buffer("hello", 5)); - BOOST_TEST(!ec2); - BOOST_TEST_EQ(n2, 5u); - }()); - - std::string expected(req.buffer()); - expected += "10\r\n" + body + "\r\n"; - expected += "5\r\nhello\r\n"; - expected += "0\r\n\r\n"; - BOOST_TEST(req.chunked()); - BOOST_TEST_EQ(server.data(), expected); - } - - void - testChunkedEmptyBody() - { - auto req = make_request(); - auto [client, server] = capy::test::make_stream_pair(); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr({}, &ws); - sr.reset(&req); - BOOST_TEST(!sr.is_done()); - - auto [ec] = co_await sr.write_eof(); - BOOST_TEST(!ec); - BOOST_TEST(sr.is_done()); - }()); - - BOOST_TEST(!req.chunked()); - BOOST_TEST( - server.data().find("Content-Length: 0\r\n") != - std::string_view::npos); - BOOST_TEST_EQ(server.data(), req.buffer()); - } - - void - testPrepareCommitContentLength() - { - auto req = make_request(5); - auto [client, server] = capy::test::make_stream_pair(); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req); - - capy::mutable_buffer tmp[2]; - auto n = capy::buffer_copy( - sr.prepare(tmp), - capy::const_buffer("hello", 5)); - BOOST_TEST_EQ(n, 5u); - - auto [ec] = co_await sr.commit_eof(n); - BOOST_TEST(!ec); - BOOST_TEST(sr.is_done()); - }()); - - BOOST_TEST_EQ( - server.data(), - std::string(req.buffer()) + "hello"); - } - - void - testPrepareCommitPartialFlush() - { - // Committing enough data to drop the remaining capacity - // below min_prepare triggers a partial flush. - std::string const body(60, 'z'); - - auto req = make_request(body.size()); - auto [client, server] = capy::test::make_stream_pair(); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req); - - capy::mutable_buffer tmp[2]; - auto n = capy::buffer_copy( - sr.prepare(tmp), capy::make_buffer(body)); - BOOST_TEST_EQ(n, body.size()); - - auto [ec1] = co_await sr.commit(n); - BOOST_TEST(!ec1); - BOOST_TEST(!sr.is_done()); - - auto [ec2] = co_await sr.write_eof(); - BOOST_TEST(!ec2); - BOOST_TEST(sr.is_done()); - }()); - - BOOST_TEST_EQ( - server.data(), - std::string(req.buffer()) + body); - } - - void - testPrepareCommitChunked() - { - // A drain while chunked emits the buffered bytes as a - // single chunk with a minimal-width size; 60 == 0x3C. - std::string const body(60, 'z'); - - auto req = make_request(); - auto [client, server] = capy::test::make_stream_pair(); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req); - - capy::mutable_buffer tmp[2]; - auto n = capy::buffer_copy( - sr.prepare(tmp), capy::make_buffer(body)); - BOOST_TEST_EQ(n, body.size()); - - auto [ec1] = co_await sr.commit(n); - BOOST_TEST(!ec1); - - auto [ec2] = co_await sr.write_eof(); - BOOST_TEST(!ec2); - }()); - - std::string expected(req.buffer()); - expected += "3c\r\n" + body + "\r\n"; - expected += "0\r\n\r\n"; - BOOST_TEST(req.chunked()); - BOOST_TEST_EQ(server.data(), expected); - } - - void - testPrepareCommitNoFlush() - { - // Committing a small amount keeps the remaining - // capacity above min_prepare, so commit() completes - // without flushing anything to the stream. - auto req = make_request(5); - auto [client, server] = capy::test::make_stream_pair(); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req); - - capy::mutable_buffer tmp[2]; - auto n = capy::buffer_copy( - sr.prepare(tmp), - capy::const_buffer("hello", 5)); - BOOST_TEST_EQ(n, 5u); - - auto [ec1] = co_await sr.commit(n); - BOOST_TEST(!ec1); - BOOST_TEST(server.data().empty()); - - auto [ec2] = co_await sr.write_eof(); - BOOST_TEST(!ec2); - }()); - - BOOST_TEST_EQ( - server.data(), - std::string(req.buffer()) + "hello"); - } - - void - testPrepareEmptyDest() - { - // prepare() with no destination buffers reports no - // writable space. - auto req = make_request(5); - auto [client, server] = capy::test::make_stream_pair(); - - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req); - - BOOST_TEST(sr.prepare({}).empty()); - - // The staging buffer is untouched and remains fully - // available. - capy::mutable_buffer tmp[2]; - auto dest = sr.prepare(tmp); - BOOST_TEST_EQ(dest.size(), 1u); - BOOST_TEST_EQ(capy::buffer_size(dest), cfg.out_buffer); - } - - void - testPrepareFullBuffer() - { - // A commit that fills the staging buffer triggers a - // drain. When the drain fails, the buffered bytes are - // kept; prepare() must report no writable space rather - // than hand out a zero-sized buffer. - capy::test::fuse f; - auto r = f.armed([&](capy::test::fuse&) -> capy::task<> - { - std::string const body(cfg.out_buffer, 'z'); - - auto req = make_request(body.size()); - capy::test::write_stream ws_impl(f); - - capy::any_write_stream ws(&ws_impl); - serializer sr(cfg, &ws); - sr.reset(&req); - - capy::mutable_buffer tmp[2]; - auto n = capy::buffer_copy( - sr.prepare(tmp), capy::make_buffer(body)); - BOOST_TEST_EQ(n, body.size()); - - if(auto [ec] = co_await sr.commit(n); ec) - { - BOOST_TEST(sr.prepare(tmp).empty()); - co_return; - } - - // The successful drain empties the staging buffer, - // so the full capacity is writable again. - auto dest = sr.prepare(tmp); - BOOST_TEST_EQ(dest.size(), 1u); - BOOST_TEST_EQ( - capy::buffer_size(dest), cfg.out_buffer); - }); - BOOST_TEST(r.success); - } - - void - testChunkedShortWrites() - { - // The staging buffer holds raw body bytes only, so the - // full capacity is stageable as one chunk; 64 == 0x40. - std::string const body(cfg.out_buffer, 'z'); - - auto req = make_request(); - capy::test::write_stream ws_impl({}, 1); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&ws_impl); - serializer sr(cfg, &ws); - sr.reset(&req); - - capy::mutable_buffer tmp[2]; - auto n = capy::buffer_copy( - sr.prepare(tmp), capy::make_buffer(body)); - BOOST_TEST_EQ(n, body.size()); - - auto [ec1] = co_await sr.commit(n); - BOOST_TEST(!ec1); - - // The drain writes the header and the staged chunk - // to completion, even over a stream that transfers - // one byte per write; the chunk's closing CRLF is - // deferred to the next write. - BOOST_TEST_EQ(ws_impl.data(), - std::string(req.buffer()) + - "40\r\n" + body); - - auto [ec2, m] = co_await sr.write( - capy::const_buffer("hello", 5)); - BOOST_TEST(!ec2); - BOOST_TEST_EQ(m, 5u); - - auto [ec3] = co_await sr.write_eof(); - BOOST_TEST(!ec3); - }()); - - std::string expected(req.buffer()); - expected += "40\r\n" + body + "\r\n"; - expected += "5\r\nhello\r\n"; - expected += "0\r\n\r\n"; - BOOST_TEST(req.chunked()); - BOOST_TEST_EQ(ws_impl.data(), expected); - } - - void - testContentLengthShortWrites() - { - // A sized body streamed through the direct path must - // arrive intact over a stream that transfers one byte - // per write: bytes already on the wire may not be sent - // again, and every call must consume at least one byte - // of the caller's buffer. - std::string const b1(cfg.direct_thr, 'x'); - std::string const b2(cfg.direct_thr, 'y'); - - auto req = make_request(b1.size() + b2.size()); - capy::test::write_stream ws_impl({}, 1); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&ws_impl); - serializer sr(cfg, &ws); - sr.reset(&req); - - // The first drain writes the header in full and - // exactly one body byte. - auto [ec1, n1] = co_await sr.write_some( - capy::make_buffer(b1)); - BOOST_TEST(!ec1); - BOOST_TEST_EQ(n1, 1u); - BOOST_TEST_EQ( - ws_impl.data(), - std::string(req.buffer()) + "x"); - - // The rest of the first buffer is below direct_thr - // and is staged without touching the wire. - auto [ec2, n2] = co_await sr.write( - capy::const_buffer(b1.data() + 1, b1.size() - 1)); - BOOST_TEST(!ec2); - BOOST_TEST_EQ(n2, b1.size() - 1); - BOOST_TEST_EQ( - ws_impl.data().size(), - req.buffer().size() + 1); - - // Even when a full staged prefix must be flushed - // first, at least one byte of the caller's buffer - // is consumed. - auto [ec3, n3] = co_await sr.write_some( - capy::make_buffer(b2)); - BOOST_TEST(!ec3); - BOOST_TEST_EQ(n3, 1u); - BOOST_TEST_EQ( - ws_impl.data(), - std::string(req.buffer()) + b1 + "y"); - - auto [ec4, n4] = co_await sr.write( - capy::const_buffer(b2.data() + 1, b2.size() - 1)); - BOOST_TEST(!ec4); - BOOST_TEST_EQ(n4, b2.size() - 1); - - auto [ec5] = co_await sr.write_eof(); - BOOST_TEST(!ec5); - }()); - - BOOST_TEST_EQ( - ws_impl.data(), - std::string(req.buffer()) + b1 + b2); - } - - void - testPrepareCommitShortWrites() - { - // A commit-triggered drain has no caller buffers; the - // header and staged bytes are written to completion - // over a stream that transfers one byte per write. - std::string const body(60, 'z'); - - auto req = make_request(body.size()); - capy::test::write_stream ws_impl({}, 1); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&ws_impl); - serializer sr(cfg, &ws); - sr.reset(&req); - - capy::mutable_buffer tmp[2]; - auto n = capy::buffer_copy( - sr.prepare(tmp), capy::make_buffer(body)); - BOOST_TEST_EQ(n, body.size()); - - auto [ec1] = co_await sr.commit(n); - BOOST_TEST(!ec1); - BOOST_TEST_EQ( - ws_impl.data(), - std::string(req.buffer()) + body); - - auto [ec2] = co_await sr.write_eof(); - BOOST_TEST(!ec2); - }()); - - BOOST_TEST_EQ( - ws_impl.data(), - std::string(req.buffer()) + body); - } - - void - testShortWriteEofDoesNotTruncate() - { - // Everything due at eof is written to completion, even - // over a stream that transfers one byte per write; the - // body must not be silently dropped after the header. - auto req = make_request(); - capy::test::write_stream ws_impl({}, 1); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&ws_impl); - serializer sr(cfg, &ws); - sr.reset(&req); - - auto [ec1, n] = co_await sr.write( - capy::const_buffer("hello", 5)); - BOOST_TEST(!ec1); - BOOST_TEST_EQ(n, 5u); - - auto [ec2] = co_await sr.write_eof(); - BOOST_TEST(!ec2); - }()); - - // The body was fully buffered before the header went - // out, so the request converts to Content-Length. - BOOST_TEST(!req.chunked()); - BOOST_TEST_EQ( - ws_impl.data(), - std::string(req.buffer()) + "hello"); - } - - void - testChunkedPreservesWriteOrder() - { - // A large write arriving while a small write is still - // staged must not overtake it: both leave in a single - // gather as one chunk with the staged bytes first, even - // when the stream accepts one byte at a time; - // 3 + 16 == 0x13. - std::string const body(cfg.direct_thr, 'x'); - - auto req = make_request(); - capy::test::write_stream ws_impl({}, 1); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&ws_impl); - serializer sr(cfg, &ws); - sr.reset(&req); - - auto [ec1, n1] = co_await sr.write( - capy::const_buffer("abc", 3)); - BOOST_TEST(!ec1); - BOOST_TEST_EQ(n1, 3u); - - auto [ec2, n2] = co_await sr.write( - capy::make_buffer(body)); - BOOST_TEST(!ec2); - BOOST_TEST_EQ(n2, body.size()); - - auto [ec3] = co_await sr.write_eof(); - BOOST_TEST(!ec3); - }()); - - std::string expected(req.buffer()); - expected += "13\r\nabc" + body + "\r\n"; - expected += "0\r\n\r\n"; - BOOST_TEST(req.chunked()); - BOOST_TEST_EQ(ws_impl.data(), expected); - } - - void - testErrorInjection() - { - capy::test::fuse f; - auto r = f.armed([&](capy::test::fuse&) -> capy::task<> - { - std::string const body(cfg.direct_thr, 'x'); - - auto req = make_request(); - - auto [client, server] = - capy::test::make_stream_pair(f); - - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req); - - if(auto [ec, n] = co_await sr.write( - capy::make_buffer(body)); ec) - { - BOOST_TEST(!sr.is_done()); - co_return; - } - - if(auto [ec, n] = co_await sr.write( - capy::const_buffer("hello", 5)); ec) - { - BOOST_TEST(!sr.is_done()); - co_return; - } - - if(auto [ec] = co_await sr.write_eof(); ec) - { - BOOST_TEST(!sr.is_done()); - co_return; - } - - BOOST_TEST(sr.is_done()); - - std::string expected(req.buffer()); - expected += "10\r\n" + body + "\r\n"; - expected += "5\r\nhello\r\n"; - expected += "0\r\n\r\n"; - BOOST_TEST_EQ(server.data(), expected); - }); - BOOST_TEST(r.success); - } - - void - testEncoderSmallBodyDropsEncoder() - { - // A body that stays below the encoder threshold skips - // encoding entirely: the encoder is dropped at eof, the - // Content-Encoding header is removed, and the identity - // body is sent with a Content-Length. - auto req = make_request(); - req.set(http::field::content_encoding, "test"); - auto [client, server] = capy::test::make_stream_pair(); - test_encoder enc; - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req, &enc); - - // below the threshold: coalesced into the encoder's - // staging buffer without invoking the encoder - auto [ec1, n] = co_await sr.write( - capy::const_buffer("hello", 5)); - BOOST_TEST(!ec1); - BOOST_TEST_EQ(n, 5u); - BOOST_TEST_EQ(enc.calls, 0u); - BOOST_TEST(server.data().empty()); - - auto [ec2] = co_await sr.write_eof(); - BOOST_TEST(!ec2); - BOOST_TEST(sr.is_done()); - }()); - - BOOST_TEST_EQ(enc.calls, 0u); - BOOST_TEST(!req.chunked()); - BOOST_TEST_EQ(req.content_length().value(), 5u); - BOOST_TEST( - server.data().find("Content-Encoding") == - std::string_view::npos); - BOOST_TEST_EQ( - server.data(), - std::string(req.buffer()) + "hello"); - } - - void - testEncoderEmptyBody() - { - auto req = make_request(); - req.set(http::field::content_encoding, "test"); - auto [client, server] = capy::test::make_stream_pair(); - test_encoder enc; - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req, &enc); - - auto [ec] = co_await sr.write_eof(); - BOOST_TEST(!ec); - BOOST_TEST(sr.is_done()); - }()); - - // an empty body is below any threshold: the encoder is - // dropped and the request converts to Content-Length: 0 - BOOST_TEST_EQ(enc.calls, 0u); - BOOST_TEST(!req.chunked()); - BOOST_TEST( - server.data().find("Content-Length: 0\r\n") != - std::string_view::npos); - BOOST_TEST( - server.data().find("Content-Encoding") == - std::string_view::npos); - BOOST_TEST_EQ(server.data(), req.buffer()); - } - - void - testEncoderPrepareCommitEof() - { - // With an encoder, prepare() exposes the encoder's - // staging buffer rather than the output buffer. - auto req = make_request(); - req.set(http::field::content_encoding, "test"); - auto [client, server] = capy::test::make_stream_pair(); - test_encoder enc; - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req, &enc); - - capy::mutable_buffer tmp[2]; - auto dest = sr.prepare(tmp); - BOOST_TEST_EQ( - capy::buffer_size(dest), cfg.enc_buffer); - - auto n = capy::buffer_copy( - dest, capy::const_buffer("hello", 5)); - BOOST_TEST_EQ(n, 5u); - - // below the threshold at eof: the encoder is - // dropped and the staged bytes are sent as-is - auto [ec] = co_await sr.commit_eof(n); - BOOST_TEST(!ec); - BOOST_TEST(sr.is_done()); - }()); - - BOOST_TEST_EQ(enc.calls, 0u); - BOOST_TEST(!req.chunked()); - BOOST_TEST_EQ(req.content_length().value(), 5u); - BOOST_TEST_EQ( - server.data(), - std::string(req.buffer()) + "hello"); - } - - void - testEncoderThreshold() - { - // exactly at the threshold: the encoder is kept - { - std::string const body = make_body(8); - - auto req = make_request(); - req.set(http::field::content_encoding, "test"); - auto [client, server] = capy::test::make_stream_pair(); - test_encoder enc; - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req, &enc); - - auto [ec, n] = co_await sr.write_eof( - capy::make_buffer(body)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, body.size()); - BOOST_TEST(sr.is_done()); - }()); - - // the whole encoded output was buffered before the - // header went out, so chunked is replaced with the - // encoded length while Content-Encoding is kept - BOOST_TEST(enc.finished); - BOOST_TEST(!req.chunked()); - BOOST_TEST_EQ(req.content_length().value(), 8u); - BOOST_TEST( - server.data().find("Content-Encoding: test\r\n") != - std::string_view::npos); - BOOST_TEST_EQ( - server.data(), - std::string(req.buffer()) + encoded(body)); - } - - // one byte below the threshold: the encoder is dropped - { - std::string const body = make_body(7); - - auto req = make_request(); - req.set(http::field::content_encoding, "test"); - auto [client, server] = capy::test::make_stream_pair(); - test_encoder enc; - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req, &enc); - - auto [ec, n] = co_await sr.write_eof( - capy::make_buffer(body)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, body.size()); - }()); - - BOOST_TEST_EQ(enc.calls, 0u); - BOOST_TEST_EQ(req.content_length().value(), 7u); - BOOST_TEST( - server.data().find("Content-Encoding") == - std::string_view::npos); - BOOST_TEST_EQ( - server.data(), - std::string(req.buffer()) + body); - } - } - - void - testEncoderStagedAndTail() - { - // Staged bytes and the caller's tail together reach the - // threshold at eof, so the encoder is kept; it consumes - // the staged bytes first, then the tail, and the count - // returned to the caller covers only the tail. - auto req = make_request(); - req.set(http::field::content_encoding, "test"); - auto [client, server] = capy::test::make_stream_pair(); - test_encoder enc; - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req, &enc); - - auto [ec1, n1] = co_await sr.write( - capy::const_buffer("abc", 3)); - BOOST_TEST(!ec1); - BOOST_TEST_EQ(n1, 3u); - BOOST_TEST_EQ(enc.calls, 0u); - - auto [ec2, n2] = co_await sr.write_eof( - capy::const_buffer("defgh", 5)); - BOOST_TEST(!ec2); - BOOST_TEST_EQ(n2, 5u); - BOOST_TEST(sr.is_done()); - }()); - - BOOST_TEST(enc.finished); - BOOST_TEST_EQ(req.content_length().value(), 8u); - BOOST_TEST_EQ( - server.data(), - std::string(req.buffer()) + encoded("abcdefgh")); - } - - void - testEncoderLargeBodyChunked() - { - // A body larger than the output buffer forces the - // header out early and streams encoded chunks; once the - // encoder has started, even small writes are encoded - // rather than coalesced. Chunk payloads hold encoded - // bytes; 64 == 0x40. - std::string const body = make_body(200); - - auto req = make_request(); - req.set(http::field::content_encoding, "test"); - auto [client, server] = capy::test::make_stream_pair(); - test_encoder enc; - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req, &enc); - - auto [ec1, n] = co_await sr.write( - capy::make_buffer(body)); - BOOST_TEST(!ec1); - BOOST_TEST_EQ(n, body.size()); - BOOST_TEST(!sr.is_done()); - - auto [ec2] = co_await sr.write_eof(); - BOOST_TEST(!ec2); - BOOST_TEST(sr.is_done()); - }()); - - auto const enc_body = encoded(body); - std::string expected(req.buffer()); - expected += "40\r\n" + enc_body.substr(0, 64); - expected += "\r\n40\r\n" + enc_body.substr(64, 64); - expected += "\r\n40\r\n" + enc_body.substr(128, 64); - expected += "\r\n8\r\n" + enc_body.substr(192, 8); - expected += "\r\n0\r\n\r\n"; - BOOST_TEST(enc.finished); - BOOST_TEST(req.chunked()); - BOOST_TEST( - server.data().find("Content-Encoding: test\r\n") != - std::string_view::npos); - BOOST_TEST_EQ(server.data(), expected); - } - - void - testEncoderCommit() - { - // commit() stages input for the encoder; a drain is - // deferred until the remaining capacity drops below - // min_prepare, and encoded bytes stay buffered until - // the output buffer needs room. - std::string const body = make_body(54); - - auto req = make_request(); - req.set(http::field::content_encoding, "test"); - auto [client, server] = capy::test::make_stream_pair(); - test_encoder enc; - - // a staging buffer large enough to hold all commits - auto ecfg = cfg; - ecfg.enc_buffer = 64; - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(ecfg, &ws); - sr.reset(&req, &enc); - - capy::mutable_buffer tmp[2]; - - // capacity stays >= min_prepare: no drain - auto n1 = capy::buffer_copy( - sr.prepare(tmp), - capy::const_buffer(body.data(), 20)); - BOOST_TEST_EQ(n1, 20u); - auto [ec1] = co_await sr.commit(n1); - BOOST_TEST(!ec1); - BOOST_TEST_EQ(enc.calls, 0u); - - // capacity drops below min_prepare: the staged - // bytes are encoded, but the encoded output still - // fits and nothing reaches the wire - auto n2 = capy::buffer_copy( - sr.prepare(tmp), - capy::const_buffer(body.data() + 20, 24)); - BOOST_TEST_EQ(n2, 24u); - auto [ec2] = co_await sr.commit(n2); - BOOST_TEST(!ec2); - BOOST_TEST_EQ(enc.calls, 1u); - BOOST_TEST(server.data().empty()); - - // staged bytes present at commit_eof() are encoded - // before the encoder is finished - auto n3 = capy::buffer_copy( - sr.prepare(tmp), - capy::const_buffer(body.data() + 44, 10)); - BOOST_TEST_EQ(n3, 10u); - auto [ec3] = co_await sr.commit_eof(n3); - BOOST_TEST(!ec3); - BOOST_TEST(sr.is_done()); - }()); - - // everything fit before the header went out: chunked - // is replaced with the encoded length - BOOST_TEST_EQ(enc.calls, 2u); - BOOST_TEST(enc.finished); - BOOST_TEST(!req.chunked()); - BOOST_TEST_EQ(req.content_length().value(), 54u); - BOOST_TEST_EQ( - server.data(), - std::string(req.buffer()) + encoded(body)); - } - - void - testEncoderCommitTriggersFlush() - { - // A commit-triggered drain that overflows the output - // buffer sends the header and a full chunk mid-commit; - // the remainder stays buffered for the final flush; - // 64 == 0x40, 32 == 0x20. - std::string const body = make_body(96); - - auto req = make_request(); - req.set(http::field::content_encoding, "test"); - auto [client, server] = capy::test::make_stream_pair(); - test_encoder enc; - - // a staging buffer larger than the output buffer, so a - // single commit overflows it - auto ecfg = cfg; - ecfg.enc_buffer = 96; - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(ecfg, &ws); - sr.reset(&req, &enc); - - capy::mutable_buffer tmp[2]; - auto n = capy::buffer_copy( - sr.prepare(tmp), capy::make_buffer(body)); - BOOST_TEST_EQ(n, body.size()); - - auto [ec1] = co_await sr.commit(n); - BOOST_TEST(!ec1); - BOOST_TEST_EQ( - server.data(), - std::string(req.buffer()) + - "40\r\n" + encoded(body).substr(0, 64)); - - auto [ec2] = co_await sr.write_eof(); - BOOST_TEST(!ec2); - }()); - - std::string expected(req.buffer()); - expected += "40\r\n" + encoded(body).substr(0, 64); - expected += "\r\n20\r\n" + encoded(body).substr(64, 32); - expected += "\r\n0\r\n\r\n"; - BOOST_TEST(req.chunked()); - BOOST_TEST_EQ(server.data(), expected); - } - - void - testEncoderTrailerSpansFlush() - { - // The encoder finishes over multiple eof calls when its - // trailer does not fit in the remaining output space; a - // flush runs between the calls; 64 == 0x40. - std::string const body = make_body(60); - - auto req = make_request(); - req.set(http::field::content_encoding, "test"); - auto [client, server] = capy::test::make_stream_pair(); - test_encoder enc("0123456789"); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req, &enc); - - auto [ec, n] = co_await sr.write_eof( - capy::make_buffer(body)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, body.size()); - BOOST_TEST(sr.is_done()); - }()); - - std::string expected(req.buffer()); - expected += "40\r\n" + encoded(body) + "0123"; - expected += "\r\n6\r\n456789"; - expected += "\r\n0\r\n\r\n"; - BOOST_TEST_EQ(enc.calls, 2u); - BOOST_TEST(enc.finished); - BOOST_TEST(req.chunked()); - BOOST_TEST_EQ(server.data(), expected); - } - - void - testEncoderContentLength() - { - // A sized request works with an encoder when the - // declared length matches the encoded output (body - // plus trailer). - std::string const body = make_body(30); - - auto req = make_request(34); - req.set(http::field::content_encoding, "test"); - auto [client, server] = capy::test::make_stream_pair(); - test_encoder enc("eof!"); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req, &enc); - - auto [ec, n] = co_await sr.write_eof( - capy::make_buffer(body)); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, body.size()); - BOOST_TEST(sr.is_done()); - }()); - - BOOST_TEST(enc.finished); - BOOST_TEST_EQ( - server.data(), - std::string(req.buffer()) + - encoded(body) + "eof!"); - } - - void - testEncoderContentLengthMismatch() - { - // encoded output exceeds the declared length - { - std::string const body = make_body(30); - - auto req = make_request(30); - req.set(http::field::content_encoding, "test"); - auto [client, server] = capy::test::make_stream_pair(); - test_encoder enc("eof!"); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req, &enc); - - auto [ec, n] = co_await sr.write_eof( - capy::make_buffer(body)); - BOOST_TEST(ec == error::body_size_mismatch); - BOOST_TEST(!sr.is_done()); - }()); - - BOOST_TEST(server.data().empty()); - } - - // encoded output falls short of the declared length - { - std::string const body = make_body(30); - - auto req = make_request(40); - req.set(http::field::content_encoding, "test"); - auto [client, server] = capy::test::make_stream_pair(); - test_encoder enc("eof!"); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req, &enc); - - auto [ec, n] = co_await sr.write_eof( - capy::make_buffer(body)); - BOOST_TEST(ec == error::body_size_mismatch); - BOOST_TEST(!sr.is_done()); - }()); - - BOOST_TEST(server.data().empty()); - } - } - - void - testEncoderFailure() - { - // the encoder fails before anything is flushed - { - std::string const body = make_body(20); - - auto req = make_request(); - req.set(http::field::content_encoding, "test"); - auto [client, server] = capy::test::make_stream_pair(); - test_encoder enc; - enc.fail = make_error_code(std::errc::io_error); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req, &enc); - - auto [ec, n] = co_await sr.write_eof( - capy::make_buffer(body)); - BOOST_TEST(ec == std::errc::io_error); - BOOST_TEST_EQ(n, 0u); - BOOST_TEST(!sr.is_done()); - }()); - - BOOST_TEST_EQ(enc.calls, 1u); - BOOST_TEST(server.data().empty()); - } - - // the encoder fails mid-stream, after encoded output - // has already been buffered - { - std::string const body = make_body(64); - - auto req = make_request(); - req.set(http::field::content_encoding, "test"); - auto [client, server] = capy::test::make_stream_pair(); - test_encoder enc; - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req, &enc); - - auto [ec1, n1] = co_await sr.write( - capy::make_buffer(body)); - BOOST_TEST(!ec1); - BOOST_TEST_EQ(n1, body.size()); - BOOST_TEST(server.data().empty()); - - enc.fail = make_error_code(std::errc::io_error); - auto [ec2, n2] = co_await sr.write( - capy::const_buffer("more", 4)); - BOOST_TEST(ec2 == std::errc::io_error); - BOOST_TEST_EQ(n2, 0u); - BOOST_TEST(!sr.is_done()); - }()); - - // the bytes encoded before the failure were flushed - // while making room for more output; 64 == 0x40 - BOOST_TEST_EQ( - server.data(), - std::string(req.buffer()) + - "40\r\n" + encoded(body)); - } - } - - void - testEncoderMultipleBuffers() - { - // the encoder walks every caller buffer in order, - // including empty ones - std::string const b1 = make_body(6); - std::string const b2 = make_body(6); - - auto req = make_request(); - req.set(http::field::content_encoding, "test"); - auto [client, server] = capy::test::make_stream_pair(); - test_encoder enc; - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req, &enc); - - std::array bufs{ - capy::make_buffer(b1), - capy::const_buffer{}, - capy::make_buffer(b2) }; - - auto [ec, n] = co_await sr.write_eof(bufs); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, b1.size() + b2.size()); - BOOST_TEST(sr.is_done()); - }()); - - BOOST_TEST(enc.finished); - BOOST_TEST_EQ(req.content_length().value(), 12u); - BOOST_TEST_EQ( - server.data(), - std::string(req.buffer()) + - encoded(b1) + encoded(b2)); - } - - void - testEncoderWriteEofManyBuffers() - { - // More caller buffers than fit in one gather window: - // write_eof() feeds the encoder window by window, with - // eof deferred until the last window. - std::string const body = make_body(24); - - auto req = make_request(); - req.set(http::field::content_encoding, "test"); - auto [client, server] = capy::test::make_stream_pair(); - test_encoder enc; - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req, &enc); - - std::array< - capy::const_buffer, 24> bufs; - for(std::size_t i = 0; i != bufs.size(); ++i) - bufs[i] = { body.data() + i, 1 }; - - auto [ec, n] = co_await sr.write_eof(bufs); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, body.size()); - BOOST_TEST(sr.is_done()); - }()); - - BOOST_TEST(enc.finished); - BOOST_TEST(!req.chunked()); - BOOST_TEST_EQ(req.content_length().value(), 24u); - BOOST_TEST_EQ( - server.data(), - std::string(req.buffer()) + encoded(body)); - } - - void - testEncoderWriteSome() - { - // A write_some() that would cross the encoder threshold - // is not coalesced: the staged bytes are encoded first, - // then the caller's bytes, and the returned count covers - // only the caller's bytes. - auto req = make_request(); - req.set(http::field::content_encoding, "test"); - auto [client, server] = capy::test::make_stream_pair(); - test_encoder enc; - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req, &enc); - - // stays below the threshold: staged, encoder not - // started - auto [ec1, n1] = co_await sr.write_some( - capy::const_buffer("abcd", 4)); - BOOST_TEST(!ec1); - BOOST_TEST_EQ(n1, 4u); - BOOST_TEST_EQ(enc.calls, 0u); - - // reaches the threshold: everything goes through - // the encoder, and the encoded bytes stay buffered - auto [ec2, n2] = co_await sr.write_some( - capy::const_buffer("efgh", 4)); - BOOST_TEST(!ec2); - BOOST_TEST_EQ(n2, 4u); - BOOST_TEST_EQ(enc.calls, 2u); - BOOST_TEST(server.data().empty()); - - auto [ec3] = co_await sr.write_eof(); - BOOST_TEST(!ec3); - }()); - - BOOST_TEST(enc.finished); - BOOST_TEST_EQ(req.content_length().value(), 8u); - BOOST_TEST_EQ( - server.data(), - std::string(req.buffer()) + encoded("abcdefgh")); - } - - void - testEncoderMove() - { - // moving mid-stream transfers staged input and encoder - // state - { - auto req = make_request(); - req.set(http::field::content_encoding, "test"); - auto [client, server] = capy::test::make_stream_pair(); - test_encoder enc; - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr1(cfg); - sr1.reset(&ws, &req, &enc); - - auto [ec1, n1] = co_await sr1.write( - capy::const_buffer("abc", 3)); - BOOST_TEST(!ec1); - BOOST_TEST_EQ(n1, 3u); - - serializer sr2(std::move(sr1)); - auto [ec2, n2] = co_await sr2.write_eof( - capy::const_buffer("defgh", 5)); - BOOST_TEST(!ec2); - BOOST_TEST_EQ(n2, 5u); - BOOST_TEST(sr2.is_done()); - - // move-assignment into the moved-from shell - sr1 = std::move(sr2); - BOOST_TEST(sr1.is_done()); - }()); - - BOOST_TEST_EQ( - server.data(), - std::string(req.buffer()) + - encoded("abcdefgh")); - } - - // moving a serializer whose encoder was dropped must - // transfer ownership of the shifted staging buffer; - // deallocating at the wrong offset corrupts the heap - { - auto req = make_request(); - req.set(http::field::content_encoding, "test"); - auto [client, server] = capy::test::make_stream_pair(); - test_encoder enc; - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr1(cfg); - sr1.reset(&ws, &req, &enc); - - auto [ec1, n1] = co_await sr1.write( - capy::const_buffer("hello", 5)); - BOOST_TEST(!ec1); - - // dropping the encoder shifts the internal - // buffer - auto [ec2] = co_await sr1.write_eof(); - BOOST_TEST(!ec2); - - serializer sr2(std::move(sr1)); - BOOST_TEST(sr2.is_done()); - - sr1 = std::move(sr2); - BOOST_TEST(sr1.is_done()); - - // self-move is a no-op - auto* self = &sr1; - sr1 = std::move(*self); - BOOST_TEST(sr1.is_done()); - }()); - - BOOST_TEST_EQ( - server.data(), - std::string(req.buffer()) + "hello"); - } - } - - void - testReset() - { - // reset() rebinds the serializer to a new stream, - // message, and encoder for reuse. The first request - // drops its encoder, which swaps the internal buffers; - // reset() must restore the original layout so the - // second request encodes with full capacity. - auto req1 = make_request(); - req1.set(http::field::content_encoding, "test"); - auto req2 = make_request(); - req2.set(http::field::content_encoding, "test"); - std::string const body = make_body(200); - - auto [client1, server1] = capy::test::make_stream_pair(); - auto [client2, server2] = capy::test::make_stream_pair(); - test_encoder enc1; - test_encoder enc2; - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws1(&client1); - capy::any_write_stream ws2(&client2); - - serializer sr(cfg, &ws1); - sr.reset(&req1, &enc1); - BOOST_TEST(sr.stream() == &ws1); - BOOST_TEST(sr.message() == &req1); - - // the small body drops the encoder - auto [ec1, n1] = co_await sr.write_eof( - capy::const_buffer("hello", 5)); - BOOST_TEST(!ec1); - BOOST_TEST_EQ(n1, 5u); - BOOST_TEST(sr.is_done()); - - sr.reset(&ws2, &req2, &enc2); - BOOST_TEST(sr.stream() == &ws2); - BOOST_TEST(sr.message() == &req2); - BOOST_TEST(!sr.is_done()); - - auto [ec2, n2] = co_await sr.write( - capy::make_buffer(body)); - BOOST_TEST(!ec2); - BOOST_TEST_EQ(n2, body.size()); - - auto [ec3] = co_await sr.write_eof(); - BOOST_TEST(!ec3); - BOOST_TEST(sr.is_done()); - }()); - - BOOST_TEST_EQ(enc1.calls, 0u); - BOOST_TEST( - server1.data().find("Content-Encoding") == - std::string_view::npos); - BOOST_TEST_EQ( - server1.data(), - std::string(req1.buffer()) + "hello"); - - // the second request streams encoded chunks sized by - // the restored output buffer; 64 == 0x40 - auto const enc_body = encoded(body); - std::string expected(req2.buffer()); - expected += "40\r\n" + enc_body.substr(0, 64); - expected += "\r\n40\r\n" + enc_body.substr(64, 64); - expected += "\r\n40\r\n" + enc_body.substr(128, 64); - expected += "\r\n8\r\n" + enc_body.substr(192, 8); - expected += "\r\n0\r\n\r\n"; - BOOST_TEST(enc2.finished); - BOOST_TEST(req2.chunked()); - BOOST_TEST_EQ(server2.data(), expected); - } - - void - testResetAfterEncoderKept() - { - // reset() must also restore the layout when the - // previous message kept its encoder: a following plain - // message must send its own staged body, not the stale - // contents of the encoder's output buffer, and - // re-adding an encoder afterwards must not shift the - // staging buffer out of the allocation. - auto req1 = make_request(); - req1.set(http::field::content_encoding, "test"); - auto req2 = make_request(5); - auto req3 = make_request(); - req3.set(http::field::content_encoding, "test"); - std::string const body1 = make_body(20); - std::string const body3 = make_body(200); - - auto [client1, server1] = capy::test::make_stream_pair(); - auto [client2, server2] = capy::test::make_stream_pair(); - auto [client3, server3] = capy::test::make_stream_pair(); - test_encoder enc1; - test_encoder enc3; - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws1(&client1); - capy::any_write_stream ws2(&client2); - capy::any_write_stream ws3(&client3); - - serializer sr(cfg, &ws1); - sr.reset(&req1, &enc1); - - // the body crosses the threshold: the encoder is - // kept - auto [ec1, n1] = co_await sr.write_eof( - capy::make_buffer(body1)); - BOOST_TEST(!ec1); - BOOST_TEST_EQ(n1, body1.size()); - BOOST_TEST(sr.is_done()); - - sr.reset(&ws2, &req2); - auto [ec2, n2] = co_await sr.write( - capy::const_buffer("hello", 5)); - BOOST_TEST(!ec2); - auto [ec3] = co_await sr.write_eof(); - BOOST_TEST(!ec3); - BOOST_TEST(sr.is_done()); - - sr.reset(&ws3, &req3, &enc3); - auto [ec4, n4] = co_await sr.write( - capy::make_buffer(body3)); - BOOST_TEST(!ec4); - BOOST_TEST_EQ(n4, body3.size()); - auto [ec5] = co_await sr.write_eof(); - BOOST_TEST(!ec5); - BOOST_TEST(sr.is_done()); - }()); - - BOOST_TEST_EQ( - server1.data(), - std::string(req1.buffer()) + encoded(body1)); - - BOOST_TEST_EQ( - server2.data(), - std::string(req2.buffer()) + "hello"); - - // the third request streams encoded chunks sized by the - // restored output buffer; 64 == 0x40 - auto const enc_body = encoded(body3); - std::string expected(req3.buffer()); - expected += "40\r\n" + enc_body.substr(0, 64); - expected += "\r\n40\r\n" + enc_body.substr(64, 64); - expected += "\r\n40\r\n" + enc_body.substr(128, 64); - expected += "\r\n8\r\n" + enc_body.substr(192, 8); - expected += "\r\n0\r\n\r\n"; - BOOST_TEST(req3.chunked()); - BOOST_TEST_EQ(server3.data(), expected); - } - - void - testHeadContentLength() - { - // A head message sends only the header; the declared - // Content-Length describes the body a non-head message - // would have carried, and stays untouched. - auto req = make_request(5); - auto req2 = make_request(5); - - auto [client1, server1] = capy::test::make_stream_pair(); - auto [client2, server2] = capy::test::make_stream_pair(); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws1(&client1); - capy::any_write_stream ws2(&client2); - - serializer sr(cfg, &ws1); - sr.reset(&req, nullptr, true); - BOOST_TEST(!sr.is_done()); - - auto [ec1] = co_await sr.write_eof(); - BOOST_TEST(!ec1); - BOOST_TEST(sr.is_done()); - - // the head flag does not stick across reset() - sr.reset(&ws2, &req2); - auto [ec2, n] = co_await sr.write( - capy::const_buffer("hello", 5)); - BOOST_TEST(!ec2); - auto [ec3] = co_await sr.write_eof(); - BOOST_TEST(!ec3); - }()); - - BOOST_TEST_EQ(req.content_length().value(), 5u); - BOOST_TEST_EQ(server1.data(), std::string(req.buffer())); - BOOST_TEST_EQ( - server2.data(), - std::string(req2.buffer()) + "hello"); - } - - void - testHeadChunked() - { - // A chunked head message keeps Transfer-Encoding in the - // header to describe the framing a non-head message - // would have used; no framing bytes reach the wire. - auto req = make_request(); - auto [client, server] = capy::test::make_stream_pair(); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req, nullptr, true); - - auto [ec] = co_await sr.write_eof(); - BOOST_TEST(!ec); - BOOST_TEST(sr.is_done()); - }()); - - BOOST_TEST(req.chunked()); - BOOST_TEST( - server.data().find("Content-Length") == - std::string_view::npos); - BOOST_TEST_EQ(server.data(), std::string(req.buffer())); - } - - void - testHeadBodyMismatch() - { - // buffered body bytes are rejected at eof, even when - // they match the declared Content-Length - { - auto req = make_request(5); - auto [client, server] = capy::test::make_stream_pair(); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req, nullptr, true); - - auto [ec1, n] = co_await sr.write( - capy::const_buffer("hello", 5)); - BOOST_TEST(!ec1); - BOOST_TEST_EQ(n, 5u); - - auto [ec2] = co_await sr.write_eof(); - BOOST_TEST(ec2 == error::body_size_mismatch); - BOOST_TEST(!sr.is_done()); - }()); - - BOOST_TEST(server.data().empty()); - } - - // a direct write is rejected before the header goes out - { - std::string const body(cfg.direct_thr, 'x'); - - auto req = make_request(); - auto [client, server] = capy::test::make_stream_pair(); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req, nullptr, true); - - auto [ec, n] = co_await sr.write( - capy::make_buffer(body)); - BOOST_TEST(ec == error::body_size_mismatch); - BOOST_TEST(!sr.is_done()); - }()); - - BOOST_TEST(server.data().empty()); - } - - // write_eof() with caller buffers - { - auto req = make_request(); - auto [client, server] = capy::test::make_stream_pair(); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req, nullptr, true); - - auto [ec, n] = co_await sr.write_eof( - capy::const_buffer("hello", 5)); - BOOST_TEST(ec == error::body_size_mismatch); - BOOST_TEST(!sr.is_done()); - }()); - - BOOST_TEST(server.data().empty()); - } - - // prepare()/commit_eof() - { - auto req = make_request(5); - auto [client, server] = capy::test::make_stream_pair(); - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req, nullptr, true); - - capy::mutable_buffer tmp[2]; - auto n = capy::buffer_copy( - sr.prepare(tmp), - capy::const_buffer("hello", 5)); - BOOST_TEST_EQ(n, 5u); - - auto [ec] = co_await sr.commit_eof(n); - BOOST_TEST(ec == error::body_size_mismatch); - BOOST_TEST(!sr.is_done()); - }()); - - BOOST_TEST(server.data().empty()); - } - } - - void - testHeadEncoder() - { - // A head message never invokes its encoder: the header - // goes out as-is, with Content-Encoding intact, and the - // serializer remains reusable for a plain message. - { - auto req = make_request(); - req.set(http::field::content_encoding, "test"); - auto req2 = make_request(5); - - auto [client1, server1] = capy::test::make_stream_pair(); - auto [client2, server2] = capy::test::make_stream_pair(); - test_encoder enc; - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws1(&client1); - capy::any_write_stream ws2(&client2); - - serializer sr(cfg, &ws1); - sr.reset(&req, &enc, true); - - auto [ec1] = co_await sr.write_eof(); - BOOST_TEST(!ec1); - BOOST_TEST(sr.is_done()); - - sr.reset(&ws2, &req2); - auto [ec2, n] = co_await sr.write( - capy::const_buffer("hello", 5)); - BOOST_TEST(!ec2); - auto [ec3] = co_await sr.write_eof(); - BOOST_TEST(!ec3); - }()); - - BOOST_TEST_EQ(enc.calls, 0u); - BOOST_TEST(req.chunked()); - BOOST_TEST( - server1.data().find("Content-Encoding: test") != - std::string_view::npos); - BOOST_TEST_EQ( - server1.data(), std::string(req.buffer())); - BOOST_TEST_EQ( - server2.data(), - std::string(req2.buffer()) + "hello"); - } - - // body bytes staged for the encoder are rejected, and - // the encoder is never invoked - { - auto req = make_request(); - req.set(http::field::content_encoding, "test"); - auto [client, server] = capy::test::make_stream_pair(); - test_encoder enc; - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req, &enc, true); - - // below enc_thr: staged without invoking the - // encoder - auto [ec1, n] = co_await sr.write( - capy::const_buffer("hello", 5)); - BOOST_TEST(!ec1); - BOOST_TEST_EQ(n, 5u); - - auto [ec2] = co_await sr.write_eof(); - BOOST_TEST(ec2 == error::body_size_mismatch); - BOOST_TEST(!sr.is_done()); - }()); - - BOOST_TEST_EQ(enc.calls, 0u); - BOOST_TEST(server.data().empty()); - } - - // commit() surfaces the mismatch when it triggers a - // flush - { - auto req = make_request(); - req.set(http::field::content_encoding, "test"); - auto [client, server] = capy::test::make_stream_pair(); - test_encoder enc; - - capy::test::run_blocking()([&]() -> capy::task<> - { - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req, &enc, true); - - capy::mutable_buffer tmp[2]; - auto n = capy::buffer_copy( - sr.prepare(tmp), - capy::const_buffer("x", 1)); - BOOST_TEST_EQ(n, 1u); - - auto [ec] = co_await sr.commit_eof(n); - BOOST_TEST(ec == error::body_size_mismatch); - BOOST_TEST(!sr.is_done()); - }()); - - BOOST_TEST_EQ(enc.calls, 0u); - BOOST_TEST(server.data().empty()); - } - } - - void - testEncoderErrorInjection() - { - capy::test::fuse f; - auto r = f.armed([&](capy::test::fuse&) -> capy::task<> - { - std::string const body = make_body(100); - - auto req = make_request(); - req.set(http::field::content_encoding, "test"); - test_encoder enc; - - auto [client, server] = - capy::test::make_stream_pair(f); - - capy::any_write_stream ws(&client); - serializer sr(cfg, &ws); - sr.reset(&req, &enc); - - if(auto [ec, n] = co_await sr.write( - capy::make_buffer(body)); ec) - { - BOOST_TEST(!sr.is_done()); - co_return; - } - - if(auto [ec] = co_await sr.write_eof(); ec) - { - BOOST_TEST(!sr.is_done()); - co_return; - } - - BOOST_TEST(sr.is_done()); - BOOST_TEST(enc.finished); - - auto const enc_body = encoded(body); - std::string expected(req.buffer()); - expected += "40\r\n" + enc_body.substr(0, 64); - expected += "\r\n24\r\n" + enc_body.substr(64, 36); - expected += "\r\n0\r\n\r\n"; - BOOST_TEST_EQ(server.data(), expected); - }); - BOOST_TEST(r.success); - } - - void - run() - { - testContentLengthSmallBody(); - testContentLengthLargeBody(); - testContentLengthMultipleBuffers(); - testBodySizeMismatch(); - testChunkedSmallBodyConvertsToContentLength(); - testChunkedWriteEofWithBody(); - testChunkedLargeBody(); - testChunkedWriteEofWithTail(); - testChunkedWriteEofTailOnly(); - testChunkedEmptyBody(); - testPrepareCommitContentLength(); - testPrepareCommitPartialFlush(); - testPrepareCommitChunked(); - testPrepareCommitNoFlush(); - testPrepareEmptyDest(); - testPrepareFullBuffer(); - testChunkedShortWrites(); - testContentLengthShortWrites(); - testPrepareCommitShortWrites(); - testShortWriteEofDoesNotTruncate(); - testChunkedPreservesWriteOrder(); - testErrorInjection(); - testEncoderSmallBodyDropsEncoder(); - testEncoderEmptyBody(); - testEncoderPrepareCommitEof(); - testEncoderThreshold(); - testEncoderStagedAndTail(); - testEncoderLargeBodyChunked(); - testEncoderCommit(); - testEncoderCommitTriggersFlush(); - testEncoderTrailerSpansFlush(); - testEncoderContentLength(); - testEncoderContentLengthMismatch(); - testEncoderFailure(); - testEncoderMultipleBuffers(); - testEncoderWriteEofManyBuffers(); - testEncoderWriteSome(); - testEncoderMove(); - testReset(); - testResetAfterEncoderKept(); - testHeadContentLength(); - testHeadChunked(); - testHeadBodyMismatch(); - testHeadEncoder(); - testEncoderErrorInjection(); - } -}; - -TEST_SUITE(serializer_test, "boost.burl.detail.serializer"); - -} // namespace detail -} // namespace burl -} // namespace boost diff --git a/test/unit/error.cpp b/test/unit/error.cpp index f5f1489..cf72eda 100644 --- a/test/unit/error.cpp +++ b/test/unit/error.cpp @@ -61,7 +61,7 @@ struct error_test "unsupported proxy protocol version"); BOOST_TEST_EQ( msg(error::body_size_mismatch), - "request body size did not match content length"); + "message body size did not match content length"); BOOST_TEST_EQ( std::error_code(9999, burl_category()).message(), diff --git a/test/unit/message_writer.cpp b/test/unit/message_writer.cpp new file mode 100644 index 0000000..5c6be20 --- /dev/null +++ b/test/unit/message_writer.cpp @@ -0,0 +1,745 @@ +// +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +// Test that header file is self-contained. +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "test_suite.hpp" + +namespace boost +{ +namespace burl +{ + +// One type serves all three body-writing concepts. +static_assert( + capy::WriteStream>); +static_assert( + http::WriteSink>); +static_assert( + http::BufferSink>); + +class message_writer_test +{ + static constexpr serializer::config cfg{ + .stage_buffer = 64, + .min_prepare = 32, + .min_direct = 16, + .enc_buffer = 32, + .enc_threshold = 8 }; + + static request_head + make_request() + { + request_head req; + req.set_chunked(true); + return req; + } + + static request_head + make_request(std::size_t cl) + { + request_head req; + req.set_content_length(cl); + return req; + } + + static std::string + make_body(std::size_t n) + { + std::string s(n, '\0'); + for(std::size_t i = 0; i != n; ++i) + s[i] = static_cast('0' + i % 64); + return s; + } + + // The byte-incrementing mock encoder from the serializer + // suite, without the footer machinery. + struct test_encoder : serializer::encoder + { + bool finished = false; + + result + process( + capy::mutable_buffer out, + capy::const_buffer in, + bool more) override + { + auto* dst = static_cast(out.data()); + auto* src = + static_cast(in.data()); + auto const n = (std::min)(out.size(), in.size()); + for(std::size_t i = 0; i != n; ++i) + dst[i] = static_cast(src[i] + 1); + + result r{ n, n, {} }; + if(!more && n == in.size()) + { + finished = true; + r.ec = capy::error::eof; + } + return r; + } + }; + + static std::string + encoded(std::string_view s) + { + std::string r(s); + for(auto& c : r) + c = static_cast(c + 1); + return r; + } + + // A stream that accepts at most `budget` octets and then + // completes with `capy::error::canceled` alongside the + // partial count — the case neither capy::test::stream nor + // the fuse doubles produce, and the one the consume-before- + // check discipline exists for. + struct flaky_write_stream + { + std::string data; + std::size_t budget = 0; + + template + auto + write_some(CB buffers) + { + struct awaitable + { + flaky_write_stream* self_; + CB buffers_; + + bool + await_ready() const noexcept + { + return false; + } + + bool + await_suspend( + std::coroutine_handle<>, + capy::io_env const*) noexcept + { + return false; + } + + capy::io_result + await_resume() + { + auto const total = + capy::buffer_size(buffers_); + auto const n = + (std::min)(self_->budget, total); + self_->budget -= n; + + auto const old = self_->data.size(); + self_->data.resize(old + n); + capy::buffer_copy( + capy::make_buffer( + self_->data.data() + old, n), + buffers_, + n); + + if(n < total) + return { capy::error::canceled, n }; + return { std::error_code(), n }; + } + }; + return awaitable{ this, buffers }; + } + }; + +public: + void + testWrite() + { + auto req = make_request(5); + capy::test::write_stream ws; + + capy::test::run_blocking()([&]() -> capy::task<> + { + serializer sr(cfg); + message_writer writer(&ws, &sr); + sr.start(&req); + + auto [ec1, n] = co_await writer.write( + capy::const_buffer("hello", 5)); + BOOST_TEST(!ec1); + BOOST_TEST_EQ(n, 5u); + BOOST_TEST(!sr.is_done()); + + auto [ec2] = co_await writer.write_eof(); + BOOST_TEST(!ec2); + BOOST_TEST(sr.is_done()); + }()); + + BOOST_TEST_EQ( + ws.data(), std::string(req.buffer()) + "hello"); + } + + void + testWriteSome() + { + // write_some drives the same loop as write; nothing here + // makes it stop short of the whole input + auto req = make_request(5); + capy::test::write_stream ws; + + capy::test::run_blocking()([&]() -> capy::task<> + { + serializer sr(cfg); + message_writer writer(&ws, &sr); + sr.start(&req); + + auto [ec1, n] = co_await writer.write_some( + capy::const_buffer("hello", 5)); + BOOST_TEST(!ec1); + BOOST_TEST_EQ(n, 5u); + BOOST_TEST(!sr.is_done()); + + auto [ec2] = co_await writer.write_eof(); + BOOST_TEST(!ec2); + BOOST_TEST(sr.is_done()); + }()); + + BOOST_TEST_EQ( + ws.data(), std::string(req.buffer()) + "hello"); + } + + void + testCommitEof() + { + // the body is generated in place and the last commit + // ends the message + auto req = make_request(); + capy::test::write_stream ws; + + capy::test::run_blocking()([&]() -> capy::task<> + { + serializer sr(cfg); + message_writer writer(&ws, &sr); + sr.start(&req); + + capy::mutable_buffer tmp[2]; + auto const n = capy::buffer_copy( + writer.prepare(tmp), + capy::const_buffer("hello", 5)); + BOOST_TEST_EQ(n, 5u); + + auto [ec] = co_await writer.commit_eof(n); + BOOST_TEST(!ec); + BOOST_TEST(sr.is_done()); + }()); + + // the whole body was staged before the header went out, + // so chunked framing converted to Content-Length + BOOST_TEST(!req.chunked()); + BOOST_TEST_EQ(req.content_length().value(), 5u); + BOOST_TEST_EQ( + ws.data(), std::string(req.buffer()) + "hello"); + } + + void + testCommitWithoutDrainPressure() + { + // A commit that leaves the prepare window large raises + // no drain pressure, so it does no I/O at all. + auto req = make_request(); + capy::test::write_stream ws; + + capy::test::run_blocking()([&]() -> capy::task<> + { + serializer sr(cfg); + message_writer writer(&ws, &sr); + sr.start(&req); + + capy::mutable_buffer tmp[2]; + auto const n = capy::buffer_copy( + writer.prepare(tmp), + capy::const_buffer("hello", 5)); + + auto [ec1] = co_await writer.commit(n); + BOOST_TEST(!ec1); + BOOST_TEST(!sr.should_drain()); + BOOST_TEST(ws.data().empty()); + + auto [ec2] = co_await writer.write_eof(); + BOOST_TEST(!ec2); + BOOST_TEST(sr.is_done()); + }()); + + BOOST_TEST_EQ( + ws.data(), std::string(req.buffer()) + "hello"); + } + + void + testSerializerError() + { + // a framing error surfaces from the drive, with nothing + // handed to the stream + auto req = make_request(10); + capy::test::write_stream ws; + + capy::test::run_blocking()([&]() -> capy::task<> + { + serializer sr(cfg); + message_writer writer(&ws, &sr); + sr.start(&req); + + auto [ec, n] = co_await writer.write_eof( + capy::const_buffer("hello", 5)); + BOOST_TEST(ec == error::body_size_mismatch); + BOOST_TEST_EQ(n, 0u); + BOOST_TEST(!sr.is_done()); + }()); + + BOOST_TEST(ws.data().empty()); + } + + void + testWriteEofMultipleBuffers() + { + std::string const b1 = make_body(20); + std::string const b2 = make_body(20); + auto req = make_request(40); + capy::test::write_stream ws; + + capy::test::run_blocking()([&]() -> capy::task<> + { + serializer sr(cfg); + message_writer writer(&ws, &sr); + sr.start(&req); + + std::array bufs{ + capy::make_buffer(b1), + capy::make_buffer(b2) }; + auto [ec, n] = co_await writer.write_eof(bufs); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 40u); + BOOST_TEST(sr.is_done()); + }()); + + BOOST_TEST_EQ( + ws.data(), + std::string(req.buffer()) + b1 + b2); + } + + void + testShortWrites() + { + // A stream that transfers one octet per write exercises + // the drive loop's partial-consume re-materialization + // end to end; 64 == 0x40. + std::string const body(cfg.stage_buffer, 'z'); + auto req = make_request(); + capy::test::write_stream ws({}, 1); + + capy::test::run_blocking()([&]() -> capy::task<> + { + serializer sr(cfg); + message_writer writer(&ws, &sr); + sr.start(&req); + + capy::mutable_buffer tmp[2]; + auto const n = capy::buffer_copy( + writer.prepare(tmp), capy::make_buffer(body)); + BOOST_TEST_EQ(n, body.size()); + + auto [ec1] = co_await writer.commit(n); + BOOST_TEST(!ec1); + + BOOST_TEST_EQ(ws.data(), + std::string(req.buffer()) + "40\r\n" + body); + + auto [ec2, m] = co_await writer.write( + capy::const_buffer("hello", 5)); + BOOST_TEST(!ec2); + BOOST_TEST_EQ(m, 5u); + + auto [ec3] = co_await writer.write_eof(); + BOOST_TEST(!ec3); + BOOST_TEST(sr.is_done()); + }()); + + std::string expected(req.buffer()); + expected += "40\r\n" + body + "\r\n"; + expected += "5\r\nhello\r\n"; + expected += "0\r\n\r\n"; + BOOST_TEST(req.chunked()); + BOOST_TEST_EQ(ws.data(), expected); + } + + void + testAnyBufferSink() + { + // the writer drops into the type-erased sink, and the + // native write path keeps working through the erasure + std::string const body(cfg.min_direct, 'x'); + auto req = make_request(); + capy::test::write_stream ws; + + capy::test::run_blocking()([&]() -> capy::task<> + { + serializer sr(cfg); + message_writer writer(&ws, &sr); + sr.start(&req); + + http::any_buffer_sink sink(&writer); + + auto [ec1, n1] = co_await sink.write_some( + capy::make_buffer(body)); + BOOST_TEST(!ec1); + BOOST_TEST_EQ(n1, body.size()); + + auto [ec2, n2] = co_await sink.write( + capy::make_buffer(body)); + BOOST_TEST(!ec2); + BOOST_TEST_EQ(n2, body.size()); + + auto [ec3, n3] = co_await sink.write_eof( + capy::const_buffer("hello", 5)); + BOOST_TEST(!ec3); + BOOST_TEST_EQ(n3, 5u); + BOOST_TEST(sr.is_done()); + }()); + + std::string expected(req.buffer()); + expected += "10\r\n" + body + "\r\n"; + expected += "10\r\n" + body + "\r\n"; + expected += "5\r\nhello\r\n"; + expected += "0\r\n\r\n"; + BOOST_TEST_EQ(ws.data(), expected); + } + + void + testWriteEofManyBuffers() + { + // More caller buffers than fit in one gather window: + // the drive supplies window by window, with the end + // flag deferred until the last window. + std::string const body = make_body(24); + + // identity + { + auto req = make_request(body.size()); + capy::test::write_stream ws; + + capy::test::run_blocking()([&]() -> capy::task<> + { + serializer sr(cfg); + message_writer writer(&ws, &sr); + sr.start(&req); + + std::array bufs; + for(std::size_t i = 0; i != bufs.size(); ++i) + bufs[i] = { body.data() + i, 1 }; + + auto [ec, n] = co_await writer.write_eof(bufs); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, body.size()); + BOOST_TEST(sr.is_done()); + }()); + + BOOST_TEST_EQ( + ws.data(), std::string(req.buffer()) + body); + } + + // encoder: fed window by window, with eof deferred + // until the last window + { + auto req = make_request(); + req.set(http::field::content_encoding, "test"); + test_encoder enc; + capy::test::write_stream ws; + + capy::test::run_blocking()([&]() -> capy::task<> + { + serializer sr(cfg); + message_writer writer(&ws, &sr); + sr.start(&req, &enc); + + std::array bufs; + for(std::size_t i = 0; i != bufs.size(); ++i) + bufs[i] = { body.data() + i, 1 }; + + auto [ec, n] = co_await writer.write_eof(bufs); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, body.size()); + BOOST_TEST(sr.is_done()); + }()); + + BOOST_TEST(enc.finished); + BOOST_TEST(!req.chunked()); + BOOST_TEST_EQ(req.content_length().value(), 24u); + BOOST_TEST_EQ( + ws.data(), + std::string(req.buffer()) + encoded(body)); + } + } + + void + testWriteHeader() + { + // the expect-100-continue shape: the header goes out + // on its own, the body follows later + { + auto req = make_request(5); + capy::test::write_stream ws; + + capy::test::run_blocking()([&]() -> capy::task<> + { + serializer sr(cfg); + message_writer writer(&ws, &sr); + sr.start(&req); + + auto [ec1] = co_await writer.write_header(); + BOOST_TEST(!ec1); + BOOST_TEST_EQ( + ws.data(), std::string(req.buffer())); + BOOST_TEST(sr.is_header_done()); + BOOST_TEST(!sr.is_done()); + + auto [ec2, n] = co_await writer.write_eof( + capy::const_buffer("hello", 5)); + BOOST_TEST(!ec2); + BOOST_TEST_EQ(n, 5u); + BOOST_TEST(sr.is_done()); + }()); + + BOOST_TEST_EQ( + ws.data(), std::string(req.buffer()) + "hello"); + } + + // sending the header seals the framing: a small body + // of undeclared size stays chunked + { + auto req = make_request(); + capy::test::write_stream ws; + + capy::test::run_blocking()([&]() -> capy::task<> + { + serializer sr(cfg); + message_writer writer(&ws, &sr); + sr.start(&req); + + auto [ec1] = co_await writer.write_header(); + BOOST_TEST(!ec1); + + auto [ec2, n] = co_await writer.write_eof( + capy::const_buffer("hello", 5)); + BOOST_TEST(!ec2); + BOOST_TEST_EQ(n, 5u); + BOOST_TEST(sr.is_done()); + }()); + + BOOST_TEST(req.chunked()); + BOOST_TEST_EQ( + ws.data(), std::string(req.buffer()) + + "5\r\nhello\r\n0\r\n\r\n"); + } + + // an installed encoder survives a small body once the + // header advertised Content-Encoding + { + auto req = make_request(); + req.set(http::field::content_encoding, "test"); + test_encoder enc; + capy::test::write_stream ws; + + capy::test::run_blocking()([&]() -> capy::task<> + { + serializer sr(cfg); + message_writer writer(&ws, &sr); + sr.start(&req, &enc); + + auto [ec1] = co_await writer.write_header(); + BOOST_TEST(!ec1); + + auto [ec2, n] = co_await writer.write_eof( + capy::const_buffer("hello", 5)); + BOOST_TEST(!ec2); + BOOST_TEST_EQ(n, 5u); + BOOST_TEST(sr.is_done()); + }()); + + BOOST_TEST(enc.finished); + BOOST_TEST(req.chunked()); + BOOST_TEST_EQ( + ws.data(), std::string(req.buffer()) + + "5\r\nifmmp\r\n0\r\n\r\n"); + } + } + + void + testErrorInjection() + { + capy::test::fuse f; + auto const r = f.armed( + [&](capy::test::fuse&) -> capy::task<> + { + std::string const body(cfg.min_direct, 'x'); + auto req = make_request(); + auto [client, server] = + capy::test::make_stream_pair(f); + + serializer sr(cfg); + message_writer writer(&client, &sr); + sr.start(&req); + + if(auto [ec, n] = co_await writer.write( + capy::make_buffer(body)); ec) + { + BOOST_TEST(!sr.is_done()); + co_return; + } + + if(auto [ec] = co_await writer.write_eof(); ec) + { + BOOST_TEST(!sr.is_done()); + co_return; + } + + BOOST_TEST(sr.is_done()); + + std::string expected(req.buffer()); + expected += "10\r\n" + body + "\r\n"; + expected += "0\r\n\r\n"; + BOOST_TEST_EQ(server.data(), expected); + }); + BOOST_TEST(r.success); + } + + void + testCancelResume() + { + // A cancelled write completes with an error alongside a + // partial count. The drive banks the count through + // consume before it looks at the error, so the returned + // total is the caller's cursor, and supplying the + // unconsumed remainder resumes the message. The trailer + // pins chunked framing, so the seven-octet budget walks + // cancellation across the header, chunk prefix, body, + // epilogue, and trailer section; 40 == 0x28. + std::string const body = make_body(40); + auto req = make_request(); + fields trailer; + trailer.set("x-checksum", "abc123"); + flaky_write_stream ws; + + capy::test::run_blocking()([&]() -> capy::task<> + { + serializer sr(cfg); + message_writer writer(&ws, &sr); + sr.start(&req); + sr.set_trailer(&trailer); + + std::string_view rem(body); + for(;;) + { + ws.budget = 7; + auto [ec, n] = co_await writer.write_eof( + capy::const_buffer( + rem.data(), rem.size())); + rem.remove_prefix(n); + if(!ec) + break; + BOOST_TEST(ec == capy::cond::canceled); + BOOST_TEST(!sr.is_done()); + } + BOOST_TEST(sr.is_done()); + BOOST_TEST(rem.empty()); + }()); + + BOOST_TEST(req.chunked()); + std::string expected(req.buffer()); + expected += "28\r\n" + body + "\r\n"; + expected += "0\r\n"; + expected += std::string(trailer.buffer()); + BOOST_TEST_EQ(ws.data, expected); + } + + void + testCancelResumeWrite() + { + // Resuming a cancelled write() is compositional: the + // count already banked through consume is the caller's + // cursor, and the remainder is re-issued. + std::string const body = make_body(40); + auto req = make_request(body.size()); + flaky_write_stream ws; + + capy::test::run_blocking()([&]() -> capy::task<> + { + serializer sr(cfg); + message_writer writer(&ws, &sr); + sr.start(&req); + + ws.budget = req.buffer().size() + 11; + auto [ec, n] = co_await writer.write( + capy::make_buffer(body)); + BOOST_TEST(ec == capy::cond::canceled); + BOOST_TEST_EQ(n, 11u); + BOOST_TEST(!sr.is_done()); + + ws.budget = std::size_t(-1) / 2; + auto [ec2, n2] = co_await writer.write( + capy::make_buffer( + std::string_view(body).substr(n))); + BOOST_TEST(!ec2); + BOOST_TEST_EQ(n2, body.size() - n); + + auto [ec3] = co_await writer.write_eof(); + BOOST_TEST(!ec3); + BOOST_TEST(sr.is_done()); + }()); + + BOOST_TEST_EQ( + ws.data, std::string(req.buffer()) + body); + } + + void + run() + { + testWrite(); + testWriteSome(); + testCommitEof(); + testCommitWithoutDrainPressure(); + testSerializerError(); + testWriteEofMultipleBuffers(); + testWriteEofManyBuffers(); + testShortWrites(); + testAnyBufferSink(); + testWriteHeader(); + testErrorInjection(); + testCancelResume(); + testCancelResumeWrite(); + } +}; + +TEST_SUITE(message_writer_test, "boost.burl.message_writer"); + +} // namespace burl +} // namespace boost diff --git a/test/unit/parser.cpp b/test/unit/parser.cpp index 365d789..ddf092d 100644 --- a/test/unit/parser.cpp +++ b/test/unit/parser.cpp @@ -310,7 +310,7 @@ class parser_test process( capy::mutable_buffer out, capy::const_buffer in, - bool eof) override + bool more) override { if(fail_ec && consumed_total >= fail_at) return { 0, 0, fail_ec }; @@ -344,7 +344,7 @@ class parser_test r.ec = capy::error::eof; return r; } - if(eof && n == in.size() && !ignore_eof) + if(!more && n == in.size() && !ignore_eof) { auto const t = (std::min)( out.size() - n, trailer.size() - trailer_pos_); diff --git a/test/unit/serializer.cpp b/test/unit/serializer.cpp new file mode 100644 index 0000000..660ad3f --- /dev/null +++ b/test/unit/serializer.cpp @@ -0,0 +1,3271 @@ +// +// Copyright (c) 2026 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/burl +// + +// Test that header file is self-contained. +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include "test_suite.hpp" + +namespace boost +{ +namespace burl +{ + +// The serializer performs no I/O: every test below supplies the +// body to frame() as a buffer sequence and drains the returned +// descriptors through consume() into a std::string, trimming +// the body by what consume() returns. No streams are involved. +class serializer_test +{ + // A scaled-down config so that framing and encoder + // thresholds (staged vs. direct writes, commit-triggered + // flushes, encoder drop) are crossed with tiny bodies, and + // so tests keep exercising the same paths if the default + // config values change. + static constexpr serializer::config cfg{ + .stage_buffer = 64, + .min_prepare = 32, + .min_direct = 16, + .enc_buffer = 32, + .enc_threshold = 8 }; + + // header + merged prefix/run + a few body descriptors + + // epilogue + trailer + static constexpr std::size_t dest_n = 24; + + static request_head + make_request() + { + request_head req; + req.set_chunked(true); + return req; + } + + static request_head + make_request(std::size_t cl) + { + request_head req; + req.set_content_length(cl); + return req; + } + + // An encoder that increments every body byte by one, so + // encoded output is distinguishable from identity output, + // and appends an optional footer once the input ends. It + // consumes and produces as much as the given buffers allow, + // or at most `out_limit` octets per call when set. + struct test_encoder : serializer::encoder + { + std::string footer; + std::error_code fail; + std::size_t out_limit = 0; + std::size_t calls = 0; + bool finished = false; + + explicit test_encoder(std::string f = {}) + : footer(std::move(f)) + { + } + + result + process( + capy::mutable_buffer out, + capy::const_buffer in, + bool more) override + { + ++calls; + if(fail) + return { 0, 0, fail }; + if(out_limit != 0 && out.size() > out_limit) + out = { out.data(), out_limit }; + + auto* dst = static_cast(out.data()); + auto* src = static_cast(in.data()); + auto const n = (std::min)(out.size(), in.size()); + for(std::size_t i = 0; i != n; ++i) + dst[i] = static_cast(src[i] + 1); + + result r{ n, n, {} }; + if(!more && n == in.size()) + { + auto const t = (std::min)( + out.size() - n, footer.size() - footer_pos_); + std::memcpy( + dst + n, footer.data() + footer_pos_, t); + footer_pos_ += t; + r.produced += t; + if(footer_pos_ == footer.size()) + { + finished = true; + r.ec = capy::error::eof; + } + } + return r; + } + + private: + std::size_t footer_pos_ = 0; + }; + + // Returns a body with a varied byte pattern so reordered + // or duplicated regions cannot go unnoticed. + static std::string + make_body(std::size_t n) + { + std::string s(n, '\0'); + for(std::size_t i = 0; i != n; ++i) + s[i] = static_cast('0' + i % 64); + return s; + } + + // Returns the mock-encoded form of `s`. + static std::string + encoded(std::string_view s) + { + std::string r(s); + for(auto& c : r) + c = static_cast(c + 1); + return r; + } + + // One drive pass: supplies `body`, trimming it by whatever + // consume() reports, and drains the framed output into + // `wire`, consuming at most `step` octets per lap so every + // cursor split point is exercised when `step` is small. + // Returns when frame() has no work for this input. + static system::error_code + drive( + serializer& sr, + std::string& wire, + std::string_view& body, + bool more, + std::size_t step = std::size_t(-1)) + { + for(;;) + { + capy::const_buffer const b{ + body.data(), body.size() }; + system::error_code ec; + capy::const_buffer dest[dest_n]; + auto const bufs = sr.frame(dest, b, more, ec); + if(bufs.empty()) + { + body.remove_prefix(sr.consume(0)); + return ec; + } + std::size_t n = 0; + for(auto cb : bufs) + { + auto const k = (std::min)(step - n, cb.size()); + wire.append( + static_cast(cb.data()), k); + if((n += k) == step) + break; + } + body.remove_prefix(sr.consume(n)); + } + } + + // Writes the whole of `body` without ending the message; + // the sans-I/O equivalent of the awaitable write(). + static system::error_code + write( + serializer& sr, + std::string& wire, + std::string_view body, + std::size_t step = std::size_t(-1)) + { + do + { + if(auto ec = drive(sr, wire, body, true, step)) + return ec; + } + while(!body.empty()); + return {}; + } + + // Ends the body with `body`; the sans-I/O equivalent of the + // awaitable write_eof(). + static system::error_code + write_eof( + serializer& sr, + std::string& wire, + std::string_view body = {}, + std::size_t step = std::size_t(-1)) + { + for(;;) + { + if(auto ec = drive(sr, wire, body, false, step)) + return ec; + if(sr.is_done()) + return {}; + } + } + + // Flushes outstanding output (and the header) without + // supplying anything; the sans-I/O drain(). + static system::error_code + drain( + serializer& sr, + std::string& wire, + std::size_t step = std::size_t(-1)) + { + std::string_view none; + return drive(sr, wire, none, true, step); + } + + // The same, through the frame() overload that takes no + // buffer sequence at all. + static system::error_code + drain_bufferless( + serializer& sr, + std::string& wire, + bool more = true) + { + for(;;) + { + system::error_code ec; + capy::const_buffer dest[dest_n]; + auto const bufs = sr.frame(dest, more, ec); + if(bufs.empty()) + { + BOOST_TEST_EQ(sr.consume(0), 0u); + return ec; + } + std::size_t n = 0; + for(auto cb : bufs) + { + wire.append( + static_cast(cb.data()), + cb.size()); + n += cb.size(); + } + BOOST_TEST_EQ(sr.consume(n), 0u); + } + } + +public: + void + testContentLengthSmallBody() + { + auto req = make_request(5); + serializer sr(cfg); + sr.start(&req); + BOOST_TEST(!sr.is_done()); + + std::string wire; + BOOST_TEST(!write(sr, wire, "hello")); + BOOST_TEST(wire.empty()); // coalesced, header held back + BOOST_TEST(!sr.is_done()); + + BOOST_TEST(!write_eof(sr, wire)); + BOOST_TEST(sr.is_done()); + + BOOST_TEST_EQ( + wire, std::string(req.buffer()) + "hello"); + } + + void + testContentLengthLargeBody() + { + // A body at the direct-write threshold bypasses staging. + std::string const body(cfg.min_direct, 'x'); + auto req = make_request(body.size()); + serializer sr(cfg); + sr.start(&req); + + std::string wire; + BOOST_TEST(!write(sr, wire, body)); + BOOST_TEST(!write_eof(sr, wire)); + + BOOST_TEST_EQ( + wire, std::string(req.buffer()) + body); + } + + void + testContentLengthMultipleBuffers() + { + std::string const b1(4, 'a'); + std::string const b2(4, 'b'); + auto req = make_request(b1.size() + b2.size()); + serializer sr(cfg); + sr.start(&req); + + capy::const_buffer const bufs[2] = { + capy::make_buffer(b1), + capy::make_buffer(b2) }; + + std::string wire; + system::error_code ec; + capy::const_buffer dest[dest_n]; + auto const out = sr.frame(dest, bufs, false, ec); + BOOST_TEST(!ec); + BOOST_TEST(!out.empty()); + + std::size_t n = 0; + for(auto cb : out) + { + wire.append( + static_cast(cb.data()), cb.size()); + n += cb.size(); + } + BOOST_TEST_EQ(sr.consume(n), 8u); + BOOST_TEST(sr.is_done()); + + BOOST_TEST_EQ( + wire, std::string(req.buffer()) + b1 + b2); + } + + void + testBodySizeMismatch() + { + // fewer bytes than Content-Length + { + auto req = make_request(5); + serializer sr(cfg); + sr.start(&req); + + std::string wire; + BOOST_TEST(!write(sr, wire, "hell")); + BOOST_TEST_EQ( + write_eof(sr, wire), + error::body_size_mismatch); + BOOST_TEST(!sr.is_done()); + } + + // more bytes than Content-Length: caught at the + // supplying call, before any octet is handed out + { + auto req = make_request(5); + serializer sr(cfg); + sr.start(&req); + + std::string wire; + BOOST_TEST_EQ( + write(sr, wire, "helloo"), + error::body_size_mismatch); + BOOST_TEST(wire.empty()); + } + + // same on the direct path + std::string const body(cfg.min_direct, 'x'); + { + auto req = make_request(5); + serializer sr(cfg); + sr.start(&req); + + std::string wire; + BOOST_TEST_EQ( + write(sr, wire, body), + error::body_size_mismatch); + BOOST_TEST(wire.empty()); + } + + // errors preserve state: the failed call repeats, and a + // corrected input proceeds + { + auto req = make_request(5); + serializer sr(cfg); + sr.start(&req); + + std::string wire; + BOOST_TEST_EQ( + write(sr, wire, "helloo"), + error::body_size_mismatch); + BOOST_TEST_EQ( + write(sr, wire, "helloo"), + error::body_size_mismatch); + BOOST_TEST(!write_eof(sr, wire, "hello")); + BOOST_TEST(sr.is_done()); + BOOST_TEST_EQ( + wire, std::string(req.buffer()) + "hello"); + } + } + + void + testChunkedSmallBodyConvertsToContentLength() + { + auto req = make_request(); + serializer sr(cfg); + sr.start(&req); + + // The body stays below min_direct, so it is fully + // buffered before the header goes out. + std::string wire; + BOOST_TEST(!write(sr, wire, "hello world")); + BOOST_TEST(!write_eof(sr, wire)); + + // The entire body was buffered before the header was + // flushed, so chunked encoding is replaced with + // Content-Length and the body is sent unframed. + BOOST_TEST(!req.chunked()); + BOOST_TEST_EQ(req.content_length().value(), 11u); + BOOST_TEST( + wire.find("Transfer-Encoding") == + std::string::npos); + BOOST_TEST_EQ( + wire, std::string(req.buffer()) + "hello world"); + } + + void + testChunkedWriteEofWithBody() + { + auto req = make_request(); + serializer sr(cfg); + sr.start(&req); + + std::string wire; + BOOST_TEST(!write_eof(sr, wire, "hello")); + BOOST_TEST(sr.is_done()); + + BOOST_TEST(!req.chunked()); + BOOST_TEST_EQ(req.content_length().value(), 5u); + BOOST_TEST_EQ( + wire, std::string(req.buffer()) + "hello"); + } + + void + testChunkedLargeBody() + { + std::string const body(cfg.min_direct, 'x'); + auto req = make_request(); + serializer sr(cfg); + sr.start(&req); + + std::string wire; + BOOST_TEST(!write(sr, wire, body)); + BOOST_TEST(!write(sr, wire, "hello")); + BOOST_TEST(!sr.is_done()); + BOOST_TEST(!write_eof(sr, wire)); + BOOST_TEST(sr.is_done()); + + // The large write is gathered with the header and sent + // as its own chunk; the small write is buffered and + // flushed at eof as a chunk. Chunk sizes are always + // minimal-width; 16 == 0x10. + std::string expected(req.buffer()); + expected += "10\r\n" + body + "\r\n"; + expected += "5\r\nhello\r\n"; + expected += "0\r\n\r\n"; + BOOST_TEST(req.chunked()); + BOOST_TEST_EQ(wire, expected); + } + + void + testChunkedWriteEofWithTail() + { + // Once the header is on the wire, the final input must + // frame the staged bytes and the caller's bytes as the + // final chunk and terminate the body with the last-chunk + // in the same vector. + std::string const body(cfg.min_direct, 'x'); + auto req = make_request(); + serializer sr(cfg); + sr.start(&req); + + std::string wire; + BOOST_TEST(!write(sr, wire, body)); + BOOST_TEST(!write(sr, wire, "abc")); + BOOST_TEST(!write_eof(sr, wire, "hello")); + + std::string expected(req.buffer()); + expected += "10\r\n" + body + "\r\n"; + expected += "8\r\nabchello\r\n"; + expected += "0\r\n\r\n"; + BOOST_TEST(req.chunked()); + BOOST_TEST_EQ(wire, expected); + } + + void + testChunkedWriteEofTailOnly() + { + std::string const body(cfg.min_direct, 'x'); + auto req = make_request(); + serializer sr(cfg); + sr.start(&req); + + std::string wire; + BOOST_TEST(!write(sr, wire, body)); + BOOST_TEST(!write_eof(sr, wire, "hello")); + + std::string expected(req.buffer()); + expected += "10\r\n" + body + "\r\n"; + expected += "5\r\nhello\r\n"; + expected += "0\r\n\r\n"; + BOOST_TEST(req.chunked()); + BOOST_TEST_EQ(wire, expected); + } + + void + testChunkedEmptyBody() + { + auto req = make_request(); + serializer sr(serializer::config{}); + sr.start(&req); + BOOST_TEST(!sr.is_done()); + + std::string wire; + BOOST_TEST(!write_eof(sr, wire)); + BOOST_TEST(sr.is_done()); + + BOOST_TEST(!req.chunked()); + BOOST_TEST( + wire.find("Content-Length: 0\r\n") != + std::string::npos); + BOOST_TEST_EQ(wire, std::string(req.buffer())); + } + + void + testPrepareCommitContentLength() + { + auto req = make_request(5); + serializer sr(cfg); + sr.start(&req); + + capy::mutable_buffer tmp[2]; + auto const dest = sr.prepare(tmp); + BOOST_TEST_EQ( + capy::buffer_size(dest), cfg.stage_buffer); + auto const n = capy::buffer_copy( + dest, capy::const_buffer("hello", 5)); + BOOST_TEST_EQ(n, 5u); + sr.commit(n); + + std::string wire; + BOOST_TEST(!write_eof(sr, wire)); + BOOST_TEST(sr.is_done()); + BOOST_TEST_EQ( + wire, std::string(req.buffer()) + "hello"); + } + + void + testPrepareCommitPartialFlush() + { + // A commit that leaves at least min_prepare capacity + // raises no drain pressure and a drain pass moves no + // body; one that does flushes the whole staged run as + // one chunk. + auto req = make_request(); + serializer sr(cfg); + sr.start(&req); + + std::string const b1 = make_body(30); + std::string const b2 = make_body(4); + + capy::mutable_buffer tmp[2]; + capy::buffer_copy( + sr.prepare(tmp), capy::make_buffer(b1)); + sr.commit(b1.size()); + BOOST_TEST(!sr.should_drain()); + + // a drain pass flushes the header, but not the run + std::string wire; + BOOST_TEST(!drain(sr, wire)); + BOOST_TEST_EQ(wire, std::string(req.buffer())); + + capy::buffer_copy( + sr.prepare(tmp), capy::make_buffer(b2)); + sr.commit(b2.size()); + BOOST_TEST(sr.should_drain()); + + BOOST_TEST(!drain(sr, wire)); + + // 34 == 0x22 + std::string expected(req.buffer()); + expected += "22\r\n" + b1 + b2; + BOOST_TEST_EQ(wire, expected); + + BOOST_TEST(!write_eof(sr, wire)); + expected += "\r\n0\r\n\r\n"; + BOOST_TEST_EQ(wire, expected); + } + + void + testPrepareCommitChunked() + { + // The staging buffer holds raw body bytes only, so the + // full capacity is stageable as one chunk; 64 == 0x40. + std::string const body(cfg.stage_buffer, 'z'); + auto req = make_request(); + serializer sr(cfg); + sr.start(&req); + + capy::mutable_buffer tmp[2]; + auto const n = capy::buffer_copy( + sr.prepare(tmp), capy::make_buffer(body)); + BOOST_TEST_EQ(n, body.size()); + sr.commit(n); + BOOST_TEST(sr.should_drain()); + + std::string wire; + BOOST_TEST(!drain(sr, wire)); + + // the chunk's closing CRLF is deferred to the next unit + BOOST_TEST_EQ( + wire, + std::string(req.buffer()) + "40\r\n" + body); + + BOOST_TEST(!write(sr, wire, "hello")); + BOOST_TEST(!write_eof(sr, wire)); + + std::string expected(req.buffer()); + expected += "40\r\n" + body + "\r\n"; + expected += "5\r\nhello\r\n"; + expected += "0\r\n\r\n"; + BOOST_TEST(req.chunked()); + BOOST_TEST_EQ(wire, expected); + } + + void + testPrepareCommitNoFlush() + { + // Nothing is drained before eof, so the staged body + // converts to Content-Length. + auto req = make_request(); + serializer sr(cfg); + sr.start(&req); + + capy::mutable_buffer tmp[2]; + capy::buffer_copy( + sr.prepare(tmp), capy::const_buffer("hello", 5)); + sr.commit(5); + BOOST_TEST(!sr.should_drain()); + + std::string wire; + BOOST_TEST(!write_eof(sr, wire)); + BOOST_TEST(sr.is_done()); + + BOOST_TEST(!req.chunked()); + BOOST_TEST_EQ(req.content_length().value(), 5u); + BOOST_TEST_EQ( + wire, std::string(req.buffer()) + "hello"); + } + + void + testPrepareEmptyDest() + { + auto req = make_request(); + serializer sr(cfg); + sr.start(&req); + + auto const dest = sr.prepare({}); + BOOST_TEST(dest.empty()); + } + + void + testPrepareFullBuffer() + { + // A full staging buffer yields an empty prepare(); a + // drain pass flushes it and restores the full window. + std::string const body(cfg.stage_buffer, 'z'); + auto req = make_request(); + serializer sr(cfg); + sr.start(&req); + + capy::mutable_buffer tmp[2]; + auto const n = capy::buffer_copy( + sr.prepare(tmp), capy::make_buffer(body)); + BOOST_TEST_EQ(n, body.size()); + sr.commit(n); + BOOST_TEST(sr.prepare(tmp).empty()); + + std::string wire; + BOOST_TEST(!drain(sr, wire)); + + BOOST_TEST_EQ( + capy::buffer_size(sr.prepare(tmp)), + cfg.stage_buffer); + + BOOST_TEST(!write_eof(sr, wire)); + BOOST_TEST(sr.is_done()); + } + + void + testPartialConsume() + { + // The same multi-chunk message drained one octet at a + // time, and at various other split widths, must produce + // identical wire bytes: frame re-materializes remainders + // from the cursors, including a partially consumed chunk + // prefix, with the body supplied again every call. + std::string const body(cfg.min_direct, 'x'); + + std::string expected; + { + auto req = make_request(); + serializer sr(cfg); + sr.start(&req); + BOOST_TEST(!write(sr, expected, body)); + BOOST_TEST(!write(sr, expected, "abc")); + BOOST_TEST(!write_eof(sr, expected, "hello")); + } + + for(std::size_t step = 1; step != 24; ++step) + { + auto req = make_request(); + serializer sr(cfg); + sr.start(&req); + + std::string wire; + BOOST_TEST(!write(sr, wire, body, step)); + BOOST_TEST(!write(sr, wire, "abc", step)); + BOOST_TEST(!write_eof(sr, wire, "hello", step)); + BOOST_TEST(sr.is_done()); + BOOST_TEST_EQ(wire, expected); + } + } + + void + testRemainderCoalesces() + { + // What is left of a directly framed input after a short + // write is judged on its own size: once under the + // threshold it coalesces, rather than costing a write of + // its own. The end of the body is the exception — there + // is nothing left to batch with, so a final input goes + // out as it stands. + std::string const body(cfg.min_direct + 8, 'x'); + auto req = make_request(body.size()); + serializer sr(cfg); + sr.start(&req); + + std::string_view rem(body); + capy::const_buffer const b{ + rem.data(), rem.size() }; + system::error_code ec; + capy::const_buffer dest[dest_n]; + + auto out = sr.frame(dest, b, true, ec); + BOOST_TEST(!ec); + BOOST_TEST_EQ(out.size(), 2u); // header + body, no copy + + // the wire takes the header and all but 4 octets + std::string wire; + for(auto cb : out) + wire.append( + static_cast(cb.data()), cb.size()); + auto const k = req.buffer().size() + body.size() - 4; + wire.resize(k); + rem.remove_prefix(sr.consume(k)); + BOOST_TEST_EQ(rem.size(), 4u); + + // the 4-octet remainder is absorbed, not re-framed + capy::const_buffer const b2{ + rem.data(), rem.size() }; + out = sr.frame(dest, b2, true, ec); + BOOST_TEST(!ec); + BOOST_TEST(out.empty()); + BOOST_TEST_EQ(sr.consume(0), rem.size()); + + BOOST_TEST(!write_eof(sr, wire)); + BOOST_TEST(sr.is_done()); + BOOST_TEST_EQ( + wire, std::string(req.buffer()) + body); + } + + void + testFrameIdempotent() + { + // Two frames without an intervening mutation return the + // same octets, before and after a partial consume. + std::string const body(cfg.min_direct, 'x'); + auto req = make_request(); + serializer sr(cfg); + sr.start(&req); + + capy::const_buffer const b{ + body.data(), body.size() }; + auto const flatten = [&] + { + system::error_code ec; + capy::const_buffer dest[dest_n]; + auto const bufs = sr.frame(dest, b, true, ec); + BOOST_TEST(!ec); + std::string s; + for(auto cb : bufs) + s.append( + static_cast(cb.data()), + cb.size()); + return s; + }; + + auto const s1 = flatten(); + auto const s2 = flatten(); + BOOST_TEST_EQ(s1, s2); + BOOST_TEST(!s1.empty()); + + BOOST_TEST_EQ(sr.consume(7), 0u); // mid-header + auto const s3 = flatten(); + BOOST_TEST_EQ(s3, s1.substr(7)); + + BOOST_TEST_EQ(sr.consume(s3.size()), body.size()); + } + + void + testCancelAndResume() + { + // An interrupted drive leaves the accounting true to the + // wire; supplying the unconsumed remainder resumes it, + // and the serializer can even be moved mid-flight. + std::string const body(cfg.min_direct, 'x'); + auto req = make_request(); + serializer sr(cfg); + sr.start(&req); + + std::string_view rem(body); + std::string wire; + { + capy::const_buffer const b{ + rem.data(), rem.size() }; + system::error_code ec; + capy::const_buffer dest[dest_n]; + auto const bufs = sr.frame(dest, b, true, ec); + BOOST_TEST(!ec); + BOOST_TEST(!bufs.empty()); + + // take the header and half the chunk, then stop, as + // a cancelled write would + auto const k = + req.buffer().size() + 4 + body.size() / 2; + std::string flat; + for(auto cb : bufs) + flat.append( + static_cast(cb.data()), + cb.size()); + wire.append(flat.substr(0, k)); + rem.remove_prefix(sr.consume(k)); + } + BOOST_TEST_EQ(rem.size(), body.size() / 2); + + // resume on a moved-to serializer: the wire is resumable + // from the counters alone + serializer sr2(std::move(sr)); + BOOST_TEST(!write(sr2, wire, rem)); + BOOST_TEST(!write_eof(sr2, wire)); + + std::string expected(req.buffer()); + expected += "10\r\n" + body + "\r\n"; + expected += "0\r\n\r\n"; + BOOST_TEST_EQ(wire, expected); + } + + void + testCancelResumeByCommit() + { + // The other resume path: after a cancelled flight is + // consumed up to the wire's true position, the caller + // may supply the remainder through prepare/commit — an + // open chunk's owed count is indifferent to which region + // pays it. + std::string const body(cfg.min_direct, 'x'); + auto req = make_request(); + serializer sr(cfg); + sr.start(&req); + + std::string_view rem(body); + std::string wire; + { + capy::const_buffer const b{ + rem.data(), rem.size() }; + system::error_code ec; + capy::const_buffer dest[dest_n]; + auto const bufs = sr.frame(dest, b, true, ec); + BOOST_TEST(!ec); + + auto const k = + req.buffer().size() + 4 + body.size() / 2; + std::string flat; + for(auto cb : bufs) + flat.append( + static_cast(cb.data()), + cb.size()); + wire.append(flat.substr(0, k)); + rem.remove_prefix(sr.consume(k)); + } + + // commit the unconsumed remainder and drain + capy::mutable_buffer tmp[2]; + capy::buffer_copy( + sr.prepare(tmp), + capy::const_buffer(rem.data(), rem.size())); + sr.commit(rem.size()); + BOOST_TEST(!drain(sr, wire)); + BOOST_TEST(!write_eof(sr, wire)); + + std::string expected(req.buffer()); + expected += "10\r\n" + body + "\r\n"; + expected += "0\r\n\r\n"; + BOOST_TEST_EQ(wire, expected); + } + + void + testAbandonedChunkErrors() + { + // Declaring the end while an open chunk still owes + // octets that only caller memory can supply is the + // mismatch error: nothing can be handed out, and the + // wire is already committed to the chunk. + std::string const body(cfg.min_direct, 'x'); + auto req = make_request(); + serializer sr(cfg); + sr.start(&req); + + std::string wire; + { + capy::const_buffer const b{ + body.data(), body.size() }; + system::error_code ec; + capy::const_buffer dest[dest_n]; + auto const bufs = sr.frame(dest, b, true, ec); + BOOST_TEST(!ec); + + // the header and the chunk prefix, no body octet + auto const k = req.buffer().size() + 4; + std::string flat; + for(auto cb : bufs) + flat.append( + static_cast(cb.data()), + cb.size()); + wire.append(flat.substr(0, k)); + BOOST_TEST_EQ(sr.consume(k), 0u); + } + + // a drain pass is harmless and moves nothing + std::string before = wire; + BOOST_TEST(!drain(sr, wire)); + BOOST_TEST_EQ(wire, before); + + // ending the message without the owed octets fails, and + // keeps failing on repeat; the open chunk carries no + // declared size of the caller's, so it is the contract + // error, not the size mismatch + BOOST_TEST_EQ( + write_eof(sr, wire), + std::make_error_code( + std::errc::invalid_argument)); + BOOST_TEST_EQ( + write_eof(sr, wire), + std::make_error_code( + std::errc::invalid_argument)); + + // supplying the remainder again still resumes + BOOST_TEST(!write(sr, wire, body)); + BOOST_TEST(!write_eof(sr, wire)); + BOOST_TEST(sr.is_done()); + + std::string expected(req.buffer()); + expected += "10\r\n" + body + "\r\n"; + expected += "0\r\n\r\n"; + BOOST_TEST_EQ(wire, expected); + } + + void + testContentLengthCancelResume() + { + // A content-length flight interrupted mid-body resumes + // by supplying the remainder again; consumption + // reports are the caller's cursor. + std::string const body(cfg.min_direct, 'x'); + auto req = make_request(body.size()); + serializer sr(cfg); + sr.start(&req); + + std::string_view rem(body); + std::string wire; + { + capy::const_buffer const b{ + rem.data(), rem.size() }; + system::error_code ec; + capy::const_buffer dest[dest_n]; + auto const bufs = sr.frame(dest, b, true, ec); + BOOST_TEST(!ec); + + // header plus one body octet + auto const k = req.buffer().size() + 1; + std::string flat; + for(auto cb : bufs) + flat.append( + static_cast(cb.data()), + cb.size()); + wire.append(flat.substr(0, k)); + rem.remove_prefix(sr.consume(k)); + } + BOOST_TEST_EQ(rem.size(), body.size() - 1); + + BOOST_TEST(!write(sr, wire, rem)); + BOOST_TEST(!write_eof(sr, wire)); + BOOST_TEST_EQ( + wire, std::string(req.buffer()) + body); + } + + void + testManyDescriptors() + { + // No registration cap: a span of many descriptors is + // framed in one call when dest is large enough. + std::string const piece(4, 'x'); + capy::const_buffer bufs[24]; + for(auto& b : bufs) + b = capy::make_buffer(piece); + std::string const body( + std::size(bufs) * piece.size(), 'x'); + + auto req = make_request(body.size()); + serializer sr(cfg); + sr.start(&req); + + std::string wire; + system::error_code ec; + capy::const_buffer dest[32]; + auto const out = sr.frame(dest, bufs, false, ec); + BOOST_TEST(!ec); + BOOST_TEST_EQ( + out.size(), std::size(bufs) + 1); // + header + + std::size_t n = 0; + for(auto cb : out) + { + wire.append( + static_cast(cb.data()), cb.size()); + n += cb.size(); + } + BOOST_TEST_EQ(sr.consume(n), body.size()); + BOOST_TEST(sr.is_done()); + BOOST_TEST_EQ(wire, std::string(req.buffer()) + body); + } + + void + testOneSlotDest() + { + // A one-slot dest can always make progress: each debt + // item is representable alone, and the unplaced + // remainder is re-materialized next call. + std::string const body(cfg.min_direct, 'x'); + auto req = make_request(); + serializer sr(cfg); + sr.start(&req); + + fields trailer; + trailer.set("x-a", "1"); + sr.set_trailer(&trailer); + + std::string_view rem(body); + std::string wire; + bool more = true; + for(;;) + { + capy::const_buffer const b{ + rem.data(), rem.size() }; + system::error_code ec; + capy::const_buffer dest[1]; + auto const bufs = sr.frame(dest, b, more, ec); + more = false; + BOOST_TEST(!ec); + if(bufs.empty()) + { + rem.remove_prefix(sr.consume(0)); + if(sr.is_done()) + break; + continue; + } + std::string flat; + for(auto cb : bufs) + flat.append( + static_cast(cb.data()), + cb.size()); + wire.append(flat); + rem.remove_prefix(sr.consume(flat.size())); + } + + std::string expected(req.buffer()); + expected += "10\r\n" + body + "\r\n"; + expected += "0\r\n"; + expected += std::string(trailer.buffer()); + BOOST_TEST_EQ(wire, expected); + + // With the header already on the wire, the final input + // both ends the body and opens its chunk: the epilogue + // waits until the slot-starved body has been placed. + { + auto req2 = make_request(); + serializer sr2(cfg); + sr2.start(&req2); + + std::string wire2; + BOOST_TEST(!drain(sr2, wire2)); + BOOST_TEST_EQ(wire2, std::string(req2.buffer())); + + std::string_view rem2(body); + for(;;) + { + capy::const_buffer const b{ + rem2.data(), rem2.size() }; + system::error_code ec; + capy::const_buffer dest[1]; + auto const bufs = sr2.frame(dest, b, false, ec); + BOOST_TEST(!ec); + if(bufs.empty()) + { + rem2.remove_prefix(sr2.consume(0)); + if(sr2.is_done()) + break; + continue; + } + std::size_t n = 0; + for(auto cb : bufs) + { + wire2.append( + static_cast(cb.data()), + cb.size()); + n += cb.size(); + } + rem2.remove_prefix(sr2.consume(n)); + } + + std::string expected2(req2.buffer()); + expected2 += "10\r\n" + body + "\r\n0\r\n\r\n"; + BOOST_TEST_EQ(wire2, expected2); + } + } + + void + testStagedBeyondOpenChunk() + { + // Octets staged after a chunk was opened are no part of + // its debt: they wait behind it, and the call that ends + // the body — which sees them buffered with nothing owed + // — gives them a chunk of their own before the + // last-chunk. 40 == 0x28, 10 == 0xa. + std::string const b1(40, 'x'); + std::string const b2(10, 'y'); + auto req = make_request(); + serializer sr(cfg); + sr.start(&req); + + capy::mutable_buffer tmp[2]; + capy::buffer_copy( + sr.prepare(tmp), capy::make_buffer(b1)); + sr.commit(b1.size()); + BOOST_TEST(sr.should_drain()); + + // the wire takes the header, the prefix, and ten octets + std::string wire; + { + system::error_code ec; + capy::const_buffer dest[dest_n]; + auto const out = sr.frame(dest, true, ec); + BOOST_TEST(!ec); + BOOST_TEST_EQ(out.size(), 2u); + + std::size_t const n = req.buffer().size() + 4 + 10; + std::size_t taken = 0; + for(auto cb : out) + { + auto const k = (std::min)(n - taken, cb.size()); + wire.append( + static_cast(cb.data()), k); + if((taken += k) == n) + break; + } + BOOST_TEST_EQ(taken, n); + BOOST_TEST_EQ(sr.consume(n), 0u); + } + + // these belong to no chunk yet + capy::buffer_copy( + sr.prepare(tmp), capy::make_buffer(b2)); + sr.commit(b2.size()); + + // the open chunk is paid off first, then the leftovers + // are framed and the body ends + BOOST_TEST(!write_eof(sr, wire)); + BOOST_TEST(sr.is_done()); + + std::string expected(req.buffer()); + expected += "28\r\n" + b1; + expected += "\r\na\r\n" + b2; + expected += "\r\n0\r\n\r\n"; + BOOST_TEST(req.chunked()); + BOOST_TEST_EQ(wire, expected); + } + + void + testChunkedNarrowResumeWindow() + { + // The chunk is opened for the whole final input, then + // resumed in windows narrower than what it still owes: + // the last-chunk must wait until the debt is paid. + // 40 == 0x28. + std::string const body = make_body(40); + auto req = make_request(); + serializer sr(cfg); + sr.start(&req); + + // the header first, so the framing stays chunked + std::string wire; + BOOST_TEST(!drain(sr, wire)); + BOOST_TEST_EQ(wire, std::string(req.buffer())); + + std::string_view rem(body); + + // the whole body is offered once, so the chunk is opened + // for all forty octets; the wire takes the prefix and + // ten of them + { + system::error_code ec; + capy::const_buffer dest[dest_n]; + capy::const_buffer const b{ + rem.data(), rem.size() }; + auto const out = sr.frame(dest, b, false, ec); + BOOST_TEST(!ec); + // prefix + body + last-chunk + BOOST_TEST_EQ(out.size(), 3u); + + std::size_t const n = 4 + 10; + std::size_t taken = 0; + for(auto cb : out) + { + auto const k = (std::min)(n - taken, cb.size()); + wire.append( + static_cast(cb.data()), k); + if((taken += k) == n) + break; + } + BOOST_TEST_EQ(taken, n); + rem.remove_prefix(sr.consume(n)); + BOOST_TEST_EQ(rem.size(), 30u); + } + + // from here on only ten octets are offered per call, + // fewer than the chunk still owes + while(!sr.is_done()) + { + system::error_code ec; + capy::const_buffer dest[dest_n]; + capy::const_buffer const b{ rem.data(), + (std::min)(std::size_t(10), rem.size()) }; + auto const out = sr.frame(dest, b, false, ec); + BOOST_TEST(!ec); + std::size_t n = 0; + for(auto cb : out) + { + wire.append( + static_cast(cb.data()), + cb.size()); + n += cb.size(); + } + rem.remove_prefix(sr.consume(n)); + } + + BOOST_TEST(rem.empty()); + std::string expected(req.buffer()); + expected += "28\r\n" + body + "\r\n0\r\n\r\n"; + BOOST_TEST_EQ(wire, expected); + } + + void + testEmptyDescriptorsDoNotCount() + { + // Empty descriptors are dropped rather than framed, so a + // padded span still fits in one call. + std::string const piece(4, 'y'); + capy::const_buffer bufs[32]; + for(std::size_t i = 0; i != std::size(bufs); ++i) + bufs[i] = i % 2 + ? capy::const_buffer() + : capy::make_buffer(piece); + std::string const body( + std::size(bufs) / 2 * piece.size(), 'y'); + + auto req = make_request(body.size()); + serializer sr(cfg); + sr.start(&req); + + std::string wire; + system::error_code ec; + capy::const_buffer dest[32]; + auto const out = sr.frame(dest, bufs, false, ec); + BOOST_TEST(!ec); + + std::size_t n = 0; + for(auto cb : out) + { + BOOST_TEST(cb.size() != 0); + wire.append( + static_cast(cb.data()), cb.size()); + n += cb.size(); + } + BOOST_TEST_EQ(sr.consume(n), body.size()); + BOOST_TEST(sr.is_done()); + BOOST_TEST_EQ(wire, std::string(req.buffer()) + body); + } + + void + testConsumption() + { + // Consumption lands at two moments — octets absorbed + // into the staging buffer are consumed by the frame that + // absorbs them, and octets framed over caller memory are + // consumed when the wire takes them — but both are + // reported by the consume answering that frame. + auto req = make_request(); + serializer sr(cfg); + sr.start(&req); + + // absorbed, no debt: nothing to write, and the input is + // spent all the same + std::string wire; + std::string_view sv("abc"); + BOOST_TEST(!drive(sr, wire, sv, true)); + BOOST_TEST(sv.empty()); + BOOST_TEST(wire.empty()); + + // raise drain pressure, then absorb: the same call + // returns the flush and reports the absorption + capy::mutable_buffer tmp[2]; + auto const stage = make_body(40); + capy::buffer_copy( + sr.prepare(tmp), capy::make_buffer(stage)); + sr.commit(stage.size()); + BOOST_TEST(sr.should_drain()); + + capy::const_buffer const b{ "def", 3 }; + system::error_code ec; + capy::const_buffer dest[dest_n]; + auto const bufs = sr.frame(dest, b, true, ec); + BOOST_TEST(!ec); + BOOST_TEST(!bufs.empty()); + + std::size_t n = 0; + for(auto cb : bufs) + { + wire.append( + static_cast(cb.data()), cb.size()); + n += cb.size(); + } + // framing and staged octets report nothing; the + // absorbed input does + BOOST_TEST_EQ(sr.consume(n), 3u); + + BOOST_TEST(!write_eof(sr, wire)); + BOOST_TEST(sr.is_done()); + + // 46 == 0x2e + std::string expected(req.buffer()); + expected += "2e\r\nabc" + stage + "def\r\n"; + expected += "0\r\n\r\n"; + BOOST_TEST_EQ(wire, expected); + } + + void + testFrameWithoutBuffers() + { + // The overload that supplies nothing flushes the header + // on its own, and ends the body from the staged octets. + auto req = make_request(); + serializer sr(cfg); + sr.start(&req); + + capy::mutable_buffer tmp[2]; + capy::buffer_copy( + sr.prepare(tmp), capy::const_buffer("hello", 5)); + sr.commit(5); + BOOST_TEST(!sr.should_drain()); + + std::string wire; + BOOST_TEST(!drain_bufferless(sr, wire)); + BOOST_TEST(sr.is_header_done()); + BOOST_TEST_EQ(wire, std::string(req.buffer())); + + BOOST_TEST(!drain_bufferless(sr, wire, false)); + BOOST_TEST(sr.is_done()); + + // the header was already out, so the staged run goes + // out as a chunk rather than converting + BOOST_TEST(req.chunked()); + BOOST_TEST_EQ( + wire, std::string(req.buffer()) + + "5\r\nhello\r\n0\r\n\r\n"); + + // the same overload on a declared body still owing every + // octet: the header goes out and the debt waits for a + // supplying call + { + auto req2 = make_request(5); + serializer sr2(cfg); + sr2.start(&req2); + + std::string wire2; + BOOST_TEST(!drain_bufferless(sr2, wire2)); + BOOST_TEST(sr2.is_header_done()); + BOOST_TEST(!sr2.is_done()); + BOOST_TEST_EQ(wire2, std::string(req2.buffer())); + + BOOST_TEST(!write_eof(sr2, wire2, "hello")); + BOOST_TEST(sr2.is_done()); + BOOST_TEST_EQ( + wire2, std::string(req2.buffer()) + "hello"); + } + } + + void + testChunkedOctetsAfterEof() + { + // Once the body has ended there is no chunk left to + // frame octets with, and the last-chunk is already on + // the wire: supplying more violates the call contract. + auto req = make_request(); + serializer sr(cfg); + sr.start(&req); + + std::string wire; + BOOST_TEST(!drain(sr, wire)); + BOOST_TEST(!write_eof(sr, wire)); + BOOST_TEST(sr.is_done()); + BOOST_TEST_EQ( + wire, std::string(req.buffer()) + "0\r\n\r\n"); + + // a drain pass over a finished message moves nothing + BOOST_TEST(!drain(sr, wire)); + BOOST_TEST_EQ( + wire, std::string(req.buffer()) + "0\r\n\r\n"); + + std::string_view rem("hello"); + BOOST_TEST_EQ( + drive(sr, wire, rem, true), + std::make_error_code( + std::errc::invalid_argument)); + BOOST_TEST_EQ(rem.size(), 5u); + BOOST_TEST_EQ( + wire, std::string(req.buffer()) + "0\r\n\r\n"); + } + + void + testStagedRemainderDrain() + { + // A staged run the wire took only half of stays owed + // even though the window it left raises no drain + // pressure; a drain pass flushes the remainder, ahead of + // the caller's octets, which are supplied again. + std::string const staged = make_body(16); + std::string const body = make_body(16); + auto req = make_request(staged.size() + body.size()); + serializer sr(cfg); + sr.start(&req); + + capy::mutable_buffer tmp[2]; + capy::buffer_copy( + sr.prepare(tmp), capy::make_buffer(staged)); + sr.commit(staged.size()); + BOOST_TEST(!sr.should_drain()); + + std::string wire; + std::string_view rem(body); + { + system::error_code ec; + capy::const_buffer dest[dest_n]; + capy::const_buffer const b{ + rem.data(), rem.size() }; + auto const out = sr.frame(dest, b, true, ec); + BOOST_TEST(!ec); + // header + staged run + caller's octets + BOOST_TEST_EQ(out.size(), 3u); + + auto const n = req.buffer().size() + 8; + std::size_t taken = 0; + for(auto cb : out) + { + auto const k = (std::min)(n - taken, cb.size()); + wire.append( + static_cast(cb.data()), k); + if((taken += k) == n) + break; + } + BOOST_TEST_EQ(taken, n); + rem.remove_prefix(sr.consume(n)); + BOOST_TEST_EQ(rem.size(), body.size()); + } + + BOOST_TEST(!sr.should_drain()); + BOOST_TEST(!drain(sr, wire)); + BOOST_TEST_EQ( + wire, std::string(req.buffer()) + staged); + + BOOST_TEST(!write_eof(sr, wire, rem)); + BOOST_TEST(sr.is_done()); + BOOST_TEST_EQ( + wire, std::string(req.buffer()) + staged + body); + } + + void + testToEof() + { + // close-delimited response: no prefix, no epilogue, no + // size check; the final input completes the message and + // closing the connection is the caller's act + std::string const body(cfg.min_direct, 'x'); + { + response_head res; + serializer sr(cfg); + sr.start(&res); + + std::string wire; + BOOST_TEST(!write(sr, wire, body)); + BOOST_TEST(!write_eof(sr, wire, "hello")); + BOOST_TEST(sr.is_done()); + + BOOST_TEST(!res.content_length().has_value()); + BOOST_TEST_EQ( + wire, + std::string(res.buffer()) + body + "hello"); + } + + // fully staged before the header goes out: late framing + // converts to Content-Length, saving the connection + { + response_head res; + serializer sr(cfg); + sr.start(&res); + + std::string wire; + BOOST_TEST(!write_eof(sr, wire, "hello")); + + BOOST_TEST_EQ(res.content_length().value(), 5u); + BOOST_TEST_EQ( + wire, std::string(res.buffer()) + "hello"); + } + + // the header on the wire pins close-delimited framing; + // ending the body with nothing left to hand out + // completes the message all the same + { + response_head res; + serializer sr(cfg); + sr.start(&res); + + std::string wire; + BOOST_TEST(!drain(sr, wire)); + BOOST_TEST(sr.is_header_done()); + BOOST_TEST_EQ(wire, std::string(res.buffer())); + + BOOST_TEST(!write_eof(sr, wire)); + BOOST_TEST(sr.is_done()); + BOOST_TEST(!res.content_length().has_value()); + BOOST_TEST_EQ(wire, std::string(res.buffer())); + } + + // a request without framing headers has no body by + // definition; body bytes are rejected when supplied + { + request_head req; + serializer sr(cfg); + sr.start(&req); + + std::string wire; + BOOST_TEST_EQ( + write(sr, wire, body), + error::body_size_mismatch); + BOOST_TEST(wire.empty()); + } + } + + void + testToEofPartialWrites() + { + // close-delimited framing across partial writes: every + // split still hands out the whole body, and is_done() + // never fires early + std::string const body = make_body(20); + for(std::size_t step : { 1u, 3u, 7u }) + { + response_head res; + serializer sr(cfg); + sr.start(&res); + + std::string wire; + BOOST_TEST(!write(sr, wire, "abc", step)); + BOOST_TEST(!write(sr, wire, body, step)); + BOOST_TEST(!sr.is_done()); + BOOST_TEST(!write_eof(sr, wire, "hello", step)); + BOOST_TEST(sr.is_done()); + BOOST_TEST(!res.content_length().has_value()); + BOOST_TEST_EQ( + wire, + std::string(res.buffer()) + + "abc" + body + "hello"); + } + } + + void + testToEofBodyContract() + { + // close-delimited framing declares no size; the first + // more=false call fixes the body instead, and deviating + // from it afterwards is the call-contract error, not + // the size mismatch + std::string const body(cfg.min_direct, 'x'); + + // octets after the end: rejected, nothing framed + { + response_head res; + serializer sr(cfg); + sr.start(&res); + + std::string wire; + BOOST_TEST(!write(sr, wire, body)); + BOOST_TEST(!write_eof(sr, wire)); + BOOST_TEST(sr.is_done()); + + std::string_view rem("hello"); + BOOST_TEST_EQ( + drive(sr, wire, rem, true), + std::make_error_code( + std::errc::invalid_argument)); + BOOST_TEST_EQ(rem.size(), 5u); + BOOST_TEST(sr.is_done()); + BOOST_TEST_EQ( + wire, std::string(res.buffer()) + body); + } + + // a shorted re-supply after eof: the leftover debt is + // an error rather than a truncated completion, and the + // corrected remainder still resumes + { + response_head res; + serializer sr(cfg); + sr.start(&res); + + std::string wire; + BOOST_TEST(!drain(sr, wire)); + + std::string const b = make_body(10); + { + // declare eof; the wire takes nothing + system::error_code ec; + capy::const_buffer dest[dest_n]; + capy::const_buffer const cb{ + b.data(), b.size() }; + BOOST_TEST( + !sr.frame(dest, cb, false, ec).empty()); + BOOST_TEST(!ec); + BOOST_TEST_EQ(sr.consume(0), 0u); + } + + // only a prefix of the remainder comes back + std::string_view rem(b.data(), 4); + BOOST_TEST_EQ( + drive(sr, wire, rem, false), + std::make_error_code( + std::errc::invalid_argument)); + BOOST_TEST(rem.empty()); + BOOST_TEST(!sr.is_done()); + + // supplying the rest recovers + BOOST_TEST(!write_eof( + sr, wire, std::string_view(b.data() + 4, 6))); + BOOST_TEST(sr.is_done()); + BOOST_TEST_EQ( + wire, std::string(res.buffer()) + b); + } + } + + void + testToEofDestExhaustion() + { + // a fully conforming caller: short header write, then + // the whole body offered with a single descriptor slot, + // which the header remainder takes up. Settling here + // would truncate a body whose framing gives the peer no + // way to notice. + response_head res; + serializer sr(cfg); + sr.start(&res); + + std::string wire; + system::error_code ec; + + // the wire takes half the header + { + capy::const_buffer dest[dest_n]; + auto const out = sr.frame(dest, true, ec); + BOOST_TEST(!ec); + auto const half = res.buffer().size() / 2; + std::size_t n = 0; + for(auto cb : out) + { + auto const k = (std::min)(half - n, cb.size()); + wire.append( + static_cast(cb.data()), k); + if((n += k) == half) + break; + } + BOOST_TEST_EQ(sr.consume(half), 0u); + } + + // one slot: the header remainder claims it, no body + // octet is placed, and the message must not settle + std::string_view body("hello"); + { + capy::const_buffer one[1]; + capy::const_buffer const cb{ + body.data(), body.size() }; + auto const out = sr.frame({ one, 1 }, cb, false, ec); + BOOST_TEST(!ec); + BOOST_TEST_EQ(out.size(), 1u); + std::string flat; + for(auto b : out) + flat.append( + static_cast(b.data()), + b.size()); + wire.append(flat); + body.remove_prefix(sr.consume(flat.size())); + } + BOOST_TEST_EQ(body.size(), 5u); + BOOST_TEST(!sr.is_done()); + BOOST_TEST_EQ(wire, std::string(res.buffer())); + + // the conforming loop continues and completes + BOOST_TEST(!write_eof(sr, wire, body)); + BOOST_TEST(sr.is_done()); + BOOST_TEST_EQ( + wire, std::string(res.buffer()) + "hello"); + } + + void + testEmptyDest() + { + // A zero-slot dest places nothing, which is not a + // statement about the body: the call must neither fail + // the message nor settle it, and the loop must complete + // once a slot is offered. + + // content-length, whole body declared at once + { + auto req = make_request(5); + serializer sr(cfg); + sr.start(&req); + + std::string_view body("hello"); + { + system::error_code ec; + capy::const_buffer const cb{ + body.data(), body.size() }; + BOOST_TEST( + sr.frame({}, cb, false, ec).empty()); + BOOST_TEST(!ec); + // nothing was placed, so nothing is released + body.remove_prefix(sr.consume(0)); + } + BOOST_TEST_EQ(body.size(), 5u); + BOOST_TEST(!sr.is_done()); + + std::string wire; + BOOST_TEST(!write_eof(sr, wire, body)); + BOOST_TEST(sr.is_done()); + BOOST_TEST_EQ( + wire, std::string(req.buffer()) + "hello"); + } + + // chunked, with a trailer and an open chunk's debt + { + auto req = make_request(); + fields trailer; + trailer.set("x", "1"); + serializer sr(cfg); + sr.start(&req); + sr.set_trailer(&trailer); + + std::string wire; + std::string_view body("hello"); + // small enough to stage: no chunk is opened yet + BOOST_TEST(!write(sr, wire, body.substr(0, 2))); + // the empty-dest call opens the chunk covering the + // staged and the supplied octets + { + system::error_code ec; + capy::const_buffer const cb{ + body.data() + 2, 3 }; + BOOST_TEST( + sr.frame({}, cb, false, ec).empty()); + BOOST_TEST(!ec); + BOOST_TEST_EQ(sr.consume(0), 0u); + } + BOOST_TEST(!sr.is_done()); + BOOST_TEST(!write_eof(sr, wire, body.substr(2))); + BOOST_TEST(sr.is_done()); + BOOST_TEST_EQ( + wire, + std::string(req.buffer()) + + "5\r\nhello\r\n0\r\nx: 1\r\n\r\n"); + } + + // the bufferless overload, declaring an empty body + { + response_head res; + serializer sr(cfg); + sr.start(&res); + + { + system::error_code ec; + BOOST_TEST(sr.frame({}, false, ec).empty()); + BOOST_TEST(!ec); + BOOST_TEST_EQ(sr.consume(0), 0u); + } + BOOST_TEST(!sr.is_done()); + + std::string wire; + BOOST_TEST(!drain_bufferless(sr, wire, false)); + BOOST_TEST(sr.is_done()); + BOOST_TEST_EQ(wire, std::string(res.buffer())); + } + + // a genuinely short body is still diagnosed + { + auto req = make_request(5); + serializer sr(cfg); + sr.start(&req); + + std::string wire; + std::string_view body("hel"); + BOOST_TEST_EQ( + drive(sr, wire, body, false), + error::body_size_mismatch); + } + } + + void + testChunkedOctetsAfterEofDeferred() + { + // while an open chunk's debt stands, a re-supply of the + // owed octets is indistinguishable from new data, so the + // post-eof check waits for the debt to clear and rejects + // only the surplus, one call later + auto req = make_request(); + fields trailer; + trailer.set("x", "1"); + serializer sr(cfg); + sr.start(&req); + sr.set_trailer(&trailer); + + std::string wire; + // end the body; the wire takes nothing, so the whole + // chunk stands as debt + std::string_view body("hello"); + { + system::error_code ec; + capy::const_buffer dest[dest_n]; + capy::const_buffer const cb{ + body.data(), body.size() }; + BOOST_TEST(!sr.frame(dest, cb, false, ec).empty()); + BOOST_TEST(!ec); + BOOST_TEST_EQ(sr.consume(0), 0u); + } + // a longer re-supply while the debt stands: the owed + // prefix is accepted, the surplus is not — and no error + // yet + { + system::error_code ec; + capy::const_buffer dest[dest_n]; + std::string_view rem("helloworld"); + capy::const_buffer const cb{ + rem.data(), rem.size() }; + auto const out = sr.frame(dest, cb, false, ec); + BOOST_TEST(!ec); + std::string flat; + for(auto b : out) + flat.append( + static_cast(b.data()), + b.size()); + wire.append(flat); + BOOST_TEST_EQ(sr.consume(flat.size()), 5u); + } + // with the debt cleared, the surplus is rejected + { + std::string_view rem("world"); + BOOST_TEST_EQ( + drive(sr, wire, rem, false), + std::make_error_code( + std::errc::invalid_argument)); + BOOST_TEST_EQ(rem.size(), 5u); + } + // dropping the surplus recovers + BOOST_TEST(!write_eof(sr, wire)); + BOOST_TEST(sr.is_done()); + BOOST_TEST_EQ( + wire, + std::string(req.buffer()) + + "5\r\nhello\r\n0\r\nx: 1\r\n\r\n"); + } + + void + testTrailer() + { + std::string const body(cfg.min_direct, 'x'); + + // basic: the trailer section replaces the bare CRLF + // after the last chunk + { + auto req = make_request(); + serializer sr(cfg); + sr.start(&req); + + fields trailer; + sr.set_trailer(&trailer); + + std::string wire; + BOOST_TEST(!write(sr, wire, body)); + + // fill-late: values computed from the body land + // just before the final input + trailer.set("x-checksum", "abc123"); + + BOOST_TEST(!write_eof(sr, wire)); + BOOST_TEST(sr.is_done()); + + std::string expected(req.buffer()); + expected += "10\r\n" + body + "\r\n"; + expected += "0\r\n"; + expected += "x-checksum: abc123\r\n\r\n"; + BOOST_TEST_EQ(wire, expected); + } + + // a set trailer suppresses the small-body conversion + // to Content-Length + { + auto req = make_request(); + serializer sr(cfg); + sr.start(&req); + + fields trailer; + trailer.set("x-a", "1"); + trailer.set("x-b", "2"); + sr.set_trailer(&trailer); + + std::string wire; + BOOST_TEST(!write_eof(sr, wire, "hello")); + + BOOST_TEST(req.chunked()); + std::string expected(req.buffer()); + expected += "5\r\nhello\r\n"; + expected += "0\r\n"; + expected += std::string(trailer.buffer()); + BOOST_TEST_EQ(wire, expected); + } + + // an empty container and no trailer are byte-identical + { + fields const empty; + BOOST_TEST_EQ( + std::string(empty.buffer()), "\r\n"); + + auto req1 = make_request(); + auto req2 = make_request(); + serializer sr1(cfg); + serializer sr2(cfg); + sr1.start(&req1); + sr2.start(&req2); + sr2.set_trailer(&empty); + + std::string w1, w2; + BOOST_TEST(!write(sr1, w1, body)); + BOOST_TEST(!write(sr2, w2, body)); + BOOST_TEST(!write_eof(sr1, w1)); + BOOST_TEST(!write_eof(sr2, w2)); + BOOST_TEST_EQ(w1, w2); + } + + // set_trailer(nullptr) clears + { + auto req = make_request(5); + serializer sr(cfg); + sr.start(&req); + + fields trailer; + sr.set_trailer(&trailer); + sr.set_trailer(nullptr); + + std::string wire; + BOOST_TEST(!write_eof(sr, wire, "hello")); + BOOST_TEST(sr.is_done()); + } + } + + void + testTrailerIgnoredWhenNotChunked() + { + fields trailer; + trailer.set("x-a", "1"); + + // explicit Content-Length: the trailer is + // silently discarded + { + auto req = make_request(5); + serializer sr(cfg); + sr.start(&req); + sr.set_trailer(&trailer); + + std::string wire; + BOOST_TEST(!write_eof(sr, wire, "hello")); + BOOST_TEST(sr.is_done()); + BOOST_TEST_EQ( + wire, std::string(req.buffer()) + "hello"); + } + + // close-delimited framing cannot carry + // trailers either + { + response_head res; + serializer sr(cfg); + sr.start(&res); + sr.set_trailer(&trailer); + + std::string wire; + BOOST_TEST(!write_eof(sr, wire, "hello")); + BOOST_TEST(sr.is_done()); + BOOST_TEST_EQ( + wire, std::string(res.buffer()) + "hello"); + } + + // head mode suppresses the body and its framing + { + auto req = make_request(); + serializer sr(cfg); + sr.start(&req, nullptr, true); + sr.set_trailer(&trailer); + + std::string wire; + BOOST_TEST(!write_eof(sr, wire)); + BOOST_TEST(sr.is_done()); + BOOST_TEST_EQ(wire, std::string(req.buffer())); + } + } + + void + testTrailerWithEncoder() + { + // encoder footer and HTTP trailer section compose: the + // footer is payload of the last data chunk, the trailer + // section follows the last-chunk + std::string const body = make_body(8); + auto req = make_request(); + req.set(http::field::content_encoding, "test"); + test_encoder enc("eof!"); + serializer sr(cfg); + sr.start(&req, &enc); + + fields trailer; + trailer.set("x-digest", "42"); + sr.set_trailer(&trailer); + + std::string wire; + BOOST_TEST(!write_eof(sr, wire, body)); + BOOST_TEST(sr.is_done()); + BOOST_TEST(enc.finished); + + // the trailer suppressed the conversion, so the encoded + // bytes and footer went out chunked; 12 == 0xc + BOOST_TEST(req.chunked()); + std::string expected(req.buffer()); + expected += "c\r\n" + encoded(body) + "eof!"; + expected += "\r\n0\r\n"; + expected += std::string(trailer.buffer()); + BOOST_TEST_EQ(wire, expected); + } + + void + testEncoderSmallBodyDropsEncoder() + { + // A body that stays below the encoder threshold skips + // encoding entirely: the encoder is dropped at eof, the + // Content-Encoding header is removed, and the identity + // body is sent with a Content-Length. + auto req = make_request(); + req.set(http::field::content_encoding, "test"); + test_encoder enc; + serializer sr(cfg); + sr.start(&req, &enc); + + std::string wire; + BOOST_TEST(!write(sr, wire, "hello")); + BOOST_TEST_EQ(enc.calls, 0u); + BOOST_TEST(wire.empty()); + + BOOST_TEST(!write_eof(sr, wire)); + BOOST_TEST(sr.is_done()); + + BOOST_TEST_EQ(enc.calls, 0u); + BOOST_TEST(!req.chunked()); + BOOST_TEST_EQ(req.content_length().value(), 5u); + BOOST_TEST( + wire.find("Content-Encoding") == + std::string::npos); + BOOST_TEST_EQ( + wire, std::string(req.buffer()) + "hello"); + } + + void + testEncoderKeptAfterHeaderFlush() + { + // Once the header is on the wire its Content-Encoding + // is a promise: a small body no longer drops the + // encoder, and the framing stays as declared. + auto req = make_request(); + req.set(http::field::content_encoding, "test"); + test_encoder enc; + serializer sr(cfg); + sr.start(&req, &enc); + + std::string wire; + BOOST_TEST(!sr.is_header_done()); + BOOST_TEST(!drain(sr, wire)); + BOOST_TEST_EQ(wire, std::string(req.buffer())); + BOOST_TEST(sr.is_header_done()); + + BOOST_TEST(!write_eof(sr, wire, "hello")); + BOOST_TEST(sr.is_done()); + BOOST_TEST(enc.finished); + BOOST_TEST(req.chunked()); + BOOST_TEST_EQ( + wire, std::string(req.buffer()) + + "5\r\nifmmp\r\n0\r\n\r\n"); + } + + void + testEncoderEmptyBody() + { + auto req = make_request(); + req.set(http::field::content_encoding, "test"); + test_encoder enc; + serializer sr(cfg); + sr.start(&req, &enc); + + std::string wire; + BOOST_TEST(!write_eof(sr, wire)); + BOOST_TEST(sr.is_done()); + + // an empty body is below any threshold: the encoder is + // dropped and the request converts to Content-Length: 0 + BOOST_TEST_EQ(enc.calls, 0u); + BOOST_TEST(!req.chunked()); + BOOST_TEST( + wire.find("Content-Length: 0\r\n") != + std::string::npos); + BOOST_TEST( + wire.find("Content-Encoding") == + std::string::npos); + BOOST_TEST_EQ(wire, std::string(req.buffer())); + } + + void + testTrailerWithDroppedEncoder() + { + // A trailer suppresses the conversion to Content-Length, + // so a dropped encoder leaves the message chunked with + // its octets still staged in the encoder's region: the + // chunk prefix is written backwards below them, into the + // output buffer the encoder never touched. + auto req = make_request(); + req.set(http::field::content_encoding, "test"); + test_encoder enc; + serializer sr(cfg); + sr.start(&req, &enc); + + fields trailer; + trailer.set("x-digest", "42"); + sr.set_trailer(&trailer); + + capy::mutable_buffer tmp[2]; + auto const n = capy::buffer_copy( + sr.prepare(tmp), capy::const_buffer("hello", 5)); + BOOST_TEST_EQ(n, 5u); + sr.commit(n); + + std::string wire; + BOOST_TEST(!write_eof(sr, wire)); + BOOST_TEST(sr.is_done()); + + BOOST_TEST_EQ(enc.calls, 0u); + BOOST_TEST(req.chunked()); + BOOST_TEST( + wire.find("Content-Encoding") == + std::string::npos); + BOOST_TEST_EQ( + wire, + std::string(req.buffer()) + "5\r\nhello\r\n0\r\n" + + std::string(trailer.buffer())); + } + + void + testEncoderPrepareCommitEof() + { + // With an encoder, prepare() exposes the encoder's + // staging buffer rather than the output buffer. + auto req = make_request(); + req.set(http::field::content_encoding, "test"); + test_encoder enc; + serializer sr(cfg); + sr.start(&req, &enc); + + capy::mutable_buffer tmp[2]; + auto const dest = sr.prepare(tmp); + BOOST_TEST_EQ( + capy::buffer_size(dest), cfg.enc_buffer); + + auto const n = capy::buffer_copy( + dest, capy::const_buffer("hello", 5)); + BOOST_TEST_EQ(n, 5u); + sr.commit(n); + + // below the threshold at eof: the encoder is dropped + // and the staged bytes are sent as-is + std::string wire; + BOOST_TEST(!write_eof(sr, wire)); + BOOST_TEST(sr.is_done()); + + BOOST_TEST_EQ(enc.calls, 0u); + BOOST_TEST(!req.chunked()); + BOOST_TEST_EQ(req.content_length().value(), 5u); + BOOST_TEST_EQ( + wire, std::string(req.buffer()) + "hello"); + } + + void + testEncoderThreshold() + { + // exactly at the threshold: the encoder is kept + { + std::string const body = make_body(8); + auto req = make_request(); + req.set(http::field::content_encoding, "test"); + test_encoder enc; + serializer sr(cfg); + sr.start(&req, &enc); + + std::string wire; + BOOST_TEST(!write_eof(sr, wire, body)); + BOOST_TEST(sr.is_done()); + + // the whole encoded output was buffered before the + // header went out, so chunked is replaced with the + // encoded length while Content-Encoding is kept + BOOST_TEST(enc.finished); + BOOST_TEST(!req.chunked()); + BOOST_TEST_EQ(req.content_length().value(), 8u); + BOOST_TEST( + wire.find("Content-Encoding: test\r\n") != + std::string::npos); + BOOST_TEST_EQ( + wire, + std::string(req.buffer()) + encoded(body)); + } + + // one byte below the threshold: the encoder is dropped + { + std::string const body = make_body(7); + auto req = make_request(); + req.set(http::field::content_encoding, "test"); + test_encoder enc; + serializer sr(cfg); + sr.start(&req, &enc); + + std::string wire; + BOOST_TEST(!write_eof(sr, wire, body)); + + BOOST_TEST_EQ(enc.calls, 0u); + BOOST_TEST_EQ(req.content_length().value(), 7u); + BOOST_TEST( + wire.find("Content-Encoding") == + std::string::npos); + BOOST_TEST_EQ( + wire, std::string(req.buffer()) + body); + } + } + + void + testEncoderStagedAndTail() + { + // Staged bytes and the caller's final input together + // reach the threshold at eof, so the encoder is kept; it + // consumes the staged bytes first, then the input, whose + // intake is reported through consume's return. + auto req = make_request(); + req.set(http::field::content_encoding, "test"); + test_encoder enc; + serializer sr(cfg); + sr.start(&req, &enc); + + std::string wire; + BOOST_TEST(!write(sr, wire, "abc")); + BOOST_TEST_EQ(enc.calls, 0u); + + BOOST_TEST(!write_eof(sr, wire, "defgh")); + BOOST_TEST(sr.is_done()); + + BOOST_TEST(enc.finished); + BOOST_TEST_EQ(req.content_length().value(), 8u); + BOOST_TEST_EQ( + wire, + std::string(req.buffer()) + encoded("abcdefgh")); + } + + void + testEncoderLargeBodyChunked() + { + // A body larger than the output buffer forces the + // header out early and streams encoded chunks; once the + // encoder has started, even small writes are encoded + // rather than coalesced. Chunk payloads hold encoded + // bytes; 64 == 0x40. + std::string const body = make_body(200); + auto req = make_request(); + req.set(http::field::content_encoding, "test"); + test_encoder enc; + serializer sr(cfg); + sr.start(&req, &enc); + + std::string wire; + BOOST_TEST(!write(sr, wire, body)); + BOOST_TEST(!sr.is_done()); + BOOST_TEST(!write_eof(sr, wire)); + BOOST_TEST(sr.is_done()); + + auto const enc_body = encoded(body); + std::string expected(req.buffer()); + expected += "40\r\n" + enc_body.substr(0, 64); + expected += "\r\n40\r\n" + enc_body.substr(64, 64); + expected += "\r\n40\r\n" + enc_body.substr(128, 64); + expected += "\r\n8\r\n" + enc_body.substr(192, 8); + expected += "\r\n0\r\n\r\n"; + BOOST_TEST(enc.finished); + BOOST_TEST(req.chunked()); + BOOST_TEST( + wire.find("Content-Encoding: test\r\n") != + std::string::npos); + BOOST_TEST_EQ(wire, expected); + } + + void + testEncoderLargeBodyChunkedEof() + { + // the same oversized body arrives whole on the + // declaring call: the encoder leaves a tail undigested + // each time the output buffer fills, and the + // re-supplied remainder must not trip the + // octets-after-eof check + std::string const body = make_body(200); + auto req = make_request(); + req.set(http::field::content_encoding, "test"); + test_encoder enc; + serializer sr(cfg); + sr.start(&req, &enc); + + std::string wire; + BOOST_TEST(!write_eof(sr, wire, body)); + BOOST_TEST(sr.is_done()); + BOOST_TEST(enc.finished); + + auto const enc_body = encoded(body); + std::string expected(req.buffer()); + expected += "40\r\n" + enc_body.substr(0, 64); + expected += "\r\n40\r\n" + enc_body.substr(64, 64); + expected += "\r\n40\r\n" + enc_body.substr(128, 64); + expected += "\r\n8\r\n" + enc_body.substr(192, 8); + expected += "\r\n0\r\n\r\n"; + BOOST_TEST_EQ(wire, expected); + } + + void + testEncoderCommit() + { + // commit() stages input for the encoder; encoded bytes + // stay buffered until the output buffer fills, and the + // final flush carries the remainder. + auto req = make_request(); + req.set(http::field::content_encoding, "test"); + test_encoder enc; + serializer sr(cfg); + sr.start(&req, &enc); + + std::string wire; + capy::mutable_buffer tmp[2]; + + // 4 commits of 20 raw bytes; the encoder's output + // crosses stage_buffer on the last one + std::string fed; + for(int i = 0; i != 4; ++i) + { + auto const piece = make_body(20); + capy::buffer_copy( + sr.prepare(tmp), capy::make_buffer(piece)); + sr.commit(piece.size()); + fed += piece; + BOOST_TEST(!drain(sr, wire)); + } + BOOST_TEST(enc.calls != 0); + + // only the first full output unit went out; 64 == 0x40 + auto const enc_body = encoded(fed); + std::string expected(req.buffer()); + expected += "40\r\n" + enc_body.substr(0, 64); + BOOST_TEST_EQ(wire, expected); + + BOOST_TEST(!write_eof(sr, wire)); + BOOST_TEST(sr.is_done()); + + expected += "\r\n10\r\n" + enc_body.substr(64, 16); + expected += "\r\n0\r\n\r\n"; + BOOST_TEST_EQ(wire, expected); + } + + void + testEncoderFooterSpansFlush() + { + // The encoder's footer does not fit the output buffer + // in one pass; it spans a flush and lands in a second + // chunk. 64 == 0x40. + std::string const body = make_body(60); + auto req = make_request(); + req.set(http::field::content_encoding, "test"); + test_encoder enc("0123456789"); + serializer sr(cfg); + sr.start(&req, &enc); + + std::string wire; + BOOST_TEST(!write_eof(sr, wire, body)); + BOOST_TEST(sr.is_done()); + BOOST_TEST(enc.finished); + + auto const enc_body = encoded(body); + std::string expected(req.buffer()); + expected += "40\r\n" + enc_body + "0123"; + expected += "\r\n6\r\n456789"; + expected += "\r\n0\r\n\r\n"; + BOOST_TEST(req.chunked()); + BOOST_TEST_EQ(wire, expected); + } + + void + testEncoderContentLength() + { + // an explicitly sized message is encoded without any + // chunked framing; the declared size covers the encoded + // output plus the footer + std::string const body = make_body(8); + auto req = make_request(12); + req.set(http::field::content_encoding, "test"); + test_encoder enc("eof!"); + serializer sr(cfg); + sr.start(&req, &enc); + + std::string wire; + BOOST_TEST(!write_eof(sr, wire, body)); + BOOST_TEST(sr.is_done()); + BOOST_TEST(enc.finished); + BOOST_TEST_EQ(req.content_length().value(), 12u); + BOOST_TEST_EQ( + wire, + std::string(req.buffer()) + + encoded(body) + "eof!"); + } + + void + testEncoderContentLengthMismatch() + { + // encoded output larger than declared + { + std::string const body = make_body(8); + auto req = make_request(5); + req.set(http::field::content_encoding, "test"); + test_encoder enc("eof!"); + serializer sr(cfg); + sr.start(&req, &enc); + + std::string wire; + BOOST_TEST_EQ( + write_eof(sr, wire, body), + error::body_size_mismatch); + BOOST_TEST(wire.empty()); + + // the error latches: is_done() reports the message + // over, and a retry — even without an intervening + // consume — eats nothing and fails + BOOST_TEST(sr.is_done()); + auto const calls = enc.calls; + std::string_view rem = "xyz"; + BOOST_TEST_EQ( + drive(sr, wire, rem, false), + std::make_error_code( + std::errc::state_not_recoverable)); + BOOST_TEST_EQ(rem, "xyz"); + BOOST_TEST(wire.empty()); + BOOST_TEST_EQ(enc.calls, calls); + } + + // encoded output smaller than declared + { + std::string const body = make_body(8); + auto req = make_request(100); + req.set(http::field::content_encoding, "test"); + test_encoder enc("eof!"); + serializer sr(cfg); + sr.start(&req, &enc); + + std::string wire; + BOOST_TEST_EQ( + write_eof(sr, wire, body), + error::body_size_mismatch); + BOOST_TEST(sr.is_done()); + + // supplying the exact shortfall raw would satisfy + // the quota arithmetic; the latch keeps it from + // completing a half-encoded body silently + std::string const shortfall(88, 'x'); + std::string_view rem = shortfall; + BOOST_TEST_EQ( + drive(sr, wire, rem, false), + std::make_error_code( + std::errc::state_not_recoverable)); + BOOST_TEST_EQ(rem.size(), shortfall.size()); + BOOST_TEST(wire.empty()); + } + } + + void + testEncoderErrorRetryNoConsume() + { + // the encoder digests input before the size check + // fires; a retry without an intervening consume must + // reach the latch, not the digested-input assert, and + // the failing call's accounting stays intact + std::string const body = make_body(8); + auto req = make_request(5); + req.set(http::field::content_encoding, "test"); + test_encoder enc("eof!"); + serializer sr(cfg); + sr.start(&req, &enc); + + system::error_code ec; + capy::const_buffer dest[dest_n]; + capy::const_buffer const b{ + body.data(), body.size() }; + auto out = sr.frame(dest, b, false, ec); + BOOST_TEST_EQ(ec, error::body_size_mismatch); + BOOST_TEST(out.empty()); + + out = sr.frame(dest, b, false, ec); + BOOST_TEST_EQ( + ec, + std::make_error_code( + std::errc::state_not_recoverable)); + BOOST_TEST(out.empty()); + + BOOST_TEST_EQ(sr.consume(0), body.size()); + } + + void + testEncoderFailure() + { + // an encoder error surfaces from frame and poisons the + // message + std::string const body = make_body(20); + auto req = make_request(); + req.set(http::field::content_encoding, "test"); + test_encoder enc; + enc.fail = std::make_error_code( + std::errc::invalid_argument); + serializer sr(cfg); + sr.start(&req, &enc); + + std::string wire; + BOOST_TEST_EQ( + write_eof(sr, wire, body), enc.fail); + BOOST_TEST(wire.empty()); + + // terminal: the encoder is never called again, and + // retries fail + BOOST_TEST(sr.is_done()); + auto const calls = enc.calls; + std::string_view rem = body; + BOOST_TEST_EQ( + drive(sr, wire, rem, false), + std::make_error_code( + std::errc::state_not_recoverable)); + BOOST_TEST(wire.empty()); + BOOST_TEST_EQ(enc.calls, calls); + } + + void + testEncoderStagedFailure() + { + // the same error, raised while the staging buffer — not + // the caller's memory — is what the encoder is draining + auto req = make_request(); + req.set(http::field::content_encoding, "test"); + test_encoder enc; + enc.fail = std::make_error_code( + std::errc::invalid_argument); + serializer sr(cfg); + sr.start(&req, &enc); + + capy::mutable_buffer tmp[2]; + auto const dest = sr.prepare(tmp); + // an installed encoder takes the staging buffer over for + // its output, leaving enc_buffer for its input + BOOST_TEST_EQ( + capy::buffer_size(dest), cfg.enc_buffer); + std::string const staged = make_body(20); + capy::buffer_copy(dest, capy::make_buffer(staged)); + sr.commit(staged.size()); + + std::string wire; + BOOST_TEST_EQ(write_eof(sr, wire), enc.fail); + BOOST_TEST(wire.empty()); + BOOST_TEST(sr.is_done()); + } + + void + testEncoderFailureAtEof() + { + // the encoder accepts the body and then fails while + // finishing the stream + std::string const body = make_body(20); + auto req = make_request(); + req.set(http::field::content_encoding, "test"); + test_encoder enc; + serializer sr(cfg); + sr.start(&req, &enc); + + std::string wire; + BOOST_TEST(!write(sr, wire, body)); + + enc.fail = std::make_error_code( + std::errc::invalid_argument); + BOOST_TEST_EQ(write_eof(sr, wire), enc.fail); + BOOST_TEST(sr.is_done()); + } + + void + testEncoderToEof() + { + // close-delimited framing, pinned by the flushed header: + // encoded output goes out as it is produced, against no + // declared size at all + std::string const body = make_body(20); + response_head res; + res.set(http::field::content_encoding, "test"); + test_encoder enc("eof!"); + serializer sr(cfg); + sr.start(&res, &enc); + + std::string wire; + BOOST_TEST(!drain(sr, wire)); + BOOST_TEST_EQ(wire, std::string(res.buffer())); + + BOOST_TEST(!write_eof(sr, wire, body)); + BOOST_TEST(sr.is_done()); + BOOST_TEST(enc.finished); + BOOST_TEST(!res.content_length().has_value()); + BOOST_TEST(!res.chunked()); + BOOST_TEST_EQ( + wire, + std::string(res.buffer()) + encoded(body) + "eof!"); + } + + void + testEncoderToEofOctetsAfterEof() + { + // a finished encoder must not let post-eof octets ride + // the close-delimited framing unencoded; the framed + // output stays exactly the encoded stream + std::string const body = make_body(20); + response_head res; + res.set(http::field::content_encoding, "test"); + test_encoder enc("eof!"); + serializer sr(cfg); + sr.start(&res, &enc); + + std::string wire; + BOOST_TEST(!drain(sr, wire)); + BOOST_TEST(!write_eof(sr, wire, body, 3)); + BOOST_TEST(sr.is_done()); + BOOST_TEST(enc.finished); + + std::string_view rem("hello"); + BOOST_TEST_EQ( + drive(sr, wire, rem, true), + std::make_error_code( + std::errc::invalid_argument)); + BOOST_TEST_EQ(rem.size(), 5u); + BOOST_TEST_EQ( + wire, + std::string(res.buffer()) + encoded(body) + "eof!"); + } + + void + testEncoderToEofEmptyBody() + { + // a pinned close-delimited stream whose encoder receives + // no input at all still completes; the empty case must + // not read as leftover debt + response_head res; + res.set(http::field::content_encoding, "test"); + test_encoder enc; + serializer sr(cfg); + sr.start(&res, &enc); + + std::string wire; + BOOST_TEST(!drain(sr, wire)); + BOOST_TEST(!write_eof(sr, wire)); + BOOST_TEST(sr.is_done()); + BOOST_TEST(enc.finished); + BOOST_TEST_EQ(wire, std::string(res.buffer())); + } + + void + testEncoderContentLengthPartialConsume() + { + // An encoded content-length flight drained one octet at + // a time: the encoder refills its output buffer as the + // wire frees it, across a partially consumed run, and + // the declared size still comes out exact. + std::string const body = make_body(70); + auto req = make_request(74); + req.set(http::field::content_encoding, "test"); + test_encoder enc("eof!"); + serializer sr(cfg); + sr.start(&req, &enc); + + std::string wire; + BOOST_TEST(!write_eof(sr, wire, body, 1)); + BOOST_TEST(sr.is_done()); + BOOST_TEST(enc.finished); + BOOST_TEST_EQ(req.content_length().value(), 74u); + BOOST_TEST_EQ( + wire, + std::string(req.buffer()) + encoded(body) + "eof!"); + } + + void + testEncoderChunkedEmptyFinalFlush() + { + // The wire emptied the encoder's output buffer before + // the body ended, so the finishing pass produces nothing + // and opens no chunk of its own; 64 == 0x40. + std::string const body = make_body(cfg.stage_buffer); + auto req = make_request(); + req.set(http::field::content_encoding, "test"); + test_encoder enc; + serializer sr(cfg); + sr.start(&req, &enc); + + std::string wire; + BOOST_TEST(!drain(sr, wire)); + BOOST_TEST_EQ(wire, std::string(req.buffer())); + + BOOST_TEST(!write(sr, wire, body)); + std::string expected(req.buffer()); + expected += "40\r\n" + encoded(body); + BOOST_TEST_EQ(wire, expected); + + BOOST_TEST(!write_eof(sr, wire)); + BOOST_TEST(sr.is_done()); + BOOST_TEST(enc.finished); + BOOST_TEST(req.chunked()); + expected += "\r\n0\r\n\r\n"; + BOOST_TEST_EQ(wire, expected); + } + + void + testEncoderDribbles() + { + // An encoder is free to produce less than the room it + // was given, footer included: the finishing pass is + // re-entered until the stream ends rather than being + // taken as finished. 14 == 0xe. + std::string const body = make_body(10); + auto req = make_request(); + req.set(http::field::content_encoding, "test"); + test_encoder enc("abcd"); + enc.out_limit = 1; + serializer sr(cfg); + sr.start(&req, &enc); + + std::string wire; + BOOST_TEST(!drain(sr, wire)); + BOOST_TEST_EQ(wire, std::string(req.buffer())); + + BOOST_TEST(!write_eof(sr, wire, body)); + BOOST_TEST(sr.is_done()); + BOOST_TEST(enc.finished); + BOOST_TEST(req.chunked()); + + std::string expected(req.buffer()); + expected += "e\r\n" + encoded(body) + "abcd"; + expected += "\r\n0\r\n\r\n"; + BOOST_TEST_EQ(wire, expected); + } + + void + testEncoderMultipleBuffers() + { + auto req = make_request(); + req.set(http::field::content_encoding, "test"); + test_encoder enc; + serializer sr(cfg); + sr.start(&req, &enc); + + std::string const b1 = make_body(6); + std::string const b2 = make_body(6); + capy::const_buffer const bufs[2] = { + capy::make_buffer(b1), + capy::make_buffer(b2) }; + + std::string wire; + system::error_code ec; + capy::const_buffer dest[dest_n]; + auto const out = sr.frame(dest, bufs, false, ec); + BOOST_TEST(!ec); + + std::size_t n = 0; + for(auto cb : out) + { + wire.append( + static_cast(cb.data()), cb.size()); + n += cb.size(); + } + // the encoder ate the whole input as it ran + BOOST_TEST_EQ(sr.consume(n), 12u); + BOOST_TEST(sr.is_done()); + BOOST_TEST(enc.finished); + BOOST_TEST_EQ(req.content_length().value(), 12u); + BOOST_TEST_EQ( + wire, + std::string(req.buffer()) + encoded(b1 + b2)); + } + + void + testEncoderOutputFillFlushes() + { + // A single large input pumps until the output buffer + // fills, flushes it as one chunk, and leaves the + // unconsumed input with the caller; 64 == 0x40. + std::string const body = make_body(200); + auto req = make_request(); + req.set(http::field::content_encoding, "test"); + test_encoder enc; + serializer sr(cfg); + sr.start(&req, &enc); + + capy::const_buffer const b{ + body.data(), body.size() }; + std::string wire; + system::error_code ec; + capy::const_buffer dest[dest_n]; + auto const out = sr.frame(dest, b, true, ec); + BOOST_TEST(!ec); + BOOST_TEST(!out.empty()); + + std::size_t n = 0; + for(auto cb : out) + { + wire.append( + static_cast(cb.data()), cb.size()); + n += cb.size(); + } + BOOST_TEST_EQ(sr.consume(n), cfg.stage_buffer); + + auto const enc_body = encoded(body); + BOOST_TEST_EQ( + wire, + std::string(req.buffer()) + "40\r\n" + + enc_body.substr(0, 64)); + } + + void + testEncoderRegionSeam() + { + // The encoder's input staging sits directly above its + // output region in one allocation. Fill both to capacity + // while the last output octet is still unconsumed, so a + // misplaced boundary either corrupts that octet (wire + // mismatch) or writes past the allocation. + std::string const body1 = make_body(cfg.stage_buffer); + std::string const body2 = make_body(cfg.enc_buffer); + auto req = make_request(); + req.set(http::field::content_encoding, "test"); + test_encoder enc; + serializer sr(cfg); + sr.start(&req, &enc); + + // fill the encoder's output; 64 == 0x40 + capy::const_buffer const b{ + body1.data(), body1.size() }; + system::error_code ec; + capy::const_buffer dest[dest_n]; + auto const out = sr.frame(dest, b, true, ec); + BOOST_TEST(!ec); + BOOST_TEST(!out.empty()); + + // take the header and part of the chunk, keeping the + // tail of the output region live + std::string wire; + auto const partial = req.buffer().size() + 14; + std::size_t n = 0; + for(auto cb : out) + { + auto const k = (std::min)(partial - n, cb.size()); + wire.append( + static_cast(cb.data()), k); + if((n += k) == partial) + break; + } + // the encoder ate the whole input as it ran + BOOST_TEST_EQ(sr.consume(n), body1.size()); + + // fill the staging window to its last byte while the + // output tail still awaits the wire + capy::mutable_buffer tmp[2]; + auto const m = capy::buffer_copy( + sr.prepare(tmp), capy::make_buffer(body2)); + BOOST_TEST_EQ(m, cfg.enc_buffer); + sr.commit(m); + + BOOST_TEST(!write_eof(sr, wire)); + BOOST_TEST(sr.is_done()); + + std::string expected(req.buffer()); + expected += "40\r\n" + encoded(body1); + expected += "\r\n20\r\n" + encoded(body2); + expected += "\r\n0\r\n\r\n"; + BOOST_TEST_EQ(wire, expected); + } + + void + testStart() + { + // start() rebinds the serializer to a new message and + // encoder for reuse. The first request drops its + // encoder; the second must encode with full capacity. + auto req1 = make_request(); + req1.set(http::field::content_encoding, "test"); + auto req2 = make_request(); + req2.set(http::field::content_encoding, "test"); + std::string const body = make_body(200); + test_encoder enc1; + test_encoder enc2; + + serializer sr(cfg); + // no message yet, so nothing of a header is out + BOOST_TEST(!sr.is_header_done()); + BOOST_TEST(!sr.is_done()); + sr.start(&req1, &enc1); + + // the small body drops the encoder + std::string wire1; + BOOST_TEST(!write_eof(sr, wire1, "hello")); + BOOST_TEST(sr.is_done()); + BOOST_TEST_EQ(enc1.calls, 0u); + BOOST_TEST_EQ( + wire1, std::string(req1.buffer()) + "hello"); + + sr.start(&req2, &enc2); + BOOST_TEST(!sr.is_done()); + + std::string wire2; + BOOST_TEST(!write(sr, wire2, body)); + BOOST_TEST(!write_eof(sr, wire2)); + BOOST_TEST(sr.is_done()); + + auto const enc_body = encoded(body); + std::string expected(req2.buffer()); + expected += "40\r\n" + enc_body.substr(0, 64); + expected += "\r\n40\r\n" + enc_body.substr(64, 64); + expected += "\r\n40\r\n" + enc_body.substr(128, 64); + expected += "\r\n8\r\n" + enc_body.substr(192, 8); + expected += "\r\n0\r\n\r\n"; + BOOST_TEST(enc2.finished); + BOOST_TEST(req2.chunked()); + BOOST_TEST_EQ(wire2, expected); + } + + void + testStartAfterEncoderKept() + { + // A following plain message must send its own staged + // body, not stale encoder output, and re-adding an + // encoder afterwards must keep full capacity. + auto req1 = make_request(); + req1.set(http::field::content_encoding, "test"); + auto req2 = make_request(5); + auto req3 = make_request(); + req3.set(http::field::content_encoding, "test"); + std::string const body1 = make_body(20); + std::string const body3 = make_body(200); + test_encoder enc1; + test_encoder enc3; + + serializer sr(cfg); + sr.start(&req1, &enc1); + + // the body crosses the threshold: the encoder is kept + std::string wire1; + BOOST_TEST(!write_eof(sr, wire1, body1)); + BOOST_TEST(sr.is_done()); + BOOST_TEST_EQ( + wire1, + std::string(req1.buffer()) + encoded(body1)); + + sr.start(&req2); + std::string wire2; + BOOST_TEST(!write(sr, wire2, "hello")); + BOOST_TEST(!write_eof(sr, wire2)); + BOOST_TEST(sr.is_done()); + BOOST_TEST_EQ( + wire2, std::string(req2.buffer()) + "hello"); + + sr.start(&req3, &enc3); + std::string wire3; + BOOST_TEST(!write(sr, wire3, body3)); + BOOST_TEST(!write_eof(sr, wire3)); + BOOST_TEST(sr.is_done()); + + auto const enc_body = encoded(body3); + std::string expected(req3.buffer()); + expected += "40\r\n" + enc_body.substr(0, 64); + expected += "\r\n40\r\n" + enc_body.substr(64, 64); + expected += "\r\n40\r\n" + enc_body.substr(128, 64); + expected += "\r\n8\r\n" + enc_body.substr(192, 8); + expected += "\r\n0\r\n\r\n"; + BOOST_TEST(req3.chunked()); + BOOST_TEST_EQ(wire3, expected); + } + + void + testStartAbandons() + { + // start() abandons outstanding debt; the next message + // is unaffected + std::string const body(cfg.min_direct, 'x'); + auto req1 = make_request(); + auto req2 = make_request(5); + serializer sr(cfg); + sr.start(&req1); + + capy::const_buffer const b{ + body.data(), body.size() }; + system::error_code ec; + capy::const_buffer dest[dest_n]; + auto const out = sr.frame(dest, b, true, ec); + BOOST_TEST(!out.empty()); + sr.consume(3); // part of the header, then abandon + + sr.start(&req2); + std::string wire; + BOOST_TEST(!write_eof(sr, wire, "hello")); + BOOST_TEST_EQ( + wire, std::string(req2.buffer()) + "hello"); + } + + void + testHeadContentLength() + { + // A head message sends only the header; the declared + // Content-Length describes the body a non-head message + // would have carried, and stays untouched. + auto req = make_request(5); + serializer sr(cfg); + sr.start(&req, nullptr, true); + BOOST_TEST(!sr.is_done()); + + std::string wire; + BOOST_TEST(!write_eof(sr, wire)); + BOOST_TEST(sr.is_done()); + + BOOST_TEST_EQ(req.content_length().value(), 5u); + BOOST_TEST_EQ(wire, std::string(req.buffer())); + + // the head flag does not stick across start() + auto req2 = make_request(5); + sr.start(&req2); + std::string wire2; + BOOST_TEST(!write(sr, wire2, "hello")); + BOOST_TEST(!write_eof(sr, wire2)); + BOOST_TEST_EQ( + wire2, std::string(req2.buffer()) + "hello"); + } + + void + testHeadChunked() + { + // A chunked head message keeps Transfer-Encoding in the + // header to describe the framing a non-head message + // would have used; no framing bytes reach the wire. + auto req = make_request(); + serializer sr(cfg); + sr.start(&req, nullptr, true); + + std::string wire; + BOOST_TEST(!write_eof(sr, wire)); + BOOST_TEST(sr.is_done()); + + BOOST_TEST(req.chunked()); + BOOST_TEST( + wire.find("Content-Length") == std::string::npos); + BOOST_TEST_EQ(wire, std::string(req.buffer())); + } + + void + testHeadBodyMismatch() + { + // body bytes are rejected when supplied, even when they + // match the declared Content-Length + { + auto req = make_request(5); + serializer sr(cfg); + sr.start(&req, nullptr, true); + + std::string wire; + BOOST_TEST_EQ( + write(sr, wire, "hello"), + error::body_size_mismatch); + BOOST_TEST(wire.empty()); + } + + // same on the direct path + { + std::string const body(cfg.min_direct, 'x'); + auto req = make_request(body.size()); + serializer sr(cfg); + sr.start(&req, nullptr, true); + + std::string wire; + BOOST_TEST_EQ( + write(sr, wire, body), + error::body_size_mismatch); + BOOST_TEST(wire.empty()); + } + } + + void + testHeadEncoder() + { + // an encoder passed alongside head is ignored entirely + auto req = make_request(5); + req.set(http::field::content_encoding, "test"); + test_encoder enc; + serializer sr(cfg); + sr.start(&req, &enc, true); + + std::string wire; + BOOST_TEST(!write_eof(sr, wire)); + BOOST_TEST(sr.is_done()); + + BOOST_TEST_EQ(enc.calls, 0u); + BOOST_TEST( + wire.find("Content-Encoding: test\r\n") != + std::string::npos); + BOOST_TEST_EQ(wire, std::string(req.buffer())); + } + + void + run() + { + testContentLengthSmallBody(); + testContentLengthLargeBody(); + testContentLengthMultipleBuffers(); + testBodySizeMismatch(); + testChunkedSmallBodyConvertsToContentLength(); + testChunkedWriteEofWithBody(); + testChunkedLargeBody(); + testChunkedWriteEofWithTail(); + testChunkedWriteEofTailOnly(); + testChunkedEmptyBody(); + testPrepareCommitContentLength(); + testPrepareCommitPartialFlush(); + testPrepareCommitChunked(); + testPrepareCommitNoFlush(); + testPrepareEmptyDest(); + testPrepareFullBuffer(); + testPartialConsume(); + testRemainderCoalesces(); + testFrameIdempotent(); + testCancelAndResume(); + testCancelResumeByCommit(); + testAbandonedChunkErrors(); + testContentLengthCancelResume(); + testManyDescriptors(); + testOneSlotDest(); + testStagedBeyondOpenChunk(); + testChunkedNarrowResumeWindow(); + testEmptyDescriptorsDoNotCount(); + testConsumption(); + testFrameWithoutBuffers(); + testChunkedOctetsAfterEof(); + testStagedRemainderDrain(); + testToEof(); + testToEofPartialWrites(); + testToEofBodyContract(); + testToEofDestExhaustion(); + testEmptyDest(); + testChunkedOctetsAfterEofDeferred(); + testTrailer(); + testTrailerIgnoredWhenNotChunked(); + testTrailerWithEncoder(); + testTrailerWithDroppedEncoder(); + testEncoderSmallBodyDropsEncoder(); + testEncoderKeptAfterHeaderFlush(); + testEncoderEmptyBody(); + testEncoderPrepareCommitEof(); + testEncoderThreshold(); + testEncoderStagedAndTail(); + testEncoderLargeBodyChunked(); + testEncoderLargeBodyChunkedEof(); + testEncoderCommit(); + testEncoderFooterSpansFlush(); + testEncoderContentLength(); + testEncoderContentLengthMismatch(); + testEncoderErrorRetryNoConsume(); + testEncoderFailure(); + testEncoderStagedFailure(); + testEncoderFailureAtEof(); + testEncoderToEof(); + testEncoderToEofOctetsAfterEof(); + testEncoderToEofEmptyBody(); + testEncoderContentLengthPartialConsume(); + testEncoderChunkedEmptyFinalFlush(); + testEncoderDribbles(); + testEncoderMultipleBuffers(); + testEncoderOutputFillFlushes(); + testEncoderRegionSeam(); + testStart(); + testStartAfterEncoderKept(); + testStartAbandons(); + testHeadContentLength(); + testHeadChunked(); + testHeadBodyMismatch(); + testHeadEncoder(); + } +}; + +TEST_SUITE(serializer_test, "boost.burl.serializer"); + +} // namespace burl +} // namespace boost diff --git a/test/unit/temp_file.hpp b/test/unit/temp_file.hpp index 25c558a..567e0b4 100644 --- a/test/unit/temp_file.hpp +++ b/test/unit/temp_file.hpp @@ -12,6 +12,7 @@ #include #include +#include #include namespace boost @@ -29,7 +30,7 @@ struct temp_file { path = std::filesystem::temp_directory_path() / ("burl_test_" + - std::to_string(std::rand()) + + std::to_string(std::random_device{}()) + std::string(extension)); std::ofstream ofs(path, std::ios::binary);