From d3c59248a2efb661593d56e3271bf8c5e9969be5 Mon Sep 17 00:00:00 2001 From: Kurt Sassenrath Date: Wed, 6 Sep 2023 22:52:33 -0700 Subject: [PATCH] import msgpack, begin work on object API --- .bazelrc | 4 +- WORKSPACE | 22 + source/BUILD | 14 + source/include/parselink/msgpack.h | 6 + source/include/parselink/msgpack/core.h | 8 + .../include/parselink/msgpack/core/format.h | 276 +++++++ .../include/parselink/msgpack/core/reader.h | 334 +++++++++ .../include/parselink/msgpack/core/writer.h | 274 +++++++ source/include/parselink/msgpack/object.h | 259 +++++++ .../parselink/msgpack/util/endianness.h | 155 ++++ tests/msgpack/BUILD | 40 + tests/msgpack/rng.h | 39 + tests/msgpack/test_main.cpp | 2 + tests/msgpack/test_object.cpp | 155 ++++ tests/msgpack/test_reader_relaxed.cpp | 130 ++++ tests/msgpack/test_reader_strict.cpp | 707 ++++++++++++++++++ tests/msgpack/test_writer.cpp | 424 +++++++++++ 17 files changed, 2847 insertions(+), 2 deletions(-) create mode 100644 source/include/parselink/msgpack.h create mode 100644 source/include/parselink/msgpack/core.h create mode 100644 source/include/parselink/msgpack/core/format.h create mode 100644 source/include/parselink/msgpack/core/reader.h create mode 100644 source/include/parselink/msgpack/core/writer.h create mode 100644 source/include/parselink/msgpack/object.h create mode 100644 source/include/parselink/msgpack/util/endianness.h create mode 100644 tests/msgpack/BUILD create mode 100644 tests/msgpack/rng.h create mode 100644 tests/msgpack/test_main.cpp create mode 100644 tests/msgpack/test_object.cpp create mode 100644 tests/msgpack/test_reader_relaxed.cpp create mode 100644 tests/msgpack/test_reader_strict.cpp create mode 100644 tests/msgpack/test_writer.cpp diff --git a/.bazelrc b/.bazelrc index a409600..17024f6 100644 --- a/.bazelrc +++ b/.bazelrc @@ -1,2 +1,2 @@ -build --action_env=BAZEL_CXXOPTS="-std=c++20:-g:-O2" -run --action_env=BAZEL_CXXOPTS="-std=c++20:-g:-O2" +build --action_env=BAZEL_CXXOPTS="-std=c++20:-g:-O3" +run --action_env=BAZEL_CXXOPTS="-std=c++20:-g:-O3" diff --git a/WORKSPACE b/WORKSPACE index a481b77..e88bf1c 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -54,6 +54,28 @@ http_archive( strip_prefix = "fmt-" + fmt_version, ) +#------------------------------------------------------------------------------- +# tl/expected: An implementation of std::expected with monadic extensions. +#------------------------------------------------------------------------------- +tl_expected_version = "1.1.0" +tl_expected_base_url = \ + "https://github.com/TartanLlama/expected/archive/refs/tags/v" + +http_archive( + name = "expected", + url = tl_expected_base_url + tl_expected_version + ".zip", + strip_prefix = "expected-" + tl_expected_version, + build_file_content = +""" +cc_library( + name = "expected", + includes = ["include"], + hdrs = ["include/tl/expected.hpp"], + visibility = ["//visibility:public"], +) +""" +) + #------------------------------------------------------------------------------- # ut: Unit test framework. # TODO(kss): Only if tests are needed? diff --git a/source/BUILD b/source/BUILD index 237fee6..b87c4c3 100644 --- a/source/BUILD +++ b/source/BUILD @@ -5,8 +5,22 @@ cc_library( "include/parselink/server.h", ], strip_include_prefix = "include/parselink", + visibility = ["//visibility:public"], ) +cc_library( + name = "msgpack", + hdrs = glob([ + "include/parselink/msgpack/**/*.h" + ]), + deps = [ + "@expected", + ], + strip_include_prefix = "include/parselink", + visibility = ["//visibility:public"], +) + + cc_binary( name = "parselinkd", srcs = [ diff --git a/source/include/parselink/msgpack.h b/source/include/parselink/msgpack.h new file mode 100644 index 0000000..32ad5dd --- /dev/null +++ b/source/include/parselink/msgpack.h @@ -0,0 +1,6 @@ +#ifndef msgpack_9865a64955e64703 +#define msgpack_9865a64955e64703 + +#include "msgpack/core.h" + +#endif // msgpack_9865a64955e64703 diff --git a/source/include/parselink/msgpack/core.h b/source/include/parselink/msgpack/core.h new file mode 100644 index 0000000..1a066d6 --- /dev/null +++ b/source/include/parselink/msgpack/core.h @@ -0,0 +1,8 @@ +#ifndef msgpack_core_2e1a9d55129a666e +#define msgpack_core_2e1a9d55129a666e + +#include "core/format.h" +#include "core/reader.h" +#include "core/writer.h" + +#endif // msgpack_core_2e1a9d55129a666e diff --git a/source/include/parselink/msgpack/core/format.h b/source/include/parselink/msgpack/core/format.h new file mode 100644 index 0000000..5d7c641 --- /dev/null +++ b/source/include/parselink/msgpack/core/format.h @@ -0,0 +1,276 @@ +//----------------------------------------------------------------------------- +// ___ __ _ _ +// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __ +// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ / +// / ___/ (_| | | \__ \ __/ /__| | | | | < +// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ . +// +//----------------------------------------------------------------------------- +// Author: Kurt Sassenrath +// Module: msgpack +// +// An implementation of the MessagePack binary format specification. +// +// Copyright (c) 2023 Kurt Sassenrath. +// +// License TBD. +//----------------------------------------------------------------------------- +#ifndef msgpack_format_849b5c5238d8212 +#define msgpack_format_849b5c5238d8212 + +#include +#include +#include +#include +#include + +namespace msgpack { + +//----------------------------------------------------------------------------- +// Supporting types/tags for formats +//----------------------------------------------------------------------------- +struct nil { + nil() = default; + // This constructor is used by the reader implementation. + nil(auto) {}; +}; +struct invalid {}; +using boolean = bool; + +struct array_desc { + constexpr array_desc() noexcept = default; + constexpr array_desc(auto sz) noexcept : count(std::size_t(sz)) {}; + // This constructor is implemented for use by reader + constexpr array_desc(auto, auto sz) noexcept : count(std::size_t(sz)) {}; + constexpr bool operator==(array_desc const& other) const noexcept { + return count == other.count; + } + std::size_t count = 0; +}; + +struct map_desc { + constexpr map_desc() noexcept = default; + constexpr map_desc(auto sz) noexcept : count(std::size_t(sz)) {}; + // This constructor is implemented for use by reader + constexpr map_desc(auto, auto sz) noexcept : count(std::size_t(sz)) {}; + std::size_t count = 0; +}; + +namespace format { + + // Flags that may control the behavior of readers/writers. + enum flag { + apply_mask = 1 + }; + + // MessagePack formats break down into one of the following schemes: + // Marker byte + Fixed-length value + // Marker byte + Fixed-length payload length + payload + // + // In addition, there are "fix" types that embed the literal value or the + // payload length within the marker byte. + enum payload { + fixed, + variable + }; + + // Base structure for all MessagePack formats to inherit from. + struct base_format { + constexpr static flag flags = {}; + }; + + struct fixtype_format : base_format {}; + + // The library's representation of certain data types does not always + // match the underlying data serialized in the message. For example, + // arrays and maps are encoded as a marker byte + fixed length value, but + // msgpack will present them wrapped in a structure for stronger type + // semantics. + + template + struct format : base_format { + using first_type = First; + using value_type = first_type; // Can be overridden + constexpr static std::byte marker{Marker}; + constexpr static payload payload_type{payload::fixed}; + }; + + template + struct fixtype : fixtype_format { + using first_type = First; + using value_type = first_type; // Can be overridden + constexpr static std::byte marker{Marker}; + constexpr static std::byte mask{Mask}; + constexpr static payload payload_type{payload::fixed}; + constexpr static auto flags{flag::apply_mask}; + }; + + /* + * Integral types + */ + + // Positive/negative fixint represent the literal value specified, do not + // need to mask it off. + struct positive_fixint : fixtype<0x00, 0x7f> { + constexpr static flag flags = {}; + }; + struct negative_fixint : fixtype<0xe0, 0x1f, std::int8_t> { + constexpr static flag flags = {}; + }; + + struct uint8 : format<0xcc, std::uint8_t> {}; + struct uint16 : format<0xcd, std::uint16_t> {}; + struct uint32 : format<0xce, std::uint32_t> {}; + struct uint64 : format<0xcf, std::uint64_t> {}; + + struct int8 : format<0xd0, std::int8_t> {}; + struct int16 : format<0xd1, std::int16_t> {}; + struct int32 : format<0xd2, std::int32_t> {}; + struct int64 : format<0xd3, std::int64_t> {}; + + /* + * Other primitive types + */ + struct nil : fixtype<0xc0, 0x00> { using value_type = msgpack::nil; }; + struct invalid : fixtype<0xc1, 0x00> { using value_type = msgpack::invalid; }; + struct boolean : fixtype<0xc2, 0x01> { using value_type = bool; }; + + /* + * Maps + */ + + template struct map_format : Fmt { + using value_type = map_desc; + }; + + struct fixmap : map_format> {}; + struct map16 : map_format> {}; + struct map32 : map_format> {}; + + /* + * Arrays + */ + + template struct array_format : Fmt { + using value_type = array_desc; + }; + + struct fixarray : array_format> {}; + struct array16 : array_format> {}; + struct array32 : array_format> {}; + + /* + * Strings + */ + + template struct string_format : Fmt { + using value_type = std::string_view; + constexpr static auto payload_type = payload::variable; + }; + + struct fixstr : string_format> {}; + struct str8 : string_format> {}; + struct str16 : string_format> {}; + struct str32 : string_format> {}; + + /* + * Binary arrays + */ + + template struct bin_format : Fmt { + using value_type = std::span; + constexpr static auto payload_type = payload::variable; + }; + + struct bin8 : bin_format> {}; + struct bin16 : bin_format> {}; + struct bin32 : bin_format> {}; + + /* + * Extension types, not yet supported. + */ + + template struct unimplemented_format : Fmt {}; + + struct fixext1 : unimplemented_format> {}; + struct fixext2 : unimplemented_format> {}; + struct fixext4 : unimplemented_format> {}; + struct fixext8 : unimplemented_format> {}; + struct fixext16 : unimplemented_format> {}; + struct ext8 : unimplemented_format> {}; + struct ext16 : unimplemented_format> {}; + struct ext32 : unimplemented_format> {}; + + template + struct unimplemented : std::false_type {}; + + template + struct unimplemented> : std::true_type {}; + +namespace detail { + // Simple typelist for looking up compatible types. + // TODO: Use traits to generate the compatibility list automatically? + template + struct type_list_entry { using type = T; }; + + template + struct type_list_impl; + + template + struct type_list_impl, Ts...> : + type_list_entry... { + static constexpr auto size = sizeof...(Ts); + }; + + template struct typelist_lookup_tag { using type = T; }; + + template + typelist_lookup_tag typelist_lookup(type_list_entry const&); + + template + using type_list_at = typename decltype( + typelist_lookup(std::declval()))::type; + + template + using type_list = detail::type_list_impl< + std::make_index_sequence, Ts...>; + + template typename> + struct filter_type_list; + + template typename Predicate> + struct filter_type_list, type_list, Predicate> { + using type = type_list; + }; + + template typename Predicate> + struct filter_type_list, type_list, Predicate> { + using type = typename std::conditional_t< + Predicate::value, + filter_type_list, type_list, Predicate>, + filter_type_list, type_list, Predicate>>::type; + }; + + template typename Predicate> + using filter = filter_type_list, Predicate>; + + template typename Predicate> + using filter_t = typename filter::type; +} // namespace detail +} // namespace format + +template +concept format_type = std::is_base_of_v; + +template +concept is_fixtype = std::is_base_of_v; + +template +using format_list = format::detail::type_list; + +template +using format_list_at = format::detail::type_list_at; + +} // namespace msgpack + +#endif // oh_msgpack_format_849b5c5238d8212 diff --git a/source/include/parselink/msgpack/core/reader.h b/source/include/parselink/msgpack/core/reader.h new file mode 100644 index 0000000..108c708 --- /dev/null +++ b/source/include/parselink/msgpack/core/reader.h @@ -0,0 +1,334 @@ +//----------------------------------------------------------------------------- +// ___ __ _ _ +// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __ +// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ / +// / ___/ (_| | | \__ \ __/ /__| | | | | < +// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ . +// +//----------------------------------------------------------------------------- +// Author: Kurt Sassenrath +// Module: msgpack +// +// Core reading implementation, upon which readers are built. +// +// Copyright (c) 2023 Kurt Sassenrath. +// +// License TBD. +//----------------------------------------------------------------------------- +#ifndef msgpack_core_reader_6c2f66f02585565 +#define msgpack_core_reader_6c2f66f02585565 + +#include "format.h" +#include "../util/endianness.h" + +#include + +namespace msgpack { + +/** + * reader_policy dictates how flexible the reader can be when reading data + * from a binary blob containing MessagePack formats. + * + * More flexible policies will potentially include more code as a consequence. + * If structural integrity or code size is important, use the strict policy. + */ +namespace reader_policy { +struct relaxed {}; // Similar formats are acceptable. +struct strict {}; // Formats must exactly match the caller's request. +} // namespace reader_policy + +enum class reader_error { + unsupported, + not_implemented, + wrong_type, + end_of_message, + premature_end_of_message, +}; + +namespace detail { + + // Generic mechanism for reading a number of bytes out of an interator. + template + constexpr inline decltype(auto) read_bytes(Itr& inp) noexcept { + std::array out; + auto const end = inp + N; + auto dst = out.begin(); + while (inp != end) { + *dst = std::byte{*inp}; + ++dst; ++inp; + } + return out; + } + + // Read an integral type out of the iterator. + template + constexpr inline decltype(auto) read_integral(Itr& inp) noexcept { + constexpr auto N = sizeof(T); + if constexpr (sizeof(T) == 1) { + return T(*inp++); + } else { + return be_to_host(::detail::value_cast(read_bytes(inp))); + } + } + + + // This helper structure provides a customization point for converting an + // iterator to some other type (e.g. a pointer) in the event that the + // final format does not have a suitable constructor. It's only currently + // used for string_view, but may be overridden on platforms that provide + // their own types. + template + struct iterator_adapter { + static constexpr auto convert(Iter itr) { return itr; } + }; + + template + struct iterator_adapter { + static constexpr auto convert(Iter itr) { + return ::detail::value_cast(&*itr); } + }; + + template + using expected = tl::expected; + + template + constexpr inline bool accept(Iter& itr) noexcept { + if constexpr (is_fixtype) { + // Don't advance the iterator -- part of the format is locked away + // within the format byte. + return (std::byte{*itr} & ~F::mask) == F::marker; + } else { + // Advance the iterator past the format byte. + return std::byte{*itr++} == F::marker; + } + } + + // Read a value from a MessagePack message. Assumes that itr != end to + // start with. + template + requires (!format::unimplemented::value) + constexpr inline auto read(Iter& itr, Iter const end) noexcept -> + expected { + // Copy off the iterator. Update it at the end _if_ everything goes + // smoothly. + auto cur = itr; + if (!accept(cur)) return tl::make_unexpected(reader_error::wrong_type); + + // First thing's first. Read the format's "first_type", which is either + // the payload or the length of the payload. Ensure we have enough data + // from the span before we do so, though. + using first_type = typename F::first_type; + using value_type = typename F::value_type; + using diff_type = typename std::iterator_traits::difference_type; + if (std::distance(cur, end) < diff_type{sizeof(first_type)}) { + return tl::make_unexpected(reader_error::premature_end_of_message); + } + + auto f = read_integral(cur); + if constexpr (is_fixtype && (F::flags & format::flag::apply_mask)) { + f &= decltype(f)(F::mask); + } + + if constexpr (F::payload_type == format::payload::fixed) { + // We've read all we need to read. Update itr and return the value. + itr = cur; + return value_type(f); + } else { + // We're reading a variable length payload. `f` is the length of the + // payload. Ensure that the span has enough data and construct the + // value_type accordingly. + if (std::distance(cur, end) < diff_type{f}) { + return tl::make_unexpected(reader_error::premature_end_of_message); + } + + itr = cur + f; + using adapt = iterator_adapter; + return value_type(adapt::convert(cur), f); + } + } + + // "Relaxed" reader policies allow for certain conversions to take place + // at runtime. For instance, any smaller type of the same category is + // allowed. + // + // TODO: Possible improvement to readability; specify a single format list + // containing all formats, and filter them down for each instantiated + // format. Note that this would probably have compile-time performance + // implications. + + template + struct relaxed_conversions {}; + + template <> + struct relaxed_conversions { + using type_list = format_list; + }; + + template <> + struct relaxed_conversions { + using type_list = format_list; + }; + + template <> + struct relaxed_conversions { + using type_list = format_list; + }; + + template <> + struct relaxed_conversions { + using type_list = format_list; + }; + + template <> + struct relaxed_conversions { + using type_list = format_list; + }; + + template <> + struct relaxed_conversions { + using type_list = format_list; + }; + + template <> + struct relaxed_conversions { + using type_list = format_list; + }; + + template <> + struct relaxed_conversions { + using type_list = format_list; + }; + + template <> + struct relaxed_conversions { + using type_list = format_list; + }; + + template <> + struct relaxed_conversions { + using type_list = format_list; + }; + + template <> + struct relaxed_conversions { + using type_list = format_list; + }; + + template <> + struct relaxed_conversions { + using type_list = format_list; + }; + + template + concept has_conversions = format_type && requires { + typename relaxed_conversions::type_list; + }; + + template + struct reader_policy_traits {}; + + template <> + struct reader_policy_traits { + template + constexpr static bool accept(Iter itr) noexcept { + return detail::accept(itr); + } + + template + constexpr static decltype(auto) read(Iter& itr, Iter const end) + noexcept { + return detail::read(itr, end); + }; + }; + + template + constexpr static auto relaxed_read_impl(Iter& itr, Iter const end) + noexcept -> expected { + if constexpr (I < fmtlist::size) { + using this_format = format_list_at; + auto v = detail::read(itr, end); + if (v.has_value()) { + return v.value(); + } else if (v.error() == reader_error::wrong_type) { + return relaxed_read_impl(itr, end); + } else { + return v; + } + } else { + return tl::make_unexpected(reader_error::wrong_type); + } + } + + template <> + struct reader_policy_traits { + template + constexpr static decltype(auto) read(Iter& itr, Iter const& end) + noexcept { + if constexpr (has_conversions) { + using format_list = typename relaxed_conversions::type_list; + return relaxed_read_impl(itr, end); + } else { + // Fall back on strict policy + return detail::read(itr, end); + } + } + }; + +} // namespace detail + +template +class reader { +public: + + using error_code = reader_error; + + template + using expected = detail::expected; + + constexpr reader(std::span const src) : + data(src), curr(std::begin(data)), end(std::end(data)) {} + + template + constexpr expected read() noexcept { + if (curr == end) return tl::make_unexpected(error_code::end_of_message); + return detail::reader_policy_traits::template read(curr, end); + } + + constexpr auto pos() const noexcept { + return curr; + } + + constexpr auto subview() const noexcept { + return std::span(curr, end); + } + + constexpr auto tell() const noexcept { + return std::distance(data.begin(), curr); + } + + constexpr void skip(std::size_t count) noexcept { + if (std::distance(curr, end) < count) { + curr = end; + } else { + curr += count; + } + } + +private: + std::span data; + decltype(data)::iterator curr; + decltype(data)::iterator end; +}; + +} // namespace msgpack + +#endif // msgpack_core_reader_6c2f66f02585565 diff --git a/source/include/parselink/msgpack/core/writer.h b/source/include/parselink/msgpack/core/writer.h new file mode 100644 index 0000000..d8f78a9 --- /dev/null +++ b/source/include/parselink/msgpack/core/writer.h @@ -0,0 +1,274 @@ +//----------------------------------------------------------------------------- +// ___ __ _ _ +// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __ +// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ / +// / ___/ (_| | | \__ \ __/ /__| | | | | < +// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ . +// +//----------------------------------------------------------------------------- +// Author: Kurt Sassenrath +// Module: msgpack +// +// Core writing implementation, upon which writers are built. +// +// Copyright (c) 2023 Kurt Sassenrath. +// +// License TBD. +//----------------------------------------------------------------------------- +#ifndef msgpack_core_writer_ce48a51aa6ed0858 +#define msgpack_core_writer_ce48a51aa6ed0858 + +#include "format.h" +#include "../util/endianness.h" +#include +#include + +#include + +namespace msgpack { + +enum class writer_error { + none, + unsupported, + not_implemented, + bad_value, + out_of_space +}; + +// Helper template for writing a non-integral datatype to an output. +//template +//struct write_adapter { + //template + //static constexpr tl::expected write(T const& t); +//}; + +template +constexpr inline decltype(auto) write_bytes(std::array&& data, + Itr out) noexcept { + for (auto b : data) { + *out++ = b; + } + return out; +} + +template +struct write_adapter {}; + +template +struct write_adapter { + static constexpr auto size(T) noexcept { + return sizeof(T); + } + + template + static constexpr auto write(T t, Itr out) noexcept { + return write_bytes(detail::raw_cast(host_to_be(t)), out); + } +}; + +template <> +struct write_adapter { + static constexpr auto size(std::string_view str) noexcept { + return str.size(); + } + + template + static constexpr auto write(std::string_view str, Itr out) noexcept { + std::byte const* beg = reinterpret_cast(&*str.begin()); + std::copy(beg, beg + str.size(), out); + return out += str.size(); + } +}; + +template <> +struct write_adapter> { + static constexpr auto size(std::span bytes) noexcept { + return bytes.size(); + } + + template + static constexpr auto write(std::span bytes, + Itr out) noexcept { + std::copy(bytes.begin(), bytes.end(), out); + return out += bytes.size(); + } +}; + +template <> +struct write_adapter { + static constexpr auto value(map_desc desc) noexcept { + return desc.count; + } +}; + +template <> +struct write_adapter { + static constexpr auto value(array_desc desc) noexcept { + return desc.count; + } +}; + +// TODO: These could be optimized away because invalid/nil never contain real +// data. +template <> +struct write_adapter { + static constexpr auto value(invalid) noexcept { + return 0; + } +}; + +template <> +struct write_adapter { + static constexpr auto value(nil) noexcept { + return 0; + } +}; + +namespace detail { + template + using expected = tl::expected; + + template + constexpr inline std::size_t calculate_space(typename F::value_type val) + noexcept { + // At a minimum, one byte is needed to store the format. + std::size_t size = sizeof(typename F::first_type); + + if (!is_fixtype) { + ++size; // For format + } + + if constexpr (F::payload_type == format::payload::variable) { + size += write_adapter::size(val); + } + + return size; + } + + // The "first type" is either the size of the variable length payload or + // a fixed-length value. Additionally, this "first type" may be inlined + // into the marker byte if it's a fix type. + + template + constexpr inline expected pack_first( + typename F::value_type value) noexcept { + using value_type = typename F::value_type; + if constexpr (F::payload_type == format::payload::variable) { + return typename F::first_type(write_adapter::size(value)); + } else { + if constexpr (requires { write_adapter::value; }) { + return typename F::first_type(write_adapter::value(value)); + } else { + return typename F::first_type(value); + } + } + } + + template + constexpr inline expected write( + typename F::value_type&& value, Itr out, Itr const end) { + using diff_type = typename std::iterator_traits::difference_type; + if (diff_type(calculate_space(value)) > std::distance(out, end)) { + return tl::make_unexpected(writer_error::out_of_space); + } + + auto marker = F::marker; + + auto result = pack_first(value); + if (!result) { + return tl::make_unexpected(result.error()); + } + + if constexpr (is_fixtype) { + if (*result > 0xff) { + return tl::make_unexpected(writer_error::bad_value); + } + if constexpr (F::flags & format::flag::apply_mask) { + marker |= std::byte(*result); + } else { + marker = std::byte(*result); + } + if ((marker & ~F::mask) != F::marker) { + return tl::make_unexpected(writer_error::bad_value); + } + } + + *out++ = marker; + + if constexpr (!is_fixtype) { + out = write_adapter::write(*result, out); + } + + if constexpr (F::payload_type == format::payload::variable) { + out = write_adapter::write(value, out); + } + + return out; + } + + template + struct format_hint; + + template <> + struct format_hint { + using type = format::positive_fixint; + }; + + template <> + struct format_hint { + using type = format::uint16; + }; +} // namespace detail + +class writer { +public: + + using error_code = writer_error; + + template + using expected = detail::expected; + + constexpr writer(std::span dest) : + data(dest), curr(std::begin(data)), end(std::end(data)) {} + + template + constexpr expected write(typename F::value_type&& v) + noexcept { + using value_type = typename F::value_type; + if (curr == end) return tl::make_unexpected(error_code::out_of_space); + auto result = detail::write(std::forward(v), curr, end); + if (!result) { + return tl::make_unexpected(result.error()); + } + + curr = *result; + return tl::monostate{}; + } + + template + requires requires { typename detail::format_hint::type; } + constexpr expected write(T&& v) { + return write::type>(std::forward(v)); + } + + constexpr auto pos() const noexcept { + return curr; + } + + constexpr auto tell() const noexcept { + return std::distance(std::begin(data), curr); + } + + constexpr auto subspan() const noexcept { + return std::span(std::begin(data), tell()); + } + +private: + std::span data; + decltype(data)::iterator curr; + decltype(data)::iterator end; +}; + +} // namespace msgpack + +#endif // msgpack_core_writer_ce48a51aa6ed0858 diff --git a/source/include/parselink/msgpack/object.h b/source/include/parselink/msgpack/object.h new file mode 100644 index 0000000..b5ccf69 --- /dev/null +++ b/source/include/parselink/msgpack/object.h @@ -0,0 +1,259 @@ +//----------------------------------------------------------------------------- +// ___ __ _ _ +// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __ +// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ / +// / ___/ (_| | | \__ \ __/ /__| | | | | < +// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ . +// +//----------------------------------------------------------------------------- +// Author: Kurt Sassenrath +// Module: msgpack +// +// Generic (non-owning) wrapper for MessagePack data. +// +// Emphasis on the "non-owning" aspect of these objects. They're intended for +// use in parsing contexts only where the lifetime of the backing data buffer +// exceeds the lifetime of the object instances themselves, so for strings, +// byte-strings, etc. an explicit copy must be made. +// +// TBD: How best to handle arrays and maps. +// +// Copyright (c) 2023 Kurt Sassenrath. +// +// License TBD. +//----------------------------------------------------------------------------- +#ifndef msgpack_object_f43c22522692063f +#define msgpack_object_f43c22522692063f + +#include "core/format.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace msgpack { + +// This API is _currently_ optimizing on the fact that most desktop/server +// platforms are running on 64-bit, but lengths are at most 32 bits. This means +// that a generic object type can utilize the remaining 32-bit padding for type +// information, resulting in an object that only occupies 16 bytes, rather than +// 24. This could be further optimized to include type-specific flags, such as +// whether the data referenced is heap-allocated/managed by the object, or if +// it's merely a view of some other data. +// +// On the flip side, on 32-bit platforms, we manage with just 12 bytes. There +// might be room for optimization on that front, but it's left unimplemented +// for now. +// +// Of course, this means custom code! + +template +requires (sizeof(SizeType) + sizeof(std::underlying_type_t) <= sizeof(uintptr_t)) +class size_and_enum { +public: + using size_type = SizeType; + using enum_type = E; + using enum_int_type = std::underlying_type_t; + constexpr static uintptr_t enum_mask = ((1 << (sizeof(enum_int_type) * 8)) - 1); + constexpr static uintptr_t size_shift = (sizeof(SizeType) * 8); + constexpr static uintptr_t size_mask = ~enum_mask; + + // Constructors + constexpr size_and_enum() noexcept = default; + constexpr size_and_enum(size_type size, enum_type enum_value) noexcept { + set_both(size, enum_value); + } + + // Accessors + constexpr auto get_size() const noexcept { + return static_cast((value & size_mask) >> size_shift); + } + constexpr auto get_enum() const noexcept { + return static_cast((value & enum_mask)); + } + constexpr auto rep() const noexcept { return value; } + + // Mutators + constexpr auto set_size(size_type size) noexcept { + value = (static_cast(size) << size_shift) | (value & enum_mask); + } + constexpr auto set_enum(enum_type enum_value) noexcept { + value = (static_cast(enum_value) & enum_mask) | (value & size_mask); + } + + constexpr auto set_both(size_type size, enum_type enum_value) noexcept { + value = (static_cast(size) << size_shift) | + (static_cast(enum_value) & enum_mask); + } + + // Explicit conversion + constexpr explicit operator size_type() const noexcept { + return get_size(); + } + constexpr explicit operator enum_type() const noexcept { + return get_enum(); + } + +private: + uintptr_t value{}; +}; + +enum class object_type : std::uint8_t { + invalid, + unsigned_int, + signed_int, + string, + bytes, + nil, + boolean, + array, + map +}; + +template +class object_base; + +// TODO(ksassenrath): Move to own file. +enum class object_error { + wrong_type, + will_truncate +}; + +// Specialization for 64-bit systems. +template <> +class object_base<8> { +public: + object_base() noexcept = default; + object_base(object_base const& other) noexcept + : value_(other.value_) + , size_and_type_(other.size_and_type_) {} + + template + explicit object_base(T value) noexcept { + if constexpr (std::is_same_v) { + size_and_type_.set_enum(object_type::boolean); + value_.b = value; + } else if constexpr (std::is_signed_v) { + size_and_type_.set_enum(object_type::signed_int); + value_.i = value; + } else { + size_and_type_.set_enum(object_type::unsigned_int); + value_.u = value; + } + } + + template T> + explicit object_base(T const& value) noexcept { + std::string_view sv(value); + size_and_type_.set_enum(object_type::string); + size_and_type_.set_size(sv.size()); + value_.str = sv.data(); + } + + template > T> + explicit object_base(T const& value) noexcept { + std::span bv(value); + size_and_type_.set_enum(object_type::bytes); + size_and_type_.set_size(bv.size()); + value_.bp = bv.data(); + } + + constexpr object_type type() const noexcept { + return size_and_type_.get_enum(); + } + + template + constexpr tl::expected get() const noexcept; + + template + constexpr tl::expected get() const noexcept { + constexpr auto expected_type = std::is_same_v ? + object_type::boolean : std::is_signed_v ? + object_type::signed_int : object_type::unsigned_int; + + if (type() != expected_type) { + return tl::make_unexpected(object_error::wrong_type); + } + if constexpr (expected_type == object_type::boolean) { + return T(value_.b); + } else if constexpr (expected_type == object_type::signed_int) { + if (std::numeric_limits::max() < value_.i || + std::numeric_limits::lowest() > value_.i) { + return tl::make_unexpected(object_error::will_truncate); + } + return T(value_.i); + } else { + if (std::numeric_limits::max() < value_.u) { + return tl::make_unexpected(object_error::will_truncate); + } + return T(value_.u); + } + } + +private: + union { + std::uint64_t u; + std::int64_t i; + char const* str; + std::byte const* bp; + bool b; + object_base* obj; + } value_; + size_and_enum size_and_type_{}; +}; + +template<> +inline tl::expected object_base<8>::get() + const noexcept +{ + if (type() != object_type::string) { + return tl::make_unexpected(object_error::wrong_type); + } + return std::string{value_.str, size_and_type_.get_size()}; +} + +template<> +constexpr tl::expected object_base<8>::get() + const noexcept +{ + if (type() != object_type::string) { + return tl::make_unexpected(object_error::wrong_type); + } + return std::string_view{value_.str, size_and_type_.get_size()}; +} + +template<> +inline tl::expected, object_error> object_base<8>::get() + const noexcept +{ + tl::expected, object_error> result; + if (type() != object_type::bytes) { + result = tl::make_unexpected(object_error::wrong_type); + } else { + result = std::vector(value_.bp, + value_.bp + size_and_type_.get_size()); + } + return result; +} + +template<> +constexpr tl::expected, object_error> object_base<8>::get() const noexcept +{ + if (type() != object_type::bytes) { + return tl::make_unexpected(object_error::wrong_type); + } + return std::span(value_.bp, size_and_type_.get_size()); +} + +using object = object_base; + +} // namespace msgpack + + +#endif // msgpack_object_f43c22522692063f diff --git a/source/include/parselink/msgpack/util/endianness.h b/source/include/parselink/msgpack/util/endianness.h new file mode 100644 index 0000000..e31ed09 --- /dev/null +++ b/source/include/parselink/msgpack/util/endianness.h @@ -0,0 +1,155 @@ +//----------------------------------------------------------------------------- +// ___ __ _ _ +// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __ +// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ / +// / ___/ (_| | | \__ \ __/ /__| | | | | < +// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ . +// +//----------------------------------------------------------------------------- +// Author: Kurt Sassenrath +// Module: msgpack +// +// Endianness helper file. Might be replaced with 's endian in the near +// future. +// +// Copyright (c) 2023 Kurt Sassenrath. +// +// License TBD. +//----------------------------------------------------------------------------- +#ifndef msgpack_endianness_d8ae54f45851ed13 +#define msgpack_endianness_d8ae54f45851ed13 + +#include +#include +#include +#include + +enum class endianness { + big, + little, + other +}; + +namespace detail { + +/** + * Structure which determines the target's endianness at compile time. + */ +struct host_endianness_check { + constexpr static inline std::uint32_t impl = 0x01020304; + constexpr static inline auto test = static_cast(impl); + constexpr static inline auto value = []{ + switch (test) { + case 4: return endianness::little; + case 1: return endianness::big; + default: return endianness::other; + } + }(); +}; + +/** + * Helper function that swaps bytes of arbitrary lengths by copying input to + * output in reverse order. + */ +template +constexpr void byte_swap(Iter begin, Iter end, Iter dest) { + while (begin != end) { + --end; + *dest = *end; + ++dest; + } +} + +/** + * Optionally swap the byte order of an array based on endianness. If the + * endianness is identical, this should optimize away. Otherwise, a call to + * byte_swap above should optimize to bswap or some equivalent assembly. + */ +template +requires (From != endianness::other && To != endianness::other) +constexpr auto maybe_swap(std::array val) noexcept { + if constexpr (From == To) { + return val; + } else { + decltype(val) dest; + byte_swap(val.begin(), val.end(), dest.begin()); + return dest; + } +} + +/** + * A helper function for converting a data type into an std::array for use + * with byte swap operations. May also come in use for reinterpreting data + * at a byte level. + */ +template +constexpr auto raw_cast(T val) noexcept { + union trick_to_array { + T val; + std::array data; + }; + trick_to_array u{val}; + return u.data; +} + +/** + * A helper function for converting a std::array into some arbitrary type. + * Beware, this must be used on trivial data types. + */ +template +constexpr auto value_cast(Array&& data) noexcept { + union trick_to_value { + Array data; + T val; + }; + trick_to_value u{std::forward(data)}; + return u.val; +} + +/** + * Byte swap implementation for arbitrary endiannesses. + */ +template +requires std::is_trivial_v> +constexpr T swap(T val) noexcept { + return value_cast( + maybe_swap(raw_cast(val))); +} + +} // namespace detail; + +/** + * Exposes endianness information about a target. + */ +struct endian { + constexpr static inline auto big = endianness::big; + constexpr static inline auto little = endianness::little; + constexpr static inline auto network = endianness::big; + constexpr static inline auto host = detail::host_endianness_check::value; +}; + +/*---------------------------------------------------------------------------- + * The functions below implement byte-swapping on a type. + *---------------------------------------------------------------------------*/ + +template +constexpr T be_to_host(T val) noexcept { + return detail::swap(val); +} + +template +constexpr T host_to_be(T val) noexcept { + return detail::swap(val); +} + +template +constexpr T le_to_host(T val) noexcept { + return detail::swap(val); +} + +template +constexpr T host_to_le(T val) noexcept { + return detail::swap(val); +} + +#endif // msgpack_endianness_d8ae54f45851ed13 diff --git a/tests/msgpack/BUILD b/tests/msgpack/BUILD new file mode 100644 index 0000000..fa17475 --- /dev/null +++ b/tests/msgpack/BUILD @@ -0,0 +1,40 @@ + +cc_library( + name = "test_deps", + srcs = [ + "test_main.cpp", + "rng.h" + ], + deps = [ + "//source:msgpack", + "@expected", + "@ut", + "@fmt", + ], +) + +cc_test( + name = "reader", + srcs = [ + "test_reader_relaxed.cpp", + "test_reader_strict.cpp", + ], + deps = ["test_deps"], +) + +cc_test( + name = "writer", + srcs = [ + "test_writer.cpp", + ], + deps = ["test_deps"], +) + +cc_test( + name = "object", + srcs = [ + "test_object.cpp", + ], + deps = ["test_deps"], +) + diff --git a/tests/msgpack/rng.h b/tests/msgpack/rng.h new file mode 100644 index 0000000..239d7d4 --- /dev/null +++ b/tests/msgpack/rng.h @@ -0,0 +1,39 @@ +#ifndef tests_rng_483f8f25f09ae06 +#define tests_rng_483f8f25f09ae06 + +#include +#include + +struct rng { + rng(auto s) : seed(s), generator(seed) {} + rng() : rng([]{ return std::random_device{}(); }()) {} + + template + requires std::is_integral_v + T get() noexcept { + union { + std::array data; + T v; + } u; + for (auto& d : u.data) { + d = generator(); + } + + return u.v; + } + + template + requires std::is_integral_v + auto get() noexcept { + std::array values; + for (auto& value : values) { + value = get(); + } + return values; + } + + std::random_device::result_type seed; + std::mt19937 generator; +}; + +#endif // tests_rng_483f8f25f09ae06 diff --git a/tests/msgpack/test_main.cpp b/tests/msgpack/test_main.cpp new file mode 100644 index 0000000..2abeaa7 --- /dev/null +++ b/tests/msgpack/test_main.cpp @@ -0,0 +1,2 @@ + +int main(int, char**) {} diff --git a/tests/msgpack/test_object.cpp b/tests/msgpack/test_object.cpp new file mode 100644 index 0000000..212d7f0 --- /dev/null +++ b/tests/msgpack/test_object.cpp @@ -0,0 +1,155 @@ +#include "source/include/parselink/msgpack/object.h" +#include +#include +#include + +#include + +using namespace boost::ut; + +namespace { + template + constexpr std::array make_bytes(Bytes &&...bytes) { + return {std::byte(std::forward(bytes))...}; + } + + template + constexpr bool wrong_types(auto const& obj) { + auto err = tl::make_unexpected(msgpack::object_error::wrong_type); + if (obj.template get() != err) return false; + if constexpr (sizeof...(Others)) { + return wrong_types(obj); + } else { + return true; + } + } + + template + std::ostream &operator<<(std::ostream &os, tl::expected const &exp) { + if (exp.has_value()) { + os << "Value: '" << *exp << "'"; + } else { + os << "Error"; + } + return os; + } +} + +enum class foo : std::uint8_t { + a, b, c, d, e +}; + +suite size_and_enum = [] { + "expected use case"_test = [] { + msgpack::size_and_enum value; + expect(sizeof(value) == 8); + expect(value.rep() == uintptr_t{}); + expect(value.get_size() == std::uint32_t{}); + expect(value.get_enum() == foo::a); + value.set_size(std::uint32_t(-1)); + expect(value.get_size() == std::uint32_t(-1)); + expect(value.get_enum() == foo::a); + expect(value.rep() == 0xffffffff00000000); + value.set_enum(foo::c); + expect(value.get_size() == std::uint32_t(-1)); + expect(value.get_enum() == foo::c); + expect(value.rep() == 0xffffffff00000002); + value.set_both(0xaa, foo::e); + expect(value.get_size() == 0xaa); + expect(value.get_enum() == foo::e); + expect(value.rep() == 0x000000aa00000004); + }; +}; + +suite assignment_and_access = [] { + "object::object()"_test = [] { + msgpack::object obj; + expect(obj.type() == msgpack::object_type::invalid); + }; + "object::object(bool)"_test = [] { + msgpack::object obj(true); + expect(obj.type() == msgpack::object_type::boolean); + auto retrieved = obj.get(); + expect(retrieved && *retrieved); + expect(wrong_types(obj)); + }; + "object::object(std::int8_t)"_test = [] { + std::int8_t val = 0x32; + msgpack::object obj(val); + expect(obj.type() == msgpack::object_type::signed_int); + auto retrieved = obj.get(); + expect(retrieved && *retrieved == val); + expect(wrong_types(obj)); + }; + "object::object(std::uint8_t)"_test = [] { + std::uint8_t val = 0xaa; + msgpack::object obj(val); + expect(obj.type() == msgpack::object_type::unsigned_int); + auto retrieved = obj.get(); + expect(retrieved && *retrieved == val); + expect(wrong_types(obj)); + }; + "object::object(char const*)"_test = [] { + std::string extracted_val; + { + char const* val = "hello world"; + msgpack::object obj(val); + expect(obj.type() == msgpack::object_type::string); + auto retrieved = obj.get(); + expect(bool(retrieved)); + if (*retrieved != std::string_view(val)) { + expect(false); + } + expect(wrong_types(obj)); + auto string_retrieved = obj.get(); + expect(bool(string_retrieved)); + extracted_val = std::move(*string_retrieved); + } + expect(extracted_val == "hello world"); + }; + "object::object(std::string)"_test = [] { + std::string val = "std::string"; + msgpack::object obj(val); + expect(obj.type() == msgpack::object_type::string); + auto retrieved = obj.get(); + expect(bool(retrieved)); + expect(*retrieved == val); + expect(wrong_types(obj)); + }; + "object::object(std::span)"_test = [] { + auto expected_val = make_bytes(0x32, 0xff, 0xaa, 0xce); + std::vector extracted_val; + { + auto val = make_bytes(0x32, 0xff, 0xaa, 0xce); + msgpack::object obj(val); + expect(obj.type() == msgpack::object_type::bytes); + auto retrieved = obj.get>(); + expect(bool(retrieved)); + expect(std::equal(retrieved->begin(), retrieved->end(), + val.begin(), val.end())); + expect(wrong_types(obj)); + auto bytes_retrieved = obj.get>(); + expect(bool(bytes_retrieved)); + extracted_val = std::move(*bytes_retrieved); + } + expect(std::equal(expected_val.begin(), expected_val.end(), + extracted_val.begin(), extracted_val.end())); + }; +}; + +suite int_truncation = [] { + "unsigned truncation"_test = [] { + msgpack::object obj(0xffffffffu); + expect(obj.type() == msgpack::object_type::unsigned_int); + auto retrieved = obj.get(); + auto err = tl::make_unexpected(msgpack::object_error::will_truncate); + expect(retrieved == err); + }; + "signed truncation"_test = [] { + msgpack::object obj(-0xffff); + expect(obj.type() == msgpack::object_type::signed_int); + auto retrieved = obj.get(); + auto err = tl::make_unexpected(msgpack::object_error::will_truncate); + expect(retrieved == err); + }; +}; diff --git a/tests/msgpack/test_reader_relaxed.cpp b/tests/msgpack/test_reader_relaxed.cpp new file mode 100644 index 0000000..c45e182 --- /dev/null +++ b/tests/msgpack/test_reader_relaxed.cpp @@ -0,0 +1,130 @@ +#include + +#include + +#include + +namespace { + +using namespace boost::ut; +namespace format = msgpack::format; + +template +constexpr std::array make_bytes(Bytes &&...bytes) { + return {std::byte(std::forward(bytes))...}; +} + +template struct oversized_array { + std::array data; + std::size_t size; +}; + +constexpr auto to_bytes_array_oversized(auto const &container) { + oversized_array arr; + std::copy(std::begin(container), std::end(container), std::begin(arr.data)); + arr.size = std::distance(std::begin(container), std::end(container)); + return arr; +} + +consteval auto generate_bytes(auto callable) { + constexpr auto oversized = to_bytes_array_oversized(callable()); + std::array out; + std::copy(std::begin(oversized.data), + std::next(std::begin(oversized.data), oversized.size), + std::begin(out)); + return out; +} + +template +consteval auto cat(std::array const &a, + std::array const &b) { + std::array out; + std::copy(std::begin(a), std::next(std::begin(a), std::size(a)), + std::begin(out)); + std::copy(std::begin(b), std::next(std::begin(b), std::size(b)), + std::next(std::begin(out), std::size(a))); + return out; +} + +template +constexpr auto zip(T val, Itr begin, Itr end) { + std::vector output; + for (auto itr = begin; itr != end; ++itr) { + output.emplace_back(val); + output.emplace_back(*itr); + } + return output; +} + +template +constexpr auto zip(T val, Iterable const &itr) { + return zip(val, std::begin(itr), std::end(itr)); +} + +using relaxed_reader = msgpack::reader; + +} // namespace + +suite relaxed = [] { + "empty span"_test = [] { + using error = msgpack::reader_error; + std::span bytes; + relaxed_reader reader(bytes); + auto v = reader.read(); + expect(v.error() == error::end_of_message); + }; + +//////////////////////////////////////////////////////////////////////////////// +// Unsigned integers +//////////////////////////////////////////////////////////////////////////////// + "reader::read"_test = [] { + using fmt = format::uint8; + using error = msgpack::reader_error; + /** + * All bytes 0x00->0x79 are effectively literal uint8s. + */ + { + constexpr auto payload = make_bytes(0x52, 0xcc, 0x84); + relaxed_reader reader(payload); + { + auto result = reader.read(); + expect(result == std::uint8_t(0x52)); + } + { + auto result = reader.read(); + expect(result == std::uint8_t(0x84)); + } + + auto result = reader.read(); + expect(result == tl::make_unexpected(error::end_of_message)); + } + }; + + "reader::read"_test = [] { + using fmt = msgpack::format::uint16; + using error = msgpack::reader_error; + /** + * All bytes 0x00->0x79 are effectively literal uint8s. + */ + { + constexpr auto payload = make_bytes(0x52, 0xcc, 0x84, 0xcd, 0xaa, 0xcc); + relaxed_reader reader(payload); + { + auto result = reader.read(); + expect(result == std::uint16_t(0x52)); + } + { + auto result = reader.read(); + expect(result == std::uint16_t(0x84)); + } + { + auto result = reader.read(); + expect(result == std::uint16_t(0xaacc)); + } + + auto result = reader.read(); + expect(result == tl::make_unexpected(error::end_of_message)); + } + }; +}; + diff --git a/tests/msgpack/test_reader_strict.cpp b/tests/msgpack/test_reader_strict.cpp new file mode 100644 index 0000000..29b0f19 --- /dev/null +++ b/tests/msgpack/test_reader_strict.cpp @@ -0,0 +1,707 @@ +#include +#include "rng.h" + +#include + +#include + +namespace { +constexpr static std::size_t rng_samples_count = 10; + +template constexpr auto enum_name() noexcept { + return __PRETTY_FUNCTION__; +} + +using namespace boost::ut; +namespace format = msgpack::format; + +template +constexpr std::array make_bytes(Bytes &&...bytes) { + return {std::byte(std::forward(bytes))...}; +} + +template struct oversized_array { + std::array data; + std::size_t size; +}; + +constexpr auto to_bytes_array_oversized(auto const &container) { + oversized_array arr; + std::copy(std::begin(container), std::end(container), std::begin(arr.data)); + arr.size = std::distance(std::begin(container), std::end(container)); + return arr; +} + +consteval auto generate_bytes(auto callable) { + constexpr auto oversized = to_bytes_array_oversized(callable()); + std::array out; + std::copy(std::begin(oversized.data), + std::next(std::begin(oversized.data), oversized.size), + std::begin(out)); + return out; +} + +template +consteval auto cat(std::array const &a, + std::array const &b) { + std::array out; + std::copy(std::begin(a), std::next(std::begin(a), std::size(a)), + std::begin(out)); + std::copy(std::begin(b), std::next(std::begin(b), std::size(b)), + std::next(std::begin(out), std::size(a))); + return out; +} + +template +constexpr auto zip(T val, Itr begin, Itr end) { + std::vector output; + for (auto itr = begin; itr != end; ++itr) { + output.emplace_back(val); + output.emplace_back(*itr); + } + return output; +} + +template +constexpr auto zip(T val, Iterable const &itr) { + return zip(val, std::begin(itr), std::end(itr)); +} + +constexpr auto from_string_view(std::string_view sv) { + std::vector range; + range.resize(sv.size()); + auto itr = range.begin(); + for (auto c : sv) { + *itr = std::byte(c); + ++itr; + } + return range; +} + +constexpr auto make_contiguous_range(std::uint8_t start, std::uint8_t end) { + auto count = std::size_t(end) - std::size_t(start) + 1; + std::vector range; + range.resize(count); + for (auto i = std::size_t(start); i <= std::size_t(end); ++i) { + range[i - std::size_t(start)] = std::byte(i); + } + return range; +} +} // namespace + +suite reader = [] { + "empty span"_test = [] { + std::span bytes; + msgpack::reader reader(bytes); + auto v = reader.read(); + expect(v.error() == msgpack::reader_error::end_of_message); + }; + + //////////////////////////////////////////////////////////////////////////////// + // Unsigned integers + //////////////////////////////////////////////////////////////////////////////// + "reader::read"_test = [] { + using fmt = format::positive_fixint; + using error = msgpack::reader_error; + /** + * All bytes 0x00->0x79 are effectively literal uint8s. + */ + { + constexpr auto payload = + generate_bytes([] { return make_contiguous_range(0, 0x79); }); + msgpack::reader reader(payload); + static_assert( + std::is_same_v())::value_type, std::uint8_t>); + for (auto byte : payload) { + auto result = reader.read(); + expect(result == std::uint8_t(byte)); + } + + auto result = reader.read(); + expect(result == tl::make_unexpected(error::end_of_message)); + } + { + constexpr auto payload = + generate_bytes([] { return make_contiguous_range(0x80, 0xff); }); + // Continually narrow the span over the range, as read will not + // advance the internal span on failure. + for (auto itr = payload.begin(); itr != payload.end(); ++itr) { + msgpack::reader reader(std::span(itr, std::end(payload))); + auto result = reader.read(); + expect(result == tl::make_unexpected(error::wrong_type)); + } + } + }; + + "reader::read"_test = [] { + using fmt = format::uint8; + using error = msgpack::reader_error; + /** + * The uint8 format is 0xcc followed by one byte of data. + */ + { + constexpr auto payload = generate_bytes([] { + return zip(std::byte{0xcc}, make_contiguous_range(0x00, 0xff)); + }); + msgpack::reader reader(payload); + static_assert( + std::is_same_v())::value_type, std::uint8_t>); + for (auto i = 0; i < 0x100; ++i) { + auto result = reader.read(); + expect(result == std::uint8_t(i)); + } + auto result = reader.read(); + expect(result == tl::make_unexpected(error::end_of_message)); + } + { + using error = msgpack::reader_error; + // Test that partial read fails. + constexpr auto payload = make_bytes(0xcc); + msgpack::reader reader(payload); + auto result = reader.read(); + expect(result == tl::make_unexpected(error::premature_end_of_message)); + // Retry, ensure the error remains the same. + result = reader.read(); + expect(result == tl::make_unexpected(error::premature_end_of_message)); + } + }; + + "reader::read"_test = [] { + using fmt = format::uint16; + using error = msgpack::reader_error; + rng rng; + { + auto samples = rng.get(); + std::vector payload; + for (auto sample : samples) { + auto bytes = ::detail::raw_cast(host_to_be(sample)); + payload.push_back(std::byte{0xcd}); + for (auto byte : bytes) { + payload.push_back(byte); + } + } + + msgpack::reader reader(payload); + for (auto sample : samples) { + auto result = reader.read(); + expect(result == sample); + } + + auto result = reader.read(); + expect(result == tl::make_unexpected(error::end_of_message)); + } + + { + constexpr auto payload = make_bytes(0xee); + msgpack::reader reader(payload); + auto result = reader.read(); + expect(result == tl::make_unexpected(error::wrong_type)); + } + + // Test that partial read fails. + { + constexpr auto payload = make_bytes(0xcd); + msgpack::reader reader(payload); + auto result = reader.read(); + expect(result == tl::make_unexpected(error::premature_end_of_message)); + constexpr auto payload2 = make_bytes(0xcd, 0x01); + reader = msgpack::reader(payload2); + expect(result == tl::make_unexpected(error::premature_end_of_message)); + } + }; + + "reader::read"_test = [] { + using fmt = format::uint32; + using error = msgpack::reader_error; + rng rng; + auto samples = rng.get(); + std::vector payload; + for (auto sample : samples) { + auto bytes = ::detail::raw_cast(host_to_be(sample)); + payload.push_back(std::byte{0xce}); + for (auto byte : bytes) { + payload.push_back(byte); + } + } + + msgpack::reader reader(payload); + for (auto sample : samples) { + auto result = reader.read(); + expect(bool(result)); + expect(result == sample); + } + // Test that partial read fails. + { + constexpr auto payload = make_bytes(0xce, 0x01, 0x02, 0x03, 0x09); + for (auto itr = payload.begin() + 1; itr != payload.end(); ++itr) { + msgpack::reader reader({payload.begin(), itr}); + auto result = reader.read(); + expect(result == tl::make_unexpected(error::premature_end_of_message)); + } + msgpack::reader reader(payload); + auto result = reader.read(); + expect(result == 0x01020309); + } + }; + + "reader::read"_test = [] { + using fmt = format::uint64; + using error = msgpack::reader_error; + rng rng; + auto samples = rng.get(); + std::vector payload; + for (auto sample : samples) { + auto bytes = ::detail::raw_cast(host_to_be(sample)); + payload.push_back(std::byte{0xcf}); + for (auto byte : bytes) { + payload.push_back(byte); + } + } + + msgpack::reader reader(payload); + for (auto sample : samples) { + auto result = reader.read(); + expect(bool(result)); + expect(result == sample); + } + + { + constexpr auto payload = + make_bytes(0xcf, 0x01, 0x02, 0x03, 0x09, 0x10, 0x20, 0x30, 0x90); + for (auto itr = payload.begin() + 1; itr != payload.end(); ++itr) { + msgpack::reader reader({payload.begin(), itr}); + auto result = reader.read(); + expect(result == tl::make_unexpected(error::premature_end_of_message)); + } + msgpack::reader reader(payload); + auto result = reader.read(); + expect(result == 0x0102030910203090); + } + }; + + //////////////////////////////////////////////////////////////////////////////// + // Signed integers + //////////////////////////////////////////////////////////////////////////////// + "reader::read"_test = [] { + using fmt = format::negative_fixint; + using error = msgpack::reader_error; + /** + * All bytes 0x00->0x79 are effectively literal uint8s. + */ + { + constexpr auto payload = + generate_bytes([] { return make_contiguous_range(0xe0, 0xff); }); + msgpack::reader reader(payload); + static_assert( + std::is_same_v())::value_type, std::int8_t>); + for (auto byte : payload) { + auto result = reader.read(); + expect(result == std::int8_t(byte)); + } + + auto result = reader.read(); + expect(result == tl::make_unexpected(error::end_of_message)); + } + { + constexpr auto payload = + generate_bytes([] { return make_contiguous_range(0x00, 0xdf); }); + // Continually narrow the span over the range, as read will not + // advance the internal span on failure. + for (auto itr = payload.begin(); itr != payload.end(); ++itr) { + msgpack::reader reader(std::span(itr, std::end(payload))); + auto result = reader.read(); + expect(result == tl::make_unexpected(error::wrong_type)); + } + } + }; + + "reader::read"_test = [] { + using fmt = format::int8; + using error = msgpack::reader_error; + /** + * The uint8 format is 0xcc followed by one byte of data. + */ + { + constexpr auto payload = generate_bytes([] { + return zip(std::byte{0xd0}, make_contiguous_range(0x00, 0xff)); + }); + msgpack::reader reader(payload); + static_assert( + std::is_same_v())::value_type, std::int8_t>); + for (auto i = 0; i < 0x100; ++i) { + auto result = reader.read(); + expect(*result == std::int8_t(i)); + } + auto result = reader.read(); + expect(result == tl::make_unexpected(error::end_of_message)); + } + { + using error = msgpack::reader_error; + // Test that partial read fails. + constexpr auto payload = make_bytes(0xd0); + msgpack::reader reader(payload); + auto result = reader.read(); + expect(result == tl::make_unexpected(error::premature_end_of_message)); + // Retry, ensure the error remains the same. + result = reader.read(); + expect(result == tl::make_unexpected(error::premature_end_of_message)); + } + }; + + "reader::read"_test = [] { + using fmt = format::int16; + using error = msgpack::reader_error; + rng rng; + { + auto samples = rng.get(); + std::vector payload; + for (auto sample : samples) { + auto bytes = ::detail::raw_cast(host_to_be(sample)); + payload.push_back(std::byte{0xd1}); + for (auto byte : bytes) { + payload.push_back(byte); + } + } + + msgpack::reader reader(payload); + for (auto sample : samples) { + auto result = reader.read(); + expect(result == sample); + } + + auto result = reader.read(); + expect(result == tl::make_unexpected(error::end_of_message)); + } + + { + constexpr auto payload = make_bytes(0xd2); + msgpack::reader reader(payload); + auto result = reader.read(); + expect(result == tl::make_unexpected(error::wrong_type)); + } + + // Test that partial read fails. + { + constexpr auto payload = make_bytes(0xd1); + msgpack::reader reader(payload); + auto result = reader.read(); + expect(result == tl::make_unexpected(error::premature_end_of_message)); + constexpr auto payload2 = make_bytes(0xd1, 0x01); + reader = msgpack::reader(payload2); + expect(result == tl::make_unexpected(error::premature_end_of_message)); + } + }; + + "reader::read"_test = [] { + using fmt = format::int32; + using error = msgpack::reader_error; + rng rng; + auto samples = rng.get(); + std::vector payload; + for (auto sample : samples) { + auto bytes = ::detail::raw_cast(host_to_be(sample)); + payload.push_back(std::byte{0xd2}); + for (auto byte : bytes) { + payload.push_back(byte); + } + } + + msgpack::reader reader(payload); + for (auto sample : samples) { + auto result = reader.read(); + expect(bool(result)); + expect(result == sample); + } + // Test that partial read fails. + { + constexpr auto payload = make_bytes(0xd2, 0xff, 0xff, 0xff, 0xfe); + for (auto itr = payload.begin() + 1; itr != payload.end(); ++itr) { + msgpack::reader reader({payload.begin(), itr}); + auto result = reader.read(); + expect(result == tl::make_unexpected(error::premature_end_of_message)); + } + msgpack::reader reader(payload); + auto result = reader.read(); + expect(result == -2); + } + }; + + "reader::read"_test = [] { + using fmt = format::int64; + using error = msgpack::reader_error; + rng rng; + auto samples = rng.get(); + std::vector payload; + for (auto sample : samples) { + auto bytes = ::detail::raw_cast(host_to_be(sample)); + payload.push_back(std::byte{0xd3}); + for (auto byte : bytes) { + payload.push_back(byte); + } + } + + msgpack::reader reader(payload); + for (auto sample : samples) { + auto result = reader.read(); + expect(bool(result)); + expect(result == sample); + } + + { + constexpr auto payload = + make_bytes(0xd3, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0); + for (auto itr = payload.begin() + 1; itr != payload.end(); ++itr) { + msgpack::reader reader({payload.begin(), itr}); + auto result = reader.read(); + expect(result == tl::make_unexpected(error::premature_end_of_message)); + } + msgpack::reader reader(payload); + auto result = reader.read(); + expect(result == -16); + } + }; + + //////////////////////////////////////////////////////////////////////////////// + // Strings + //////////////////////////////////////////////////////////////////////////////// + + "reader::read"_test = [] { + using fmt = msgpack::format::fixstr; + using error = msgpack::reader_error; + constexpr std::string_view sv = "hello"; + constexpr auto payload = + cat(make_bytes(0xa0 + sv.size()), + generate_bytes([sv] { return from_string_view(sv); })); + + for (auto itr = payload.begin() + 1; itr != payload.end(); ++itr) { + msgpack::reader reader({payload.begin(), itr}); + auto result = reader.read(); + expect(result == tl::make_unexpected(error::premature_end_of_message)); + } + + msgpack::reader reader(payload); + auto result = reader.read(); + expect(result == sv); + }; + + "reader::read"_test = [] { + using fmt = msgpack::format::str8; + using error = msgpack::reader_error; + constexpr std::string_view sv = "hello d"; + constexpr auto payload = + cat(make_bytes(0xd9, sv.size()), + generate_bytes([sv] { return from_string_view(sv); })); + + for (auto itr = payload.begin() + 1; itr != payload.end(); ++itr) { + msgpack::reader reader({payload.begin(), itr}); + auto result = reader.read(); + expect(result == tl::make_unexpected(error::premature_end_of_message)); + } + + msgpack::reader reader(payload); + auto result = reader.read(); + expect(result == sv); + }; + + "reader::read"_test = [] { + using fmt = msgpack::format::str16; + using error = msgpack::reader_error; + constexpr std::string_view sv = "hello world"; + constexpr auto payload = + cat(make_bytes(0xda, 0x00, sv.size()), + generate_bytes([sv] { return from_string_view(sv); })); + + for (auto itr = payload.begin() + 1; itr != payload.end(); ++itr) { + msgpack::reader reader({payload.begin(), itr}); + auto result = reader.read(); + expect(result == tl::make_unexpected(error::premature_end_of_message)); + } + + msgpack::reader reader(payload); + auto result = reader.read(); + expect(result == sv); + }; + + "reader::read"_test = [] { + using fmt = msgpack::format::str32; + using error = msgpack::reader_error; + constexpr std::string_view sv = "hello world"; + constexpr auto payload = + cat(make_bytes(0xdb, 0x00, 0x00, 0x00, sv.size()), + generate_bytes([sv] { return from_string_view(sv); })); + + for (auto itr = payload.begin() + 1; itr != payload.end(); ++itr) { + msgpack::reader reader({payload.begin(), itr}); + auto result = reader.read(); + expect(result == tl::make_unexpected(error::premature_end_of_message)); + } + + msgpack::reader reader(payload); + auto result = reader.read(); + expect(result == sv); + }; + + //////////////////////////////////////////////////////////////////////////////// + // Binary payloads + //////////////////////////////////////////////////////////////////////////////// + + "reader::read"_test = [] { + using fmt = msgpack::format::bin8; + using error = msgpack::reader_error; + constexpr auto bv = make_bytes(0x0, 0x01, 0x02, 0x04); + constexpr auto payload = cat(make_bytes(0xc4, bv.size()), bv); + + for (auto itr = payload.begin() + 1; itr != payload.end(); ++itr) { + msgpack::reader reader({payload.begin(), itr}); + auto result = reader.read(); + expect(result == tl::make_unexpected(error::premature_end_of_message)); + } + + msgpack::reader reader(payload); + auto result = reader.read(); + expect(result.has_value()); + expect( + std::equal((*result).begin(), (*result).end(), bv.begin(), bv.end())); + }; + + "reader::read"_test = [] { + using fmt = msgpack::format::bin16; + using error = msgpack::reader_error; + constexpr auto bv = generate_bytes([] { + auto rg = make_contiguous_range(0, 0xff); + std::vector rg2(rg.size() * 2); + auto itr = std::copy(rg.begin(), rg.end(), rg2.begin()); + std::copy(rg.begin(), rg.end(), itr); + return rg2; + }); + constexpr auto payload = + cat(make_bytes(0xc5, bv.size() >> 8, bv.size() & 0xff), bv); + + for (auto itr = payload.begin() + 1; itr != payload.end(); ++itr) { + msgpack::reader reader({payload.begin(), itr}); + auto result = reader.read(); + expect(result == tl::make_unexpected(error::premature_end_of_message)); + } + + msgpack::reader reader(payload); + auto result = reader.read(); + expect( + std::equal((*result).begin(), (*result).end(), bv.begin(), bv.end())); + }; + + // TODO: Support this with a proper test payload + "reader::read"_test = [] { + using fmt = msgpack::format::bin32; + using error = msgpack::reader_error; + constexpr auto bv = generate_bytes([] { + auto rg = make_contiguous_range(0, 0xff); + std::vector rg2(rg.size() * 2); + auto itr = std::copy(rg.begin(), rg.end(), rg2.begin()); + std::copy(rg.begin(), rg.end(), itr); + return rg2; + }); + constexpr auto payload = + cat(make_bytes(0xc6, bv.size() >> 24, bv.size() >> 16, bv.size() >> 8, + bv.size() & 0xff), + bv); + + for (auto itr = payload.begin() + 1; itr != payload.end(); ++itr) { + msgpack::reader reader({payload.begin(), itr}); + auto result = reader.read(); + expect(result == tl::make_unexpected(error::premature_end_of_message)); + } + + msgpack::reader reader(payload); + auto result = reader.read(); + expect( + std::equal((*result).begin(), (*result).end(), bv.begin(), bv.end())); + }; + + //////////////////////////////////////////////////////////////////////////////// + // Misc formats + //////////////////////////////////////////////////////////////////////////////// + "reader::read"_test = [] { + using fmt = msgpack::format::nil; + constexpr auto payload = make_bytes(0xc0); + + msgpack::reader reader(payload); + auto result = reader.read(); + expect(result.has_value()); + }; + + "reader::read"_test = [] { + using fmt = msgpack::format::boolean; + constexpr auto payload = make_bytes(0xc2, 0xc3); + msgpack::reader reader(payload); + auto result = reader.read(); + expect(result == false); + result = reader.read(); + expect(result == true); + }; + + //////////////////////////////////////////////////////////////////////////////// + // Structural formats + //////////////////////////////////////////////////////////////////////////////// + "reader::read"_test = [] { + using fmt = msgpack::format::fixarray; + using error = msgpack::reader_error; + // A MessagePack array of 5 8-bit unsigned integers. + constexpr auto payload = make_bytes(0x95, 0xcc, 0x85, 0xcc, 0x84, 0xcc, + 0x83, 0xcc, 0x82, 0xcc, 0x81); + + msgpack::reader reader(payload); + auto result = reader.read(); + expect(result == msgpack::array_desc{5}); + + for (std::size_t i = 0; i < (*result).count; ++i) { + auto v = reader.read(); + expect(v == 0x85 - i); + } + + auto end = reader.read(); + expect(end == tl::make_unexpected(error::end_of_message)); + }; + + "reader::read"_test = [] { + using fmt = msgpack::format::array16; + using error = msgpack::reader_error; + // A MessagePack array of 5 8-bit unsigned integers. + constexpr auto payload = + make_bytes(0xdc, 0x00, 0x05, 0xcc, 0x85, 0xcc, 0x84, 0xcc, 0x83, 0xcc, + 0x82, 0xcc, 0x81); + + msgpack::reader reader(payload); + auto result = reader.read(); + expect(result == msgpack::array_desc{5}); + + for (std::size_t i = 0; i < (*result).count; ++i) { + auto v = reader.read(); + expect(v == 0x85 - i); + } + + auto end = reader.read(); + expect(end == tl::make_unexpected(error::end_of_message)); + }; + + "reader::read"_test = [] { + using fmt = msgpack::format::array32; + using error = msgpack::reader_error; + // A MessagePack array of 5 8-bit unsigned integers. + constexpr auto payload = + make_bytes(0xdd, 0x00, 0x00, 0x00, 0x05, 0xcc, 0x85, 0xcc, 0x84, 0xcc, + 0x83, 0xcc, 0x82, 0xcc, 0x81); + + msgpack::reader reader(payload); + auto result = reader.read(); + expect(result == msgpack::array_desc{5}); + + for (std::size_t i = 0; i < (*result).count; ++i) { + auto v = reader.read(); + expect(v == 0x85 - i); + } + + auto end = reader.read(); + expect(end == tl::make_unexpected(error::end_of_message)); + }; +}; + diff --git a/tests/msgpack/test_writer.cpp b/tests/msgpack/test_writer.cpp new file mode 100644 index 0000000..c512db6 --- /dev/null +++ b/tests/msgpack/test_writer.cpp @@ -0,0 +1,424 @@ +#include + +#include + +#include + +using namespace boost::ut; +namespace format = msgpack::format; + +namespace { + +template +constexpr std::array make_bytes(Bytes &&...bytes) { + return {std::byte(std::forward(bytes))...}; +} + +template struct oversized_array { + std::array data; + std::size_t size; +}; + +constexpr auto to_bytes_array_oversized(auto const &container) { + oversized_array arr; + std::copy(std::begin(container), std::end(container), std::begin(arr.data)); + arr.size = std::distance(std::begin(container), std::end(container)); + return arr; +} + +consteval auto generate_bytes(auto callable) { + constexpr auto oversized = to_bytes_array_oversized(callable()); + std::array out; + std::copy(std::begin(oversized.data), + std::next(std::begin(oversized.data), oversized.size), + std::begin(out)); + return out; +} + +template +consteval auto cat(std::array const &a, + std::array const &b) { + std::array out; + std::copy(std::begin(a), std::next(std::begin(a), std::size(a)), + std::begin(out)); + std::copy(std::begin(b), std::next(std::begin(b), std::size(b)), + std::next(std::begin(out), std::size(a))); + return out; +} + +template +constexpr auto zip(T val, Itr begin, Itr end) { + std::vector output; + for (auto itr = begin; itr != end; ++itr) { + output.emplace_back(val); + output.emplace_back(*itr); + } + return output; +} + +template +constexpr auto zip(T val, Iterable const &itr) { + return zip(val, std::begin(itr), std::end(itr)); +} + +constexpr auto from_string_view(std::string_view sv) { + std::vector range; + range.resize(sv.size()); + auto itr = range.begin(); + for (auto c : sv) { + *itr = std::byte(c); + ++itr; + } + return range; +} + +constexpr auto make_contiguous_range(std::uint8_t start, std::uint8_t end) { + auto count = std::size_t(end) - std::size_t(start) + 1; + std::vector range; + range.resize(count); + for (auto i = std::size_t(start); i <= std::size_t(end); ++i) { + range[i - std::size_t(start)] = std::byte(i); + } + return range; +} + +constexpr auto equal(auto a, auto b) { + return std::equal(std::begin(a), std::end(a), std::begin(b), std::end(b)); +} + +} // namespace + +suite writer = [] { + "writer empty span"_test = [] { + std::array payload; + + msgpack::writer writer(payload); + expect(writer.tell() == 0); + }; + + "writer::write"_test = [] { + using fmt = format::positive_fixint; + using error = msgpack::writer_error; + + std::array payload; + auto constexpr expected = make_bytes(0x32, 0x55); + msgpack::writer writer(payload); + auto result = writer.write(std::uint8_t{0x32}); + expect(!!result); + expect(writer.tell() == 1); + expect(*writer.subspan().begin() == std::byte{0x32}); + expect(writer.write(std::uint8_t{0x82}) == tl::make_unexpected(error::bad_value)); + + writer.write(std::uint8_t{0x55}); + expect(writer.tell() == 2); + expect(equal(writer.subspan(), expected)); + expect(writer.write(std::uint8_t{0x01}) == tl::make_unexpected(error::out_of_space)); + }; + + "writer::write"_test = [] { + using fmt = format::uint8; + using error = msgpack::writer_error; + + std::array payload; + auto constexpr expected = make_bytes(0xcc, 0x32, 0xcc, 0x82); + msgpack::writer writer(payload); + expect(!!writer.write(std::uint8_t{0x32})); + expect(writer.tell() == 2); + expect(equal(writer.subspan(), std::span{expected.begin(), 2})); + expect(!!writer.write(std::uint8_t{0x82})); + expect(equal(writer.subspan(), expected)); + expect(writer.write(std::uint8_t{0x01}) == tl::make_unexpected(error::out_of_space)); + }; + + "writer::write"_test = [] { + using fmt = format::uint16; + using error = msgpack::writer_error; + + std::array payload; + auto constexpr expected = make_bytes(0xcd, 0x32, 0xcc, 0xcd, 0xaa, 0xff); + msgpack::writer writer(payload); + expect(!!writer.write(std::uint16_t{0x32cc})); + expect(writer.tell() == 3); + expect(equal(writer.subspan(), std::span{expected.begin(), 3})); + expect(!!writer.write(0xaaff)); + expect(equal(writer.subspan(), expected)); + expect(writer.write(0x01) == tl::make_unexpected(error::out_of_space)); + }; + + "writer::write"_test = [] { + using fmt = format::uint32; + using error = msgpack::writer_error; + + std::array payload; + auto constexpr expected = make_bytes(0xce, 0x01, 0x02, 0x03, 0x04); + msgpack::writer writer(payload); + expect(!!writer.write(0x01020304)); + expect(writer.tell() == 5); + expect(equal(writer.subspan(), expected)); + expect(writer.write(0x01) == tl::make_unexpected(error::out_of_space)); + }; + + "writer::write"_test = [] { + using fmt = format::uint64; + using error = msgpack::writer_error; + + std::array payload; + auto constexpr expected = make_bytes( + 0xcf, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08); + msgpack::writer writer(payload); + expect(!!writer.write(0x0102030405060708)); + expect(writer.tell() == 9); + expect(equal(writer.subspan(), expected)); + expect(writer.write(0x01) == tl::make_unexpected(error::out_of_space)); + }; + + "writer::write"_test = [] { + using fmt = format::negative_fixint; + using error = msgpack::writer_error; + + std::array payload; + auto constexpr expected = make_bytes(0xff, 0xe0); + msgpack::writer writer(payload); + expect(!!writer.write(-1)); + expect(writer.tell() == 1); + expect(*writer.subspan().begin() == std::byte{0xff}); + expect(writer.write(-33) == tl::make_unexpected(error::bad_value)); + expect(!!writer.write(-32)); + expect(writer.tell() == 2); + expect(equal(writer.subspan(), expected)); + expect(writer.write(-5) == tl::make_unexpected(error::out_of_space)); + }; + + "writer::write"_test = [] { + using fmt = format::int8; + using error = msgpack::writer_error; + + std::array payload; + auto constexpr expected = make_bytes(0xd0, 0x32, 0xd0, 0xfb); + msgpack::writer writer(payload); + expect(!!writer.write(std::int8_t{0x32})); + expect(writer.tell() == 2); + expect(equal(writer.subspan(), std::span{expected.begin(), 2})); + expect(!!writer.write(std::int8_t{-5})); + expect(equal(writer.subspan(), expected)); + expect(writer.write(std::uint8_t{0x01}) == tl::make_unexpected(error::out_of_space)); + }; + + "writer::write"_test = [] { + using fmt = format::int16; + using error = msgpack::writer_error; + + std::array payload; + auto constexpr expected = make_bytes(0xd1, 0x32, 0xcc, 0xd1, 0xff, 0xfa); + msgpack::writer writer(payload); + expect(!!writer.write(std::int16_t{0x32cc})); + expect(writer.tell() == 3); + expect(equal(writer.subspan(), std::span{expected.begin(), 3})); + expect(!!writer.write(-6)); + expect(equal(writer.subspan(), expected)); + expect(writer.write(0x01) == tl::make_unexpected(error::out_of_space)); + }; + + "writer::write"_test = [] { + using fmt = format::int32; + using error = msgpack::writer_error; + + std::array payload; + auto constexpr expected = make_bytes(0xd2, 0x01, 0x02, 0x03, 0x04); + msgpack::writer writer(payload); + expect(!!writer.write(0x01020304)); + expect(writer.tell() == 5); + expect(equal(writer.subspan(), expected)); + expect(writer.write(0x01) == tl::make_unexpected(error::out_of_space)); + }; + + "writer::write"_test = [] { + using fmt = format::int64; + using error = msgpack::writer_error; + + std::array payload; + auto constexpr expected = make_bytes( + 0xd3, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08); + msgpack::writer writer(payload); + expect(!!writer.write(std::int64_t{0x0102030405060708})); + expect(writer.tell() == 9); + expect(equal(writer.subspan(), expected)); + expect(writer.write(0x01) == tl::make_unexpected(error::out_of_space)); + }; + + "writer::write"_test = [] { + using fmt = format::fixstr; + + std::array payload; + auto constexpr expected = make_bytes(0xa2, 'o', 'h'); + msgpack::writer writer(payload); + expect(!!writer.write("oh")); + expect(equal(writer.subspan(), expected)); + }; + + "writer::write"_test = [] { + using fmt = format::str8; + std::array payload; + auto constexpr expected = make_bytes(0xd9, 44, + 't', 'h', 'e', ' ', 'q', 'u', 'i', 'c', 'k', ' ', + 'b', 'r', 'o', 'w', 'n', ' ', 'f', 'o', 'x', ' ', + 'j', 'u', 'm', 'p', 'e', 'd', ' ', 'o', 'v', 'e', 'r', ' ', + 't', 'h', 'e', ' ', 'l', 'a', 'z', 'y', ' ', 'd', 'o', 'g'); + + msgpack::writer writer(payload); + std::string_view txt = + "the quick brown fox jumped over the lazy dog"; + expect(!!writer.write(std::move(txt))); + expect(equal(writer.subspan(), expected)); + }; + + "writer::write"_test = [] { + using fmt = format::str16; + std::array payload; + auto constexpr expected = make_bytes(0xda, 0, 44, + 't', 'h', 'e', ' ', 'q', 'u', 'i', 'c', 'k', ' ', + 'b', 'r', 'o', 'w', 'n', ' ', 'f', 'o', 'x', ' ', + 'j', 'u', 'm', 'p', 'e', 'd', ' ', 'o', 'v', 'e', 'r', ' ', + 't', 'h', 'e', ' ', 'l', 'a', 'z', 'y', ' ', 'd', 'o', 'g'); + + msgpack::writer writer(payload); + std::string_view txt = + "the quick brown fox jumped over the lazy dog"; + expect(!!writer.write(std::move(txt))); + expect(equal(writer.subspan(), expected)); + }; + + "writer::write"_test = [] { + using fmt = format::str32; + std::array payload; + auto constexpr expected = make_bytes(0xdb, 0, 0, 0, 44, + 't', 'h', 'e', ' ', 'q', 'u', 'i', 'c', 'k', ' ', + 'b', 'r', 'o', 'w', 'n', ' ', 'f', 'o', 'x', ' ', + 'j', 'u', 'm', 'p', 'e', 'd', ' ', 'o', 'v', 'e', 'r', ' ', + 't', 'h', 'e', ' ', 'l', 'a', 'z', 'y', ' ', 'd', 'o', 'g'); + + msgpack::writer writer(payload); + std::string_view txt = + "the quick brown fox jumped over the lazy dog"; + expect(!!writer.write(std::move(txt))); + expect(equal(writer.subspan(), expected)); + }; + + "writer::write"_test = [] { + using fmt = format::bin8; + std::array payload; + auto constexpr expected = make_bytes(0xc4, 0x07, 0x01, 0x02, 0x03, 0x04, + 0xf8, 0xf9, 0xfa); + + msgpack::writer writer(payload); + std::span bv(expected.begin() + 2, expected.end()); + expect(!!writer.write(std::move(bv))); + expect(equal(writer.subspan(), expected)); + }; + + "writer::write"_test = [] { + using fmt = format::bin16; + std::array payload; + auto constexpr expected = make_bytes(0xc5, 0x0, 0x07, 0x01, 0x02, 0x03, + 0x04, 0xf8, 0xf9, 0xfa); + + msgpack::writer writer(payload); + std::span bv(expected.begin() + 3, expected.end()); + expect(!!writer.write(std::move(bv))); + expect(equal(writer.subspan(), expected)); + }; + + "writer::write"_test = [] { + using fmt = format::bin32; + std::array payload; + auto constexpr expected = make_bytes(0xc6, 0x0, 0x0, 0x0, 0x07, 0x01, + 0x02, 0x03, 0x04, 0xf8, 0xf9, 0xfa); + + msgpack::writer writer(payload); + std::span bv(expected.begin() + 5, expected.end()); + expect(!!writer.write(std::move(bv))); + expect(equal(writer.subspan(), expected)); + }; + + "writer::write"_test = [] { + using fmt = format::fixmap; + std::array payload; + auto constexpr expected = make_bytes(0x83); + msgpack::writer writer(payload); + expect(!!writer.write(msgpack::map_desc{3})); + expect(equal(writer.subspan(), expected)); + }; + + "writer::write"_test = [] { + using fmt = format::map16; + std::array payload; + auto constexpr expected = make_bytes(0xde, 0x01, 0x00); + msgpack::writer writer(payload); + expect(!!writer.write(msgpack::map_desc{256})); + expect(equal(writer.subspan(), expected)); + }; + + "writer::write"_test = [] { + using fmt = format::map32; + std::array payload; + auto constexpr expected = make_bytes(0xdf, 0x00, 0x01, 0x00, 0x00); + msgpack::writer writer(payload); + expect(!!writer.write(msgpack::map_desc{0x10000})); + expect(equal(writer.subspan(), expected)); + }; + + "writer::write"_test = [] { + using fmt = format::fixarray; + std::array payload; + auto constexpr expected = make_bytes(0x93); + msgpack::writer writer(payload); + expect(!!writer.write(msgpack::array_desc{3})); + expect(equal(writer.subspan(), expected)); + }; + + "writer::write"_test = [] { + using fmt = format::array16; + std::array payload; + auto constexpr expected = make_bytes(0xdc, 0x01, 0x00); + msgpack::writer writer(payload); + expect(!!writer.write(msgpack::array_desc{256})); + expect(equal(writer.subspan(), expected)); + }; + + "writer::write"_test = [] { + using fmt = format::array32; + std::array payload; + auto constexpr expected = make_bytes(0xdd, 0x00, 0x01, 0x00, 0x00); + msgpack::writer writer(payload); + expect(!!writer.write(msgpack::array_desc{0x10000})); + expect(equal(writer.subspan(), expected)); + }; + + "writer::write"_test = [] { + using fmt = format::nil; + std::array payload; + auto constexpr expected = make_bytes(0xc0); + msgpack::writer writer(payload); + expect(!!writer.write({})); + expect(equal(writer.subspan(), expected)); + }; + + "writer::write"_test = [] { + using fmt = format::invalid; + std::array payload; + auto constexpr expected = make_bytes(0xc1); + msgpack::writer writer(payload); + expect(!!writer.write({})); + expect(equal(writer.subspan(), expected)); + }; + + "writer::write"_test = [] { + using fmt = format::boolean; + std::array payload; + auto constexpr expected = make_bytes(0xc2, 0xc3); + msgpack::writer writer(payload); + expect(!!writer.write(false)); + expect(!!writer.write(true)); + expect(equal(writer.subspan(), expected)); + }; +};