diff --git a/include/boost/burl.hpp b/include/boost/burl.hpp index f9a3373..39b37f1 100644 --- a/include/boost/burl.hpp +++ b/include/boost/burl.hpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include diff --git a/include/boost/burl/encoder_config.hpp b/include/boost/burl/encoder_config.hpp new file mode 100644 index 0000000..d61b130 --- /dev/null +++ b/include/boost/burl/encoder_config.hpp @@ -0,0 +1,134 @@ +// +// 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_ENCODER_CONFIG_HPP +#define BOOST_BURL_ENCODER_CONFIG_HPP + +#include + +#include +#include + +#include + +namespace boost +{ +namespace burl +{ + +/** Settings for the content encoders. */ +struct encoder_config +{ + /** Settings for the gzip and deflate codings. */ + struct zlib_settings + { + /** The compression level. + + Ranges from 0, no compression, to 9, + best compression; this is the lever + trading CPU for ratio. + */ + int level = 5; + + /** The base-2 logarithm of the window size. + + Ranges from 9 to 15. A stream uses about + twice the window size in memory for the + window. + */ + int window_bits = 15; + + /** The memory level. + + Ranges from 1 to 9 and sizes the internal + state independently of the window, at + about `1 << (mem_level + 9)` octets. + */ + int mem_level = 8; + }; + + /** Settings for the br coding. */ + struct brotli_settings + { + /** The compression quality. + + Ranges from 0, fastest, to 11, best + compression. Quality 4 is comparable in + speed to zlib level 6; the library + default of 11 is unsuitable for encoding + on the fly. + */ + int quality = 4; + + /** The base-2 logarithm of the window size. + + Ranges from 10 to 24; the window holds + `(1 << lgwin) - 16` octets. The library + default of 22 costs about sixteen times + the memory of 18 for a marginal gain on + typical bodies. + */ + int lgwin = 18; + + /** The base-2 logarithm of the input block size. + + Ranges from 16 to 24, or zero to let the + encoder choose. Pinning it only serves + to bound the memory footprint. + */ + int lgblock = 0; + + /** The encoder mode. + + Describes the input; `text` improves the + ratio for UTF-8 text. + */ + http::brotli::encoder_mode mode = + http::brotli::encoder_mode::generic; + }; + + /** Settings for the zstd coding. */ + struct zstd_settings + { + /** The compression level. + + Ranges from the most negative level the + library allows, fastest, to 22, best + compression. Negative levels suit + high-throughput proxying. + */ + int level = 3; + + /** The base-2 logarithm of the window size. + + Ranges from 10 to 31, or zero to derive + it from the level. The decoder allocates + a window of the same size, and HTTP has + no way to signal it. + */ + int window_log = 0; + + /** The compression strategy. + + When unset, the strategy is derived from + the level; set it only to decouple speed + from level. + */ + std::optional strategy; + }; + + zlib_settings zlib = {}; + brotli_settings brotli = {}; + zstd_settings zstd = {}; +}; + +} // namespace burl +} // namespace boost + +#endif diff --git a/include/boost/burl/head_parser.hpp b/include/boost/burl/head_parser.hpp index 3e14a50..7447641 100644 --- a/include/boost/burl/head_parser.hpp +++ b/include/boost/burl/head_parser.hpp @@ -15,6 +15,7 @@ #include #include +#include #include #include @@ -246,20 +247,17 @@ class head_parser @param n The total number of bytes received at the parse base. - @param ec Set to: - - Zero if the header completed and its - payload framing is valid. + @return Nothing if the header completed and + its payload framing is valid, otherwise: - @ref http::error::need_data if more input is required and room remains below @ref ceiling. - @ref http::error::in_place_overflow if more input is required but no room remains. - - A syntax, framing, or limit error otherwise. + - A syntax, framing, or limit error. */ BOOST_BURL_DECL - void - parse( - std::size_t n, - std::error_code& ec) noexcept; + system::result + parse(std::size_t n) noexcept; /** Return the limits enforced by the parser. diff --git a/include/boost/burl/message_reader.hpp b/include/boost/burl/message_reader.hpp index e5d6bca..078361b 100644 --- a/include/boost/burl/message_reader.hpp +++ b/include/boost/burl/message_reader.hpp @@ -131,7 +131,7 @@ class message_reader @return An awaitable yielding `(error_code,std::string_view)`. - @see @ref parser::body. + @see @ref parser::flatten_body. */ capy::io_task read_body() @@ -268,12 +268,11 @@ read_header_(S& stream, parser& pr) { for(;;) { - std::error_code ec; - pr.parse_header(ec); - if(!ec) + auto const rv = pr.parse_header(); + if(rv.has_value()) co_return {}; - if(ec != http::error::need_data) - co_return { std::error_code(ec) }; + if(rv.error() != http::error::need_data) + co_return { rv.error() }; if(auto [rec] = co_await refill_(stream, pr); rec) co_return { rec }; } @@ -286,10 +285,11 @@ read_body_(S& stream, parser& pr) { for(;;) { - std::error_code ec; - auto const sv = pr.flatten_body(ec); - if(ec != http::error::need_data) - co_return { std::error_code(ec), sv }; + auto const r = pr.flatten_body(); + if(r.has_value()) + co_return { std::error_code(), *r }; + if(r.error() != http::error::need_data) + co_return { r.error(), {} }; if(auto [rec] = co_await refill_(stream, pr); rec) co_return { rec, {} }; } @@ -306,10 +306,11 @@ read_some_( { for(;;) { - std::error_code ec; - auto const n = pr.read_some(buffers, ec); - if(ec != http::error::need_data) - co_return { std::error_code(ec), n }; + auto const r = pr.read_some(buffers); + if(r.has_value()) + co_return { std::error_code(), *r }; + if(r.error() != http::error::need_data) + co_return { r.error(), 0 }; if(auto const lim = pr.direct_capacity(); lim != 0) { @@ -365,10 +366,11 @@ pull_( { for(;;) { - std::error_code ec; - auto const bufs = pr.pull(dest, ec); - if(ec != http::error::need_data) - co_return { std::error_code(ec), bufs }; + auto const r = pr.pull(dest); + if(r.has_value()) + co_return { std::error_code(), *r }; + if(r.error() != http::error::need_data) + co_return { r.error(), {} }; if(auto [rec] = co_await refill_(stream, pr); rec) co_return { rec, {} }; } diff --git a/include/boost/burl/message_writer.hpp b/include/boost/burl/message_writer.hpp index 61fdc03..63a6699 100644 --- a/include/boost/burl/message_writer.hpp +++ b/include/boost/burl/message_writer.hpp @@ -13,7 +13,7 @@ #include #include -#include +#include #include #include @@ -256,25 +256,20 @@ drive_( CB buffers, bool more) { - capy::const_buffer_param bp(buffers); + capy::consuming_buffers cb(buffers); capy::const_buffer dest[16]; std::size_t total = 0; for(;;) { - std::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 fr = sr.frame(dest, cb.data(), more); + if(fr.has_error()) + co_return { fr.error(), total }; + auto [ec, n] = co_await stream.write_some(*fr); auto const k = sr.consume(n); - bp.consume(k); + cb.consume(k); total += k; - if(ec) + if(ec || n == 0) co_return { ec, total }; - if(wec) - co_return { wec, total }; - if(bufs.empty() && !bp.more()) - co_return { std::error_code(), total }; } } diff --git a/include/boost/burl/parser.hpp b/include/boost/burl/parser.hpp index 657baa2..4e7274b 100644 --- a/include/boost/burl/parser.hpp +++ b/include/boost/burl/parser.hpp @@ -12,13 +12,13 @@ #include #include -#include #include #include #include #include #include +#include #include #include @@ -33,6 +33,11 @@ namespace boost namespace burl { +namespace detail +{ +struct decoder; +} // namespace detail + /** A parser for HTTP/1 messages. The parser performs no I/O. Received bytes are @@ -83,8 +88,7 @@ namespace burl @par Errors - Every parsing operation reports through an - `error_code` out parameter: + Errors which ask for more input are: @li @ref http::error::need_data — fill @ref prepare, call @ref commit, and try again. @@ -97,9 +101,6 @@ namespace burl @li @ref http::error::end_of_stream — the stream closed cleanly before the message began. - An operation reports either transferred octets - or an error, never both. - @see @ref message_reader, @ref request_parser, @@ -341,11 +342,12 @@ class parser @par Preconditions @ref start has been called. - @param ec Set to the error, if any occurred. + @return Nothing on success, otherwise the + error. */ BOOST_BURL_DECL - void - parse_header(std::error_code& ec); + system::result + parse_header(); /** Flatten the body in place and return it. @@ -359,20 +361,13 @@ class parser @par Preconditions @ref start has been called. - @param ec Set to the error, if any occurred. - Set to @ref http::error::need_data until the - complete body is buffered, or to - @ref http::error::in_place_overflow if the - body does not fit in the buffer. + @return A view of the complete body, + otherwise the error. - @return A view of the body octets flattened - so far, valid until the parser is modified. - The body is complete when no error is - reported. */ BOOST_BURL_DECL - std::string_view - flatten_body(std::error_code& ec); + system::result + flatten_body(); /** Copy body octets into caller-supplied memory. @@ -386,17 +381,13 @@ class parser @param buffers The destination. - @param ec Set to the error, if any occurred. - Set to `capy::error::eof` once the body is - complete. - - @return The number of octets written. + @return The number of octets written, + otherwise the error. `capy::error::eof` + once the body is complete. */ template - std::size_t - read_some( - MB const& buffers, - std::error_code& ec); + system::result + read_some(MB const& buffers); /** Return available body octets in place. @@ -410,20 +401,18 @@ class parser @param dest The descriptors to fill. - @param ec Set to the error, if any occurred. - Set to `capy::error::eof` once the body is - complete. - @return The filled prefix of `dest`, valid - until the parser is modified. + until the parser is modified, otherwise the + error. `capy::error::eof` once the body is + complete. @see @ref consume. */ BOOST_BURL_DECL - std::span - pull( - std::span dest, - std::error_code& ec); + system::result< + std::span, + std::error_code> + pull(std::span dest); /** Release body octets returned by @ref pull. @@ -456,34 +445,36 @@ class parser @param f The container to append to. - @param ec Set to the error, if any - occurred. + @return Nothing on success, otherwise the + error. */ BOOST_BURL_DECL - void - parse_trailer( - fields_base& f, - std::error_code& ec); + system::result + parse_trailer(fields_base& f); protected: - parser() = default; + BOOST_BURL_DECL + parser(); BOOST_BURL_DECL parser( config const& cfg, bool is_req); - parser(parser&& other) noexcept = default; + BOOST_BURL_DECL + parser(parser&& other) noexcept; + BOOST_BURL_DECL parser& - operator=(parser&& other) noexcept = default; + operator=(parser&& other) noexcept; parser(const parser&) = delete; parser& operator=(const parser&) = delete; - ~parser() = default; + BOOST_BURL_DECL + ~parser(); BOOST_BURL_DECL void @@ -518,15 +509,11 @@ class parser flatten_chunks_(); BOOST_BURL_DECL - std::size_t - read_some_( - capy::mutable_buffer dest, - std::error_code& ec); + system::result + read_some_(capy::mutable_buffer dest); - std::size_t - decode_some_( - capy::mutable_buffer dest, - std::error_code& ec); + system::result + decode_some_(capy::mutable_buffer dest); std::unique_ptr buf_; head_parser hp_; @@ -551,11 +538,9 @@ class parser //------------------------------------------------ template -std::size_t +system::result parser:: -read_some( - MB const& buffers, - std::error_code& ec) +read_some(MB const& buffers) { std::size_t n = 0; auto const end = capy::end(buffers); @@ -564,16 +549,16 @@ read_some( capy::mutable_buffer const b(*it); if(b.size() == 0) continue; - auto const m = read_some_(b, ec); - if(ec) + auto const r = read_some_(b); + if(r.has_error()) { + if(n == 0) + return r; // reported on the next call - if(n != 0) - ec = {}; break; } - n += m; - if(m < b.size()) + n += *r; + if(*r < b.size()) break; } return n; diff --git a/include/boost/burl/serializer.hpp b/include/boost/burl/serializer.hpp index 6a0a3ca..7762400 100644 --- a/include/boost/burl/serializer.hpp +++ b/include/boost/burl/serializer.hpp @@ -12,11 +12,13 @@ #include #include +#include #include #include #include #include +#include #include #include @@ -30,6 +32,11 @@ namespace boost namespace burl { +namespace detail +{ +struct encoder; +} // namespace detail + /** A serializer for HTTP/1.1 messages. Objects of this type incrementally produce the @@ -74,15 +81,20 @@ namespace burl 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. + When @ref config::encoder is set, a body whose + message names a `Content-Encoding` of `gzip`, + `deflate`, `br`, or `zstd` is encoded as it is + serialized, using the encode service installed + for that coding in the system context and the + settings for that coding; the encoder's output + is framed in place of the body. A coding + without an installed service, or one the + serializer does not know, leaves the body as + supplied. 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 @@ -93,83 +105,6 @@ namespace burl 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 { @@ -227,6 +162,17 @@ class serializer serializer may skip encoding entirely. */ std::size_t enc_threshold = 4 * 1024; + + /** The content encoder settings. + + When set, a body whose message names a + `Content-Encoding` with an encode + service installed in the system context + is encoded as it is serialized, using + these settings. When null, no body is + encoded. + */ + std::shared_ptr encoder = nullptr; }; /** Constructor. @@ -234,7 +180,8 @@ class serializer The serializer allocates a single internal buffer whose size is derived from `cfg`; no further allocations are performed - afterwards. + afterwards, except for the content encoder + selected by @ref start. @param cfg The configuration settings to use. @@ -256,7 +203,8 @@ class serializer @param other The serializer to move from. */ - serializer(serializer&& other) noexcept = default; + BOOST_BURL_DECL + serializer(serializer&& other) noexcept; /** Assignment. @@ -269,14 +217,20 @@ class serializer @param other The serializer to move from. */ + BOOST_BURL_DECL serializer& - operator=(serializer&& other) noexcept = default; + operator=(serializer&& other) noexcept; serializer(serializer const&) = delete; serializer& operator=(serializer const&) = delete; + /** Destructor. + */ + BOOST_BURL_DECL + ~serializer(); + /** Return `true` if the message is finished. The message is finished when every @@ -343,22 +297,17 @@ class serializer 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. + + @throws std::bad_alloc Allocation of the + content encoder failed. */ BOOST_BURL_DECL void start( message_head_base* msg, - encoder* enc = nullptr, - bool head = false) noexcept; + bool head = false); /** Set the trailer fields. @@ -455,21 +404,21 @@ class serializer @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. + the descriptors, which may be empty, + otherwise the error. */ template - std::span + system::result< + std::span, + std::error_code> frame( std::span dest, CB const& buffers, - bool more, - std::error_code& ec) + bool more) { source_of src(buffers); - return frame_(dest, src, more, ec); + return frame_(dest, src, more); } /** Obtain buffers for the next serialized octets. @@ -486,19 +435,19 @@ class serializer @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. + @return The prefix of `dest` containing + the descriptors, which may be empty, + otherwise the error. */ - std::span + system::result< + std::span, + std::error_code> frame( std::span dest, - bool more, - std::error_code& ec) + bool more) { source src; - return frame_(dest, src, more, ec); + return frame_(dest, src, more); } /** Release transferred octets. @@ -531,6 +480,12 @@ class serializer std::size_t consume(std::size_t n) noexcept; +protected: + BOOST_BURL_DECL + void + set_encoder( + std::unique_ptr enc) noexcept; + private: static constexpr std::size_t margin = 24; @@ -580,12 +535,13 @@ class serializer }; BOOST_BURL_DECL - std::span + system::result< + std::span, + std::error_code> frame_( std::span dest, source& src, - bool more, - std::error_code& ec); + bool more); bool chunked_() const noexcept; @@ -621,11 +577,10 @@ class serializer source& src, std::error_code& ec); - bool + system::result ingest_( source& src, - bool more, - std::error_code& ec); + bool more); std::span gather_( @@ -643,7 +598,8 @@ class serializer detail::flat_buffer enc_out_; message_head_base* msg_ = nullptr; - encoder* enc_ = nullptr; + std::unique_ptr enc_; + std::shared_ptr enc_cfg_; fields_base const* trailer_ = nullptr; std::uint32_t header_offset_ = 0; diff --git a/src/detail/decoders.cpp b/src/detail/decoder.cpp similarity index 99% rename from src/detail/decoders.cpp rename to src/detail/decoder.cpp index 4962543..80c7c34 100644 --- a/src/detail/decoders.cpp +++ b/src/detail/decoder.cpp @@ -7,7 +7,7 @@ // Official repository: https://github.com/cppalliance/burl // -#include "decoders.hpp" +#include "decoder.hpp" #include #include diff --git a/include/boost/burl/detail/decoder.hpp b/src/detail/decoder.hpp similarity index 62% rename from include/boost/burl/detail/decoder.hpp rename to src/detail/decoder.hpp index cbc76dc..d383da5 100644 --- a/include/boost/burl/detail/decoder.hpp +++ b/src/detail/decoder.hpp @@ -7,12 +7,14 @@ // Official repository: https://github.com/cppalliance/burl // -#ifndef BOOST_BURL_DETAIL_DECODER_HPP -#define BOOST_BURL_DETAIL_DECODER_HPP +#ifndef BOOST_BURL_SRC_DETAIL_DECODER_HPP +#define BOOST_BURL_SRC_DETAIL_DECODER_HPP #include +#include #include +#include #include namespace boost @@ -22,28 +24,17 @@ namespace burl namespace detail { -// A content decoder, transforming the payload -// octets as they arrive. struct decoder { struct result { - // The number of input octets consumed. std::size_t consumed; - - // The number of output octets produced. std::size_t produced; - - // The error, if any. Set to `capy::error::eof` - // once the decoder has produced the complete - // output. std::error_code ec; }; virtual ~decoder() = default; - // Transform payload octets. `more` is false when - // `in` ends the payload. virtual result process( capy::mutable_buffer out, @@ -51,6 +42,9 @@ struct decoder bool more) = 0; }; +std::unique_ptr +make_decoder(http::content_coding coding); + } // namespace detail } // namespace burl } // namespace boost diff --git a/src/detail/decoders.hpp b/src/detail/decoders.hpp deleted file mode 100644 index 879577f..0000000 --- a/src/detail/decoders.hpp +++ /dev/null @@ -1,33 +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_DECODERS_HPP -#define BOOST_BURL_SRC_DETAIL_DECODERS_HPP - -#include - -#include - -#include - -namespace boost -{ -namespace burl -{ -namespace detail -{ - -std::unique_ptr -make_decoder(http::content_coding coding); - -} // namespace detail -} // namespace burl -} // namespace boost - -#endif diff --git a/src/detail/encoder.cpp b/src/detail/encoder.cpp new file mode 100644 index 0000000..314779b --- /dev/null +++ b/src/detail/encoder.cpp @@ -0,0 +1,335 @@ +// +// 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 "encoder.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace boost +{ +namespace burl +{ +namespace detail +{ + +namespace +{ + +class zlib_encoder final + : public encoder +{ + http::zlib::deflate_service& svc_; + http::zlib::stream strm_ = {}; + +public: + // gzip selects the gzip wrapper over zlib's. + zlib_encoder( + http::zlib::deflate_service& svc, + encoder_config::zlib_settings const& cfg, + bool gzip) + : svc_(svc) + { + // with the settings clamped, only + // allocation can fail + if(svc_.init2( + strm_, + std::clamp(cfg.level, 0, 9), + http::zlib::deflated, + std::clamp(cfg.window_bits, 9, 15) + + (gzip ? 16 : 0), + std::clamp(cfg.mem_level, 1, 9), + http::zlib::default_strategy) != + static_cast(http::zlib::error::ok)) + throw std::bad_alloc(); + } + + ~zlib_encoder() override + { + svc_.deflate_end(strm_); + } + + result + process( + capy::mutable_buffer out, + capy::const_buffer in, + bool more) override + { + strm_.next_in = static_cast( + const_cast(in.data())); + strm_.avail_in = saturate(in.size()); + strm_.next_out = + static_cast(out.data()); + strm_.avail_out = saturate(out.size()); + + auto const rs = static_cast( + svc_.deflate( + strm_, + more ? http::zlib::no_flush + : http::zlib::finish)); + + auto const ec = [&]() -> std::error_code + { + if(rs == http::zlib::error::stream_end) + return capy::error::eof; + if(rs != http::zlib::error::ok && + rs != http::zlib::error::buf_err) + return rs; + return {}; + }(); + + return { + .consumed = saturate(in.size()) - strm_.avail_in, + .produced = saturate(out.size()) - strm_.avail_out, + .ec = ec }; + } + + static + unsigned + saturate(std::size_t n) noexcept + { + constexpr auto max = + (std::numeric_limits::max)(); + if(n >= max) + return max; + return static_cast(n); + } +}; + +class brotli_encoder final + : public encoder +{ + http::brotli::encode_service& svc_; + http::brotli::encoder_state* state_; + +public: + brotli_encoder( + http::brotli::encode_service& svc, + encoder_config::brotli_settings const& cfg) + : svc_(svc) + , state_(svc.create_instance( + nullptr, nullptr, nullptr)) + { + if(!state_) + throw std::bad_alloc(); + + using http::brotli::encoder_parameter; + set(encoder_parameter::quality, + std::clamp( + cfg.quality, + http::brotli::min_quality, + http::brotli::max_quality)); + set(encoder_parameter::lgwin, + std::clamp( + cfg.lgwin, + http::brotli::min_window_bits, + http::brotli::max_window_bits)); + if(cfg.lgblock != 0) + set(encoder_parameter::lgblock, + std::clamp( + cfg.lgblock, + http::brotli::min_input_block_bits, + http::brotli::max_input_block_bits)); + set(encoder_parameter::mode, + static_cast(cfg.mode)); + } + + ~brotli_encoder() override + { + svc_.destroy_instance(state_); + } + + result + process( + capy::mutable_buffer out, + capy::const_buffer in, + bool more) override + { + auto* next_in = static_cast(in.data()); + auto available_in = in.size(); + auto* next_out = static_cast(out.data()); + auto available_out = out.size(); + + auto const ok = svc_.compress_stream( + state_, + more ? http::brotli::encoder_operation::process + : http::brotli::encoder_operation::finish, + &available_in, + &next_in, + &available_out, + &next_out, + nullptr); + + auto const ec = [&]() -> std::error_code + { + // brotli reports no error code; failure + // means its lazily allocated state could + // not be obtained + if(!ok) + return make_error_code( + std::errc::not_enough_memory); + if(svc_.is_finished(state_)) + return capy::error::eof; + return {}; + }(); + + return { + .consumed = in.size() - available_in, + .produced = out.size() - available_out, + .ec = ec }; + } + +private: + void + set(http::brotli::encoder_parameter p, int v) noexcept + { + // fails only for an unknown parameter or + // once the stream has started + BOOST_VERIFY(svc_.set_parameter( + state_, p, static_cast(v))); + } +}; + +class zstd_encoder final + : public encoder +{ + http::zstd::compress_service& svc_; + http::zstd::cctx* ctx_; + +public: + zstd_encoder( + http::zstd::compress_service& svc, + encoder_config::zstd_settings const& cfg) + : svc_(svc) + , ctx_(svc.create_cctx()) + { + if(!ctx_) + throw std::bad_alloc(); + + using http::zstd::c_parameter; + set(c_parameter::compression_level, cfg.level); + if(cfg.window_log != 0) + set(c_parameter::window_log, cfg.window_log); + if(cfg.strategy) + set(c_parameter::strategy, + static_cast(*cfg.strategy)); + } + + ~zstd_encoder() override + { + svc_.free_cctx(ctx_); + } + + result + process( + capy::mutable_buffer out, + capy::const_buffer in, + bool more) override + { + http::zstd::in_buffer in_buf{ in.data(), in.size(), 0 }; + http::zstd::out_buffer out_buf{ out.data(), out.size(), 0 }; + + auto const rs = svc_.compress_stream( + ctx_, + out_buf, + in_buf, + more ? http::zstd::end_directive::continue_ + : http::zstd::end_directive::end); + + auto const ec = [&]() -> std::error_code + { + if(svc_.is_error(rs)) + return svc_.get_error_code(rs); + // zero reports the frame complete and + // fully flushed + if(!more && rs == 0 && + in_buf.pos == in_buf.size) + return capy::error::eof; + return {}; + }(); + + return { + .consumed = in_buf.pos, + .produced = out_buf.pos, + .ec = ec }; + } + +private: + // clamps to the bounds the library reports + void + set(http::zstd::c_parameter p, int v) noexcept + { + auto const b = svc_.param_bounds(p); + BOOST_ASSERT(!svc_.is_error(b.error)); + BOOST_VERIFY(!svc_.is_error(svc_.set_parameter( + ctx_, + p, + std::clamp(v, b.lower_bound, b.upper_bound)))); + } +}; + +} // namespace + +std::unique_ptr +make_encoder( + http::content_coding coding, + encoder_config const& cfg) +{ + auto const& ctx = capy::get_system_context(); + switch(coding) + { + case http::content_coding::deflate: + if(auto* svc = ctx.find_service< + http::zlib::deflate_service>()) + return std::make_unique( + *svc, cfg.zlib, false); + break; + case http::content_coding::gzip: + if(auto* svc = ctx.find_service< + http::zlib::deflate_service>()) + return std::make_unique( + *svc, cfg.zlib, true); + break; + case http::content_coding::br: + if(auto* svc = ctx.find_service< + http::brotli::encode_service>()) + return std::make_unique( + *svc, cfg.brotli); + break; + case http::content_coding::zstd: + if(auto* svc = ctx.find_service< + http::zstd::compress_service>()) + return std::make_unique( + *svc, cfg.zstd); + break; + default: + break; + } + return nullptr; +} + +} // namespace detail +} // namespace burl +} // namespace boost diff --git a/src/detail/encoder.hpp b/src/detail/encoder.hpp new file mode 100644 index 0000000..1c0d623 --- /dev/null +++ b/src/detail/encoder.hpp @@ -0,0 +1,56 @@ +// +// 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_ENCODER_HPP +#define BOOST_BURL_SRC_DETAIL_ENCODER_HPP + +#include + +#include +#include + +#include +#include +#include + +namespace boost +{ +namespace burl +{ +namespace detail +{ + +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 more) = 0; +}; + +std::unique_ptr +make_encoder( + http::content_coding coding, + encoder_config const& cfg); + +} // namespace detail +} // namespace burl +} // namespace boost + +#endif diff --git a/src/head_parser.cpp b/src/head_parser.cpp index 608e572..d2dc663 100644 --- a/src/head_parser.cpp +++ b/src/head_parser.cpp @@ -305,13 +305,11 @@ rebase(char* base) noexcept h.buf_ = base + h.prefix_; } -void +system::result head_parser:: -parse( - std::size_t n, - std::error_code& ec) noexcept +parse(std::size_t n) noexcept { - ec.clear(); + std::error_code ec; auto const& h = h_(); char const* it = h.buf_ + h.size_; BOOST_ASSERT(n >= std::size_t(h.prefix_) + h.size_); @@ -362,6 +360,10 @@ parse( if(ec == error::need_data && end >= ceiling()) ec = error::in_place_overflow; + + if(ec) + return ec; + return {}; } void diff --git a/src/parser.cpp b/src/parser.cpp index 8a211a6..13161c0 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -12,7 +12,7 @@ #include #include "detail/content_coding.hpp" -#include "detail/decoders.hpp" +#include "detail/decoder.hpp" #include "detail/grammar.hpp" #include "detail/util.hpp" @@ -227,24 +227,6 @@ skip_crlf(chained_sequence& cs) noexcept return {}; } -std::span -collect( - std::span dest, - std::array const& src, - std::size_t at_most = std::size_t(-1)) noexcept -{ - std::size_t n = 0; - for(auto b : src) - { - auto const take = clamp(b.size(), at_most); - if(take == 0 || n == dest.size()) - break; - at_most -= take; - dest[n++] = { b.data(), take }; - } - return dest.first(n); -} - auto prefix(auto buf, std::size_t n) noexcept -> decltype(buf) @@ -264,22 +246,25 @@ struct parser::chunk_fn chunk_fn(F&& f) noexcept : obj_(std::addressof(f)) , invoke_( - [](void* obj, capy::const_buffer b, bool last) + [](void* obj, capy::const_buffer b, bool more) -> capy::io_result { return (*static_cast< - std::remove_reference_t*>(obj))(b, last); + std::remove_reference_t*>(obj))(b, more); }) { } capy::io_result - operator()(capy::const_buffer b, bool last) const + operator()(capy::const_buffer b, bool more) const { - return invoke_(obj_, b, last); + return invoke_(obj_, b, more); } }; +parser:: +parser() = default; + parser:: parser( config const& cfg, @@ -297,6 +282,16 @@ parser( out_ = { buf_.get() + h_cap, cfg.dec_buffer }; } +parser:: +parser(parser&& other) noexcept = default; + +parser& +parser:: +operator=(parser&& other) noexcept = default; + +parser:: +~parser() = default; + bool parser:: got_header() const noexcept @@ -455,7 +450,7 @@ walk_chunks_(chunk_fn f, bool dry) { // from flatten_chunks_ auto const b = in_.first(clamp(rem_)); - auto const [ec, n] = f(b, true); + auto const [ec, n] = f(b, false); if(!dry) { in_.consume(n); @@ -485,7 +480,7 @@ walk_chunks_(chunk_fn f, bool dry) fin_chunk_ = true; in_.consume(in_.size() - t0); } - return std::get<0>(f({}, true)); + return std::get<0>(f({}, false)); } invoke: @@ -493,7 +488,7 @@ walk_chunks_(chunk_fn f, bool dry) { if(b.size() == 0) break; - auto const [ec, n] = f(b, false); + auto const [ec, n] = f(b, true); cs.advance(n); size -= n; if(!dry) @@ -575,20 +570,19 @@ flatten_chunks_() } } -void +system::result parser:: -parse_header(std::error_code& ec) +parse_header() { BOOST_ASSERT(started_); - ec = {}; - if(got_header_) - return; + return {}; + std::error_code ec; for(;;) { - hp_.parse(in_.size(), ec); + ec = hp_.parse(in_.size()).error(); if(ec == in_place_overflow && in_.ptr != buf_.get()) { in_.slide(buf_.get()); @@ -600,17 +594,14 @@ parse_header(std::error_code& ec) if(ec) { if(ec != need_more_input) - return; + return ec; if(eof_) { if(in_.empty()) - ec = end_of_stream; - else - ec = incomplete; - return; + return end_of_stream; + return incomplete; } - ec = need_data; - return; + return need_data; } auto const& h = hp_.message_head(); @@ -622,8 +613,7 @@ parse_header(std::error_code& ec) switch(payload_) { case payload::error: - ec = bad_payload; - return; + return bad_payload; case payload::none: got_body_ = true; break; @@ -651,6 +641,8 @@ parse_header(std::error_code& ec) } in_.shed(head_size); + + return {}; } void @@ -670,35 +662,31 @@ set_body_limit(std::uint64_t n) noexcept body_limit_ = n; } -std::string_view +system::result parser:: -flatten_body(std::error_code& ec) +flatten_body() { - parse_header(ec); - if(ec) - return {}; + if(auto rv = parse_header(); rv.has_error()) + return rv.error(); if(dec_) { for(;;) { if(out_.full()) + return in_place_overflow; + auto const r = decode_some_( + out_.prepare_one()); + if(r.has_error()) { - ec = in_place_overflow; - break; - } - auto const n = decode_some_( - out_.prepare_one(), ec); - out_.commit(n); - if(ec == capy::cond::eof) - { - ec = {}; + if(r.error() != capy::cond::eof) + return r.error(); break; } - if(ec) - break; + out_.commit(*r); } - return { out_.linearize(out_.ptr), out_.len }; + return std::string_view( + out_.linearize(out_.ptr), out_.len); } switch(payload_) @@ -706,44 +694,42 @@ flatten_body(std::error_code& ec) case payload::chunked: { in_.linearize(in_.ptr); - for(;;) + while(!fin_chunk_) { if(rem_ > limit_rem_) - { - ec = body_too_large; - break; - } - if(fin_chunk_) - break; + return body_too_large; if(auto fec = flatten_chunks_(); fec) { if(fec != need_more_input) - ec = fec; - else - ec = need_more_(); - break; + return fec; + return need_more_(); } } - return { in_.ptr, clamp(rem_, in_.len) }; + if(rem_ > limit_rem_) + return body_too_large; + return std::string_view( + in_.ptr, clamp(rem_, in_.len)); } case payload::size: { if(rem_ > limit_rem_) - ec = body_too_large; - else if(!got_body_) - ec = need_more_(); - return { in_.linearize(in_.ptr), clamp(rem_, in_.len) }; + return body_too_large; + if(!got_body_) + return need_more_(); + return std::string_view( + in_.linearize(in_.ptr), clamp(rem_, in_.len)); } case payload::to_eof: { if(in_.size() > limit_rem_) - ec = body_too_large; - else if(!got_body_) - ec = need_more_(); - return { in_.linearize(in_.ptr), in_.len }; + return body_too_large; + if(!got_body_) + return need_more_(); + return std::string_view( + in_.linearize(in_.ptr), in_.len); } default: - return {}; + return std::string_view(); } } @@ -761,25 +747,23 @@ get_request() const return hp_.request_head(); } -std::size_t +system::result parser:: -decode_some_( - capy::mutable_buffer dest, - std::error_code& ec) +decode_some_(capy::mutable_buffer dest) { if(dest.size() == 0) - return 0; + return std::size_t(0); std::size_t prod = 0; - auto decode = - [&](capy::const_buffer in, bool last) + auto process = + [&](capy::const_buffer in, bool more) -> capy::io_result { if(dec_err_) { if(dec_err_ == capy::cond::eof) { - if(!last || in.size() != 0) + if(more || in.size() != 0) return { bad_payload, 0 }; } return { dec_err_, 0 }; @@ -789,7 +773,7 @@ decode_some_( { auto const lim = clamp(limit_rem_); auto const r = dec_->process( - prefix(dest, lim), in, !last); + prefix(dest, lim), in, more); dest += r.produced; in += r.consumed; cons += r.consumed; @@ -816,14 +800,14 @@ decode_some_( { case payload::chunked: { - auto const wec = walk_chunks_(decode); + auto const wec = walk_chunks_(process); if(prod != 0) return prod; - if(wec != need_more_input) - ec = wec; - else - ec = need_more_(); - return 0; + if(wec == need_more_input) + return need_more_(); + if(wec) + return wec; + return std::size_t(0); } case payload::size: case payload::to_eof: @@ -832,40 +816,29 @@ decode_some_( { auto const in = in_.first(clamp(rem_)); if(in.size() == 0 && !got_body_) - { - ec = need_more_(); - return 0; - } - auto [dec_ec, cons] = decode( - in, got_body_ && in.size() == clamp(rem_, in_.size())); + return need_more_(); + auto const more = + !got_body_ || in.size() < clamp(rem_, in_.size()); + auto [dec_ec, cons] = process(in, more); in_.consume(cons); rem_ -= cons; if(prod != 0) return prod; if(dec_ec) - { - ec = dec_ec; - return 0; - } + return dec_ec; } } default: - { - ec = capy::error::eof; - return 0; - } + return capy::error::eof; } } -std::size_t +system::result parser:: -read_some_( - capy::mutable_buffer dest, - std::error_code& ec) +read_some_(capy::mutable_buffer dest) { - parse_header(ec); - if(ec) - return 0; + if(auto rv = parse_header(); rv.has_error()) + return rv.error(); if(dec_) { @@ -876,7 +849,7 @@ read_some_( out_.consume(n); return n; } - return decode_some_(dest, ec); + return decode_some_(dest); } auto copy = [&](std::size_t at_most) @@ -911,87 +884,77 @@ read_some_( if(read != 0) return read; if(wec == need_more_input) - { - ec = need_more_(); - return 0; - } + return need_more_(); if(wec) - { - ec = wec; - return 0; - } + return wec; BOOST_ASSERT(got_body_); - ec = capy::error::eof; - return 0; + return capy::error::eof; } case payload::size: { if(rem_ != 0) { if(rem_ > limit_rem_) - { - ec = body_too_large; - return 0; - } + return body_too_large; if(!in_.empty()) return copy(clamp(rem_)); } if(got_body_) - { - ec = capy::error::eof; - return 0; - } - ec = need_more_(); - return 0; + return capy::error::eof; + return need_more_(); } case payload::to_eof: { if(!in_.empty()) { if(limit_rem_ == 0) - { - ec = body_too_large; - return 0; - } + return body_too_large; return copy(clamp(limit_rem_)); } if(got_body_) - { - ec = capy::error::eof; - return 0; - } - ec = need_more_(); - return 0; + return capy::error::eof; + return need_more_(); } default: - { - ec = capy::error::eof; - return 0; - } + return capy::error::eof; } } -std::span +system::result< + std::span, + std::error_code> parser:: -pull( - std::span dest, - std::error_code& ec) +pull(std::span dest) { - parse_header(ec); - if(ec) - return {}; + if(auto rv = parse_header(); rv.has_error()) + return rv.error(); + + auto collect = [&]( + std::array const& src, + std::size_t at_most = std::size_t(-1)) + { + std::size_t n = 0; + for(auto b : src) + { + auto const take = clamp(b.size(), at_most); + if(take == 0 || n == dest.size()) + break; + at_most -= take; + dest[n++] = { b.data(), take }; + } + return dest.first(n); + }; if(dec_) { if(!out_.empty()) - return collect(dest, out_.data()); - auto const n = decode_some_( - out_.prepare_one(), ec); - out_.commit(n); - if(ec && n == 0) - return {}; - ec = {}; - return collect(dest, out_.data()); + return collect(out_.data()); + auto const r = decode_some_( + out_.prepare_one()); + if(r.has_error()) + return r.error(); + out_.commit(*r); + return collect(out_.data()); } switch(payload_) @@ -1025,56 +988,40 @@ pull( // finish the framing, retain the trailer consume(0); } - ec = wec; - return {}; + if(wec) + return wec; + return dest.first(0); } - ec = need_more_(); - return {}; + return need_more_(); } case payload::size: { if(rem_ != 0) { if(rem_ > limit_rem_) - { - ec = body_too_large; - return {}; - } + return body_too_large; if(!in_.empty()) - return collect(dest, in_.data(), clamp(rem_)); + return collect(in_.data(), clamp(rem_)); } if(got_body_) - { - ec = capy::error::eof; - return {}; - } - ec = need_more_(); - return {}; + return capy::error::eof; + return need_more_(); } case payload::to_eof: { if(!in_.empty()) { if(limit_rem_ == 0) - { - ec = body_too_large; - return {}; - } - return collect(dest, in_.data(), clamp(limit_rem_)); + return body_too_large; + return collect( + in_.data(), clamp(limit_rem_)); } if(got_body_) - { - ec = capy::error::eof; - return {}; - } - ec = need_more_(); - return {}; + return capy::error::eof; + return need_more_(); } default: - { - ec = capy::error::eof; - return {}; - } + return capy::error::eof; } } @@ -1105,22 +1052,15 @@ consume(std::size_t n) noexcept } } -void +system::result parser:: -parse_trailer( - fields_base& f, - std::error_code& ec) +parse_trailer(fields_base& f) { - ec = {}; - if(payload_ != payload::chunked) - return; + return {}; if(!fin_chunk_) - { - ec = incomplete; - return; - } + return incomplete; char const* it = in_.ptr + in_.pos + clamp(rem_); char const* end = in_.ptr + clamp(in_.pos + in_.len, in_.cap); @@ -1129,6 +1069,7 @@ parse_trailer( { auto const it0 = it; std::string_view name, value; + std::error_code ec; parse_limited( [&name, &value](auto& it, auto end, auto& ec) { @@ -1141,20 +1082,19 @@ parse_trailer( ec); if(ec) { - if(ec == need_data) - { - BOOST_ASSERT(in_.wrapped()); - auto const off = distance(it0, in_.ptr + in_.pos); - in_.linearize(in_.ptr); - it = in_.ptr + off; - end = in_.ptr + in_.len; - ec = {}; - continue; - } - return; + if(ec != need_data) + return ec; + BOOST_ASSERT(in_.wrapped()); + auto const off = distance(it0, in_.ptr + in_.pos); + in_.linearize(in_.ptr); + it = in_.ptr + off; + end = in_.ptr + in_.len; + continue; } f.append(name, value); } + + return {}; } } // namespace burl diff --git a/src/serializer.cpp b/src/serializer.cpp index 65b2a99..3f79baa 100644 --- a/src/serializer.cpp +++ b/src/serializer.cpp @@ -10,6 +10,8 @@ #include #include +#include "detail/content_coding.hpp" +#include "detail/encoder.hpp" #include "detail/util.hpp" #include @@ -32,7 +34,10 @@ static_assert( (std::numeric_limits::max)()); using http::payload; + using detail::clamp; +using detail::content_coding; +using detail::make_encoder; namespace { @@ -56,29 +61,40 @@ serializer(config const& cfg) , enc_threshold_(cfg.enc_threshold) , stage_{ buf_.get() + margin, cfg.stage_buffer } , enc_out_{ buf_.get() + margin, cfg.enc_buffer } + , enc_cfg_(cfg.encoder) { } +serializer:: +serializer(serializer&& other) noexcept = default; + +serializer& +serializer:: +operator=(serializer&& other) noexcept = default; + +serializer:: +~serializer() = default; + void serializer:: start( message_head_base* msg, - encoder* enc, - bool head) noexcept + bool head) { BOOST_ASSERT(msg != nullptr); - if(head) - enc = nullptr; - msg_ = msg; - enc_ = enc; trailer_ = nullptr; + payload_ = head ? payload::none : msg->payload(); - if((enc != nullptr) != (stage_.ptr != enc_out_.ptr)) + enc_.reset(); + if(enc_cfg_ && payload_ != payload::none) + enc_ = make_encoder(content_coding(*msg), *enc_cfg_); + + 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_.ptr = enc_out_.ptr + (enc_ ? enc_out_.cap : 0); } stage_.clear(); @@ -89,7 +105,6 @@ start( input_framed_ = 0; input_digested_ = 0; prefix_rem_ = 0; - payload_ = head ? payload::none : msg->payload(); crlf_owed_ = false; enc_started_ = false; sealed_ = false; @@ -109,6 +124,20 @@ start( }(); } +void +serializer:: +set_encoder( + std::unique_ptr enc) noexcept +{ + enc_ = std::move(enc); + + 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); + } +} + std::span serializer:: prepare(std::span dest) @@ -257,7 +286,7 @@ void serializer:: encode_(source& src, std::error_code& ec) { - auto feed = [&]( + auto process = [&]( capy::const_buffer in, bool more) { auto const r = enc_->process( @@ -284,7 +313,8 @@ encode_(source& src, std::error_code& ec) if(enc_out_.capacity() == 0) return; auto const more = !sealed_ || src.remain != 0; - stage_.consume(feed(stage_.data(), more)); + auto const n = process(stage_.data(), more); + stage_.consume(n); if(ec || !enc_) return; } @@ -294,8 +324,8 @@ encode_(source& src, std::error_code& ec) { if(enc_out_.capacity() == 0) return; - auto const n = feed( - cur, !sealed_ || src.remain != 0); + auto const more = !sealed_ || src.remain != 0; + auto const n = process(cur, more); cur += n; input_digested_ += n; if(ec || !enc_) @@ -309,29 +339,25 @@ encode_(source& src, std::error_code& ec) { if(enc_out_.capacity() == 0) return; - feed({}, false); + process({}, false); if(ec || !enc_) return; } } -bool +system::result serializer:: ingest_( source& src, - bool more, - std::error_code& ec) + bool more) { auto const sealing = !more && !sealed_; if(chunked_()) { if(sealed_ && !enc_ && src.remain != 0) - { - ec = make_error_code( + return make_error_code( std::errc::invalid_argument); - return false; - } } else if(!enc_) { @@ -341,11 +367,9 @@ ingest_( (!to_eof_() && sealing && have < owed_)) { if(to_eof_()) - ec = make_error_code( + return make_error_code( std::errc::invalid_argument); - else - ec = error::body_size_mismatch; - return false; + return error::body_size_mismatch; } } @@ -383,12 +407,13 @@ ingest_( } else if(enc_) { + std::error_code ec; encode_(src, ec); if(ec) { enc_ = nullptr; done_ = true; - return false; + return ec; } auto const finished = (enc_ == nullptr); @@ -398,8 +423,7 @@ ingest_( if(encoded > owed_ || (finished && encoded != owed_)) { done_ = true; - ec = error::body_size_mismatch; - return false; + return error::body_size_mismatch; } } auto const flush = finished || enc_out_.capacity() == 0; @@ -489,24 +513,20 @@ gather_( return { dest.data(), n }; } -std::span +system::result< + std::span, + std::error_code> serializer:: frame_( std::span dest, source& src, - bool more, - std::error_code& ec) + bool more) { BOOST_ASSERT(msg_ != nullptr); - ec = {}; - if(done_ && !settled_()) - { - ec = make_error_code( + return make_error_code( std::errc::state_not_recoverable); - return {}; - } BOOST_ASSERT(input_digested_ == 0); @@ -515,9 +535,10 @@ frame_( bool flush_body = true; if(owed_ == 0 || !chunked_()) { - flush_body = ingest_(src, more, ec); - if(ec) - return {}; + auto const r = ingest_(src, more); + if(r.has_error()) + return r.error(); + flush_body = *r; } auto const out = gather_( @@ -528,15 +549,12 @@ frame_( if(!more && owed_ != 0) { if(to_eof_() || chunked_()) - ec = make_error_code( + return make_error_code( std::errc::invalid_argument); - else - ec = error::body_size_mismatch; + return error::body_size_mismatch; } - else if(sealed_) - { + if(sealed_) done_ = settled_(); - } } return out; diff --git a/test/unit/detail/decoders.cpp b/test/unit/detail/decoder.cpp similarity index 99% rename from test/unit/detail/decoders.cpp rename to test/unit/detail/decoder.cpp index 75f90d6..8d7143b 100644 --- a/test/unit/detail/decoders.cpp +++ b/test/unit/detail/decoder.cpp @@ -8,7 +8,7 @@ // // Test that header file is self-contained. -#include "src/detail/decoders.hpp" +#include "src/detail/decoder.hpp" #include @@ -33,7 +33,7 @@ namespace burl namespace detail { -class decoders_test +class decoder_test { struct decode_result { @@ -714,7 +714,7 @@ class decoders_test } }; -TEST_SUITE(decoders_test, "boost.burl.detail.decoders"); +TEST_SUITE(decoder_test, "boost.burl.detail.decoder"); } // namespace detail } // namespace burl diff --git a/test/unit/detail/encoder.cpp b/test/unit/detail/encoder.cpp new file mode 100644 index 0000000..9a334a6 --- /dev/null +++ b/test/unit/detail/encoder.cpp @@ -0,0 +1,315 @@ +// +// 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/encoder.hpp" + +#include "src/detail/decoder.hpp" + +#include +#include +#include +#include +#include +#include + +#include "test_suite.hpp" + +#include +#include +#include + +namespace boost +{ +namespace burl +{ +namespace detail +{ + +class encoder_test +{ + struct coder_result + { + std::error_code ec; + std::string output; + bool finished = false; + }; + + // Drives an encoder or a decoder: both expose + // process(out, in, more) with the same result shape. + template + static coder_result + run_coder( + Coder& c, + std::string_view input, + std::size_t in_step, + std::size_t out_step) + { + coder_result r; + std::string out(out_step, '\0'); + for(std::size_t i = 0; i < 100000; ++i) + { + auto const n = + input.size() < in_step ? input.size() : in_step; + auto const more = n != input.size(); + auto const res = c.process( + capy::mutable_buffer(out.data(), out.size()), + capy::const_buffer(input.data(), n), + more); + r.output.append(out.data(), res.produced); + input.remove_prefix(res.consumed); + if(res.ec) + { + r.finished = res.ec == capy::cond::eof; + if(!r.finished) + r.ec = res.ec; + break; + } + // no forward progress at the end of input + // would repeat forever + if(!more && res.consumed == 0 && res.produced == 0) + break; + } + return r; + } + + static std::string + make_body(std::size_t size) + { + std::string body; + body.reserve(size + 64); + for(std::size_t i = 0; body.size() < size; ++i) + { + body += "the quick brown fox jumps over the lazy dog "; + body += std::to_string(i); + body += ' '; + } + body.resize(size); + return body; + } + + // Round trip: the encoder's output must decode back + // to the body through the matching decoder. Returns + // the encoded output. + static std::string + round_trip( + http::content_coding coding, + std::string_view body, + std::size_t in_step, + std::size_t out_step, + encoder_config const& cfg = {}) + { + auto enc = make_encoder(coding, cfg); + if(!BOOST_TEST(enc != nullptr)) + return {}; + auto const e = run_coder(*enc, body, in_step, out_step); + BOOST_TEST(!e.ec); + BOOST_TEST(e.finished); + + auto dec = make_decoder(coding); + if(!BOOST_TEST(dec != nullptr)) + return e.output; + auto const d = run_coder( + *dec, e.output, in_step, out_step); + BOOST_TEST(!d.ec); + BOOST_TEST(d.finished); + BOOST_TEST(d.output == body); + return e.output; + } + + static unsigned + byte_at(std::string const& s, std::size_t i) + { + return static_cast(s.at(i)); + } + + static void + test_coding(http::content_coding coding) + { + auto const body = make_body(200); + + // single pass + round_trip(coding, body, body.size(), 1024); + + // starved input and output + round_trip(coding, body, 3, 7); + + // empty body + round_trip(coding, {}, 64, 64); + + // large body + round_trip(coding, make_body(64 * 1024), 1024, 1024); + } + +public: + void + test_make_encoder() + { + BOOST_TEST( + make_encoder(http::content_coding::identity, {}) == nullptr); + BOOST_TEST( + make_encoder(http::content_coding::unknown, {}) == nullptr); + BOOST_TEST( + make_encoder(http::content_coding::compress, {}) == nullptr); + + // Availability of each encoder follows the installed services. +#ifdef BOOST_HTTP_HAS_ZLIB + BOOST_TEST( + make_encoder(http::content_coding::deflate, {}) != nullptr); + BOOST_TEST( + make_encoder(http::content_coding::gzip, {}) != nullptr); +#else + BOOST_TEST( + make_encoder(http::content_coding::deflate, {}) == nullptr); + BOOST_TEST( + make_encoder(http::content_coding::gzip, {}) == nullptr); +#endif +#ifdef BOOST_HTTP_HAS_BROTLI + BOOST_TEST( + make_encoder(http::content_coding::br, {}) != nullptr); +#else + BOOST_TEST( + make_encoder(http::content_coding::br, {}) == nullptr); +#endif +#ifdef BOOST_HTTP_HAS_ZSTD + BOOST_TEST( + make_encoder(http::content_coding::zstd, {}) != nullptr); +#else + BOOST_TEST( + make_encoder(http::content_coding::zstd, {}) == nullptr); +#endif + } + + void + test_settings() + { + auto const body = make_body(200); + [[maybe_unused]] encoder_config cfg; + +#ifdef BOOST_HTTP_HAS_ZLIB + // zlib's header records the window size: + // CINFO = window_bits - 8, CM = 8 + cfg = {}; + cfg.zlib.window_bits = 12; + BOOST_TEST_EQ( + byte_at(round_trip( + http::content_coding::deflate, body, 64, 64, cfg), 0), + 0x48u); + + // out-of-range values are clamped + cfg = {}; + cfg.zlib.level = -1; + cfg.zlib.window_bits = 99; + cfg.zlib.mem_level = 0; + BOOST_TEST_EQ( + byte_at(round_trip( + http::content_coding::deflate, body, 64, 64, cfg), 0), + 0x78u); + + // level 0 stores the body verbatim + cfg = {}; + cfg.zlib.level = 0; + BOOST_TEST( + round_trip(http::content_coding::gzip, body, 64, 64, cfg) + .find(body) != std::string::npos); +#endif + +#ifdef BOOST_HTTP_HAS_BROTLI + // the stream header records the window size: + // a leading 0 bit means 16, otherwise the + // next 3 bits hold lgwin - 17 + cfg = {}; + cfg.brotli.lgwin = 16; + BOOST_TEST_EQ( + byte_at(round_trip( + http::content_coding::br, body, 64, 64, cfg), 0) & 0x01u, + 0u); + cfg = {}; + BOOST_TEST_EQ( + byte_at(round_trip( + http::content_coding::br, body, 64, 64, cfg), 0) & 0x0Fu, + 0x03u); + + // the remaining settings, and clamped + // values, still yield a valid stream + cfg = {}; + cfg.brotli.quality = 11; + cfg.brotli.lgblock = 16; + cfg.brotli.mode = http::brotli::encoder_mode::text; + round_trip(http::content_coding::br, body, 64, 64, cfg); + cfg = {}; + cfg.brotli.quality = 99; + cfg.brotli.lgwin = 0; + cfg.brotli.lgblock = 5; + round_trip(http::content_coding::br, body, 64, 64, cfg); +#endif + +#ifdef BOOST_HTTP_HAS_ZSTD + // the window descriptor follows the 4-octet + // magic number and the frame header + // descriptor; its exponent is window_log - 10 + cfg = {}; + cfg.zstd.window_log = 12; + BOOST_TEST_EQ( + byte_at(round_trip( + http::content_coding::zstd, body, 64, 64, cfg), 5), + (12u - 10u) << 3); + cfg = {}; + cfg.zstd.window_log = 3; + BOOST_TEST_EQ( + byte_at(round_trip( + http::content_coding::zstd, body, 64, 64, cfg), 5), + 0u); + + cfg = {}; + cfg.zstd.level = -1000000; + round_trip(http::content_coding::zstd, body, 64, 64, cfg); + cfg = {}; + cfg.zstd.level = 7; + cfg.zstd.strategy = http::zstd::strategy::btultra2; + round_trip(http::content_coding::zstd, body, 64, 64, cfg); +#endif + } + + void + run() + { + [[maybe_unused]] auto& ctx = capy::get_system_context(); +#ifdef BOOST_HTTP_HAS_ZLIB + if(!ctx.has_service()) + http::zlib::install_deflate_service(ctx); + if(!ctx.has_service()) + http::zlib::install_inflate_service(ctx); + test_coding(http::content_coding::deflate); + test_coding(http::content_coding::gzip); +#endif +#ifdef BOOST_HTTP_HAS_BROTLI + if(!ctx.has_service()) + http::brotli::install_encode_service(ctx); + if(!ctx.has_service()) + http::brotli::install_decode_service(ctx); + test_coding(http::content_coding::br); +#endif +#ifdef BOOST_HTTP_HAS_ZSTD + if(!ctx.has_service()) + http::zstd::install_compress_service(ctx); + if(!ctx.has_service()) + http::zstd::install_decompress_service(ctx); + test_coding(http::content_coding::zstd); +#endif + test_make_encoder(); + test_settings(); + } +}; + +TEST_SUITE(encoder_test, "boost.burl.detail.encoder"); + +} // namespace detail +} // namespace burl +} // namespace boost diff --git a/test/unit/encoder_config.cpp b/test/unit/encoder_config.cpp new file mode 100644 index 0000000..a9e0adb --- /dev/null +++ b/test/unit/encoder_config.cpp @@ -0,0 +1,45 @@ +// +// 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 "test_suite.hpp" + +namespace boost +{ +namespace burl +{ + +class encoder_config_test +{ +public: + void + run() + { + // the defaults suit encoding on the fly + encoder_config const cfg{}; + BOOST_TEST_EQ(cfg.zlib.level, 5); + BOOST_TEST_EQ(cfg.zlib.window_bits, 15); + BOOST_TEST_EQ(cfg.zlib.mem_level, 8); + BOOST_TEST_EQ(cfg.brotli.quality, 4); + BOOST_TEST_EQ(cfg.brotli.lgwin, 18); + BOOST_TEST_EQ(cfg.brotli.lgblock, 0); + BOOST_TEST( + cfg.brotli.mode == http::brotli::encoder_mode::generic); + BOOST_TEST_EQ(cfg.zstd.level, 3); + BOOST_TEST_EQ(cfg.zstd.window_log, 0); + BOOST_TEST(!cfg.zstd.strategy.has_value()); + } +}; + +TEST_SUITE(encoder_config_test, "boost.burl.encoder_config"); + +} // namespace burl +} // namespace boost diff --git a/test/unit/head_parser.cpp b/test/unit/head_parser.cpp index 01900c0..8505feb 100644 --- a/test/unit/head_parser.cpp +++ b/test/unit/head_parser.cpp @@ -85,9 +85,7 @@ class head_parser_test BOOST_ASSERT(base + size + s.size() <= pr.ceiling()); std::memcpy(base + size, s.data(), s.size()); size += s.size(); - std::error_code ec; - pr.parse(size, ec); - return ec; + return pr.parse(size).error(); } // one shot into a parser with nothing in it yet @@ -121,9 +119,7 @@ class head_parser_test std::memcpy(buf_, msg.data(), msg.size()); std::memcpy(buf_ + msg.size(), body.data(), body.size()); auto const fed = msg.size() + body.size(); - std::error_code ec; - pr.parse(fed, ec); - BOOST_TEST(!ec); + BOOST_TEST(pr.parse(fed).has_value()); auto const& h = pr.request_head(); BOOST_TEST(h.method() == http::method::get); @@ -172,8 +168,7 @@ class head_parser_test BOOST_TEST(pr.ceiling() == buf_ + sizeof(buf_) - reserve); // parsing again is a no-op success - pr.parse(fed, ec); - BOOST_TEST(!ec); + BOOST_TEST(pr.parse(fed).has_value()); } void @@ -186,16 +181,13 @@ class head_parser_test "\r\n"; head_parser pr(true, buf_, sizeof(buf_)); - std::error_code ec; std::size_t n = 0; for(char const c : msg) { - pr.parse(n, ec); - BOOST_TEST(ec == http::error::need_data); + BOOST_TEST(pr.parse(n).error() == http::error::need_data); buf_[n++] = c; } - pr.parse(n, ec); - BOOST_TEST(!ec); + BOOST_TEST(pr.parse(n).has_value()); auto const& h = pr.request_head(); BOOST_TEST(h.method() == http::method::post); @@ -417,19 +409,16 @@ class head_parser_test "\r\n"; head_parser pr(true, buf_, sizeof(buf_)); - std::error_code ec; // bytes are appended one at a time; // those already parsed are resolved in // place and never rewritten std::size_t n = 0; for(char const c : msg) { - pr.parse(n, ec); - BOOST_TEST(ec == http::error::need_data); + BOOST_TEST(pr.parse(n).error() == http::error::need_data); buf_[n++] = c; } - pr.parse(n, ec); - BOOST_TEST(!ec); + BOOST_TEST(pr.parse(n).has_value()); BOOST_TEST_EQ(pr.request_head().at("X"), "1 continued"); } @@ -596,8 +585,7 @@ class head_parser_test std::error_code ec = feed(pr, n, msg); BOOST_TEST(ec == e); // errors are sticky - pr.parse(n, ec); - BOOST_TEST(ec == e); + BOOST_TEST(pr.parse(n).error() == e); }; // bare LF @@ -891,7 +879,7 @@ class head_parser_test std::error_code ec; for(std::size_t n = 0;; ++n) { - pr.parse(n, ec); + ec = pr.parse(n).error(); if(ec != http::error::need_data || n == s.size()) break; @@ -982,12 +970,9 @@ class head_parser_test head_parser pr(true, tiny, sizeof(tiny)); BOOST_TEST(pr.ceiling() == tiny); // default max_fields = 100 - std::error_code ec; - pr.parse(0, ec); - BOOST_TEST(ec == http::error::in_place_overflow); + BOOST_TEST(pr.parse(0).error() == http::error::in_place_overflow); // the error is derived again on request - pr.parse(0, ec); - BOOST_TEST(ec == http::error::in_place_overflow); + BOOST_TEST(pr.parse(0).error() == http::error::in_place_overflow); // with fitting limits the same buffer works head_parser pr2(true, tiny, sizeof(tiny), { .max_fields = 3 }); @@ -1055,12 +1040,10 @@ class head_parser_test // limits with it header_limits const lim{ .max_fields = 4 }; head_parser pr(true, buf, sizeof(buf), lim); - std::error_code ec; std::size_t n = 0; std::memcpy(buf, msg.data(), 21); n = 21; - pr.parse(n, ec); - BOOST_TEST(ec == http::error::need_data); + BOOST_TEST(pr.parse(n).error() == http::error::need_data); // the new object continues over the same // bytes @@ -1090,8 +1073,7 @@ class head_parser_test // no room to receive anything head_parser pr3; BOOST_TEST(pr3.ceiling() == base_of(pr3)); - pr3.parse(0, ec); - BOOST_TEST(ec == http::error::in_place_overflow); + BOOST_TEST(pr3.parse(0).error() == http::error::in_place_overflow); } void @@ -1149,9 +1131,7 @@ class head_parser_test std::memmove(buf, lo.data(), lo.size()); pr.reset(buf); n = lo.size(); - std::error_code ec; - pr.parse(n, ec); - BOOST_TEST(! ec); + BOOST_TEST(pr.parse(n).has_value()); BOOST_TEST_EQ(pr.request_head().target(), "/b"); BOOST_TEST_EQ(leftovers(pr, n).size(), 0u); } @@ -1185,9 +1165,7 @@ class head_parser_test { head_parser pr(true, raw, n, lim); BOOST_TEST(pr.ceiling() == raw); - std::error_code ec; - pr.parse(0, ec); - BOOST_TEST(ec == http::error::in_place_overflow); + BOOST_TEST(pr.parse(0).error() == http::error::in_place_overflow); } } @@ -1201,8 +1179,7 @@ class head_parser_test auto ec = feed(pr, n, msg.substr(0, 24)); BOOST_TEST(ec == http::error::in_place_overflow); // and stays so - pr.parse(n, ec); - BOOST_TEST(ec == http::error::in_place_overflow); + BOOST_TEST(pr.parse(n).error() == http::error::in_place_overflow); } } @@ -1224,7 +1201,7 @@ class head_parser_test fed = 0; for(;;) { - pr.parse(fed, ec); + ec = pr.parse(fed).error(); if(ec != http::error::need_data) break; auto const room = static_cast( @@ -1683,7 +1660,7 @@ class head_parser_test std::error_code ec; for(;;) { - pr.parse(held, ec); + ec = pr.parse(held).error(); if(ec != http::error::need_data) break; auto* const at = base_of(pr) + held; diff --git a/test/unit/message_writer.cpp b/test/unit/message_writer.cpp index 5c6be20..819d504 100644 --- a/test/unit/message_writer.cpp +++ b/test/unit/message_writer.cpp @@ -43,7 +43,7 @@ static_assert( class message_writer_test { - static constexpr serializer::config cfg{ + static inline serializer::config const cfg{ .stage_buffer = 64, .min_prepare = 32, .min_direct = 16, @@ -75,44 +75,6 @@ class message_writer_test 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 @@ -437,9 +399,9 @@ class message_writer_test 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. + // More caller buffers than the descriptor storage of + // the drive holds: frame() takes what fits, and the + // remainder is supplied again until the body is done. std::string const body = make_body(24); // identity @@ -467,19 +429,20 @@ class message_writer_test ws.data(), std::string(req.buffer()) + body); } - // encoder: fed window by window, with eof deferred - // until the last window + // the drive presents the whole remaining body on every + // call, so the end flag is never deferred: a chunked + // message supplied entirely at eof is rewritten to + // Content-Length although the body spans more + // descriptors than one frame() call can return { 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); + sr.start(&req); std::array bufs; for(std::size_t i = 0; i != bufs.size(); ++i) @@ -491,12 +454,11 @@ class message_writer_test 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)); + req.content_length().value_or(0), body.size()); + BOOST_TEST_EQ( + ws.data(), std::string(req.buffer()) + body); } } @@ -560,37 +522,6 @@ class message_writer_test 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 diff --git a/test/unit/parser.cpp b/test/unit/parser.cpp index 03a9ede..4b2dbc4 100644 --- a/test/unit/parser.cpp +++ b/test/unit/parser.cpp @@ -34,6 +34,7 @@ #include #include +#include "src/detail/decoder.hpp" #include "test_suite.hpp" namespace boost @@ -170,8 +171,7 @@ struct test_parser : parser { for(;;) { - std::error_code ec; - parse_header(ec); + auto const ec = parse_header().error(); if(ec != http::error::need_data) return ec; refill(); @@ -187,10 +187,11 @@ struct test_parser : parser for(;;) { - std::error_code ec; - auto const sv = flatten_body(ec); - if(ec != http::error::need_data) - return { ec, sv }; + auto const r = flatten_body(); + if(r.has_value()) + return { std::error_code(), *r }; + if(r.error() != http::error::need_data) + return { r.error(), {} }; refill(); } } @@ -205,10 +206,11 @@ struct test_parser : parser for(;;) { - std::error_code ec; - auto const n = parser::read_some(buffers, ec); - if(ec != http::error::need_data) - return { ec, n }; + auto const r = parser::read_some(buffers); + if(r.has_value()) + return { std::error_code(), *r }; + if(r.error() != http::error::need_data) + return { r.error(), 0 }; if(auto const lim = direct_capacity(); lim != 0) { @@ -252,10 +254,11 @@ struct test_parser : parser for(;;) { - std::error_code ec; - auto const bufs = parser::pull(dest, ec); - if(ec != http::error::need_data) - return { ec, bufs }; + auto const r = parser::pull(dest); + if(r.has_value()) + return { std::error_code(), *r }; + if(r.error() != http::error::need_data) + return { r.error(), {} }; refill(); } } @@ -552,9 +555,7 @@ class parser_test pr.start(); for(int i = 0; i != 5; ++i) { - std::error_code ec; - pr.parse_header(ec); - BOOST_TEST(ec == http::error::need_data); + BOOST_TEST(pr.parse_header().error() == http::error::need_data); BOOST_TEST(!pr.got_header()); pr.refill(); } @@ -817,15 +818,15 @@ class parser_test "hel"); pr.start(); - // the octets which did arrive are still flattened and - // viewable; the error says the body is not all there + // the whole body can never arrive: the error is + // reported and no view is delivered auto [ec, body] = pr.read_body(); BOOST_TEST(ec == http::error::incomplete); - BOOST_TEST(body == "hel"); + BOOST_TEST(body.empty()); auto [ec2, body2] = pr.read_body(); BOOST_TEST(ec2 == http::error::incomplete); - BOOST_TEST(body2 == "hel"); + BOOST_TEST(body2.empty()); } void @@ -939,11 +940,11 @@ class parser_test pr.start(); { - // the buffer cannot hold the whole body; what fits is - // still flattened and viewable + // the buffer cannot hold the whole body, so + // nothing is delivered auto [ec, part] = pr.read_body(); BOOST_TEST(ec == http::error::in_place_overflow); - BOOST_TEST(part == std::string_view(body).substr(0, 25)); + BOOST_TEST(part.empty()); } // delivery-side overflow is transient: streaming reads // drain the whole message (read_body did not consume) @@ -1217,9 +1218,7 @@ class parser_test BOOST_TEST(pr.has_buffered_data()); fields f; - std::error_code tec; - pr.parse_trailer(f, tec); - BOOST_TEST(!tec); + BOOST_TEST(pr.parse_trailer(f).has_value()); BOOST_TEST_EQ(f.size(), 3u); // insertion order, known-field resolution, and @@ -1232,8 +1231,7 @@ class parser_test BOOST_TEST(*++it == "2"); // each call appends again - pr.parse_trailer(f, tec); - BOOST_TEST(!tec); + BOOST_TEST(pr.parse_trailer(f).has_value()); BOOST_TEST_EQ(f.size(), 6u); } @@ -1261,9 +1259,7 @@ class parser_test BOOST_TEST(body == "hello world"); fields f; - std::error_code tec; - pr.parse_trailer(f, tec); - BOOST_TEST(!tec); + BOOST_TEST(pr.parse_trailer(f).has_value()); BOOST_TEST_EQ(f.size(), 1u); BOOST_TEST(f.at("X-Trailer") == "v"); @@ -1305,9 +1301,7 @@ class parser_test BOOST_TEST(!ec); BOOST_TEST(pr.got_body()); fields f; - std::error_code tec; - pr.parse_trailer(f, tec); - BOOST_TEST(tec == http::error::incomplete); + BOOST_TEST(pr.parse_trailer(f).error() == http::error::incomplete); BOOST_TEST_EQ(f.size(), 0u); } @@ -1329,9 +1323,7 @@ class parser_test BOOST_TEST(got == "abc"); fields f; - std::error_code tec; - pr.parse_trailer(f, tec); - BOOST_TEST(!tec); + BOOST_TEST(pr.parse_trailer(f).has_value()); BOOST_TEST_EQ(f.size(), 1u); BOOST_TEST(f.at("X-T") == "v"); } @@ -1355,9 +1347,7 @@ class parser_test BOOST_TEST(!pr.has_buffered_data()); fields f; - std::error_code tec; - pr.parse_trailer(f, tec); - BOOST_TEST(!tec); + BOOST_TEST(pr.parse_trailer(f).has_value()); BOOST_TEST(f.empty()); } { @@ -1375,9 +1365,7 @@ class parser_test BOOST_TEST(body == "hello"); fields f; - std::error_code tec; - pr.parse_trailer(f, tec); - BOOST_TEST(!tec); + BOOST_TEST(pr.parse_trailer(f).has_value()); BOOST_TEST(f.empty()); } } @@ -1401,16 +1389,13 @@ class parser_test BOOST_TEST(!ec); fields f; - std::error_code tec; - pr.parse_trailer(f, tec); - BOOST_TEST(!tec); + BOOST_TEST(pr.parse_trailer(f).has_value()); BOOST_TEST_EQ(f.size(), 2u); BOOST_TEST(f.at("X-A") == "1 fold"); BOOST_TEST(f.at("X-B") == "2"); // the in-place unfolding is stable under re-parsing - pr.parse_trailer(f, tec); - BOOST_TEST(!tec); + BOOST_TEST(pr.parse_trailer(f).has_value()); BOOST_TEST_EQ(f.size(), 4u); BOOST_TEST(f.at("X-A") == "1 fold"); } @@ -1438,9 +1423,7 @@ class parser_test BOOST_TEST(body == "hello"); fields f; - std::error_code tec; - pr.parse_trailer(f, tec); - BOOST_TEST(!tec); + BOOST_TEST(pr.parse_trailer(f).has_value()); BOOST_TEST_EQ(f.size(), 2u); BOOST_TEST(f.at("X-A") == "1"); BOOST_TEST(f.at("X-B") == "2"); @@ -1472,8 +1455,7 @@ class parser_test BOOST_TEST(!ec); fields f; - std::error_code tec; - pr.parse_trailer(f, tec); + auto const tec = pr.parse_trailer(f).error(); if(over) { BOOST_TEST(tec == http::error::field_size_limit); @@ -1512,9 +1494,7 @@ class parser_test BOOST_TEST(pr.got_body()); fields f; - std::error_code tec; - pr.parse_trailer(f, tec); - BOOST_TEST(tec == http::error::bad_field_name); + BOOST_TEST(pr.parse_trailer(f).error() == http::error::bad_field_name); BOOST_TEST_EQ(f.size(), 1u); BOOST_TEST(f.at("X-A") == "v"); @@ -1541,9 +1521,7 @@ class parser_test BOOST_TEST(!ec); fields f; - std::error_code tec; - pr.parse_trailer(f, tec); - BOOST_TEST(tec == http::error::bad_line_ending); + BOOST_TEST(pr.parse_trailer(f).error() == http::error::bad_line_ending); BOOST_TEST(f.empty()); } } @@ -1568,13 +1546,11 @@ class parser_test // the trailer remains available for a retry char small[8]; static_fields sf(small, sizeof(small)); - std::error_code tec; BOOST_TEST_THROWS( - pr.parse_trailer(sf, tec), std::length_error); + pr.parse_trailer(sf), std::length_error); fields f; - pr.parse_trailer(f, tec); - BOOST_TEST(!tec); + BOOST_TEST(pr.parse_trailer(f).has_value()); BOOST_TEST_EQ(f.size(), 1u); BOOST_TEST(f.at("X-Trailer") == "value"); } @@ -1650,15 +1626,12 @@ class parser_test } fields f; - std::error_code tec; - pr.parse_trailer(f, tec); - BOOST_TEST(!tec); + BOOST_TEST(pr.parse_trailer(f).has_value()); BOOST_TEST_EQ(f.size(), 1u); BOOST_TEST(f.at("X-T") == value); // the rearrangement preserves the section - pr.parse_trailer(f, tec); - BOOST_TEST(!tec); + BOOST_TEST(pr.parse_trailer(f).has_value()); BOOST_TEST_EQ(f.size(), 2u); BOOST_TEST(pr.has_buffered_data() == @@ -2246,16 +2219,15 @@ class parser_test pr.start(); std::string got; { - // the chunks flattened before the overflow are - // viewable; the rest of the body is not + // the whole body cannot be flattened, so nothing + // is delivered auto [ec, part] = pr.read_body(); BOOST_TEST(ec == http::error::in_place_overflow); - BOOST_TEST(part == "hello"); + BOOST_TEST(part.empty()); } // the switch to streaming first drains the bytes parked - // in the decode buffer (the same bytes the partial view - // exposed — read_body does not consume), then continues - // with the wire + // in the decode buffer by the failed flatten (read_body + // does not consume), then continues with the wire for(;;) { char buf[4]; @@ -3257,11 +3229,11 @@ class parser_test BOOST_TEST(!hec); pr.set_decoder(dec); - // the in-place body cannot be completed; the output - // produced before the failure is still viewable + // the in-place body cannot be completed: the failure + // is reported and no view is delivered auto [ec, body] = pr.read_body(); BOOST_TEST(ec == capy::error::test_failure); - BOOST_TEST(body == decoded("hel")); + BOOST_TEST(body.empty()); } void @@ -3377,9 +3349,7 @@ class parser_test BOOST_TEST(pr.got_body()); fields f; - std::error_code tec; - pr.parse_trailer(f, tec); - BOOST_TEST(!tec); + BOOST_TEST(pr.parse_trailer(f).has_value()); BOOST_TEST_EQ(f.size(), 1u); BOOST_TEST(f.at("X-T") == "v"); } @@ -3661,11 +3631,11 @@ class parser_test std::string got; { - // what the decode buffer holds is viewable; the rest - // of the output has nowhere to go + // the rest of the output has nowhere to go, so + // nothing is delivered auto [ec, part] = pr.read_body(); BOOST_TEST(ec == http::error::in_place_overflow); - BOOST_TEST(part == decoded("hell")); + BOOST_TEST(part.empty()); } for(;;) { diff --git a/test/unit/request_head.cpp b/test/unit/request_head.cpp index 0a3806a..31aa289 100644 --- a/test/unit/request_head.cpp +++ b/test/unit/request_head.cpp @@ -1060,9 +1060,7 @@ class request_head_test [&](head_parser& pr) { std::memcpy(buf, msg.data(), msg.size()); - std::error_code ec; - pr.parse(msg.size(), ec); - BOOST_TEST(!ec); + BOOST_TEST(pr.parse(msg.size()).has_value()); }; head_parser pr(true, buf, sizeof(buf)); diff --git a/test/unit/request_parser.cpp b/test/unit/request_parser.cpp index 12460ff..f6b26be 100644 --- a/test/unit/request_parser.cpp +++ b/test/unit/request_parser.cpp @@ -52,9 +52,7 @@ class request_parser_test "\r\n" "hello"); - std::error_code ec; - pr.parse_header(ec); - BOOST_TEST(!ec); + BOOST_TEST(!pr.parse_header().has_error()); BOOST_TEST(pr.got_header()); BOOST_TEST(pr.get().method() == http::method::get); @@ -62,14 +60,13 @@ class request_parser_test char buf[8]; capy::mutable_buffer mb(buf, sizeof(buf)); - auto n = pr.read_some(mb, ec); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 5); - BOOST_TEST(std::string_view(buf, n) == "hello"); + auto r = pr.read_some(mb); + BOOST_TEST(r.has_value()); + BOOST_TEST_EQ(*r, 5); + BOOST_TEST(std::string_view(buf, *r) == "hello"); - n = pr.read_some(mb, ec); - BOOST_TEST(ec == capy::cond::eof); - BOOST_TEST_EQ(n, 0); + r = pr.read_some(mb); + BOOST_TEST(r.error() == capy::cond::eof); } void diff --git a/test/unit/response_head.cpp b/test/unit/response_head.cpp index 65171c4..5d415a6 100644 --- a/test/unit/response_head.cpp +++ b/test/unit/response_head.cpp @@ -474,9 +474,7 @@ class response_head_test [&](head_parser& pr) { std::memcpy(buf, msg.data(), msg.size()); - std::error_code ec; - pr.parse(msg.size(), ec); - BOOST_TEST(!ec); + BOOST_TEST(pr.parse(msg.size()).has_value()); }; head_parser pr(false, buf, sizeof(buf)); @@ -523,9 +521,7 @@ class response_head_test "\r\n"; head_parser pr(false, buf, sizeof(buf)); std::memcpy(buf, msg.data(), msg.size()); - std::error_code ec; - pr.parse(msg.size(), ec); - BOOST_TEST(!ec); + BOOST_TEST(pr.parse(msg.size()).has_value()); response_head_base const& base = pr.response_head(); // small target: the assignment reallocates diff --git a/test/unit/response_parser.cpp b/test/unit/response_parser.cpp index 83dab01..c942aeb 100644 --- a/test/unit/response_parser.cpp +++ b/test/unit/response_parser.cpp @@ -51,9 +51,7 @@ class response_parser_test "\r\n" "hello"); - std::error_code ec; - pr.parse_header(ec); - BOOST_TEST(!ec); + BOOST_TEST(!pr.parse_header().has_error()); BOOST_TEST(pr.got_header()); // the whole body arrived with the header, so the message // is already complete (arrival semantics) @@ -65,14 +63,13 @@ class response_parser_test char buf[8]; capy::mutable_buffer mb(buf, sizeof(buf)); - auto n = pr.read_some(mb, ec); - BOOST_TEST(!ec); - BOOST_TEST_EQ(n, 5); - BOOST_TEST(std::string_view(buf, n) == "hello"); - - n = pr.read_some(mb, ec); - BOOST_TEST(ec == capy::cond::eof); - BOOST_TEST_EQ(n, 0); + auto r = pr.read_some(mb); + BOOST_TEST(r.has_value()); + BOOST_TEST_EQ(*r, 5); + BOOST_TEST(std::string_view(buf, *r) == "hello"); + + r = pr.read_some(mb); + BOOST_TEST(r.error() == capy::cond::eof); } void @@ -88,9 +85,7 @@ class response_parser_test "Content-Length: 5\r\n" "\r\n"); - std::error_code ec; - pr.parse_header(ec); - BOOST_TEST(!ec); + BOOST_TEST(!pr.parse_header().has_error()); BOOST_TEST(pr.got_header()); BOOST_TEST(pr.got_body()); BOOST_TEST_EQ(pr.get().status_int(), 200); diff --git a/test/unit/serializer.cpp b/test/unit/serializer.cpp index 0ebd8d0..439d1e4 100644 --- a/test/unit/serializer.cpp +++ b/test/unit/serializer.cpp @@ -13,15 +13,20 @@ #include #include #include +#include #include #include #include #include +#include +#include #include +#include #include +#include "src/detail/encoder.hpp" #include "test_suite.hpp" namespace boost @@ -40,7 +45,7 @@ class serializer_test // 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{ + static inline serializer::config const cfg{ .stage_buffer = 64, .min_prepare = 32, .min_direct = 16, @@ -67,12 +72,47 @@ class serializer_test return req; } + struct test_serializer : serializer + { + using serializer::serializer; + + // Install a test encoder, which the test + // keeps owning so that it can inspect it + // afterwards. + template + void + set_encoder(E& enc) + { + struct forwarder : detail::encoder + { + E& e; + + explicit + forwarder(E& e_) noexcept + : e(e_) + { + } + + result + process( + capy::mutable_buffer out, + capy::const_buffer in, + bool more) override + { + return e.process(out, in, more); + } + }; + serializer::set_encoder( + std::make_unique(enc)); + } + }; + // 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 + struct test_encoder : detail::encoder { std::string footer; std::error_code fail; @@ -163,16 +203,15 @@ class serializer_test { capy::const_buffer const b{ body.data(), body.size() }; - std::error_code ec; capy::const_buffer dest[dest_n]; - auto const bufs = sr.frame(dest, b, more, ec); - if(bufs.empty()) + auto const r = sr.frame(dest, b, more); + if(r.has_error() || r->empty()) { body.remove_prefix(sr.consume(0)); - return ec; + return r.error(); } std::size_t n = 0; - for(auto cb : bufs) + for(auto cb : *r) { auto const k = (std::min)(step - n, cb.size()); wire.append( @@ -242,16 +281,15 @@ class serializer_test { for(;;) { - std::error_code ec; capy::const_buffer dest[dest_n]; - auto const bufs = sr.frame(dest, more, ec); - if(bufs.empty()) + auto const r = sr.frame(dest, more); + if(r.has_error() || r->empty()) { BOOST_TEST_EQ(sr.consume(0), 0u); - return ec; + return r.error(); } std::size_t n = 0; - for(auto cb : bufs) + for(auto cb : *r) { wire.append( static_cast(cb.data()), @@ -267,7 +305,7 @@ class serializer_test testContentLengthSmallBody() { auto req = make_request(5); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); BOOST_TEST(!sr.is_done()); @@ -289,7 +327,7 @@ class serializer_test // 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); + test_serializer sr(cfg); sr.start(&req); std::string wire; @@ -306,7 +344,7 @@ class serializer_test std::string const b1(4, 'a'); std::string const b2(4, 'b'); auto req = make_request(b1.size() + b2.size()); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); capy::const_buffer const bufs[2] = { @@ -314,10 +352,8 @@ class serializer_test capy::make_buffer(b2) }; std::string wire; - std::error_code ec; capy::const_buffer dest[dest_n]; - auto const out = sr.frame(dest, bufs, false, ec); - BOOST_TEST(!ec); + auto const out = sr.frame(dest, bufs, false).value(); BOOST_TEST(!out.empty()); std::size_t n = 0; @@ -340,7 +376,7 @@ class serializer_test // fewer bytes than Content-Length { auto req = make_request(5); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); std::string wire; @@ -355,7 +391,7 @@ class serializer_test // supplying call, before any octet is handed out { auto req = make_request(5); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); std::string wire; @@ -369,7 +405,7 @@ class serializer_test std::string const body(cfg.min_direct, 'x'); { auto req = make_request(5); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); std::string wire; @@ -383,7 +419,7 @@ class serializer_test // corrected input proceeds { auto req = make_request(5); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); std::string wire; @@ -404,7 +440,7 @@ class serializer_test testChunkedSmallBodyConvertsToContentLength() { auto req = make_request(); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); // The body stays below min_direct, so it is fully @@ -429,7 +465,7 @@ class serializer_test testChunkedWriteEofWithBody() { auto req = make_request(); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); std::string wire; @@ -447,7 +483,7 @@ class serializer_test { std::string const body(cfg.min_direct, 'x'); auto req = make_request(); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); std::string wire; @@ -478,7 +514,7 @@ class serializer_test // in the same vector. std::string const body(cfg.min_direct, 'x'); auto req = make_request(); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); std::string wire; @@ -499,7 +535,7 @@ class serializer_test { std::string const body(cfg.min_direct, 'x'); auto req = make_request(); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); std::string wire; @@ -537,7 +573,7 @@ class serializer_test testPrepareCommitContentLength() { auto req = make_request(5); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); capy::mutable_buffer tmp[2]; @@ -564,7 +600,7 @@ class serializer_test // body; one that does flushes the whole staged run as // one chunk. auto req = make_request(); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); std::string const b1 = make_body(30); @@ -605,7 +641,7 @@ class serializer_test // full capacity is stageable as one chunk; 64 == 0x40. std::string const body(cfg.stage_buffer, 'z'); auto req = make_request(); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); capy::mutable_buffer tmp[2]; @@ -640,7 +676,7 @@ class serializer_test // Nothing is drained before eof, so the staged body // converts to Content-Length. auto req = make_request(); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); capy::mutable_buffer tmp[2]; @@ -663,7 +699,7 @@ class serializer_test testPrepareEmptyDest() { auto req = make_request(); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); auto const dest = sr.prepare({}); @@ -677,7 +713,7 @@ class serializer_test // drain pass flushes it and restores the full window. std::string const body(cfg.stage_buffer, 'z'); auto req = make_request(); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); capy::mutable_buffer tmp[2]; @@ -711,7 +747,7 @@ class serializer_test std::string expected; { auto req = make_request(); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); BOOST_TEST(!write(sr, expected, body)); BOOST_TEST(!write(sr, expected, "abc")); @@ -721,7 +757,7 @@ class serializer_test for(std::size_t step = 1; step != 24; ++step) { auto req = make_request(); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); std::string wire; @@ -744,17 +780,15 @@ class serializer_test // out as it stands. std::string const body(cfg.min_direct + 8, 'x'); auto req = make_request(body.size()); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); std::string_view rem(body); capy::const_buffer const b{ rem.data(), rem.size() }; - std::error_code ec; capy::const_buffer dest[dest_n]; - auto out = sr.frame(dest, b, true, ec); - BOOST_TEST(!ec); + auto out = sr.frame(dest, b, true).value(); BOOST_TEST_EQ(out.size(), 2u); // header + body, no copy // the wire takes the header and all but 4 octets @@ -770,8 +804,7 @@ class serializer_test // 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); + out = sr.frame(dest, b2, true).value(); BOOST_TEST(out.empty()); BOOST_TEST_EQ(sr.consume(0), rem.size()); @@ -788,17 +821,15 @@ class serializer_test // same octets, before and after a partial consume. std::string const body(cfg.min_direct, 'x'); auto req = make_request(); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); capy::const_buffer const b{ body.data(), body.size() }; auto const flatten = [&] { - std::error_code ec; capy::const_buffer dest[dest_n]; - auto const bufs = sr.frame(dest, b, true, ec); - BOOST_TEST(!ec); + auto const bufs = sr.frame(dest, b, true).value(); std::string s; for(auto cb : bufs) s.append( @@ -827,7 +858,7 @@ class serializer_test // and the serializer can even be moved mid-flight. std::string const body(cfg.min_direct, 'x'); auto req = make_request(); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); std::string_view rem(body); @@ -835,10 +866,8 @@ class serializer_test { capy::const_buffer const b{ rem.data(), rem.size() }; - std::error_code ec; capy::const_buffer dest[dest_n]; - auto const bufs = sr.frame(dest, b, true, ec); - BOOST_TEST(!ec); + auto const bufs = sr.frame(dest, b, true).value(); BOOST_TEST(!bufs.empty()); // take the header and half the chunk, then stop, as @@ -877,7 +906,7 @@ class serializer_test // pays it. std::string const body(cfg.min_direct, 'x'); auto req = make_request(); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); std::string_view rem(body); @@ -885,10 +914,8 @@ class serializer_test { capy::const_buffer const b{ rem.data(), rem.size() }; - std::error_code ec; capy::const_buffer dest[dest_n]; - auto const bufs = sr.frame(dest, b, true, ec); - BOOST_TEST(!ec); + auto const bufs = sr.frame(dest, b, true).value(); auto const k = req.buffer().size() + 4 + body.size() / 2; @@ -925,17 +952,15 @@ class serializer_test // wire is already committed to the chunk. std::string const body(cfg.min_direct, 'x'); auto req = make_request(); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); std::string wire; { capy::const_buffer const b{ body.data(), body.size() }; - std::error_code ec; capy::const_buffer dest[dest_n]; - auto const bufs = sr.frame(dest, b, true, ec); - BOOST_TEST(!ec); + auto const bufs = sr.frame(dest, b, true).value(); // the header and the chunk prefix, no body octet auto const k = req.buffer().size() + 4; @@ -985,7 +1010,7 @@ class serializer_test // reports are the caller's cursor. std::string const body(cfg.min_direct, 'x'); auto req = make_request(body.size()); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); std::string_view rem(body); @@ -993,10 +1018,8 @@ class serializer_test { capy::const_buffer const b{ rem.data(), rem.size() }; - std::error_code ec; capy::const_buffer dest[dest_n]; - auto const bufs = sr.frame(dest, b, true, ec); - BOOST_TEST(!ec); + auto const bufs = sr.frame(dest, b, true).value(); // header plus one body octet auto const k = req.buffer().size() + 1; @@ -1029,14 +1052,12 @@ class serializer_test std::size(bufs) * piece.size(), 'x'); auto req = make_request(body.size()); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); std::string wire; - std::error_code ec; capy::const_buffer dest[32]; - auto const out = sr.frame(dest, bufs, false, ec); - BOOST_TEST(!ec); + auto const out = sr.frame(dest, bufs, false).value(); BOOST_TEST_EQ( out.size(), std::size(bufs) + 1); // + header @@ -1060,7 +1081,7 @@ class serializer_test // remainder is re-materialized next call. std::string const body(cfg.min_direct, 'x'); auto req = make_request(); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); fields trailer; @@ -1074,11 +1095,9 @@ class serializer_test { capy::const_buffer const b{ rem.data(), rem.size() }; - std::error_code ec; capy::const_buffer dest[1]; - auto const bufs = sr.frame(dest, b, more, ec); + auto const bufs = sr.frame(dest, b, more).value(); more = false; - BOOST_TEST(!ec); if(bufs.empty()) { rem.remove_prefix(sr.consume(0)); @@ -1118,10 +1137,8 @@ class serializer_test { capy::const_buffer const b{ rem2.data(), rem2.size() }; - std::error_code ec; capy::const_buffer dest[1]; - auto const bufs = sr2.frame(dest, b, false, ec); - BOOST_TEST(!ec); + auto const bufs = sr2.frame(dest, b, false).value(); if(bufs.empty()) { rem2.remove_prefix(sr2.consume(0)); @@ -1157,7 +1174,7 @@ class serializer_test std::string const b1(40, 'x'); std::string const b2(10, 'y'); auto req = make_request(); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); capy::mutable_buffer tmp[2]; @@ -1169,10 +1186,8 @@ class serializer_test // the wire takes the header, the prefix, and ten octets std::string wire; { - std::error_code ec; capy::const_buffer dest[dest_n]; - auto const out = sr.frame(dest, true, ec); - BOOST_TEST(!ec); + auto const out = sr.frame(dest, true).value(); BOOST_TEST_EQ(out.size(), 2u); std::size_t const n = req.buffer().size() + 4 + 10; @@ -1216,7 +1231,7 @@ class serializer_test // 40 == 0x28. std::string const body = make_body(40); auto req = make_request(); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); // the header first, so the framing stays chunked @@ -1230,12 +1245,10 @@ class serializer_test // for all forty octets; the wire takes the prefix and // ten of them { - std::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); + auto const out = sr.frame(dest, b, false).value(); // prefix + body + last-chunk BOOST_TEST_EQ(out.size(), 3u); @@ -1258,12 +1271,10 @@ class serializer_test // fewer than the chunk still owes while(!sr.is_done()) { - std::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); + auto const out = sr.frame(dest, b, false).value(); std::size_t n = 0; for(auto cb : out) { @@ -1296,14 +1307,12 @@ class serializer_test std::size(bufs) / 2 * piece.size(), 'y'); auto req = make_request(body.size()); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); std::string wire; - std::error_code ec; capy::const_buffer dest[32]; - auto const out = sr.frame(dest, bufs, false, ec); - BOOST_TEST(!ec); + auto const out = sr.frame(dest, bufs, false).value(); std::size_t n = 0; for(auto cb : out) @@ -1327,7 +1336,7 @@ class serializer_test // consumed when the wire takes them — but both are // reported by the consume answering that frame. auto req = make_request(); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); // absorbed, no debt: nothing to write, and the input is @@ -1348,10 +1357,8 @@ class serializer_test BOOST_TEST(sr.should_drain()); capy::const_buffer const b{ "def", 3 }; - std::error_code ec; capy::const_buffer dest[dest_n]; - auto const bufs = sr.frame(dest, b, true, ec); - BOOST_TEST(!ec); + auto const bufs = sr.frame(dest, b, true).value(); BOOST_TEST(!bufs.empty()); std::size_t n = 0; @@ -1381,7 +1388,7 @@ class serializer_test // 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); + test_serializer sr(cfg); sr.start(&req); capy::mutable_buffer tmp[2]; @@ -1433,7 +1440,7 @@ class serializer_test // 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); + test_serializer sr(cfg); sr.start(&req); std::string wire; @@ -1468,7 +1475,7 @@ class serializer_test 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); + test_serializer sr(cfg); sr.start(&req); capy::mutable_buffer tmp[2]; @@ -1480,12 +1487,10 @@ class serializer_test std::string wire; std::string_view rem(body); { - std::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); + auto const out = sr.frame(dest, b, true).value(); // header + staged run + caller's octets BOOST_TEST_EQ(out.size(), 3u); @@ -1524,7 +1529,7 @@ class serializer_test std::string const body(cfg.min_direct, 'x'); { response_head res; - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&res); std::string wire; @@ -1542,7 +1547,7 @@ class serializer_test // converts to Content-Length, saving the connection { response_head res; - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&res); std::string wire; @@ -1558,7 +1563,7 @@ class serializer_test // completes the message all the same { response_head res; - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&res); std::string wire; @@ -1576,7 +1581,7 @@ class serializer_test // definition; body bytes are rejected when supplied { request_head req; - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); std::string wire; @@ -1597,7 +1602,7 @@ class serializer_test for(std::size_t step : { 1u, 3u, 7u }) { response_head res; - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&res); std::string wire; @@ -1626,7 +1631,7 @@ class serializer_test // octets after the end: rejected, nothing framed { response_head res; - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&res); std::string wire; @@ -1650,7 +1655,7 @@ class serializer_test // corrected remainder still resumes { response_head res; - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&res); std::string wire; @@ -1659,13 +1664,11 @@ class serializer_test std::string const b = make_body(10); { // declare eof; the wire takes nothing - std::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); + !sr.frame(dest, cb, false).value().empty()); BOOST_TEST_EQ(sr.consume(0), 0u); } @@ -1696,17 +1699,15 @@ class serializer_test // would truncate a body whose framing gives the peer no // way to notice. response_head res; - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&res); std::string wire; - std::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 out = sr.frame(dest, true).value(); auto const half = res.buffer().size() / 2; std::size_t n = 0; for(auto cb : out) @@ -1727,8 +1728,7 @@ class serializer_test 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); + auto const out = sr.frame({ one, 1 }, cb, false).value(); BOOST_TEST_EQ(out.size(), 1u); std::string flat; for(auto b : out) @@ -1760,17 +1760,15 @@ class serializer_test // content-length, whole body declared at once { auto req = make_request(5); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); std::string_view body("hello"); { - std::error_code ec; capy::const_buffer const cb{ body.data(), body.size() }; BOOST_TEST( - sr.frame({}, cb, false, ec).empty()); - BOOST_TEST(!ec); + sr.frame({}, cb, false).value().empty()); // nothing was placed, so nothing is released body.remove_prefix(sr.consume(0)); } @@ -1789,7 +1787,7 @@ class serializer_test auto req = make_request(); fields trailer; trailer.set("x", "1"); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); sr.set_trailer(&trailer); @@ -1800,12 +1798,10 @@ class serializer_test // the empty-dest call opens the chunk covering the // staged and the supplied octets { - std::error_code ec; capy::const_buffer const cb{ body.data() + 2, 3 }; BOOST_TEST( - sr.frame({}, cb, false, ec).empty()); - BOOST_TEST(!ec); + sr.frame({}, cb, false).value().empty()); BOOST_TEST_EQ(sr.consume(0), 0u); } BOOST_TEST(!sr.is_done()); @@ -1820,13 +1816,11 @@ class serializer_test // the bufferless overload, declaring an empty body { response_head res; - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&res); { - std::error_code ec; - BOOST_TEST(sr.frame({}, false, ec).empty()); - BOOST_TEST(!ec); + BOOST_TEST(sr.frame({}, false).value().empty()); BOOST_TEST_EQ(sr.consume(0), 0u); } BOOST_TEST(!sr.is_done()); @@ -1840,7 +1834,7 @@ class serializer_test // a genuinely short body is still diagnosed { auto req = make_request(5); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); std::string wire; @@ -1861,7 +1855,7 @@ class serializer_test auto req = make_request(); fields trailer; trailer.set("x", "1"); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); sr.set_trailer(&trailer); @@ -1870,25 +1864,21 @@ class serializer_test // chunk stands as debt std::string_view body("hello"); { - std::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(!sr.frame(dest, cb, false).value().empty()); 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 { - std::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); + auto const out = sr.frame(dest, cb, false).value(); std::string flat; for(auto b : out) flat.append( @@ -1924,7 +1914,7 @@ class serializer_test // after the last chunk { auto req = make_request(); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); fields trailer; @@ -1951,7 +1941,7 @@ class serializer_test // to Content-Length { auto req = make_request(); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); fields trailer; @@ -1995,7 +1985,7 @@ class serializer_test // set_trailer(nullptr) clears { auto req = make_request(5); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); fields trailer; @@ -2018,7 +2008,7 @@ class serializer_test // silently discarded { auto req = make_request(5); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req); sr.set_trailer(&trailer); @@ -2033,7 +2023,7 @@ class serializer_test // trailers either { response_head res; - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&res); sr.set_trailer(&trailer); @@ -2047,8 +2037,8 @@ class serializer_test // head mode suppresses the body and its framing { auto req = make_request(); - serializer sr(cfg); - sr.start(&req, nullptr, true); + test_serializer sr(cfg); + sr.start(&req, true); sr.set_trailer(&trailer); std::string wire; @@ -2068,8 +2058,9 @@ class serializer_test auto req = make_request(); req.set(http::field::content_encoding, "test"); test_encoder enc("eof!"); - serializer sr(cfg); - sr.start(&req, &enc); + test_serializer sr(cfg); + sr.start(&req); + sr.set_encoder(enc); fields trailer; trailer.set("x-digest", "42"); @@ -2100,8 +2091,9 @@ class serializer_test auto req = make_request(); req.set(http::field::content_encoding, "test"); test_encoder enc; - serializer sr(cfg); - sr.start(&req, &enc); + test_serializer sr(cfg); + sr.start(&req); + sr.set_encoder(enc); std::string wire; BOOST_TEST(!write(sr, wire, "hello")); @@ -2130,8 +2122,9 @@ class serializer_test auto req = make_request(); req.set(http::field::content_encoding, "test"); test_encoder enc; - serializer sr(cfg); - sr.start(&req, &enc); + test_serializer sr(cfg); + sr.start(&req); + sr.set_encoder(enc); std::string wire; BOOST_TEST(!sr.is_header_done()); @@ -2154,8 +2147,9 @@ class serializer_test auto req = make_request(); req.set(http::field::content_encoding, "test"); test_encoder enc; - serializer sr(cfg); - sr.start(&req, &enc); + test_serializer sr(cfg); + sr.start(&req); + sr.set_encoder(enc); std::string wire; BOOST_TEST(!write_eof(sr, wire)); @@ -2185,8 +2179,9 @@ class serializer_test auto req = make_request(); req.set(http::field::content_encoding, "test"); test_encoder enc; - serializer sr(cfg); - sr.start(&req, &enc); + test_serializer sr(cfg); + sr.start(&req); + sr.set_encoder(enc); fields trailer; trailer.set("x-digest", "42"); @@ -2221,8 +2216,9 @@ class serializer_test auto req = make_request(); req.set(http::field::content_encoding, "test"); test_encoder enc; - serializer sr(cfg); - sr.start(&req, &enc); + test_serializer sr(cfg); + sr.start(&req); + sr.set_encoder(enc); capy::mutable_buffer tmp[2]; auto const dest = sr.prepare(tmp); @@ -2256,8 +2252,9 @@ class serializer_test auto req = make_request(); req.set(http::field::content_encoding, "test"); test_encoder enc; - serializer sr(cfg); - sr.start(&req, &enc); + test_serializer sr(cfg); + sr.start(&req); + sr.set_encoder(enc); std::string wire; BOOST_TEST(!write_eof(sr, wire, body)); @@ -2283,8 +2280,9 @@ class serializer_test auto req = make_request(); req.set(http::field::content_encoding, "test"); test_encoder enc; - serializer sr(cfg); - sr.start(&req, &enc); + test_serializer sr(cfg); + sr.start(&req); + sr.set_encoder(enc); std::string wire; BOOST_TEST(!write_eof(sr, wire, body)); @@ -2309,8 +2307,9 @@ class serializer_test auto req = make_request(); req.set(http::field::content_encoding, "test"); test_encoder enc; - serializer sr(cfg); - sr.start(&req, &enc); + test_serializer sr(cfg); + sr.start(&req); + sr.set_encoder(enc); std::string wire; BOOST_TEST(!write(sr, wire, "abc")); @@ -2326,6 +2325,42 @@ class serializer_test std::string(req.buffer()) + encoded("abcdefgh")); } + void + testEncoderStartedConvertsToContentLength() + { + // The encoder starts on a write() whose output stays + // within the output buffer, so the header is still in + // hand when the end is declared later. A started + // encoder is kept even when the final input alone is + // below the threshold, and chunked converts to the + // encoded Content-Length as for a body declared whole. + std::string const body = make_body(16); + auto req = make_request(); + req.set(http::field::content_encoding, "test"); + test_encoder enc; + test_serializer sr(cfg); + sr.start(&req); + sr.set_encoder(enc); + + std::string wire; + BOOST_TEST(!write(sr, wire, body)); + BOOST_TEST(enc.calls != 0); + BOOST_TEST(!enc.finished); + BOOST_TEST(wire.empty()); + + BOOST_TEST(!write_eof(sr, wire, "abc")); + BOOST_TEST(sr.is_done()); + BOOST_TEST(enc.finished); + BOOST_TEST(!req.chunked()); + BOOST_TEST_EQ(req.content_length().value(), 19u); + BOOST_TEST( + wire.find("Content-Encoding: test\r\n") != + std::string::npos); + BOOST_TEST_EQ( + wire, + std::string(req.buffer()) + encoded(body + "abc")); + } + void testEncoderLargeBodyChunked() { @@ -2338,8 +2373,9 @@ class serializer_test auto req = make_request(); req.set(http::field::content_encoding, "test"); test_encoder enc; - serializer sr(cfg); - sr.start(&req, &enc); + test_serializer sr(cfg); + sr.start(&req); + sr.set_encoder(enc); std::string wire; BOOST_TEST(!write(sr, wire, body)); @@ -2374,8 +2410,9 @@ class serializer_test auto req = make_request(); req.set(http::field::content_encoding, "test"); test_encoder enc; - serializer sr(cfg); - sr.start(&req, &enc); + test_serializer sr(cfg); + sr.start(&req); + sr.set_encoder(enc); std::string wire; BOOST_TEST(!write_eof(sr, wire, body)); @@ -2401,8 +2438,9 @@ class serializer_test auto req = make_request(); req.set(http::field::content_encoding, "test"); test_encoder enc; - serializer sr(cfg); - sr.start(&req, &enc); + test_serializer sr(cfg); + sr.start(&req); + sr.set_encoder(enc); std::string wire; capy::mutable_buffer tmp[2]; @@ -2445,8 +2483,9 @@ class serializer_test auto req = make_request(); req.set(http::field::content_encoding, "test"); test_encoder enc("0123456789"); - serializer sr(cfg); - sr.start(&req, &enc); + test_serializer sr(cfg); + sr.start(&req); + sr.set_encoder(enc); std::string wire; BOOST_TEST(!write_eof(sr, wire, body)); @@ -2472,8 +2511,9 @@ class serializer_test auto req = make_request(12); req.set(http::field::content_encoding, "test"); test_encoder enc("eof!"); - serializer sr(cfg); - sr.start(&req, &enc); + test_serializer sr(cfg); + sr.start(&req); + sr.set_encoder(enc); std::string wire; BOOST_TEST(!write_eof(sr, wire, body)); @@ -2495,8 +2535,9 @@ class serializer_test auto req = make_request(5); req.set(http::field::content_encoding, "test"); test_encoder enc("eof!"); - serializer sr(cfg); - sr.start(&req, &enc); + test_serializer sr(cfg); + sr.start(&req); + sr.set_encoder(enc); std::string wire; BOOST_TEST_EQ( @@ -2525,8 +2566,9 @@ class serializer_test auto req = make_request(100); req.set(http::field::content_encoding, "test"); test_encoder enc("eof!"); - serializer sr(cfg); - sr.start(&req, &enc); + test_serializer sr(cfg); + sr.start(&req); + sr.set_encoder(enc); std::string wire; BOOST_TEST_EQ( @@ -2559,23 +2601,21 @@ class serializer_test auto req = make_request(5); req.set(http::field::content_encoding, "test"); test_encoder enc("eof!"); - serializer sr(cfg); - sr.start(&req, &enc); + test_serializer sr(cfg); + sr.start(&req); + sr.set_encoder(enc); - std::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()); + BOOST_TEST_EQ( + sr.frame(dest, b, false).error(), + error::body_size_mismatch); - out = sr.frame(dest, b, false, ec); BOOST_TEST_EQ( - ec, + sr.frame(dest, b, false).error(), std::make_error_code( std::errc::state_not_recoverable)); - BOOST_TEST(out.empty()); BOOST_TEST_EQ(sr.consume(0), body.size()); } @@ -2591,8 +2631,9 @@ class serializer_test test_encoder enc; enc.fail = std::make_error_code( std::errc::invalid_argument); - serializer sr(cfg); - sr.start(&req, &enc); + test_serializer sr(cfg); + sr.start(&req); + sr.set_encoder(enc); std::string wire; BOOST_TEST_EQ( @@ -2622,8 +2663,9 @@ class serializer_test test_encoder enc; enc.fail = std::make_error_code( std::errc::invalid_argument); - serializer sr(cfg); - sr.start(&req, &enc); + test_serializer sr(cfg); + sr.start(&req); + sr.set_encoder(enc); capy::mutable_buffer tmp[2]; auto const dest = sr.prepare(tmp); @@ -2650,8 +2692,9 @@ class serializer_test auto req = make_request(); req.set(http::field::content_encoding, "test"); test_encoder enc; - serializer sr(cfg); - sr.start(&req, &enc); + test_serializer sr(cfg); + sr.start(&req); + sr.set_encoder(enc); std::string wire; BOOST_TEST(!write(sr, wire, body)); @@ -2672,8 +2715,9 @@ class serializer_test response_head res; res.set(http::field::content_encoding, "test"); test_encoder enc("eof!"); - serializer sr(cfg); - sr.start(&res, &enc); + test_serializer sr(cfg); + sr.start(&res); + sr.set_encoder(enc); std::string wire; BOOST_TEST(!drain(sr, wire)); @@ -2699,8 +2743,9 @@ class serializer_test response_head res; res.set(http::field::content_encoding, "test"); test_encoder enc("eof!"); - serializer sr(cfg); - sr.start(&res, &enc); + test_serializer sr(cfg); + sr.start(&res); + sr.set_encoder(enc); std::string wire; BOOST_TEST(!drain(sr, wire)); @@ -2728,8 +2773,9 @@ class serializer_test response_head res; res.set(http::field::content_encoding, "test"); test_encoder enc; - serializer sr(cfg); - sr.start(&res, &enc); + test_serializer sr(cfg); + sr.start(&res); + sr.set_encoder(enc); std::string wire; BOOST_TEST(!drain(sr, wire)); @@ -2750,8 +2796,9 @@ class serializer_test auto req = make_request(74); req.set(http::field::content_encoding, "test"); test_encoder enc("eof!"); - serializer sr(cfg); - sr.start(&req, &enc); + test_serializer sr(cfg); + sr.start(&req); + sr.set_encoder(enc); std::string wire; BOOST_TEST(!write_eof(sr, wire, body, 1)); @@ -2773,8 +2820,9 @@ class serializer_test auto req = make_request(); req.set(http::field::content_encoding, "test"); test_encoder enc; - serializer sr(cfg); - sr.start(&req, &enc); + test_serializer sr(cfg); + sr.start(&req); + sr.set_encoder(enc); std::string wire; BOOST_TEST(!drain(sr, wire)); @@ -2805,8 +2853,9 @@ class serializer_test req.set(http::field::content_encoding, "test"); test_encoder enc("abcd"); enc.out_limit = 1; - serializer sr(cfg); - sr.start(&req, &enc); + test_serializer sr(cfg); + sr.start(&req); + sr.set_encoder(enc); std::string wire; BOOST_TEST(!drain(sr, wire)); @@ -2829,8 +2878,9 @@ class serializer_test auto req = make_request(); req.set(http::field::content_encoding, "test"); test_encoder enc; - serializer sr(cfg); - sr.start(&req, &enc); + test_serializer sr(cfg); + sr.start(&req); + sr.set_encoder(enc); std::string const b1 = make_body(6); std::string const b2 = make_body(6); @@ -2839,10 +2889,8 @@ class serializer_test capy::make_buffer(b2) }; std::string wire; - std::error_code ec; capy::const_buffer dest[dest_n]; - auto const out = sr.frame(dest, bufs, false, ec); - BOOST_TEST(!ec); + auto const out = sr.frame(dest, bufs, false).value(); std::size_t n = 0; for(auto cb : out) @@ -2871,16 +2919,15 @@ class serializer_test auto req = make_request(); req.set(http::field::content_encoding, "test"); test_encoder enc; - serializer sr(cfg); - sr.start(&req, &enc); + test_serializer sr(cfg); + sr.start(&req); + sr.set_encoder(enc); capy::const_buffer const b{ body.data(), body.size() }; std::string wire; - std::error_code ec; capy::const_buffer dest[dest_n]; - auto const out = sr.frame(dest, b, true, ec); - BOOST_TEST(!ec); + auto const out = sr.frame(dest, b, true).value(); BOOST_TEST(!out.empty()); std::size_t n = 0; @@ -2912,16 +2959,15 @@ class serializer_test auto req = make_request(); req.set(http::field::content_encoding, "test"); test_encoder enc; - serializer sr(cfg); - sr.start(&req, &enc); + test_serializer sr(cfg); + sr.start(&req); + sr.set_encoder(enc); // fill the encoder's output; 64 == 0x40 capy::const_buffer const b{ body1.data(), body1.size() }; - std::error_code ec; capy::const_buffer dest[dest_n]; - auto const out = sr.frame(dest, b, true, ec); - BOOST_TEST(!ec); + auto const out = sr.frame(dest, b, true).value(); BOOST_TEST(!out.empty()); // take the header and part of the chunk, keeping the @@ -2972,11 +3018,12 @@ class serializer_test test_encoder enc1; test_encoder enc2; - serializer sr(cfg); + test_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); + sr.start(&req1); + sr.set_encoder(enc1); // the small body drops the encoder std::string wire1; @@ -2986,7 +3033,9 @@ class serializer_test BOOST_TEST_EQ( wire1, std::string(req1.buffer()) + "hello"); - sr.start(&req2, &enc2); + sr.start(&req2); + + sr.set_encoder(enc2); BOOST_TEST(!sr.is_done()); std::string wire2; @@ -3022,8 +3071,9 @@ class serializer_test test_encoder enc1; test_encoder enc3; - serializer sr(cfg); - sr.start(&req1, &enc1); + test_serializer sr(cfg); + sr.start(&req1); + sr.set_encoder(enc1); // the body crosses the threshold: the encoder is kept std::string wire1; @@ -3041,7 +3091,9 @@ class serializer_test BOOST_TEST_EQ( wire2, std::string(req2.buffer()) + "hello"); - sr.start(&req3, &enc3); + sr.start(&req3); + + sr.set_encoder(enc3); std::string wire3; BOOST_TEST(!write(sr, wire3, body3)); BOOST_TEST(!write_eof(sr, wire3)); @@ -3066,15 +3118,13 @@ class serializer_test std::string const body(cfg.min_direct, 'x'); auto req1 = make_request(); auto req2 = make_request(5); - serializer sr(cfg); + test_serializer sr(cfg); sr.start(&req1); capy::const_buffer const b{ body.data(), body.size() }; - std::error_code ec; capy::const_buffer dest[dest_n]; - auto const out = sr.frame(dest, b, true, ec); - BOOST_TEST(!out.empty()); + BOOST_TEST(!sr.frame(dest, b, true).value().empty()); sr.consume(3); // part of the header, then abandon sr.start(&req2); @@ -3091,8 +3141,8 @@ class serializer_test // 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); + test_serializer sr(cfg); + sr.start(&req, true); BOOST_TEST(!sr.is_done()); std::string wire; @@ -3119,8 +3169,8 @@ class serializer_test // 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); + test_serializer sr(cfg); + sr.start(&req, true); std::string wire; BOOST_TEST(!write_eof(sr, wire)); @@ -3139,8 +3189,8 @@ class serializer_test // match the declared Content-Length { auto req = make_request(5); - serializer sr(cfg); - sr.start(&req, nullptr, true); + test_serializer sr(cfg); + sr.start(&req, true); std::string wire; BOOST_TEST_EQ( @@ -3153,8 +3203,8 @@ class serializer_test { std::string const body(cfg.min_direct, 'x'); auto req = make_request(body.size()); - serializer sr(cfg); - sr.start(&req, nullptr, true); + test_serializer sr(cfg); + sr.start(&req, true); std::string wire; BOOST_TEST_EQ( @@ -3167,12 +3217,13 @@ class serializer_test void testHeadEncoder() { - // an encoder passed alongside head is ignored entirely + // head mode selects no encoder, even with a + // Content-Encoding field present auto req = make_request(5); req.set(http::field::content_encoding, "test"); test_encoder enc; - serializer sr(cfg); - sr.start(&req, &enc, true); + test_serializer sr(cfg); + sr.start(&req, true); std::string wire; BOOST_TEST(!write_eof(sr, wire)); @@ -3185,6 +3236,123 @@ class serializer_test BOOST_TEST_EQ(wire, std::string(req.buffer())); } +#ifdef BOOST_HTTP_HAS_ZLIB + void + testContentEncodingGzip() + { + // The serializer selects the encoder from the + // Content-Encoding field, using the encode + // service installed in the system context; the + // parser decodes the result back. + auto& ctx = capy::get_system_context(); + if(!ctx.has_service()) + http::zlib::install_deflate_service(ctx); + if(!ctx.has_service()) + http::zlib::install_inflate_service(ctx); + + std::string const body = + "the quick brown fox jumps over the lazy dog"; + + auto cfg2 = cfg; + cfg2.encoder = std::make_shared(); + + auto req = make_request(); + req.set(http::field::content_encoding, "gzip"); + + serializer sr(cfg2); + sr.start(&req); + + std::string wire; + BOOST_TEST(!write_eof(sr, wire, body)); + BOOST_TEST(sr.is_done()); + BOOST_TEST(wire.find(body) == std::string::npos); + + request_parser pr(request_parser::config{}); + pr.start(); + pr.commit(capy::buffer_copy( + pr.prepare(), + capy::const_buffer(wire.data(), wire.size()))); + pr.commit_eof(); + + auto const r = pr.flatten_body(); + BOOST_TEST(r.has_value()); + BOOST_TEST(*r == body); + } + + void + testEncoderSettings() + { + // The encoder is constructed from the settings: + // zlib level 0 emits stored blocks, which leave + // the body visible in the wire. + auto& ctx = capy::get_system_context(); + if(!ctx.has_service()) + http::zlib::install_deflate_service(ctx); + if(!ctx.has_service()) + http::zlib::install_inflate_service(ctx); + + auto cfg2 = cfg; + cfg2.encoder = std::make_shared( + encoder_config{ .zlib = { .level = 0 } }); + + std::string const body = + "the quick brown fox jumps over the lazy dog"; + + auto req = make_request(); + req.set(http::field::content_encoding, "gzip"); + + serializer sr(cfg2); + sr.start(&req); + + std::string wire; + BOOST_TEST(!write_eof(sr, wire, body)); + BOOST_TEST(sr.is_done()); + BOOST_TEST( + wire.find("Content-Encoding: gzip\r\n") != + std::string::npos); + BOOST_TEST(wire.find(body) != std::string::npos); + + request_parser pr(request_parser::config{}); + pr.start(); + pr.commit(capy::buffer_copy( + pr.prepare(), + capy::const_buffer(wire.data(), wire.size()))); + pr.commit_eof(); + + auto const r = pr.flatten_body(); + BOOST_TEST(r.has_value()); + BOOST_TEST(*r == body); + } + + void + testEncodeDisabled() + { + // Without encoder settings the body is + // serialized as supplied, Content-Encoding + // included. + auto& ctx = capy::get_system_context(); + if(!ctx.has_service()) + http::zlib::install_deflate_service(ctx); + + std::string const body = + "the quick brown fox jumps over the lazy dog"; + + auto req = make_request(); + req.set(http::field::content_encoding, "gzip"); + + serializer sr(cfg); + sr.start(&req); + + std::string wire; + BOOST_TEST(!write_eof(sr, wire, body)); + BOOST_TEST(sr.is_done()); + BOOST_TEST(wire.find(body) != std::string::npos); + BOOST_TEST( + wire.find("Content-Encoding: gzip\r\n") != + std::string::npos); + } +#endif // BOOST_HTTP_HAS_ZLIB + void run() { @@ -3236,6 +3404,7 @@ class serializer_test testEncoderPrepareCommitEof(); testEncoderThreshold(); testEncoderStagedAndTail(); + testEncoderStartedConvertsToContentLength(); testEncoderLargeBodyChunked(); testEncoderLargeBodyChunkedEof(); testEncoderCommit(); @@ -3262,6 +3431,11 @@ class serializer_test testHeadChunked(); testHeadBodyMismatch(); testHeadEncoder(); +#ifdef BOOST_HTTP_HAS_ZLIB + testContentEncodingGzip(); + testEncoderSettings(); + testEncodeDisabled(); +#endif } };