From f6d480671088a4c43e3afcff7c7e5a5cf344a16f Mon Sep 17 00:00:00 2001 From: Lee *!* Clagett <vtnerd@users.noreply.github.com> Date: Wed, 5 Apr 2023 10:16:50 -0400 Subject: [PATCH] Adding msgpack support to ::wire:: library (#63) --- src/wire/CMakeLists.txt | 1 + src/wire/field.h | 30 +- src/wire/msgpack.h | 54 ++ src/wire/msgpack/CMakeLists.txt | 33 ++ src/wire/msgpack/base.h | 171 +++++++ src/wire/msgpack/error.cpp | 71 +++ src/wire/msgpack/error.h | 59 +++ src/wire/msgpack/fwd.h | 45 ++ src/wire/msgpack/read.cpp | 517 ++++++++++++++++++++ src/wire/msgpack/read.h | 164 +++++++ src/wire/msgpack/write.cpp | 219 +++++++++ src/wire/msgpack/write.h | 225 +++++++++ src/wire/read.h | 12 +- src/wire/write.h | 23 +- tests/unit/CMakeLists.txt | 2 +- tests/unit/wire/CMakeLists.txt | 3 +- tests/unit/wire/msgpack/CMakeLists.txt | 37 ++ tests/unit/wire/msgpack/read.write.test.cpp | 183 +++++++ tests/unit/wire/read.write.test.cpp | 77 ++- 19 files changed, 1876 insertions(+), 50 deletions(-) create mode 100644 src/wire/msgpack.h create mode 100644 src/wire/msgpack/CMakeLists.txt create mode 100644 src/wire/msgpack/base.h create mode 100644 src/wire/msgpack/error.cpp create mode 100644 src/wire/msgpack/error.h create mode 100644 src/wire/msgpack/fwd.h create mode 100644 src/wire/msgpack/read.cpp create mode 100644 src/wire/msgpack/read.h create mode 100644 src/wire/msgpack/write.cpp create mode 100644 src/wire/msgpack/write.h create mode 100644 tests/unit/wire/msgpack/CMakeLists.txt create mode 100644 tests/unit/wire/msgpack/read.write.test.cpp diff --git a/src/wire/CMakeLists.txt b/src/wire/CMakeLists.txt index 4a09e65..a1cc14d 100644 --- a/src/wire/CMakeLists.txt +++ b/src/wire/CMakeLists.txt @@ -34,3 +34,4 @@ target_include_directories(monero-lws-wire PUBLIC "${LMDB_INCLUDE}") target_link_libraries(monero-lws-wire PRIVATE monero::libraries) add_subdirectory(json) +add_subdirectory(msgpack) diff --git a/src/wire/field.h b/src/wire/field.h index 8b2f435..5e1c112 100644 --- a/src/wire/field.h +++ b/src/wire/field.h @@ -1,4 +1,4 @@ -// Copyright (c) 2020, The Monero Project +// Copyright (c) 2020-2023, The Monero Project // All rights reserved. // // Redistribution and use in source and binary forms, with or without modification, are @@ -33,9 +33,13 @@ #include "wire/filters.h" #include "wire/traits.h" +//! A required field with the same key name and C/C++ name +#define WIRE_FIELD_ID(id, name) \ + ::wire::field< id >( #name , std::ref( self . name )) + //! A required field has the same key name and C/C++ name -#define WIRE_FIELD(name) \ - ::wire::field( #name , std::ref( self . name )) +#define WIRE_FIELD(name) \ + WIRE_FIELD_ID(0, name) //! A required field has the same key name and C/C++ name AND is cheap to copy (faster output). #define WIRE_FIELD_COPY(name) \ @@ -61,12 +65,13 @@ namespace wire //! Links `name` to a `value` for object serialization. - template<typename T, bool Required> + template<typename T, bool Required, unsigned I = 0> struct field_ { using value_type = typename unwrap_reference<T>::type; static constexpr bool is_required() noexcept { return Required; } static constexpr std::size_t count() noexcept { return 1; } + static constexpr unsigned id() noexcept { return I; } const char* name; T value; @@ -85,15 +90,15 @@ namespace wire }; //! Links `name` to `value`. Use `std::ref` if de-serializing. - template<typename T> - constexpr inline field_<T, true> field(const char* name, T value) + template<unsigned I = 0, typename T = void> + constexpr inline field_<T, true, I> field(const char* name, T value) { return {name, std::move(value)}; } //! Links `name` to `value`. Use `std::ref` if de-serializing. - template<typename T> - constexpr inline field_<T, false> optional_field(const char* name, T value) + template<unsigned I = 0, typename T = void> + constexpr inline field_<T, false, I> optional_field(const char* name, T value) { return {name, std::move(value)}; } @@ -103,6 +108,7 @@ namespace wire template<typename T> struct option { + static constexpr unsigned id() noexcept { return 0; } const char* name; }; @@ -243,13 +249,13 @@ namespace wire } - template<typename T> - inline constexpr bool available(const field_<T, true>&) noexcept + template<typename T, unsigned I> + inline constexpr bool available(const field_<T, true, I>&) noexcept { return true; } - template<typename T> - inline bool available(const field_<T, false>& elem) + template<typename T, unsigned I> + inline bool available(const field_<T, false, I>& elem) { return bool(elem.get_value()); } diff --git a/src/wire/msgpack.h b/src/wire/msgpack.h new file mode 100644 index 0000000..946d1b6 --- /dev/null +++ b/src/wire/msgpack.h @@ -0,0 +1,54 @@ +// Copyright (c) 2023, The Monero Project +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "wire/msgpack/base.h" +#include "wire/msgpack/error.h" +#include "wire/msgpack/read.h" +#include "wire/msgpack/write.h" + +#define WIRE_MSGPACK_DEFINE_ENUM(type, map) \ + void read_bytes(::wire::msgpack_reader& source, type& dest) \ + { \ + dest = type(source.enumeration(map)); \ + } \ + void write_bytes(::wire::msgpack_writer& dest, const type source) \ + { \ + dest.enumeration(std::size_t(source), map); \ + } + +#define WIRE_MSGPACK_DEFINE_OBJECT(type, map) \ + void read_bytes(::wire::msgpack_reader& source, type& dest) \ + { \ + map(source, dest); \ + } \ + void write_bytes(::wire::msgpack_writer& dest, const type& source) \ + { \ + map(dest, source); \ + } + diff --git a/src/wire/msgpack/CMakeLists.txt b/src/wire/msgpack/CMakeLists.txt new file mode 100644 index 0000000..7ad3c63 --- /dev/null +++ b/src/wire/msgpack/CMakeLists.txt @@ -0,0 +1,33 @@ +# Copyright (c) 2023, The Monero Project +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are +# permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this list +# of conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may be +# used to endorse or promote products derived from this software without specific +# prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +set(monero-lws_wire-msgpack_sources error.cpp read.cpp write.cpp) +set(monero-lws_wire-msgpack_headers base.h error.h fwd.h read.h write.h) + +add_library(monero-lws-wire-msgpack ${monero-lws_wire-msgpack_sources} ${monero-lws-wire-msgpack_headers}) +target_link_libraries(monero-lws-wire-msgpack monero::libraries monero-lws-wire) diff --git a/src/wire/msgpack/base.h b/src/wire/msgpack/base.h new file mode 100644 index 0000000..5e0c2cb --- /dev/null +++ b/src/wire/msgpack/base.h @@ -0,0 +1,171 @@ +// Copyright (c) 2023, The Monero Project +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include <cstdint> +#include <string> +#include <tuple> + +#include "byte_slice.h" +#include "common/expect.h" +#include "wire/msgpack/fwd.h" + +namespace wire +{ + struct msgpack + { + using input_type = msgpack_reader; + using output_type = msgpack_writer; + + //! Tags that do not require bitmask to identify + enum class tag : std::uint8_t + { + nil = 0xc0, + unused, + False, + True, + binary8, + binary16, + binary32, + extension8, + extension16, + extension32, + float32, + float64, + uint8, + uint16, + uint32, + uint64, + int8, + int16, + int32, + int64, + fixed_extension1, + fixed_extension2, + fixed_extension4, + fixed_extension8, + fixed_extension16, + string8, + string16, + string32, + array16, + array32, + object16, + object32 + }; + + //! Link a fixed tag `T` to its corresponding mask `M` and max value `N` + template<std::uint8_t T, std::uint8_t M, std::uint8_t N> + struct fixed_tag + { + static constexpr std::uint8_t tag() noexcept { return T; } + static constexpr std::uint8_t mask() noexcept { return M; } + static constexpr std::uint8_t max() noexcept { return N; } + + //! \return True if `value` is fixed tag `T` + static constexpr bool matches(const std::uint8_t value) noexcept + { return (value & mask()) == tag(); } + + //! \return True if `value` is fixed tag `T` + static constexpr bool matches(const msgpack::tag value) noexcept + { return matches(std::uint8_t(value)); } + + //! \return Value encoded in fixed tag + static constexpr std::uint8_t extract(const std::uint8_t value) noexcept + { return value & ~mask(); } + + //! \return Value encoded in fixed tag + static constexpr std::uint8_t extract(const msgpack::tag value) noexcept + { return extract(std::uint8_t(value)); } + }; + + // Tags requiring bitmask to identify + using ftag_unsigned = fixed_tag<0x00, 0x80, 0x7f>; + using ftag_signed = fixed_tag<0xe0, 0xe0, 0>; + using ftag_string = fixed_tag<0xa0, 0xe0, 31>; + using ftag_array = fixed_tag<0x90, 0xf0, 15>; + using ftag_object = fixed_tag<0x80, 0xf0, 15>; + + //! Link a msgpack tag to a C++ numeric + template<typename T, tag V> + struct type + { + static constexpr bool is_signed() noexcept { return std::numeric_limits<T>::is_signed; } + static constexpr T min() noexcept { return std::numeric_limits<T>::min(); } + static constexpr T max() noexcept { return std::numeric_limits<T>::max(); } + static constexpr tag Tag() noexcept { return V; } + }; + + using int8 = type<std::int8_t, tag::int8>; + using int16 = type<std::int16_t, tag::int16>; + using int32 = type<std::int32_t, tag::int32>; + using int64 = type<std::int64_t, tag::int64>; + using signed_types = std::tuple<int8, int16, int32, int64>; + + using uint8 = type<std::uint8_t, tag::uint8>; + using uint16 = type<std::uint16_t, tag::uint16>; + using uint32 = type<std::uint32_t, tag::uint32>; + using uint64 = type<std::uint64_t, tag::uint64>; + using unsigned_types = std::tuple<uint8, uint16, uint32, uint64>; + + using integer_types = std::tuple< + msgpack::uint8, msgpack::int8, msgpack::uint16, msgpack::int16, + msgpack::uint32, msgpack::int32, msgpack::uint64, msgpack::int64 + >; + + using string8 = type<std::uint8_t, tag::string8>; + using string16 = type<std::uint16_t, tag::string16>; + using string32 = type<std::uint32_t, tag::string32>; + using string_types = std::tuple<string8, string16, string32>; + + using binary8 = type<std::uint8_t, tag::binary8>; + using binary16 = type<std::uint16_t, tag::binary16>; + using binary32 = type<std::uint32_t, tag::binary32>; + using binary_types = std::tuple<binary8, binary16, binary32>; + + using extension8 = type<std::uint8_t, tag::extension8>; + using extension16 = type<std::uint16_t, tag::extension16>; + using extension32 = type<std::uint32_t, tag::extension32>; + using extension_types = std::tuple<extension8, extension16, extension32>; + + using array16 = type<std::uint16_t, tag::array16>; + using array32 = type<std::uint32_t, tag::array32>; + using array_types = std::tuple<array16, array32>; + + using object16 = type<std::uint16_t, tag::object16>; + using object32 = type<std::uint32_t, tag::object32>; + using object_types = std::tuple<object16, object32>; + + template<typename T> + static expect<T> from_bytes(epee::byte_slice&& source); + + template<typename T> + static epee::byte_slice to_bytes(const T& source); + }; +} + diff --git a/src/wire/msgpack/error.cpp b/src/wire/msgpack/error.cpp new file mode 100644 index 0000000..a1278dd --- /dev/null +++ b/src/wire/msgpack/error.cpp @@ -0,0 +1,71 @@ +// Copyright (c) 2023, The Monero Project +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "error.h" + +namespace wire +{ +namespace error +{ + const char* get_string(const msgpack value) noexcept + { + switch (value) + { + default: + break; + case msgpack::incomplete: + return "Incomplete msgpack tree structure"; + case msgpack::integer_encoding: + return "Unable to encode integer in msgpack"; + case msgpack::invalid: + return "Invalid msgpack encoding"; + case msgpack::not_enough_bytes: + return "Expected more bytes in the msgpack stream"; + } + + return "Unknown msgpack error"; + } + + const std::error_category& msgpack_category() noexcept + { + struct category final : std::error_category + { + virtual const char* name() const noexcept override final + { + return "wire::error::msgpack_category()"; + } + + virtual std::string message(int value) const override final + { + return get_string(msgpack(value)); + } + }; + static const category instance{}; + return instance; + } +} // error +} // wire diff --git a/src/wire/msgpack/error.h b/src/wire/msgpack/error.h new file mode 100644 index 0000000..345d047 --- /dev/null +++ b/src/wire/msgpack/error.h @@ -0,0 +1,59 @@ +// Copyright (c) 2023, The Monero Project +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include <system_error> + +namespace wire +{ +namespace error +{ + //! Type wrapper to "grab" rapidjson errors + enum class msgpack : int + { + success = 0, // required for `expected<T>` + incomplete, + integer_encoding, + invalid, + not_enough_bytes + }; + + //! \return Static string describing error `value`. + const char* get_string(msgpack value) noexcept; + + //! \return Category for msgpack generated errors. + const std::error_category& msgpack_category() noexcept; + + //! \return Error code with `value` and `rapidjson_category()`. + inline std::error_code make_error_code(msgpack value) noexcept + { + return std::error_code{int(value), msgpack_category()}; + } +} +} + diff --git a/src/wire/msgpack/fwd.h b/src/wire/msgpack/fwd.h new file mode 100644 index 0000000..9363a73 --- /dev/null +++ b/src/wire/msgpack/fwd.h @@ -0,0 +1,45 @@ +// Copyright (c) 2023, The Monero Project +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#define WIRE_MSGPACK_DECLARE_ENUM(type) \ + const char* get_string(type) noexcept; \ + void read_bytes(::wire::msgpack_reader&, type&); \ + void write_bytes(:wire::msgpack_writer&, type) + +#define WIRE_MSGPACK_DECLARE_OBJECT(type) \ + void read_bytes(::wire::msgpack_reader&, type&); \ + void write_bytes(::wire::msgpack_writer&, const type&) + +namespace wire +{ + struct msgpack; + class msgpack_reader; + class msgpack_writer; +} + diff --git a/src/wire/msgpack/read.cpp b/src/wire/msgpack/read.cpp new file mode 100644 index 0000000..2a11e02 --- /dev/null +++ b/src/wire/msgpack/read.cpp @@ -0,0 +1,517 @@ +// Copyright (c) 2023, The Monero Project +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "read.h" + +#include <boost/endian/buffers.hpp> +#include <boost/fusion/include/any.hpp> +#include <boost/fusion/include/std_tuple.hpp> +#include <boost/numeric/conversion/cast.hpp> +#include <cstring> +#include <limits> +#include <stdexcept> +#include <type_traits> + +#include "wire/error.h" +#include "wire/msgpack/error.h" + +// Expands to every possible fixed string tag value +#define MLWS_FIXED_STRING_TAGS() \ + case wire::msgpack::tag(0xa0): case wire::msgpack::tag(0xa1): \ + case wire::msgpack::tag(0xa2): case wire::msgpack::tag(0xa3): \ + case wire::msgpack::tag(0xa4): case wire::msgpack::tag(0xa5): \ + case wire::msgpack::tag(0xa6): case wire::msgpack::tag(0xa7): \ + case wire::msgpack::tag(0xa8): case wire::msgpack::tag(0xa9): \ + case wire::msgpack::tag(0xaa): case wire::msgpack::tag(0xab): \ + case wire::msgpack::tag(0xac): case wire::msgpack::tag(0xad): \ + case wire::msgpack::tag(0xae): case wire::msgpack::tag(0xaf): \ + case wire::msgpack::tag(0xb0): case wire::msgpack::tag(0xb1): \ + case wire::msgpack::tag(0xb2): case wire::msgpack::tag(0xb3): \ + case wire::msgpack::tag(0xb4): case wire::msgpack::tag(0xb5): \ + case wire::msgpack::tag(0xb6): case wire::msgpack::tag(0xb7): \ + case wire::msgpack::tag(0xb8): case wire::msgpack::tag(0xb9): \ + case wire::msgpack::tag(0xba): case wire::msgpack::tag(0xbb): \ + case wire::msgpack::tag(0xbc): case wire::msgpack::tag(0xbd): \ + case wire::msgpack::tag(0xbe): case wire::msgpack::tag(0xbf): + +namespace +{ + template<typename T> + using limits = std::numeric_limits<T>; + + //! \return True iif `value` matches a tag in `T` tuple. + template<typename T> + bool matches(const wire::msgpack::tag tag) + { + const auto matched_type = [tag] (const auto type) + { + return type.Tag() == tag; + }; + // NOTE: This is slower than a switch but more flexible/reusable + return boost::fusion::any(T{}, matched_type); + } + + //! \return Integer `T` encoded as big endian in `source`. + template<typename T> + T read_endian(epee::byte_slice& source) + { + static_assert(std::is_integral<T>::value, "must be integral type"); + static constexpr const std::size_t bits = 8 * sizeof(T); + using buffer_type = + boost::endian::endian_buffer<boost::endian::order::big, T, bits>; + + buffer_type buffer; + static_assert(sizeof(buffer) == sizeof(T), "unexpected buffer size"); + if (source.size() < sizeof(buffer)) + WIRE_DLOG_THROW_(wire::error::msgpack::not_enough_bytes); + std::memcpy(std::addressof(buffer), source.data(), sizeof(buffer)); + source.remove_prefix(sizeof(buffer)); + return buffer.value(); + } + + //! \return Integer `T` encoded as big endian in `source`. + template<typename T, wire::msgpack::tag U> + T read_endian(epee::byte_slice& source, const wire::msgpack::type<T, U>) + { return read_endian<T>(source); } + + //! \return Integer `T` whose encoding is specified by tag `next` + template<typename T> + T read_integer(epee::byte_slice& source, const wire::msgpack::tag next) + { + try + { + // msgpack::integer_types + switch (next) + { + default: + break; + case wire::msgpack::tag::int8: + return boost::numeric_cast<T>(read_endian<std::int8_t>(source)); + case wire::msgpack::tag::uint8: + return boost::numeric_cast<T>(read_endian<std::uint8_t>(source)); + case wire::msgpack::tag::int16: + return boost::numeric_cast<T>(read_endian<std::int16_t>(source)); + case wire::msgpack::tag::uint16: + return boost::numeric_cast<T>(read_endian<std::uint16_t>(source)); + case wire::msgpack::tag::int32: + return boost::numeric_cast<T>(read_endian<std::int32_t>(source)); + case wire::msgpack::tag::uint32: + return boost::numeric_cast<T>(read_endian<std::uint32_t>(source)); + case wire::msgpack::tag::int64: + return boost::numeric_cast<T>(read_endian<std::int64_t>(source)); + case wire::msgpack::tag::uint64: + return boost::numeric_cast<T>(read_endian<std::uint64_t>(source)); + } + } + catch (const boost::numeric::positive_overflow&) + { WIRE_DLOG_THROW_(wire::error::schema::smaller_integer); } + catch (const boost::numeric::negative_overflow&) + { WIRE_DLOG_THROW_(wire::error::schema::larger_integer); } + + WIRE_DLOG_THROW_(wire::error::schema::integer); + } + + epee::byte_slice read_raw(epee::byte_slice& source, const std::size_t bytes) + { + if (source.size() < bytes) + WIRE_DLOG_THROW_(wire::error::msgpack::not_enough_bytes); + return source.take_slice(bytes); + } + + template<typename T> + epee::byte_slice read_raw(epee::byte_slice& source) + { + return read_raw(source, wire::integer::cast_unsigned<std::size_t>(read_endian<T>(source))); + } + + epee::byte_slice read_string(epee::byte_slice& source, const wire::msgpack::tag next) + { + switch (next) + { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wswitch" + MLWS_FIXED_STRING_TAGS() + return read_raw(source, wire::msgpack::ftag_string::extract(next)); +#pragma GCC diagnostic pop + case wire::msgpack::tag::string8: + return read_raw<std::uint8_t>(source); + case wire::msgpack::tag::string16: + return read_raw<std::uint16_t>(source); + case wire::msgpack::tag::string32: + return read_raw<std::uint32_t>(source); + default: + break; + } + WIRE_DLOG_THROW_(wire::error::schema::string); + } + + //! \return Binary blob encoded message + epee::byte_slice read_binary(epee::byte_slice& source, const wire::msgpack::tag next) + { + switch (next) + { + case wire::msgpack::tag::binary8: + return read_raw<std::uint8_t>(source); + case wire::msgpack::tag::binary16: + return read_raw<std::uint16_t>(source); + case wire::msgpack::tag::binary32: + return read_raw<std::uint32_t>(source); + default: + break; + } + WIRE_DLOG_THROW_(wire::error::schema::string); + } +} + +namespace wire +{ + void msgpack_reader::throw_logic_error() + { + throw std::logic_error{"Bug in msgpack_reader usage"}; + } + + void msgpack_reader::skip_value() + { + assert(remaining_); + if (limits<std::size_t>::max() == remaining_) + throw std::runtime_error{"msgpack_reader exceeded tree tracking"}; + + const std::size_t initial = remaining_; + do + { + const std::size_t size = source_.size(); + const msgpack::tag next = peek_tag(); + switch (next) + { + default: + break; + case msgpack::tag::nil: + case msgpack::tag::unused: + case msgpack::tag::False: + case msgpack::tag::True: + source_.remove_prefix(1); + break; + case msgpack::tag::binary8: + case msgpack::tag::binary16: + case msgpack::tag::binary32: + source_.remove_prefix(1); + read_binary(source_, next); + break; + case msgpack::tag::extension8: + source_.remove_prefix(1); + read_raw<std::uint8_t>(source_); + source_.remove_prefix(1); + break; + case msgpack::tag::extension16: + source_.remove_prefix(1); + read_raw<std::uint16_t>(source_); + source_.remove_prefix(1); + break; + case msgpack::tag::extension32: + source_.remove_prefix(1); + read_raw<std::uint32_t>(source_); + source_.remove_prefix(1); + break; + case msgpack::tag::int8: + case msgpack::tag::uint8: + source_.remove_prefix(2); + break; + case msgpack::tag::int16: + case msgpack::tag::uint16: + case msgpack::tag::fixed_extension1: + source_.remove_prefix(3); + break; + case msgpack::tag::int32: + case msgpack::tag::uint32: + case msgpack::tag::float32: + source_.remove_prefix(5); + break; + case msgpack::tag::int64: + case msgpack::tag::uint64: + case msgpack::tag::float64: + source_.remove_prefix(9); + break; + case msgpack::tag::fixed_extension2: + source_.remove_prefix(4); + break; + case msgpack::tag::fixed_extension4: + source_.remove_prefix(6); + break; + case msgpack::tag::fixed_extension8: + source_.remove_prefix(10); + break; + case msgpack::tag::fixed_extension16: + source_.remove_prefix(18); + break; +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wswitch" + MLWS_FIXED_STRING_TAGS() + case msgpack::tag::string8: + case msgpack::tag::string16: + case msgpack::tag::string32: + source_.remove_prefix(1); + read_string(source_, next); + break; + case msgpack::tag(0x90): case msgpack::tag(0x91): case msgpack::tag(0x92): + case msgpack::tag(0x93): case msgpack::tag(0x94): case msgpack::tag(0x95): + case msgpack::tag(0x96): case msgpack::tag(0x97): case msgpack::tag(0x98): + case msgpack::tag(0x99): case msgpack::tag(0x9a): case msgpack::tag(0x9b): + case msgpack::tag(0x9c): case msgpack::tag(0x9d): case msgpack::tag(0x9e): + case msgpack::tag(0x9f): + case msgpack::tag::array16: + case msgpack::tag::array32: + start_array(); + break; + case msgpack::tag(0x80): case msgpack::tag(0x81): case msgpack::tag(0x82): + case msgpack::tag(0x83): case msgpack::tag(0x84): case msgpack::tag(0x85): + case msgpack::tag(0x86): case msgpack::tag(0x87): case msgpack::tag(0x88): + case msgpack::tag(0x89): case msgpack::tag(0x8a): case msgpack::tag(0x8b): + case msgpack::tag(0x8c): case msgpack::tag(0x8d): case msgpack::tag(0x8e): + case msgpack::tag(0x8f): + case msgpack::tag::object16: + case msgpack::tag::object32: + start_object(); + break; +#pragma GCC diagnostic pop + }; + + if (size == source_.size()) + { + if (!msgpack::ftag_unsigned::matches(next) && !msgpack::ftag_signed::matches(next)) + WIRE_DLOG_THROW_(error::msgpack::invalid); + source_.remove_prefix(1); + } + update_remaining(); + } while (initial <= remaining_); + } + + msgpack::tag msgpack_reader::peek_tag() + { + if (source_.empty()) + WIRE_DLOG_THROW_(error::msgpack::not_enough_bytes); + return msgpack::tag(*source_.data()); + } + + msgpack::tag msgpack_reader::get_tag() + { + const msgpack::tag next = peek_tag(); + source_.remove_prefix(1); + return next; + } + + std::intmax_t msgpack_reader::do_integer(const msgpack::tag next) + { + if (msgpack::ftag_signed::matches(next)) + return *reinterpret_cast<const std::int8_t*>(std::addressof(next)); // special case + return read_integer<std::intmax_t>(source_, next); + } + + std::uintmax_t msgpack_reader::do_unsigned_integer(const msgpack::tag next) + { + return read_integer<std::uintmax_t>(source_, next); + } + + template<typename T, typename U> + std::size_t msgpack_reader::read_count(const error::schema expected) + { + const msgpack::tag next = get_tag(); + if (T::matches(next)) + return T::extract(next); + + std::size_t out = 0; + const auto matched_type = [this, &out, next](const auto type) + { + if (type.Tag() == next) + { + out = integer::cast_unsigned<std::size_t>(read_endian(source_, type)); + return true; + } + return false; + }; + + if (!boost::fusion::any(U{}, matched_type)) + WIRE_DLOG_THROW_(expected); + + return out; + } + + void msgpack_reader::check_complete() const + { + if (remaining_) + WIRE_DLOG_THROW_(error::msgpack::incomplete); + } + + bool msgpack_reader::boolean() + { + update_remaining(); + switch (get_tag()) + { + case msgpack::tag::True: + return true; + case msgpack::tag::False: + return false; + default: + break; + } + WIRE_DLOG_THROW_(error::schema::boolean); + } + + double msgpack_reader::real() + { + update_remaining(); + + const auto read_float = [this](auto value) + { + if (source_.size() < sizeof(value)) + WIRE_DLOG_THROW_(error::msgpack::not_enough_bytes); + std::memcpy(std::addressof(value), source_.data(), sizeof(value)); + source_.remove_prefix(sizeof(value)); + return value; + }; + + switch (get_tag()) + { + case msgpack::tag::float32: + return read_float(float(0)); + case msgpack::tag::float64: + return read_float(double(0)); + default: + break; + } + WIRE_DLOG_THROW_(error::schema::number); + } + + std::string msgpack_reader::string() + { + update_remaining(); + const epee::byte_slice bytes = read_string(source_, get_tag()); + return std::string{reinterpret_cast<const char*>(bytes.data()), bytes.size()}; + } + + std::vector<std::uint8_t> msgpack_reader::binary() + { + update_remaining(); + const epee::byte_slice bytes = read_binary(source_, get_tag()); + return std::vector<std::uint8_t>{bytes.begin(), bytes.end()}; + } + + void msgpack_reader::binary(epee::span<std::uint8_t> dest) + { + update_remaining(); + const epee::byte_slice bytes = read_binary(source_, get_tag()); + if (dest.size() != bytes.size()) + WIRE_DLOG_THROW(error::schema::fixed_binary, "of size " << dest.size() << " but got " << bytes.size()); + std::memcpy(dest.data(), bytes.data(), dest.size()); + } + + std::size_t msgpack_reader::enumeration(const epee::span<char const* const> enums) + { + const std::uintmax_t value = unsigned_integer(); + if (enums.size() < value) + WIRE_DLOG_THROW(error::schema::enumeration, value << " is not a valid enum"); + return std::size_t(value); + } + + std::size_t msgpack_reader::start_array() + { + const std::size_t upcoming = + read_count<msgpack::ftag_array, msgpack::array_types>(error::schema::array); + if (limits<std::size_t>::max() - remaining_ < upcoming) + throw std::runtime_error{"Exceeded max tree tracking for msgpack_reader"}; + remaining_ += upcoming; + return upcoming; + } + + bool msgpack_reader::is_array_end(const std::size_t count) + { + if (count) + return false; + update_remaining(); + return true; + } + + std::size_t msgpack_reader::start_object() + { + const std::size_t upcoming = + read_count<msgpack::ftag_object, msgpack::object_types>(error::schema::object); + if (limits<std::size_t>::max() / 2 < upcoming) + throw std::runtime_error{"Exceeded max object tracking for msgpack_reader"}; + if (limits<std::size_t>::max() - remaining_ < upcoming * 2) + throw std::runtime_error{"Exceeded msgpack_reader:: tree tracking"}; + remaining_ += upcoming * 2; + return upcoming; + } + + bool msgpack_reader::key(const epee::span<const key_map> map, std::size_t& state, std::size_t& index) + { + index = map.size(); + for ( ;state; --state) + { + update_remaining(); // for key + const msgpack::tag next = get_tag(); + const bool single = msgpack::ftag_unsigned::matches(next); + if (single || matches<msgpack::unsigned_types>(next)) + { + unsigned key = std::uint8_t(next); + if (!single) + key = read_integer<unsigned>(source_, next); + for (const key_map& elem : map) + { + if (elem.id == key) + { + index = std::addressof(elem) - map.begin(); + break; + } + } + } + else if (msgpack::ftag_string::matches(next) || matches<msgpack::string_types>(next)) + { + const epee::byte_slice key = read_string(source_, next); + for (const key_map& elem : map) + { + const boost::string_ref elem_{elem.name}; + if (key.size() == elem_.size() && std::memcmp(key.data(), elem_.data(), key.size()) == 0) + { + index = std::addressof(elem) - map.begin(); + break; + } + } + } + else + WIRE_DLOG_THROW(error::schema::invalid_key, "Invalid key type"); + + if (index < map.size()) + { + --state; + return true; + } + skip_value(); + } // until state == 0 + update_remaining(); // for end of object + return false; + } +} diff --git a/src/wire/msgpack/read.h b/src/wire/msgpack/read.h new file mode 100644 index 0000000..df0b107 --- /dev/null +++ b/src/wire/msgpack/read.h @@ -0,0 +1,164 @@ +// Copyright (c) 2023, The Monero Project +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include <boost/utility/string_ref.hpp> +#include <cstddef> +#include <string> +#include <type_traits> +#include <utility> +#include <vector> + +#include "wire/field.h" +#include "wire/msgpack/base.h" +#include "wire/read.h" +#include "wire/traits.h" + +namespace wire +{ + //! Reads MSGPACK tokens one-at-a-time for DOMless parsing + class msgpack_reader : public reader + { + epee::byte_slice source_; + std::size_t remaining_; //!< Expected number of elements remaining + + //! \throw std::logic_error + [[noreturn]] void throw_logic_error(); + //! Decrement remaining_ if not zero, \throw std::logic_error when `remaining_ == 0`. + void update_remaining() + { + if (remaining_) + --remaining_; + else + throw_logic_error(); + } + + //! Skips next value. \throw wire::exception if invalid JSON syntax. + void skip_value(); + + //! \return Next tag but leave `source_` untouched. + msgpack::tag peek_tag(); + //! \return Next tag and remove first byte from `source_`. + msgpack::tag get_tag(); + + //! \return Integer from `soure_` where positive fixed tag has been checked. + std::intmax_t do_integer(msgpack::tag); + //! \return Integer from `source_` where fixed tag has been checked. + std::uintmax_t do_unsigned_integer(msgpack::tag); + + //! \return Number of items determined by `T` fixed tag and `U` tuple of tags. + template<typename T, typename U> + std::size_t read_count(error::schema); + + public: + explicit msgpack_reader(epee::byte_slice&& source) + : reader(), source_(std::move(source)), remaining_(1) + {} + + //! \throw wire::exception if JSON parsing is incomplete. + void check_complete() const override final; + + //! \throw wire::exception if next token not a boolean. + bool boolean() override final; + + //! \throw wire::expception if next token not an integer. + std::intmax_t integer() override final + { + update_remaining(); + const msgpack::tag next = get_tag(); + if (std::uint8_t(next) <= msgpack::ftag_unsigned::max()) + return std::uint8_t(next); + return do_integer(next); + } + + //! \throw wire::exception if next token not an unsigned integer. + std::uintmax_t unsigned_integer() override final + { + update_remaining(); + const msgpack::tag next = get_tag(); + if (std::uint8_t(next) <= msgpack::ftag_unsigned::max()) + return std::uint8_t(next); + return do_unsigned_integer(next); + } + + //! \throw wire::exception if next token not a valid real number + double real() override final; + + //! \throw wire::exception if next token not a string + std::string string() override final; + + //! \throw wire::exception if next token cannot be read as hex + std::vector<std::uint8_t> binary() override final; + + //! \throw wire::exception if next token cannot be read as hex into `dest`. + void binary(epee::span<std::uint8_t> dest) override final; + + //! \throw wire::exception if invalid next token invalid enum. \return Index in `enums`. + std::size_t enumeration(epee::span<char const* const> enums) override final; + + + //! \throw wire::exception if next token not `[`. + std::size_t start_array() override final; + + //! \return true when `count == 0`. + bool is_array_end(const std::size_t count) override final; + + + //! \throw wire::exception if next token not `{`. + std::size_t start_object() override final; + + /*! \throw wire::exception if next token not key or `}`. + \param[out] index of key match within `map`. + \return True if another value to read. */ + bool key(epee::span<const key_map> map, std::size_t&, std::size_t& index) override final; + }; + + + // Don't call `read` directly in this namespace, do it from `wire_read`. + + template<typename T> + expect<T> msgpack::from_bytes(epee::byte_slice&& bytes) + { + msgpack_reader source{std::move(bytes)}; + return wire_read::to<T>(source); + } + + // specialization prevents type "downgrading" to base type in cpp files + + template<typename T> + inline void array(msgpack_reader& source, T& dest) + { + wire_read::array(source, dest); + } + + template<typename... T> + inline void object(msgpack_reader& source, T... fields) + { + wire_read::object(source, wire_read::tracker<T>{std::move(fields)}...); + } +} // wire diff --git a/src/wire/msgpack/write.cpp b/src/wire/msgpack/write.cpp new file mode 100644 index 0000000..17197b4 --- /dev/null +++ b/src/wire/msgpack/write.cpp @@ -0,0 +1,219 @@ +// Copyright (c) 2023, The Monero Project +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "write.h" + +#include <boost/endian/buffers.hpp> +#include <boost/fusion/include/any.hpp> +#include <boost/fusion/include/std_tuple.hpp> +#include <cassert> +#include <limits> +#include <ostream> +#include <stdexcept> +#include <type_traits> + +#include "wire/error.h" +#include "wire/msgpack/error.h" + +namespace +{ + template<typename T> + using limits = std::numeric_limits<T>; + + constexpr const unsigned flush_threshold = 100; + constexpr const unsigned max_buffer = 4096; + + void write_tag(epee::byte_stream& bytes, const wire::msgpack::tag value) + { + bytes.put(std::uint8_t(value)); + } + + template<typename T, typename U, wire::msgpack::tag tag> + void write_endian(epee::byte_stream& bytes, const T value, const wire::msgpack::type<U, tag> type) + { + static_assert(std::is_integral<T>::value, "input not integral"); + static_assert(std::is_integral<U>::value, "output not integral"); + + using in_limits = std::numeric_limits<T>; + using out_limits = std::numeric_limits<U>; + static_assert(in_limits::is_signed == out_limits::is_signed, "signs must match"); + + assert(type.min() <= value); + assert(value <= type.max()); + + static constexpr const std::size_t bits = 8 * sizeof(U); + using buffer_type = + boost::endian::endian_buffer<boost::endian::order::big, U, bits>; + + buffer_type buffer(value); + write_tag(bytes, type.Tag()); + bytes.write(buffer.data(), sizeof(buffer)); + } + + template<typename T> + void write_count(epee::byte_stream& bytes, const std::uintmax_t items) + { + const auto match_size = [&bytes, items] (const auto type) -> bool + { + if (type.max() < items) + return false; + write_endian(bytes, items, type); + return true; + }; + if (!boost::fusion::any(T{}, match_size)) + WIRE_DLOG_THROW_(wire::error::msgpack::integer_encoding); + } + + template<typename T, typename U> + void write_count(epee::byte_stream& bytes, const std::uintmax_t items) + { + if (items <= T::max()) + bytes.put(T::tag() | std::uint8_t(items)); + else + write_count<U>(bytes, items); + } +} + +namespace wire +{ + void msgpack_writer::do_flush(epee::span<const std::uint8_t>) + {} + + void msgpack_writer::check_flush() + { + if (needs_flush_ && (max_buffer < bytes_.size() || bytes_.available() < flush_threshold)) + flush(); + } + + void msgpack_writer::do_integer(const std::intmax_t value) + { + assert(value < 0); // constraint checked in header + if (0xe0 < value) // 0xe0 needs to be type `int` to work + { + bytes_.put(std::uint8_t(value)); + return; + } + + const auto match_size = [this, value] (const auto type) -> bool + { + if (value < type.min()) + return false; + write_endian(bytes_, value, type); + return true; + }; + if (!boost::fusion::any(wire::msgpack::signed_types{}, match_size)) + WIRE_DLOG_THROW_(wire::error::msgpack::integer_encoding); + } + + void msgpack_writer::do_unsigned_integer(const std::uintmax_t value) + { + const auto match_size = [this, value] (const auto type) -> bool + { + if (type.max() < value) + return false; + write_endian(bytes_, value, type); + return true; + }; + if (!boost::fusion::any(wire::msgpack::unsigned_types{}, match_size)) + WIRE_DLOG_THROW_(wire::error::msgpack::integer_encoding); + } + + void msgpack_writer::check_complete() + { + if (expected_) + throw std::logic_error{"msgpack_writer::take_msgpack() failed with incomplete tree"}; + } + epee::byte_slice msgpack_writer::take_msgpack() + { + check_complete(); + epee::byte_slice out{std::move(bytes_)}; + bytes_.clear(); + return out; + } + + msgpack_writer::~msgpack_writer() noexcept + {} + + void msgpack_writer::real(const double source) + { + write_tag(bytes_, msgpack::tag::float64); + bytes_.write(reinterpret_cast<const char*>(std::addressof(source)), sizeof(source)); + --expected_; + } + + void msgpack_writer::string(const boost::string_ref source) + { + write_count<msgpack::ftag_string, msgpack::string_types>(bytes_, source.size()); + bytes_.write(source.data(), source.size()); + --expected_; + } + void msgpack_writer::binary(epee::span<const std::uint8_t> source) + { + write_count<msgpack::binary_types>(bytes_, source.size()); + bytes_.write(source); + --expected_; + } + + void msgpack_writer::enumeration(const std::size_t index, const epee::span<char const* const> enums) + { + if (enums.size() < index) + throw std::logic_error{"Invalid enum/string value"}; + unsigned_integer(index); + } + + void msgpack_writer::start_array(const std::size_t items) + { + write_count<msgpack::ftag_array, msgpack::array_types>(bytes_, items); + expected_ += items; + } + + void msgpack_writer::start_object(const std::size_t items) + { + write_count<msgpack::ftag_object, msgpack::object_types>(bytes_, items); + expected_ += items * 2; + } + void msgpack_writer::key(const boost::string_ref str) + { + string(str); + } + void msgpack_writer::key(const std::uintmax_t id) + { + unsigned_integer(id); + } + void msgpack_writer::key(const unsigned id, boost::string_ref str) + { + if (integer_keys_) + key(id); + else + key(str); + } + + void msgpack_stream_writer::do_flush(epee::span<const std::uint8_t> bytes) + { + dest.write(reinterpret_cast<const char*>(bytes.data()), bytes.size()); + } +} diff --git a/src/wire/msgpack/write.h b/src/wire/msgpack/write.h new file mode 100644 index 0000000..2b755de --- /dev/null +++ b/src/wire/msgpack/write.h @@ -0,0 +1,225 @@ +// Copyright (c) 2023, The Monero Project +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include <array> +#include <boost/utility/string_ref.hpp> +#include <cstdint> +#include <iosfwd> +#include <limits> + +#include "byte_stream.h" // monero/contrib/epee/include +#include "span.h" // monero/contrib/epee/include +#include "wire/field.h" +#include "wire/filters.h" +#include "wire/msgpack/base.h" +#include "wire/traits.h" +#include "wire/write.h" + +namespace wire +{ + //! Writes MSGPACK tokens one-at-a-time for DOMless output. + class msgpack_writer : public writer + { + epee::byte_stream bytes_; + std::size_t expected_; //! Tracks number of expected elements remaining + const bool integer_keys_; + bool needs_flush_; + + //! Provided data currently in `bytes_`. + virtual void do_flush(epee::span<const uint8_t>); + + //! Flush written bytes to `do_flush(...)` if configured + void check_flush(); + + void do_integer(std::intmax_t); + void do_unsigned_integer(std::uintmax_t); + + template<typename T> + void integer_t(const T value) + { + if (0 <= value) + { + if (value <= msgpack::ftag_unsigned::max()) + bytes_.put(std::uint8_t(value)); + else // if multibyte + do_unsigned_integer(std::uintmax_t(value)); + } + else // if negative + do_integer(value); + --expected_; + } + + template<typename T> + void unsigned_integer_t(const T value) + { + if (value <= msgpack::ftag_unsigned::max()) + bytes_.put(std::uint8_t(value)); + else // if multibyte + do_unsigned_integer(value); + --expected_; + } + + protected: + msgpack_writer(bool integer_keys, bool needs_flush) + : writer(), bytes_(), expected_(1), integer_keys_(integer_keys), needs_flush_(needs_flush) + {} + + //! \throw std::logic_error if tree was not completed + void check_complete(); + + //! \throw std::logic_error if incomplete msgpack tree. \return msgpack bytes + epee::byte_slice take_msgpack(); + + //! Flush bytes in local buffer to `do_flush(...)` + void flush() + { + do_flush({bytes_.data(), bytes_.size()}); + bytes_.clear(); + } + + public: + msgpack_writer(const msgpack_writer&) = delete; + virtual ~msgpack_writer() noexcept; + msgpack_writer& operator=(const msgpack_writer&) = delete; + + void boolean(const bool value) override final + { + if (value) + bytes_.put(std::uint8_t(msgpack::tag::True)); + else + bytes_.put(std::uint8_t(msgpack::tag::False)); + --expected_; + } + + void integer(const int value) override final + { integer_t(value); } + + void integer(const std::intmax_t value) override final + { integer_t(value); } + + void unsigned_integer(const unsigned value) override final + { unsigned_integer_t(value); } + + void unsigned_integer(const std::uintmax_t value) override final + { unsigned_integer_t(value); } + + void real(double) override final; + + //! \throw wire::exception if `source.size()` exceeds 2^32-1 + void string(boost::string_ref source) override final; + //! \throw wire::exception if `source.size()` exceeds 2^32-1 + void binary(epee::span<const std::uint8_t> source) override final; + + void enumeration(std::size_t index, epee::span<char const* const> enums) override final; + + //! \throw wire::exception if `items` exceeds 2^32-1. + void start_array(std::size_t items) override final; + void end_array() override final { --expected_; } + + //! \throw wire::exception if `items` exceeds 2^32-1 + void start_object(std::size_t items) override final; + void key(std::uintmax_t) override final; + void key(boost::string_ref) override final; + void key(unsigned, boost::string_ref) override final; + void end_object() override final { --expected_; } + }; + + //! Buffers entire JSON message in memory + struct msgpack_slice_writer final : msgpack_writer + { + explicit msgpack_slice_writer(bool integer_keys = false) + : msgpack_writer(integer_keys, false) + {} + + //! \throw std::logic_error if incomplete JSON tree \return JSON bytes + epee::byte_slice take_bytes() + { + return msgpack_writer::take_msgpack(); + } + }; + + //! Periodically flushes JSON data to `std::ostream` + class msgpack_stream_writer final : public msgpack_writer + { + std::ostream& dest; + + virtual void do_flush(epee::span<const std::uint8_t>) override final; + public: + explicit msgpack_stream_writer(std::ostream& dest, bool integer_keys = false) + : msgpack_writer(integer_keys, true), dest(dest) + {} + + //! Flush remaining bytes to stream \throw std::logic_error if incomplete JSON tree + void finish() + { + check_complete(); + flush(); + } + }; + + template<typename T> + epee::byte_slice msgpack::to_bytes(const T& source) + { + return wire_write::to_bytes<msgpack_slice_writer>(source); + } + + template<typename T, typename F = identity_> + inline void array(msgpack_writer& dest, const T& source, F filter = F{}) + { + wire_write::array(dest, source, source.size(), std::move(filter)); + } + template<typename T, typename F> + inline void write_bytes(msgpack_writer& dest, as_array_<T, F> source) + { + wire::array(dest, source.get_value(), std::move(source.filter)); + } + template<typename T> + inline enable_if<is_array<T>::value> write_bytes(msgpack_writer& dest, const T& source) + { + wire::array(dest, source); + } + + template<typename T, typename F = identity_, typename G = identity_> + inline void dynamic_object(msgpack_writer& dest, const T& source, F key_filter = F{}, G value_filter = G{}) + { + // works with "lazily" computed ranges + wire_write::dynamic_object(dest, source, 0, std::move(key_filter), std::move(value_filter)); + } + template<typename T, typename F, typename G> + inline void write_bytes(msgpack_writer& dest, as_object_<T, F, G> source) + { + wire::dynamic_object(dest, source.get_map(), std::move(source.key_filter), std::move(source.value_filter)); + } + + template<typename... T> + inline void object(msgpack_writer& dest, T... fields) + { + wire_write::object(dest, std::move(fields)...); + } +} diff --git a/src/wire/read.h b/src/wire/read.h index a73560d..e57014f 100644 --- a/src/wire/read.h +++ b/src/wire/read.h @@ -1,4 +1,4 @@ -// Copyright (c) 2020, The Monero Project +// Copyright (c) 2020-2023, The Monero Project // All rights reserved. // // Redistribution and use in source and binary forms, with or without modification, are @@ -299,14 +299,14 @@ namespace wire_read unpack_variant_field(index, source, dest.get_value(), static_cast< const wire::option<U>& >(dest)...); } - template<typename R, typename T> - inline void unpack_field(std::size_t, R& source, wire::field_<T, true>& dest) + template<typename R, typename T, unsigned I> + inline void unpack_field(std::size_t, R& source, wire::field_<T, true, I>& dest) { read_bytes(source, dest.get_value()); } - template<typename R, typename T> - inline void unpack_field(std::size_t, R& source, wire::field_<T, false>& dest) + template<typename R, typename T, unsigned I> + inline void unpack_field(std::size_t, R& source, wire::field_<T, false, I>& dest) { dest.get_value().emplace(); read_bytes(source, *dest.get_value()); @@ -322,7 +322,7 @@ namespace wire_read inline void expand_field_map(std::size_t index, wire::reader::key_map (&map)[N], const T& head, const U&... tail) { map[index].name = head.name; - map[index].id = 0; + map[index].id = head.id(); expand_field_map(index + 1, map, tail...); } diff --git a/src/wire/write.h b/src/wire/write.h index c89afaa..ca1bbed 100644 --- a/src/wire/write.h +++ b/src/wire/write.h @@ -1,4 +1,4 @@ -// Copyright (c) 2020, The Monero Project +// Copyright (c) 2020-2023, The Monero Project // All rights reserved. // // Redistribution and use in source and binary forms, with or without modification, are @@ -142,13 +142,18 @@ namespace wire_write declared after these functions. */ template<typename W, typename T> - inline epee::byte_slice to_bytes(const T& value) + inline epee::byte_slice to_bytes(W&& dest, const T& value) { - W dest{}; write_bytes(dest, value); return dest.take_bytes(); } + template<typename W, typename T> + inline epee::byte_slice to_bytes(const T& value) + { + return wire_write::to_bytes(W{}, value); + } + template<typename W, typename T, typename F = wire::identity_> inline void array(W& dest, const T& source, const std::size_t count, F filter = F{}) { @@ -162,20 +167,20 @@ namespace wire_write dest.end_array(); } - template<typename W, typename T> - inline bool field(W& dest, const wire::field_<T, true> elem) + template<typename W, typename T, unsigned I> + inline bool field(W& dest, const wire::field_<T, true, I> elem) { - dest.key(0, elem.name); + dest.key(I, elem.name); write_bytes(dest, elem.get_value()); return true; } - template<typename W, typename T> - inline bool field(W& dest, const wire::field_<T, false> elem) + template<typename W, typename T, unsigned I> + inline bool field(W& dest, const wire::field_<T, false, I> elem) { if (bool(elem.get_value())) { - dest.key(0, elem.name); + dest.key(I, elem.name); write_bytes(dest, *elem.get_value()); } return true; diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index 3412c12..b5b5997 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -33,5 +33,5 @@ target_link_libraries(monero-lws-unit-framework) add_subdirectory(wire) add_executable(monero-lws-unit main.cpp) -target_link_libraries(monero-lws-unit monero-lws-unit-framework monero-lws-unit-wire monero-lws-unit-wire-json) +target_link_libraries(monero-lws-unit monero-lws-unit-framework monero-lws-unit-wire monero-lws-unit-wire-json monero-lws-unit-wire-msgpack) add_test(NAME monero-lws-unit COMMAND monero-lws-unit -v) diff --git a/tests/unit/wire/CMakeLists.txt b/tests/unit/wire/CMakeLists.txt index f04d781..9d2b11c 100644 --- a/tests/unit/wire/CMakeLists.txt +++ b/tests/unit/wire/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright (c) 2022, The Monero Project +# Copyright (c) 2022-2023, The Monero Project # # All rights reserved. # @@ -28,6 +28,7 @@ add_subdirectory(json) +add_subdirectory(msgpack) add_library(monero-lws-unit-wire OBJECT read.write.test.cpp read.test.cpp) target_link_libraries( diff --git a/tests/unit/wire/msgpack/CMakeLists.txt b/tests/unit/wire/msgpack/CMakeLists.txt new file mode 100644 index 0000000..90066e4 --- /dev/null +++ b/tests/unit/wire/msgpack/CMakeLists.txt @@ -0,0 +1,37 @@ +# Copyright (c) 2023, The Monero Project +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, are +# permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list of +# conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this list +# of conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may be +# used to endorse or promote products derived from this software without specific +# prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +add_library(monero-lws-unit-wire-msgpack OBJECT read.write.test.cpp) +target_link_libraries( + monero-lws-unit-wire-msgpack + monero-lws-unit-framework + monero-lws-wire-msgpack + monero::libraries +) + diff --git a/tests/unit/wire/msgpack/read.write.test.cpp b/tests/unit/wire/msgpack/read.write.test.cpp new file mode 100644 index 0000000..50bdb2b --- /dev/null +++ b/tests/unit/wire/msgpack/read.write.test.cpp @@ -0,0 +1,183 @@ +// Copyright (c) 2023, The Monero Project +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#include "framework.test.h" + +#include <boost/core/demangle.hpp> +#include <boost/range/algorithm/equal.hpp> +#include <cstdint> +#include <type_traits> +#include "wire/traits.h" +#include "wire/msgpack.h" +#include "wire/vector.h" + +#include "wire/base.test.h" + +namespace +{ + constexpr const char basic_string[] = u8"my_string_data"; + //constexpr const char basic_[] = + // u8"{\"utf8\":\"my_string_data\",\"vec\":[0,127],\"data\":\"00ff2211\",\"choice\":true}"; + constexpr const std::uint8_t basic_msgpack[] = { + 0x84, 0xa4, 0x75, 0x74, 0x66, 0x38, 0xae, 0x6d, 0x79, 0x5f, 0x73, 0x74, + 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x61, 0x74, 0x61, 0xa3, 0x76, 0x65, + 0x63, 0x92, 0x00, 0x7f, 0xa4, 0x64, 0x61, 0x74, 0x61, 0xc4, 0x04, 0x00, + 0xff, 0x22, 0x11, 0xa6, 0x63, 0x68, 0x6f, 0x69, 0x63, 0x65, 0xc3 + }; + constexpr const std::uint8_t advanced_msgpack[] = { + 0x84, 0x00, 0xae, 0x6d, 0x79, 0x5f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, + 0x5f, 0x64, 0x61, 0x74, 0x61, 0x01, 0x92, 0x00, 0x7f, 0x02, 0xc4, 0x04, + 0x00, 0xff, 0x22, 0x11, 0xcc, 0xfe, 0xc3 + }; + + template<typename T> + struct basic_object + { + std::string utf8; + std::vector<T> vec; + lws_test::small_blob data; + bool choice; + }; + + template<typename F, typename T> + void basic_object_map(F& format, T& self) + { + wire::object(format, + WIRE_FIELD_ID(0, utf8), + WIRE_FIELD_ID(1, vec), + WIRE_FIELD_ID(2, data), + WIRE_FIELD_ID(254, choice) + ); + } + + template<typename T> + void read_bytes(wire::msgpack_reader& source, basic_object<T>& dest) + { basic_object_map(source, dest); } + + template<typename T> + void write_bytes(wire::msgpack_writer& dest, const basic_object<T>& source) + { basic_object_map(dest, source); } + + template<typename T> + void test_basic_reading(lest::env& lest_env) + { + SETUP("Basic (string keys) with " + boost::core::demangle(typeid(T).name()) + " integers") + { + const auto result = + wire::msgpack::from_bytes<basic_object<T>>(epee::byte_slice{{basic_msgpack}}); + EXPECT(result); + EXPECT(result->utf8 == basic_string); + { + const std::vector<T> expected{0, 127}; + EXPECT(result->vec == expected); + } + EXPECT(result->data == lws_test::blob_test1); + EXPECT(result->choice); + } + } + + template<typename T> + void test_advanced_reading(lest::env& lest_env) + { + SETUP("Advanced (integer keys) with " + boost::core::demangle(typeid(T).name()) + " integers") + { + const auto result = + wire::msgpack::from_bytes<basic_object<T>>(epee::byte_slice{{advanced_msgpack}}); + EXPECT(result); + EXPECT(result->utf8 == basic_string); + { + const std::vector<T> expected{0, 127}; + EXPECT(result->vec == expected); + } + EXPECT(result->data == lws_test::blob_test1); + EXPECT(result->choice); + } + } + + template<typename T> + void test_basic_writing(lest::env& lest_env) + { + SETUP("Basic (string keys) with " + boost::core::demangle(typeid(T).name()) + " integers") + { + const basic_object<T> val{basic_string, std::vector<T>{0, 127}, lws_test::blob_test1, true}; + const auto result = wire::msgpack::to_bytes(val); + EXPECT(boost::range::equal(result, epee::byte_slice{{basic_msgpack}})); + } + } + + template<typename T> + void test_advanced_writing(lest::env& lest_env) + { + SETUP("Advanced (integer keys) with " + boost::core::demangle(typeid(T).name()) + " integers") + { + const basic_object<T> val{basic_string, std::vector<T>{0, 127}, lws_test::blob_test1, true}; + const auto result = wire_write::to_bytes(wire::msgpack_slice_writer{true}, val); + EXPECT(boost::range::equal(result, epee::byte_slice{{advanced_msgpack}})); + } + } +} + +LWS_CASE("wire::msgpack_reader") +{ + test_basic_reading<std::int16_t>(lest_env); + test_basic_reading<std::int32_t>(lest_env); + test_basic_reading<std::int64_t>(lest_env); + test_basic_reading<std::intmax_t>(lest_env); + test_basic_reading<std::uint16_t>(lest_env); + test_basic_reading<std::uint32_t>(lest_env); + test_basic_reading<std::uint64_t>(lest_env); + test_basic_reading<std::uintmax_t>(lest_env); + + test_advanced_reading<std::int16_t>(lest_env); + test_advanced_reading<std::int32_t>(lest_env); + test_advanced_reading<std::int64_t>(lest_env); + test_advanced_reading<std::intmax_t>(lest_env); + test_advanced_reading<std::uint16_t>(lest_env); + test_advanced_reading<std::uint32_t>(lest_env); + test_advanced_reading<std::uint64_t>(lest_env); + test_advanced_reading<std::uintmax_t>(lest_env); +} + +LWS_CASE("wire::msgpack_writer") +{ + test_basic_writing<std::int16_t>(lest_env); + test_basic_writing<std::int32_t>(lest_env); + test_basic_writing<std::int64_t>(lest_env); + test_basic_writing<std::intmax_t>(lest_env); + test_basic_writing<std::uint16_t>(lest_env); + test_basic_writing<std::uint32_t>(lest_env); + test_basic_writing<std::uint64_t>(lest_env); + test_basic_writing<std::uintmax_t>(lest_env); + + test_advanced_writing<std::int16_t>(lest_env); + test_advanced_writing<std::int32_t>(lest_env); + test_advanced_writing<std::int64_t>(lest_env); + test_advanced_writing<std::intmax_t>(lest_env); + test_advanced_writing<std::uint16_t>(lest_env); + test_advanced_writing<std::uint32_t>(lest_env); + test_advanced_writing<std::uint64_t>(lest_env); + test_advanced_writing<std::uintmax_t>(lest_env); +} diff --git a/tests/unit/wire/read.write.test.cpp b/tests/unit/wire/read.write.test.cpp index 598b0bd..d194133 100644 --- a/tests/unit/wire/read.write.test.cpp +++ b/tests/unit/wire/read.write.test.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2022, The Monero Project +// Copyright (c) 2022-2023, The Monero Project // All rights reserved. // // Redistribution and use in source and binary forms, with or without modification, are @@ -34,6 +34,7 @@ #include <vector> #include "wire.h" #include "wire/json.h" +#include "wire/msgpack.h" #include "wire/vector.h" #include "wire/base.test.h" @@ -93,7 +94,7 @@ namespace void fill(complex& self) { self.objects = std::vector<inner>{inner{0, limit<std::uint32_t>::max()}, inner{100, 200}, inner{44444, 83434}}; - self.ints = std::vector<std::int16_t>{limit<std::int16_t>::min(), limit<std::int16_t>::max(), 0, 31234}; + self.ints = std::vector<std::int16_t>{limit<std::int16_t>::min(), limit<std::int16_t>::max(), -3, 31234}; self.uints = std::vector<std::uint64_t>{0, limit<std::uint64_t>::max(), 34234234, 33}; self.blobs = {lws_test::blob_test1, lws_test::blob_test2, lws_test::blob_test3}; self.strings = {"string1", "string2", "string3", "string4"}; @@ -113,7 +114,7 @@ namespace EXPECT(self.ints.size() == 4); EXPECT(self.ints.at(0) == limit<std::int16_t>::min()); EXPECT(self.ints.at(1) == limit<std::int16_t>::max()); - EXPECT(self.ints.at(2) == 0); + EXPECT(self.ints.at(2) == -3); EXPECT(self.ints.at(3) == 31234); EXPECT(self.uints.size() == 4); @@ -136,7 +137,7 @@ namespace EXPECT(self.choice == true); } - template<typename T> + template<typename T, typename U> void run_complex(lest::env& lest_env) { SETUP("Complex test for " + boost::core::demangle(typeid(T).name())) @@ -148,7 +149,7 @@ namespace const expect<epee::byte_slice> bytes = T::to_bytes(base); EXPECT(bytes); - const expect<complex> derived = T::template from_bytes<complex>(std::string{bytes->begin(), bytes->end()}); + const expect<complex> derived = T::template from_bytes<complex>(U{std::string{bytes->begin(), bytes->end()}}); EXPECT(derived); verify_initial(lest_env, *derived); } @@ -159,7 +160,7 @@ namespace const expect<epee::byte_slice> bytes = T::to_bytes(base); EXPECT(bytes); - const expect<complex> derived = T::template from_bytes<complex>(std::string{bytes->begin(), bytes->end()}); + const expect<complex> derived = T::template from_bytes<complex>(U{std::string{bytes->begin(), bytes->end()}}); EXPECT(derived); verify_filled(lest_env, *derived); } @@ -180,7 +181,7 @@ namespace WIRE_DEFINE_OBJECT(big, big_map) WIRE_DEFINE_OBJECT(small, small_map) - template<typename T> + template<typename T, typename U> expect<small> round_trip(lest::env& lest_env, std::int64_t value) { expect<small> out = small{0}; @@ -188,43 +189,77 @@ namespace { const expect<epee::byte_slice> bytes = T::template to_bytes(big{value}); EXPECT(bytes); - out = T::template from_bytes<small>(std::string{bytes->begin(), bytes->end()}); + out = T::template from_bytes<small>(U{std::string{bytes->begin(), bytes->end()}}); } return out; } - template<typename T> + template<typename T, typename U> void not_overflow(lest::env& lest_env, std::int64_t value) { - const expect<small> result = round_trip<T>(lest_env, value); + const expect<small> result = round_trip<T, U>(lest_env, value); EXPECT(result); EXPECT(result->value == value); } - template<typename T> + template<typename T, typename U> void overflow(lest::env& lest_env, std::int64_t value, const std::error_code error) { - const expect<small> result = round_trip<T>(lest_env, value); + const expect<small> result = round_trip<T, U>(lest_env, value); EXPECT(result == error); } - template<typename T> + template<typename T, typename U> void run_overflow(lest::env& lest_env) { SETUP("Overflow test for " + boost::core::demangle(typeid(T).name())) { - not_overflow<T>(lest_env, limit<std::int32_t>::min()); - not_overflow<T>(lest_env, 0); - not_overflow<T>(lest_env, limit<std::int32_t>::max()); + not_overflow<T, U>(lest_env, limit<std::int32_t>::min()); + not_overflow<T, U>(lest_env, 0); + not_overflow<T, U>(lest_env, limit<std::int32_t>::max()); - overflow<T>(lest_env, std::int64_t(limit<std::int32_t>::min()) - 1, wire::error::schema::larger_integer); - overflow<T>(lest_env, std::int64_t(limit<std::int32_t>::max()) + 1, wire::error::schema::smaller_integer); + overflow<T, U>(lest_env, std::int64_t(limit<std::int32_t>::min()) - 1, wire::error::schema::larger_integer); + overflow<T, U>(lest_env, std::int64_t(limit<std::int32_t>::max()) + 1, wire::error::schema::smaller_integer); } } + + struct simple { bool choice; }; + static void read_bytes(wire::reader& source, simple& self) + { + wire::object(source, WIRE_FIELD(choice)); + } + + template<typename T, typename U> + void run_skip(lest::env& lest_env) + { + complex base{}; + verify_initial(lest_env, base); + fill(base); + + const expect<epee::byte_slice> bytes = T::to_bytes(base); + EXPECT(bytes); + + const expect<simple> derived = T::template from_bytes<simple>(U{std::string{bytes->begin(), bytes->end()}}); + EXPECT(derived); + EXPECT(derived->choice); + } } -LWS_CASE("wire::reader and wire::writer") +LWS_CASE("wire::reader and wire::writer complex") { - run_complex<wire::json>(lest_env); - run_overflow<wire::json>(lest_env); + run_complex<wire::json, std::string>(lest_env); + run_complex<wire::msgpack, epee::byte_slice>(lest_env); } + +LWS_CASE("wire::reader and wire::writer overflow") +{ + run_overflow<wire::json, std::string>(lest_env); + run_overflow<wire::msgpack, epee::byte_slice>(lest_env); +} + +LWS_CASE("wire::reader and wire::writer skip") +{ + run_skip<wire::json, std::string>(lest_env); + run_skip<wire::msgpack, epee::byte_slice>(lest_env); +} +