diff --git a/.github/workflows/build_run_unit_test_cmake.yml b/.github/workflows/build_run_unit_test_cmake.yml index 22abd42..1f6764d 100644 --- a/.github/workflows/build_run_unit_test_cmake.yml +++ b/.github/workflows/build_run_unit_test_cmake.yml @@ -12,8 +12,8 @@ jobs: build_matrix: strategy: matrix: - # os: [ubuntu-latest, windows-latest, macos-14] - os: [ubuntu-latest] + os: [ubuntu-latest, windows-latest, macos-14] + # os: [ubuntu-latest] runs-on: ${{ matrix.os }} defaults: run: diff --git a/README.md b/README.md index d4cf757..5f6d510 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Binary Serialize, Header-Only C++ 20 Binary Serialization Classes and Functions +# Binary Serialize, Header-Only C++ 20 Binary Serialization Classes and Functions Using a `std::format` Like Syntax #### Unit Test and Documentation Generation Workflow Status @@ -10,11 +10,52 @@ ![GH Tag](https://img.shields.io/github/v/tag/connectivecpp/binary-serialize?label=GH%20tag) +![License](https://img.shields.io/badge/License-Boost%201.0-blue) + ## Overview -The `binary_serialize` functions and classes provide serializing and unserializing of binary data. Serialization provides a way to transform application objects into and out of byte streams that can be sent over a network (or used for file IO). +The `binary_serialize` functions and classes provide serializing and unserializing of binary data. Serialization provides a way to transform application objects into and out of byte streams that can be sent over a network (or used for file IO). Many serialization libraries transform objects to and from text representations, but this library keeps data in binary formats. + +The serialization functionality in this repository is useful when explicit control is needed for every bit and byte. This allows a developer to match an existing wire protocol or encoding scheme or to define his or her own wire protocol. Support is provided for fundamental arithmetic types as well as many C++ vocabulary types such as `std::optional`. Both big and little endian support is provided. + +Full flexibility is provided for sequences such as `std::string` or `std::vector`. The number of elements can be specified to take 8 or 16 or 32 (etc) bits, followed by the sequence of chars or array elements. Similar flexibility is provided for vocabulary types such as `std::optional`, where the boolean flag can be specified as 8 or 16 or 32 (etc) bits, followed by the object (or none, if there is no value in the optional). + +This library uses `std::format` style formatting. For example: + +``` + struct Hike { + unsigned int distance; + int elevation; + std::optional> name; + std::vector waypoints; + }; + // ... + chops::expandable_buffer buf; + + chops::binary_serialize(buf, "{32u}{16}{8u}{16u}{16u}{64}", + hike_obj.distance, hike_obj.elevation, hike_obj.name, hike_obj.waypoints); + + // ... + net_obj.send(buf.get_buf()); + +``` + +The buffer will contain the following (note that truncation or casting will happen between the application +object types and the serialized types as needed): + +``` + 32 bit unsigned integer containing distance value + 16 bit signed integer containing elevation value + 8 bit unsigned integer corresponding to true or false for the optional + 16 bit unsigned integer for the size of the name string (if optional is true) + 0 - N 8 bit characters for the name string (if optional is true) + 16 bit unsigned integer for the size of the waypoints vector + 0 - N 64 bit signed integers for each waypoint value +``` + +The documentation overview provides a comparison with other serialization libraries as well as a rationale for the design decisions. -The serialization functionality in this repository is useful when explicit control is needed for every bit and byte. This allows a developer to match an existing wire protocol or encoding scheme or to define his or her own wire protocol. Support is provided for fundamental arithmetic types as well as certain C++ vocabulary types such as `std::optional`. Both big and little endian support is provided. +Inspiration and thanks go to [Louis Langholtz](https://github.com/louis-langholtz), who steered me towards considering the `std::format` API. ## Generated Documentation @@ -38,7 +79,7 @@ The unit test code uses [Catch2](https://github.com/catchorg/Catch2). If the `BI The unit test uses utilities from Connective C++'s [utility-rack](https://github.com/connectivecpp/utility-rack). -Specific version (or branch) specs for the dependenies are in `test/CMakeLists.txt`. +Specific version (or branch) specs for the dependencies are in the [test/CMakeLists.txt](test/CMakeLists.txt) file, look for the `CPMAddPackage` commands. ## Build and Run Unit Tests diff --git a/include/serialize/binary_serialize.hpp b/include/serialize/binary_serialize.hpp index 6b2c364..968bf0f 100644 --- a/include/serialize/binary_serialize.hpp +++ b/include/serialize/binary_serialize.hpp @@ -1,124 +1,123 @@ -/** @file - * - * @defgroup marshall_module Classes and functions for big-endian binary data - * marshalling and unmarshalling (transform objects into and out of byte streams - * for transmission over a network or for file IO). - * - * @brief Classes and functions to transform objects into a big-endian binary stream - * of bytes (marshall) and the converse (unmarshall), transform a stream of bytes into - * objects. - * - * The @c utility-rack @c marshall and @c unmarshall functions and classes provide a - * simple and light abstraction for binary big-endian serialization. There are no - * message or element definitions, no embedded preprocesser syntax, and no extra - * build steps. - * - * These facilities are useful when explicit control of every bit and byte is needed - * (and the wire protocol format is big-endian). Other marshalling and serialization - * designs have strengths and weaknesses (see higher level documentation for more - * explanation). - * - * @note The design of the binary marshall and unmarshall functions is a good fit - * for a C++ metaprogamming implementation (using variadic templates). In particular, - * the primary design concept is a mapping of two (and sometimes three) types to a - * single value. A typelist would allow a single function (or method) call to operate - * on multiple values, instead of being forced to call the @c marshall or @c unmarshall - * function once for each value (or sequence). However, the first release uses the - * simpler (no metaprogramming, no variadic templates) implementation with a hope that - * a more sophisticated version will be available in the future. - * - * The marshalling classes and functions are designed for networking (or file I/O), - * where binary data marshalling and unmarshalling is needed to send and receive - * messages (or to write or read defined portions of a file). Application code using - * this library has full control of every byte that is sent or received. Application - * objects are transformed into a @c std::byte buffer (and the converse) keeping a - * binary representation in network (big-endian) order. - * - * For example, a 32-bit binary number (either a signed or unsigned integer) in native - * endian order will be transformed into four 8-bit bytes in network (big) endian order - * for sending over a network (or for file I/O). Conversely, the four 8-bit bytes in - * network endian order will be transformed back into the original 32-bit binary number - * when received (or read as file I/O). A @c bool can be transformed into either a 8-bit, - * 16-bit, 32-bit, or 64-bit number of either 1 or 0 (and back). A sequence - * (@c std::vector or array or other container) can be transformed into a count (8-bit, - * 16-bit, et al) followed by each element of the sequence. A @c std::optional can be - * transformed into a @c bool (8-bit, 16-bit, et al) followed by the value (if present). - * - * No support is directly provided for higher level abstractions such as inheritance - * hierarchies, version numbers, type flags, or object relations. Pointers are also not - * directly supported (which would typically be part of an object relation). No specific - * wire protocol or data encoding is specified (other than big-endian). These higher - * level abstractions as well as "saving and later restoring a full application state" - * are better served by a library such as Boost Serialization or Google Protocol - * Buffers or Cap'n Proto. - * - * There is not any automatic generation of message processing code (e.g. Google - * Protocol Buffers, a language neutral message definition process that generates - * marshalling and unmarshalling code). Future C++ standards supporting reflection - * may allow higher abstractions and more automation of marshalling code, but this - * library provides a modern C++ API (post C++ 11) for direct control of the - * byte buffers. In particular, all of the build process complications required for - * code generation are not present in this (header only) library. - * - * Wire protocols that are in full text mode do not need to deal with binary endian - * swapping. However, sending or receiving data in a binary form is often desired - * for size efficiency (e.g. sending images and video, large data sets, or where - * the message size needs to be as small as possible). - * - * Functionality is provided for fundamental types, including @c bool, as well as vocabulary - * types such as @c std::string and @c std::optional. Support is also provided for sequences, - * where the number of elements is placed before the element sequence in the stream of - * bytes. - * - * Application defined types can be associated with a @c marshall and @c unmarshall - * function overload, providing a convenient way to reuse the same lower-level - * marshalling code. Specifically, a type @c MyType can be used in a sequence or in - * a @c std::optional or as part of a higher level @c struct or @c class type without needing - * to duplicate the marshalling calls within the @c MyType @c marshall and @c unmarshall - * functions. +/** @mainpage Binary Serialuze, Classes and Functions For Binary Data Serialization + * + * ## Overview + * + * Serialization transforms objects into a byte stream for transmission over a + * network or for file IO. Deserialization is the converse, transforming a byte + * stream into application level objects. + * + * This library differs from other binary serialization libraries in that the + * main interfaces is a "std::format" like interface. + * + * These functions and classes provide a simple and light abstraction for binary big-endian serialization. There are no + * message or element definitions, no embedded preprocesser syntax, and no extra + * build steps. + * + * These facilities are useful when explicit control of every bit and byte is needed + * (and the wire protocol format is big-endian). Other marshalling and serialization + * designs have strengths and weaknesses (see higher level documentation for more + * explanation). + * + * @note The design of the binary marshall and unmarshall functions is a good fit + * for a C++ metaprogamming implementation (using variadic templates). In particular, + * the primary design concept is a mapping of two (and sometimes three) types to a + * single value. A typelist would allow a single function (or method) call to operate + * on multiple values, instead of being forced to call the @c marshall or @c unmarshall + * function once for each value (or sequence). However, the first release uses the + * simpler (no metaprogramming, no variadic templates) implementation with a hope that + * a more sophisticated version will be available in the future. + * + * The marshalling classes and functions are designed for networking (or file I/O), + * where binary data marshalling and unmarshalling is needed to send and receive + * messages (or to write or read defined portions of a file). Application code using + * this library has full control of every byte that is sent or received. Application + * objects are transformed into a @c std::byte buffer (and the converse) keeping a + * binary representation in network (big-endian) order. + * + * For example, a 32-bit binary number (either a signed or unsigned integer) in native + * endian order will be transformed into four 8-bit bytes in network (big) endian order + * for sending over a network (or for file I/O). Conversely, the four 8-bit bytes in + * network endian order will be transformed back into the original 32-bit binary number + * when received (or read as file I/O). A @c bool can be transformed into either a 8-bit, + * 16-bit, 32-bit, or 64-bit number of either 1 or 0 (and back). A sequence + * (@c std::vector or array or other container) can be transformed into a count (8-bit, + * 16-bit, et al) followed by each element of the sequence. A @c std::optional can be + * transformed into a @c bool (8-bit, 16-bit, et al) followed by the value (if present). + * + * No support is directly provided for higher level abstractions such as inheritance + * hierarchies, version numbers, type flags, or object relations. Pointers are also not + * directly supported (which would typically be part of an object relation). No specific + * wire protocol or data encoding is specified (other than big-endian). These higher + * level abstractions as well as "saving and later restoring a full application state" + * are better served by a library such as Boost Serialization or Google Protocol + * Buffers or Cap'n Proto. + * + * There is not any automatic generation of message processing code (e.g. Google + * Protocol Buffers, a language neutral message definition process that generates + * marshalling and unmarshalling code). Future C++ standards supporting reflection + * may allow higher abstractions and more automation of marshalling code, but this + * library provides a modern C++ API (post C++ 11) for direct control of the + * byte buffers. In particular, all of the build process complications required for + * code generation are not present in this (header only) library. + * + * Wire protocols that are in full text mode do not need to deal with binary endian + * swapping. However, sending or receiving data in a binary form is often desired + * for size efficiency (e.g. sending images and video, large data sets, or where + * the message size needs to be as small as possible). + * + * Functionality is provided for fundamental types, including @c bool, as well as vocabulary + * types such as @c std::string and @c std::optional. Support is also provided for sequences, + * where the number of elements is placed before the element sequence in the stream of + * bytes. + * + * Application defined types can be associated with a @c marshall and @c unmarshall + * function overload, providing a convenient way to reuse the same lower-level + * marshalling code. Specifically, a type @c MyType can be used in a sequence or in + * a @c std::optional or as part of a higher level @c struct or @c class type without needing + * to duplicate the marshalling calls within the @c MyType @c marshall and @c unmarshall + * functions. * - * @c std::variant and @c std::any are not directly supported and require value extraction - * by the application. (Supporting @c std::variant or @c std::any might be a future - * enhancement if a good design is proposed.) @c std::wstring and other non-char strings are - * also not directly supported, and require additional calls from the application. - * - * Central to the design of these marshalling and unmarshalling functions is a mapping of - * two types to a single value. For marshalling, the two types are the native type (e.g. - * @c int, @c short, @c bool), and the type to be used for the marshalling, typically - * a fixed width integer type, as specified in the @c header (e.g. - * @c std::uint32_t, @c std::int16_t, @c std::int8_t). For unmarshalling, the same - * concept is used, a fixed width integer type that specifies the size in the byte - * buffer, and the native type, thus the application would specify that a @c std::int16_t - * in the byte buffer will be unmarshalled into an application @c int value. - * - * @note No support is provided for little-endian in the byte buffer. No support is provided - * for mixed endian (big-endian with little-endian) or where the endianness is specified as a - * type parameter. No support is provided for "in-place" swapping of values. All of these - * use cases can be implemented using other libraries such as Boost Endian. - * - * @note Performance considerations - for marshalling, iterative resizing of the output - * buffer is a fundamental operation. @c std::vector and @c mutable_shared_buffer - * @c resize methods use efficient logic for internal buffer allocations (@c mutable_shared_buffer - * uses @c std::vector internally). Custom containers used as the buffer parameter should - * have similar efficient @c resize method logic. Calling @c reserve at appropriate places may - * provide a small performance increase, at the cost of additional requirements on the buffer - * type. - * - * @author Cliff Green - * - * Copyright (c) 2019 by Cliff Green - * - * Distributed under the Boost Software License, Version 1.0. - * (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + * @c std::variant and @c std::any are not directly supported and require value extraction + * by the application. (Supporting @c std::variant or @c std::any might be a future + * enhancement if a good design is proposed.) @c std::wstring and other non-char strings are + * also not directly supported, and require additional calls from the application. + * + * Central to the design of these marshalling and unmarshalling functions is a mapping of + * two types to a single value. For marshalling, the two types are the native type (e.g. + * @c int, @c short, @c bool), and the type to be used for the marshalling, typically + * a fixed width integer type, as specified in the @c header (e.g. + * @c std::uint32_t, @c std::int16_t, @c std::int8_t). For unmarshalling, the same + * concept is used, a fixed width integer type that specifies the size in the byte + * buffer, and the native type, thus the application would specify that a @c std::int16_t + * in the byte buffer will be unmarshalled into an application @c int value. + * + * @note No support is provided for little-endian in the byte buffer. No support is provided + * for mixed endian (big-endian with little-endian) or where the endianness is specified as a + * type parameter. No support is provided for "in-place" swapping of values. All of these + * use cases can be implemented using other libraries such as Boost Endian. + * + * @note Performance considerations - for marshalling, iterative resizing of the output + * buffer is a fundamental operation. @c std::vector and @c mutable_shared_buffer + * @c resize methods use efficient logic for internal buffer allocations (@c mutable_shared_buffer + * uses @c std::vector internally). Custom containers used as the buffer parameter should + * have similar efficient @c resize method logic. Calling @c reserve at appropriate places may + * provide a small performance increase, at the cost of additional requirements on the buffer + * type. + * + * @author Cliff Green + * + * @copyright (c) 2019-2024 by Cliff Green + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt) * */ -#ifndef MARSHALL_HPP_INCLUDED -#define MARSHALL_HPP_INCLUDED +#ifndef BINARY_SERIALIZE_HPP_INCLUDED +#define BINARY_SERIALIZE_HPP_INCLUDED -#include "utility/cast_ptr_to.hpp" -#include "marshall/shared_buffer.hpp" -#include "marshall/extract_append.hpp" +#include "buffer/shared_buffer.hpp" +#include "serialize/extract_append.hpp" #include // std::byte, std::size_t, std::nullptr_t #include // std::uint32_t, etc @@ -130,12 +129,44 @@ #include // value type of iterator #include #include +#include #include // debugging namespace chops { +template +concept supports_expandable_buffer = + std::same_as && + requires (Ctr ctr) { + { ctr.size() } -> std::integral; + ctr.resize(std::size_t{}); + { ctr.data() } -> std::same_as; + } + +template +concept supports_endian_expandable_buffer = + supports_expandable_buffer && + requires (Ctr ctr) { + typename Ctr::endian_type; + } + +template +class expandable_buffer { +private: + Ctr m_ctr; +public: + using endian_type = Endian; + using value_type = std::byte; + Ctr& get_buf() noexcept { return m_ctr; } + std::size_t size() noexcept { return m_ctr.size(); } + std::byte* data() noexcept { return m_ctr.data(); } + void resize(std::size_t sz) noexcept m_ctr.resize(sz); } +}; + /** + * @brief Extract a sequence in network byte order from a @c std::byte buffer into the * provided container. * @@ -191,6 +222,8 @@ Container extract_sequence(const std::byte* buf) noexcept(fill in) { * the elements in the sequence. * */ + + /* template std::size_t append_sequence(std::byte* buf, Cnt cnt, Iter start, Iter end) noexcept { @@ -269,18 +302,55 @@ class fixed_size_byte_array { // similar can be used for the Buf template parameter struct adl_tag { }; -// lower-level function template that performs the actual buffer manipulation and -// marshalling of a single value, with an ADL tag for full namespace inclusion in the -// overload set; this function template is not called directly by application code, and -// is only used with arithmetic values or a std::byte -template -Buf& marshall(Buf& buf, const T& val, adl_tag) { + +namespace detail { + +template +constexpr Buf& serialize_val(Buf& buf, const T& val) { auto old_sz = buf.size(); - buf.resize(old_sz + sizeof(CastValType)); - append_val(buf.data()+old_sz, static_cast(val)); + buf.resize(old_sz + sizeof(CastTypeVal)); + append_val(buf.data()+old_sz, static_cast(val)); return buf; } +template +constexpr Buf& serialize(Buf& buf, const T& val) { + return serialize_val (buf, val); +} + +template +constexpr Buf& serialize(Buf& buf, const T* seq, std::size_t sz) { + serialize_val (buf, sz); + for (int i {0}; i < sz; ++i) { + serialize_val(buf, seq[i]); + } + return buf; +} + +template +constexpr Buf& serialize(Buf& buf, const std::string& str) { + return serialize (buf, str.data(), std.size()); +} + +template +constexpr Buf& serialize(Buf& buf, const std::optional& val) { + if (val) + serialize_val (buf, 1); + return serialize_val (buf, *val); + } + serialize_val (buf, 0); +} + +} + + /** * @brief Marshall a single arithmetic value or a @c std::byte into a buffer of bytes. * diff --git a/include/serialize/byteswap.hpp b/include/serialize/byteswap.hpp new file mode 100644 index 0000000..797f9c7 --- /dev/null +++ b/include/serialize/byteswap.hpp @@ -0,0 +1,55 @@ +/** @file + * + * @brief This is an implementation of the C++ 23 @c std::byteswap function, + * for use in pre C++ 23 applications. + * + * This implementation is taken directly from the CPP Reference page: + * https://en.cppreference.com/w/cpp/numeric/byteswap + * + * @author Cliff Green, CPP Reference + * + * @copyright (c) 2024 by Cliff Green + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + * + */ + +#ifndef BYTESWAP_HPP_INCLUDED +#define BYTESWAP_HPP_INCLUDED + +#include // std::integral concept +#include // std::bit_cast +#include +#include // std::byte +#include // std::ranges::reverse + +namespace chops { + +/** + * @brief Perform an in-place byte swap on an integral type (if size of the + * type is greater than one). + * + * @tparam T Type of value where swapping will occur. + * + * @param value Integral value to be byte swapped. + * + * @pre There must not be any padding bits in the type. + * + */ +template +constexpr T byteswap(T value) noexcept { + if constexpr (sizeof(T) == 1u) { + return value; + } + static_assert(std::has_unique_object_representations_v, + "T may not have padding bits"); + auto value_representation = std::bit_cast>(value); + std::ranges::reverse(value_representation); + return std::bit_cast(value_representation); +} + +} // end namespace + +#endif + diff --git a/include/serialize/extract_append.hpp b/include/serialize/extract_append.hpp index 490fbb4..de22452 100644 --- a/include/serialize/extract_append.hpp +++ b/include/serialize/extract_append.hpp @@ -1,305 +1,49 @@ /** @file * - * @brief Functions to extract arithmetic binary values from a byte buffer (in big-endian) to - * native format; conversely, given an arithmetic binary value, append it to a buffer of bytes - * in network endian order (big-endian). + * @brief Functions to extract arithmetic binary values from a byte buffer (in either + * endian order) to native format; conversely, given an arithmetic binary value, append + * it to a buffer of bytes in the specified endian order. * - * The functions in this file are low-level, handling fundamental arithmetic types and - * extracting or appending to @c std::byte buffers. It is meant to be the lower layer - * of marshalling utilities, where the next layer up provides buffer management, - * sequences, and overloads for specific types such as @c std::string, @c bool, and - * @c std::optional. + * The functions in this file are low-level. They handle fundamental arithmetic types and + * extracting or appending to @c std::byte buffers. It is meant to be the lower layer + * of serializing utilities, where the next higher layer provides buffer management, + * sequences, and overloads for specific types such as @c std::string, @c bool, and + * @c std::optional. * - * @note When C++ 20 @c std::endian is available, many of these functions can be made - * @c constexpr and evaluated at compile time. Until then, run-time endian detection and - * copying is performed. + * @note The variable sized integer functions (@c extract_var_int, @c append_var_int) + * support the variable byte integer type in MQTT (Message Queuing Telemetry Transport), + * a commonly used IoT protocol. The code in this header is adapted from a + * Techoverflow.net article by Uli Koehler and published under the CC0 1.0 Universal + * license: + * https://techoverflow.net/2013/01/25/efficiently-encoding-variable-length-integers-in-cc/ * - * @note The variable sized integer functions (@c extract_var_int, @c append_var_int) - * support the variable byte integer type in MQTT (Message Queuing Telemetry Transport), - * a commonly used IoT protocol. The code in this header is adapted from a - * Techoverflow.net article by Uli Koehler and published under the CC0 1.0 Universal - * license: - * https://techoverflow.net/2013/01/25/efficiently-encoding-variable-length-integers-in-cc/ + * @author Cliff Green, Roxanne Agerone, Uli Koehler * - * @note This implementation has manual generated unrolled loops for the byte moving and - * swapping. This can be improved in the future by using a compile-time unrolling utility, such - * as the @c repeat function (compile time unrolling version) by Vittorio Romeo. + * @copyright (c) 2019-2024 by Cliff Green, Roxanne Agerone * - * @author Cliff Green, Roxanne Agerone, Uli Koehler - * - * Copyright (c) 2019 by Cliff Green, Roxanne Agerone - * - * Distributed under the Boost Software License, Version 1.0. - * (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt) * */ #ifndef EXTRACT_APPEND_HPP_INCLUDED #define EXTRACT_APPEND_HPP_INCLUDED -#include "utility/cast_ptr_to.hpp" +#include "serialize/byteswap.hpp" +#include // std::unsigned_integral, std::integral +#include // std::ranges:copy +#include // std::endian, std::bit_cast +#include #include // std::byte, std::size_t #include // std::uint32_t, etc -#include // std::is_arithmetic - -namespace chops { - -namespace detail { - -// since function template partial specialization is not supported (yet) in C++, overload the -// extract_val_swap and noswap function templates with a parameter corresponding to the -// sizeof the value; no data is passed in the second parameter, only using the type for overloading - -template -struct size_tag { }; - -// if char / byte, no swap needed -template -T extract_val_swap(const std::byte* buf, const size_tag<1u>*) noexcept { - return static_cast(*buf); // static_cast needed to convert std::byte to char type -} -template -T extract_val_noswap(const std::byte* buf, const size_tag<1u>*) noexcept { - return static_cast(*buf); // see note above -} - -template -T extract_val_swap(const std::byte* buf, const size_tag<2u>*) noexcept { - T tmp{}; - std::byte* p = cast_ptr_to(&tmp); - *(p+0) = *(buf+1); - *(p+1) = *(buf+0); - return tmp; -} - -template -T extract_val_noswap(const std::byte* buf, const size_tag<2u>*) noexcept { - T tmp{}; - std::byte* p = cast_ptr_to(&tmp); - *(p+0) = *(buf+0); - *(p+1) = *(buf+1); - return tmp; -} - -template -T extract_val_swap(const std::byte* buf, const size_tag<4u>*) noexcept { - T tmp{}; - std::byte* p = cast_ptr_to(&tmp); - *(p+0) = *(buf+3); - *(p+1) = *(buf+2); - *(p+2) = *(buf+1); - *(p+3) = *(buf+0); - return tmp; -} - -template -T extract_val_noswap(const std::byte* buf, const size_tag<4u>*) noexcept { - T tmp{}; - std::byte* p = cast_ptr_to(&tmp); - *(p+0) = *(buf+0); - *(p+1) = *(buf+1); - *(p+2) = *(buf+2); - *(p+3) = *(buf+3); - return tmp; -} - -template -T extract_val_swap(const std::byte* buf, const size_tag<8u>*) noexcept { - T tmp{}; - std::byte* p = cast_ptr_to(&tmp); - *(p+0) = *(buf+7); - *(p+1) = *(buf+6); - *(p+2) = *(buf+5); - *(p+3) = *(buf+4); - *(p+4) = *(buf+3); - *(p+5) = *(buf+2); - *(p+6) = *(buf+1); - *(p+7) = *(buf+0); - return tmp; -} - -template -T extract_val_noswap(const std::byte* buf, const size_tag<8u>*) noexcept { - T tmp{}; - std::byte* p = cast_ptr_to(&tmp); - *(p+0) = *(buf+0); - *(p+1) = *(buf+1); - *(p+2) = *(buf+2); - *(p+3) = *(buf+3); - *(p+4) = *(buf+4); - *(p+5) = *(buf+5); - *(p+6) = *(buf+6); - *(p+7) = *(buf+7); - return tmp; -} - -template -T extract_val_swap(const std::byte* buf, const size_tag<16u>*) noexcept { - T tmp{}; - std::byte* p = cast_ptr_to(&tmp); - *(p+0) = *(buf+15); - *(p+1) = *(buf+14); - *(p+2) = *(buf+13); - *(p+3) = *(buf+12); - *(p+4) = *(buf+11); - *(p+5) = *(buf+10); - *(p+6) = *(buf+9); - *(p+7) = *(buf+8); - *(p+8) = *(buf+7); - *(p+9) = *(buf+6); - *(p+10) = *(buf+5); - *(p+11) = *(buf+4); - *(p+12) = *(buf+3); - *(p+13) = *(buf+2); - *(p+14) = *(buf+1); - *(p+15) = *(buf+0); - return tmp; -} +#include // std::is_same -template -T extract_val_noswap(const std::byte* buf, const size_tag<16u>*) noexcept { - T tmp{}; - std::byte* p = cast_ptr_to(&tmp); - *(p+0) = *(buf+0); - *(p+1) = *(buf+1); - *(p+2) = *(buf+2); - *(p+3) = *(buf+3); - *(p+4) = *(buf+4); - *(p+5) = *(buf+5); - *(p+6) = *(buf+6); - *(p+7) = *(buf+7); - *(p+8) = *(buf+8); - *(p+9) = *(buf+9); - *(p+10) = *(buf+10); - *(p+11) = *(buf+11); - *(p+12) = *(buf+12); - *(p+13) = *(buf+13); - *(p+14) = *(buf+14); - *(p+15) = *(buf+15); - return tmp; -} - -template -std::size_t append_val_swap(std::byte* buf, const T& val, const size_tag<1u>*) noexcept { - *buf = static_cast(val); // static_cast needed to convert char to std::byte - return 1u; -} - -template -std::size_t append_val_noswap(std::byte* buf, const T& val, const size_tag<1u>*) noexcept { - *buf = static_cast(val); // see note above - return 1u; -} - -template -std::size_t append_val_swap(std::byte* buf, const T& val, const size_tag<2u>*) noexcept { - const std::byte* p = cast_ptr_to(&val); - *(buf+0) = *(p+1); - *(buf+1) = *(p+0); - return 2u; -} -template -std::size_t append_val_noswap(std::byte* buf, const T& val, const size_tag<2u>*) noexcept { - const std::byte* p = cast_ptr_to(&val); - *(buf+0) = *(p+0); - *(buf+1) = *(p+1); - return 2u; -} - -template -std::size_t append_val_swap(std::byte* buf, const T& val, const size_tag<4u>*) noexcept { - const std::byte* p = cast_ptr_to(&val); - *(buf+0) = *(p+3); - *(buf+1) = *(p+2); - *(buf+2) = *(p+1); - *(buf+3) = *(p+0); - return 4u; -} - -template -std::size_t append_val_noswap(std::byte* buf, const T& val, const size_tag<4u>*) noexcept { - const std::byte* p = cast_ptr_to(&val); - *(buf+0) = *(p+0); - *(buf+1) = *(p+1); - *(buf+2) = *(p+2); - *(buf+3) = *(p+3); - return 4u; -} - -template -std::size_t append_val_swap(std::byte* buf, const T& val, const size_tag<8u>*) noexcept { - const std::byte* p = cast_ptr_to(&val); - *(buf+0) = *(p+7); - *(buf+1) = *(p+6); - *(buf+2) = *(p+5); - *(buf+3) = *(p+4); - *(buf+4) = *(p+3); - *(buf+5) = *(p+2); - *(buf+6) = *(p+1); - *(buf+7) = *(p+0); - return 8u; -} - -template -std::size_t append_val_noswap(std::byte* buf, const T& val, const size_tag<8u>*) noexcept { - const std::byte* p = cast_ptr_to(&val); - *(buf+0) = *(p+0); - *(buf+1) = *(p+1); - *(buf+2) = *(p+2); - *(buf+3) = *(p+3); - *(buf+4) = *(p+4); - *(buf+5) = *(p+5); - *(buf+6) = *(p+6); - *(buf+7) = *(p+7); - return 8u; -} - -template -std::size_t append_val_swap(std::byte* buf, const T& val, const size_tag<16u>*) noexcept { - const std::byte* p = cast_ptr_to(&val); - *(buf+0) = *(p+15); - *(buf+1) = *(p+14); - *(buf+2) = *(p+13); - *(buf+3) = *(p+12); - *(buf+4) = *(p+11); - *(buf+5) = *(p+10); - *(buf+6) = *(p+9); - *(buf+7) = *(p+8); - *(buf+8) = *(p+7); - *(buf+9) = *(p+6); - *(buf+10) = *(p+5); - *(buf+11) = *(p+4); - *(buf+12) = *(p+3); - *(buf+13) = *(p+2); - *(buf+14) = *(p+1); - *(buf+15) = *(p+0); - return 16u; -} - -template -std::size_t append_val_noswap(std::byte* buf, const T& val, const size_tag<16u>*) noexcept { - const std::byte* p = cast_ptr_to(&val); - *(buf+0) = *(p+0); - *(buf+1) = *(p+1); - *(buf+2) = *(p+2); - *(buf+3) = *(p+3); - *(buf+4) = *(p+4); - *(buf+5) = *(p+5); - *(buf+6) = *(p+6); - *(buf+7) = *(p+7); - *(buf+8) = *(p+8); - *(buf+9) = *(p+9); - *(buf+10) = *(p+10); - *(buf+11) = *(p+11); - *(buf+12) = *(p+12); - *(buf+13) = *(p+13); - *(buf+14) = *(p+14); - *(buf+15) = *(p+15); - return 16u; -} +namespace chops { +// Old static assertions, pre-concepts +/* template constexpr void assert_size() noexcept { static_assert(sizeof(T) == 1u || sizeof(T) == 2u || sizeof(T) == 4u || @@ -316,28 +60,30 @@ constexpr void assert_arithmetic_or_byte() noexcept { static_assert(is_arithmetic_or_byte(), "Value extraction is only supported for arithmetic or std::byte types."); } +*/ -} // end namespace detail - -// C++ 20 will contain std::endian, which allows full compile time endian detection; -// until then, the endian detection and branching will be runtime, although it -// can be computed at global initialization instead of each time it is called - -inline bool detect_big_endian () noexcept { - const std::uint32_t tmp {0xDDCCBBAA}; - return *(cast_ptr_to(&tmp) + 3) == static_cast(0xAA); -} +// using C++ 20 concepts +template +concept integral_or_byte = std::integral || std::is_same_v, std::byte>; +// old endian detection code +// inline bool detect_big_endian () noexcept { +// const std::uint32_t tmp {0xDDCCBBAA}; +// return *(cast_ptr_to(&tmp) + 3) == static_cast(0xAA); +//} // should be computed at global initialization time - -const bool big_endian = detect_big_endian(); +// const bool big_endian = detect_big_endian(); /** - * @brief Extract a value in network byte order (big-endian) from a @c std::byte buffer - * into a fundamental arithmetic type in native endianness, swapping bytes as needed. + * @brief Extract a value from a @c std::byte buffer into a fundamental integral + * or @c std::byte type in native endianness, swapping bytes as needed. + * + * @tparam BufEndian The endianness of the @c std::byte buffer. + * + * @tparam T Type of return value. * - * This function template dispatches on specific sizes. If an unsupported size is attempted - * to be swapped, a compile time error is generated. + * Since @c T cannot be deduced, it must be specified when calling the function. If + * the endianness of the buffer matches the native endianness, no swapping is performed. * * @param buf Pointer to an array of @c std::bytes containing an object of type T in network * byte order. @@ -346,8 +92,10 @@ const bool big_endian = detect_big_endian(); * * @pre The buffer must contain at least @c sizeof(T) bytes. * - * @note Floating point swapping is supported, but care must be taken. In particular, - * the floating point representation must exactly match on both sides of the marshalling + * @note Floating point swapping is not supported. + * + * Earlier versions did support floating point, but it is brittle - + * the floating point representation must exactly match on both sides of the serialization * (most modern processors use IEEE 754 floating point representations). A byte swapped * floating point value cannot be directly accessed (e.g. passed by value), due to the * bit patterns possibly representing NaN values, which can generate hardware traps, @@ -355,22 +103,27 @@ const bool big_endian = detect_big_endian(); * An integer value, however, will always have valid bit patterns, even when byte swapped. * */ -template -T extract_val(const std::byte* buf) noexcept { - - detail::assert_size(); - detail::assert_arithmetic_or_byte(); - - return big_endian ? detail::extract_val_noswap(buf, static_cast< const detail::size_tag* >(nullptr)) : - detail::extract_val_swap(buf, static_cast< const detail::size_tag* >(nullptr)); +template +constexpr T extract_val(const std::byte* buf) noexcept { + auto value_representation = std::bit_cast>(T{}); + std::ranges::copy (buf, buf + sizeof(T), value_representation.begin()); + auto tmp_val = std::bit_cast(value_representation); + if constexpr (BufEndian != std::endian::native && sizeof(T) != 1u) { + return chops::byteswap(tmp_val); + } + return tmp_val; } /** - * @brief Append a fundamental arithmetic value to a @c std::byte buffer, swapping into network - * endian order (big-endian) as needed. + * @brief Append an integral or @c std::byte value to a @c std::byte buffer, swapping into + * the specified endian order as needed. + * + * @tparam BufEndian The endianness of the buffer. * - * This function template dispatches on specific sizes. If an unsupported size is attempted - * to be swapped, a static error is generated. + * @tparam T Type of value to append to buffer. + * + * The @c BufEndian enum must be specified, but the type of the passed in value can be + * deduced. * * @param buf Pointer to array of @c std::bytes big enough to hold the bytes of the value. * @@ -383,14 +136,15 @@ T extract_val(const std::byte* buf) noexcept { * @note See note above about floating point values. * */ -template -std::size_t append_val(std::byte* buf, const T& val) noexcept { - - detail::assert_size(); - detail::assert_arithmetic_or_byte(); - - return big_endian ? detail::append_val_noswap(buf, val, static_cast< const detail::size_tag* >(nullptr)) : - detail::append_val_swap(buf, val, static_cast< const detail::size_tag* >(nullptr)); +template +constexpr std::size_t append_val(std::byte* buf, const T& val) noexcept { + T tmp_val {val}; + if constexpr (BufEndian != std::endian::native && sizeof(T) != 1u) { + tmp_val = chops::byteswap(tmp_val); + } + auto value_representation = std::bit_cast>(tmp_val); + std::ranges::copy (value_representation, buf ); + return sizeof(T); } /** @@ -406,12 +160,13 @@ std::size_t append_val(std::byte* buf, const T& val) noexcept { * large, this algorithm is inefficient, needing more buffer space for the encoded integers than * if fixed size integer buffers were used. * - * The output of this function is (by definition) in little-endian order (which is opposite - * to the rest of the @c append and @c extract functions). However, this does not matter since - * there is no byte swapping performed, and encoding and decoding will result in the native - * endianness of the platform. + * The output of this function is (by definition) in little-endian order. However, as long as + * the two corresponding functions (or equivalent algorithms) are used consistently, the + * endianness will not matter. There is no byte swapping performed, and encoding and decoding + * will result in the native endianness of the platform. I.e. this works whether serialization + * is big-endian or little-endian. * - * @note Unsigned types are not supported. + * @note Signed types are not supported. * * @param val The input value. Any standard unsigned integer type is allowed. * @@ -425,25 +180,22 @@ std::size_t append_val(std::byte* buf, const T& val) noexcept { * */ -template -std::size_t append_var_int(std::byte* output, T val) { - - static_assert(std::is_unsigned::value, "Only unsigned integer types are supported."); +template +constexpr std::size_t append_var_int(std::byte* output, T val) { - std::uint8_t* output_ptr = cast_ptr_to(output); - std::size_t output_size = 0; + std::size_t output_size = 0; - // While more than 7 bits of data are left, occupy the last output byte - // and set the next byte flag - while (val > 127) { - - output_ptr[output_size] = (static_cast (val & 127)) | 128; - //Remove the seven bits we just wrote - val >>= 7; - ++output_size; - } - output_ptr[output_size++] = static_cast (val) & 127; - return output_size; + // While more than 7 bits of data are left, occupy the last output byte + // and set the next byte flag + while (val > 127) { + + output[output_size] = std::bit_cast(static_cast((static_cast (val & 127)) | 128)); + //Remove the seven bits we just wrote + val >>= 7; + ++output_size; + } + output[output_size++] = std::bit_cast(static_cast(static_cast (val) & 127)); + return output_size; } /** * @brief Given a buffer of @c std::bytes that hold a variable sized integer, decode @@ -452,6 +204,8 @@ std::size_t append_var_int(std::byte* output, T val) { * For consistency with the @c append_var_int function, only unsigned integers are * supported for the output type of this function. * + * @note Signed types are not supported. + * * @param input A variable-length encoded integer stored in a buffer of @c std::bytes. * * @param input_size Number of bytes representing the integer. @@ -459,22 +213,17 @@ std::size_t append_var_int(std::byte* output, T val) { * @return The value in native unsigned integer format. * */ -template -T extract_var_int(const std::byte* input, std::size_t input_size) { - - static_assert(std::is_unsigned::value, "Only unsigned integer types are supported."); - - const std::uint8_t* input_ptr = cast_ptr_to (input); - - T ret = 0; - for (std::size_t i = 0; i < input_size; ++i) { - ret |= (input_ptr[i] & 127) << (7 * i); - //If the next-byte flag is set - if(!(input_ptr[i] & 128)) { - break; - } - } - return ret; +template +constexpr T extract_var_int(const std::byte* input, std::size_t input_size) { + T ret = 0; + for (std::size_t i = 0; i < input_size; ++i) { + ret |= (std::bit_cast(input[i]) & 127) << (7 * i); + //If the next-byte flag is set + if(!(std::bit_cast(input[i]) & 128)) { + break; + } + } + return ret; } } // end namespace diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 939001a..d5295bf 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -11,10 +11,11 @@ project ( binary_serialize_test LANGUAGES CXX ) # add dependencies include ( ../cmake/download_cpm.cmake ) -CPMAddPackage ( "gh:catchorg/Catch2@3.6.0" ) -CPMAddPackage ( "gh:connectivecpp/utility-rack@1.0.0" ) +CPMAddPackage ( "gh:catchorg/Catch2@3.7.0" ) +CPMAddPackage ( "gh:connectivecpp/utility-rack@1.0.3" ) -set ( test_app_names extract_append_test +set ( test_app_names byteswap_test + extract_append_test binary_serialize_test ) # add executable foreach ( test_app_name IN LISTS test_app_names ) diff --git a/test/byteswap_test.cpp b/test/byteswap_test.cpp new file mode 100644 index 0000000..d605531 --- /dev/null +++ b/test/byteswap_test.cpp @@ -0,0 +1,41 @@ +/** @file + * + * @brief Test scenarios for @c byteswap function. + * + * @author Cliff Green + * + * @copyright (c) 2024 by Cliff Green + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + * + */ + +#include "catch2/catch_test_macros.hpp" + + +#include // std::byte +#include // std::uint32_t, etc + +#include "serialize/byteswap.hpp" + +constexpr std::uint32_t val1 { 0xDDCCBBAAu }; +constexpr std::uint32_t val1_reversed { 0xAABBCCDDu }; +constexpr char val2 { static_cast(0xEE) }; +constexpr std::int16_t val3 { 0x0103 }; +constexpr std::int16_t val3_reversed { 0x0301 }; +constexpr std::uint64_t val4 { 0x0908070605040302ull }; +constexpr std::uint64_t val4_reversed { 0x0203040506070809ull }; + +constexpr std::int32_t val5 = 0xDEADBEEF; +constexpr std::int32_t val5_reversed = 0xEFBEADDE; + + +TEST_CASE ( "Byteswap", "[byteswap]" ) { + REQUIRE (chops::byteswap(val1) == val1_reversed); + REQUIRE (chops::byteswap(val2) == val2); + REQUIRE (chops::byteswap(val3) == val3_reversed); + REQUIRE (chops::byteswap(val4) == val4_reversed); + REQUIRE (chops::byteswap(val5) == val5_reversed); +} + diff --git a/test/extract_append_test.cpp b/test/extract_append_test.cpp index d53b812..ffb1293 100644 --- a/test/extract_append_test.cpp +++ b/test/extract_append_test.cpp @@ -18,9 +18,9 @@ #include // std::uint32_t, etc #include "serialize/extract_append.hpp" + #include "utility/repeat.hpp" -#include "utility/make_byte_array.hpp" -#include "utility/cast_ptr_to.hpp" +#include "utility/byte_array.hpp" constexpr std::uint32_t val1 = 0xDDCCBBAA; constexpr char val2 = static_cast(0xEE); @@ -28,119 +28,95 @@ constexpr std::int16_t val3 = 0x01FF; constexpr std::uint64_t val4 = 0x0908070605040302; constexpr std::int32_t val5 = 0xDEADBEEF; constexpr std::byte val6 = static_cast(0xAA); -constexpr float float_val = 42.0f; -constexpr double double_val = 197.0; constexpr int arr_sz = sizeof(val1)+sizeof(val2)+sizeof(val3)+ sizeof(val4)+sizeof(val5)+sizeof(val6); -auto net_buf = chops::make_byte_array(0xDD, 0xCC, 0xBB, 0xAA, 0xEE, 0x01, 0xFF, +auto net_buf_big = chops::make_byte_array(0xDD, 0xCC, 0xBB, 0xAA, 0xEE, 0x01, 0xFF, 0x09, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0xDE, 0xAD, 0xBE, 0xEF, 0xAA); +auto net_buf_little = chops::make_byte_array(0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x01, + 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xEF, 0xBE, 0xAD, 0xDE, 0xAA); -TEST_CASE ( "Size and arithmetic assertions", - "[assertions]" ) { - REQUIRE (chops::detail::is_arithmetic_or_byte()); - REQUIRE (chops::detail::is_arithmetic_or_byte()); - REQUIRE (chops::detail::is_arithmetic_or_byte()); - REQUIRE (chops::detail::is_arithmetic_or_byte()); - REQUIRE (chops::detail::is_arithmetic_or_byte()); - REQUIRE (chops::detail::is_arithmetic_or_byte()); - REQUIRE (chops::detail::is_arithmetic_or_byte()); - REQUIRE (chops::detail::is_arithmetic_or_byte()); - REQUIRE (chops::detail::is_arithmetic_or_byte()); -} - -SCENARIO ( "Endian detection", - "[endian] [little_endian]" ) { +TEST_CASE ( "Append values into a buffer", "[append_val]" ) { - GIVEN ("A little-endian system") { + std::byte buf[arr_sz]; + constexpr std::uint32_t v = 0x04030201; - WHEN ("The detect_big_endian function is called") { - THEN ("the value is false") { - REQUIRE (!chops::big_endian); - } - } - } // end given -} + SECTION ("Append_val with a single value, big endian") { + REQUIRE(chops::append_val(buf, v) == 4u); + REQUIRE (std::to_integer(buf[0]) == 0x04); + REQUIRE (std::to_integer(buf[1]) == 0x03); + REQUIRE (std::to_integer(buf[2]) == 0x02); + REQUIRE (std::to_integer(buf[3]) == 0x01); + } + SECTION ("Append_val with a single value, little endian") { + REQUIRE(chops::append_val(buf, v) == 4u); + REQUIRE (std::to_integer(buf[0]) == 0x01); + REQUIRE (std::to_integer(buf[1]) == 0x02); + REQUIRE (std::to_integer(buf[2]) == 0x03); + REQUIRE (std::to_integer(buf[3]) == 0x04); + } -SCENARIO ( "Append values into a buffer", - "[append_val]" ) { - - GIVEN ("An empty byte buffer") { - std::byte buf[arr_sz]; - - WHEN ("Append_val is called with a single value") { - constexpr std::uint32_t v = 0x04030201; - REQUIRE(chops::append_val(buf, v) == 4u); - THEN ("the buffer will contain the single value in network endian order") { - REQUIRE (buf[0] == static_cast(0x04)); - REQUIRE (buf[1] == static_cast(0x03)); - REQUIRE (buf[2] == static_cast(0x02)); - REQUIRE (buf[3] == static_cast(0x01)); - } - } - AND_WHEN ("Append_val is called with multiple values") { - std::byte* ptr = buf; - REQUIRE(chops::append_val(ptr, val1) == 4u); ptr += sizeof(val1); - REQUIRE(chops::append_val(ptr, val2) == 1u); ptr += sizeof(val2); - REQUIRE(chops::append_val(ptr, val3) == 2u); ptr += sizeof(val3); - REQUIRE(chops::append_val(ptr, val4) == 8u); ptr += sizeof(val4); - REQUIRE(chops::append_val(ptr, val5) == 4u); ptr += sizeof(val5); - REQUIRE(chops::append_val(ptr, val6) == 1u); - - THEN ("the buffer will have all of the values in network endian order") { - chops::repeat(arr_sz, [&buf] (int i) { REQUIRE (buf[i] == net_buf[i]); } ); - } - } - } // end given + SECTION ("Append_val with multiple values, big endian") { + std::byte* ptr = buf; + REQUIRE(chops::append_val(ptr, val1) == 4u); ptr += sizeof(val1); + REQUIRE(chops::append_val(ptr, val2) == 1u); ptr += sizeof(val2); + REQUIRE(chops::append_val(ptr, val3) == 2u); ptr += sizeof(val3); + REQUIRE(chops::append_val(ptr, val4) == 8u); ptr += sizeof(val4); + REQUIRE(chops::append_val(ptr, val5) == 4u); ptr += sizeof(val5); + REQUIRE(chops::append_val(ptr, val6) == 1u); + chops::repeat(arr_sz, [&buf] (int i) { + REQUIRE (std::to_integer(buf[i]) == std::to_integer(net_buf_big[i])); } ); + } + SECTION ("Append_val with multiple values, little endian") { + std::byte* ptr = buf; + REQUIRE(chops::append_val(ptr, val1) == 4u); ptr += sizeof(val1); + REQUIRE(chops::append_val(ptr, val2) == 1u); ptr += sizeof(val2); + REQUIRE(chops::append_val(ptr, val3) == 2u); ptr += sizeof(val3); + REQUIRE(chops::append_val(ptr, val4) == 8u); ptr += sizeof(val4); + REQUIRE(chops::append_val(ptr, val5) == 4u); ptr += sizeof(val5); + REQUIRE(chops::append_val(ptr, val6) == 1u); + chops::repeat(arr_sz, [&buf] (int i) { + REQUIRE (std::to_integer(buf[i]) == std::to_integer(net_buf_little[i])); } ); + } } -SCENARIO ( "Extract values from a buffer", - "[extract_val]" ) { - - GIVEN ("A buffer of bytes in network endian order") { - WHEN ("Extract_val is called for multiple values") { - - const std::byte* ptr = net_buf.data(); - std::uint32_t v1 = chops::extract_val(ptr); ptr += sizeof(v1); - char v2 = chops::extract_val(ptr); ptr += sizeof(v2); - std::int16_t v3 = chops::extract_val(ptr); ptr += sizeof(v3); - std::uint64_t v4 = chops::extract_val(ptr); ptr += sizeof(v4); - std::int32_t v5 = chops::extract_val(ptr); ptr += sizeof(v5); - std::byte v6 = chops::extract_val(ptr); - - THEN ("the values are all in native order") { - REQUIRE(v1 == val1); - REQUIRE(v2 == val2); - REQUIRE(v3 == val3); - REQUIRE(v4 == val4); - REQUIRE(v5 == val5); - REQUIRE(v6 == val6); - } - } - } // end given +TEST_CASE ( "Extract values from a buffer", "[extract_val]" ) { + + SECTION ( "Extract_val for multiple values in big endian buf") { + const std::byte* ptr = net_buf_big.data(); + std::uint32_t v1 = chops::extract_val(ptr); ptr += sizeof(v1); + char v2 = chops::extract_val(ptr); ptr += sizeof(v2); + std::int16_t v3 = chops::extract_val(ptr); ptr += sizeof(v3); + std::uint64_t v4 = chops::extract_val(ptr); ptr += sizeof(v4); + std::int32_t v5 = chops::extract_val(ptr); ptr += sizeof(v5); + std::byte v6 = chops::extract_val(ptr); + + REQUIRE(v1 == val1); + REQUIRE(v2 == val2); + REQUIRE(v3 == val3); + REQUIRE(v4 == val4); + REQUIRE(v5 == val5); + REQUIRE(std::to_integer(v6) == std::to_integer(val6)); + } + SECTION ( "Extract_val for multiple values in little endian buf") { + const std::byte* ptr = net_buf_little.data(); + std::uint32_t v1 = chops::extract_val(ptr); ptr += sizeof(v1); + char v2 = chops::extract_val(ptr); ptr += sizeof(v2); + std::int16_t v3 = chops::extract_val(ptr); ptr += sizeof(v3); + std::uint64_t v4 = chops::extract_val(ptr); ptr += sizeof(v4); + std::int32_t v5 = chops::extract_val(ptr); ptr += sizeof(v5); + std::byte v6 = chops::extract_val(ptr); + + REQUIRE(v1 == val1); + REQUIRE(v2 == val2); + REQUIRE(v3 == val3); + REQUIRE(v4 == val4); + REQUIRE(v5 == val5); + REQUIRE(std::to_integer(v6) == std::to_integer(val6)); + } } -SCENARIO ( "Floating point append and extract", - "[floating_point]" ) { - GIVEN ("An empty byte buffer") { - std::byte buf[16]; - - WHEN ("Two floating point values are appended then extracted") { - std::byte* ptr = buf; - REQUIRE(chops::append_val(ptr, float_val) == 4u); ptr += sizeof(float_val); - REQUIRE(chops::append_val(ptr, double_val) == 8u); - ptr = buf; - auto f = chops::extract_val(ptr); ptr += sizeof(f); - auto d = chops::extract_val(ptr); - - THEN ("the values are the same, in native order") { - REQUIRE (f == float_val); - REQUIRE (d == double_val); - } - } - } // end given -} template void test_round_trip_var_int (Src src, std::size_t exp_sz) { @@ -157,9 +133,9 @@ TEST_CASE ( "Append and extract variable length integers","[append_var_int]" ) { { auto outsize = chops::append_var_int(test_buf, 0xCAFE); - REQUIRE(static_cast (test_buf[0]) == 254); - REQUIRE(static_cast (test_buf[1]) == 149); - REQUIRE(static_cast (test_buf[2]) == 3); + REQUIRE(std::to_integer(test_buf[0]) == 254); + REQUIRE(std::to_integer(test_buf[1]) == 149); + REQUIRE(std::to_integer(test_buf[2]) == 3); auto output = chops::extract_var_int(test_buf, outsize); @@ -189,23 +165,23 @@ TEST_CASE ( "Append var len integer of 127","[append_var_int]" ) { std::byte test_buf [7]; auto outsize = chops::append_var_int(test_buf, 0x7F); - REQUIRE(static_cast (test_buf[0]) == 127); + REQUIRE(std::to_integer(test_buf[0]) == 127); REQUIRE(outsize == 1); } TEST_CASE ( "Append var len integer of 128","[append_var_int]" ) { std::byte test_buf [7]; auto outsize = chops::append_var_int(test_buf, 0x80); - REQUIRE(static_cast (test_buf[0]) == 128); //byte flag set - REQUIRE(static_cast (test_buf[1]) == 1); + REQUIRE(std::to_integer(test_buf[0]) == 128); //byte flag set + REQUIRE(std::to_integer(test_buf[1]) == 1); REQUIRE(outsize == 2); } TEST_CASE ( "Append var len integer larger than 4 bytes","[append_var_int]" ) { std::byte test_buf [7]; auto outsize = chops::append_var_int(test_buf, 0x10000000); - REQUIRE(static_cast (test_buf[0]) == 128); //byte flag set - REQUIRE(static_cast (test_buf[4]) == 1); + REQUIRE(std::to_integer(test_buf[0]) == 128); //byte flag set + REQUIRE(std::to_integer(test_buf[4]) == 1); REQUIRE(outsize == 5); }