diff --git a/doc/modules/ROOT/pages/api_reference.adoc b/doc/modules/ROOT/pages/api_reference.adoc index 8d5a0e0..600eb6f 100644 --- a/doc/modules/ROOT/pages/api_reference.adoc +++ b/doc/modules/ROOT/pages/api_reference.adoc @@ -212,6 +212,12 @@ https://www.boost.org/LICENSE_1_0.txt | xref:byte_conversions.adoc[`from_le`] | Converts a safe integer from little-endian to native byte order + +| xref:byte_conversions.adoc[`to_be_bytes`] +| Converts a safe integer to a big-endian byte array + +| xref:byte_conversions.adoc[`from_be_bytes`] +| Reconstructs a safe integer from a big-endian byte array |=== === Arithmetic diff --git a/doc/modules/ROOT/pages/byte_conversions.adoc b/doc/modules/ROOT/pages/byte_conversions.adoc index 8f414a3..f911d0c 100644 --- a/doc/modules/ROOT/pages/byte_conversions.adoc +++ b/doc/modules/ROOT/pages/byte_conversions.adoc @@ -261,3 +261,107 @@ constexpr auto val = verified_u32{u32{0xDEADBEEF}}; constexpr auto round_tripped = from_le(to_le(val)); static_assert(round_tripped == val); ---- + +== to_be_bytes + +Converts a safe integer value to a big-endian byte array. +The value is first converted to big-endian byte order using `to_be`, then reinterpreted as an array of `std::byte`. + +=== Runtime Overload + +[source,c++] +---- +template + requires (!is_verified_type_v) +constexpr auto to_be_bytes(const T value) noexcept -> std::array; +---- + +==== Parameters + +* `value` -- The value to convert. + +==== Return Value + +A `std::array` containing the value's bytes in big-endian order (most significant byte first). + +==== Complexity + +O(1). + +==== Example + +[source,c++] +---- +using namespace boost::safe_numbers; + +auto bytes = to_be_bytes(u32{0x01020304}); +// bytes == {0x01, 0x02, 0x03, 0x04} +---- + +=== Verified Overload + +[source,c++] +---- +template +consteval auto to_be_bytes(const verified_type_basis value) noexcept -> std::array; +---- + +Compile-time only overload for verified types. + +==== Example + +[source,c++] +---- +using namespace boost::safe_numbers; + +constexpr auto bytes = to_be_bytes(verified_u32{u32{0x01020304}}); +// bytes == {0x01, 0x02, 0x03, 0x04} +---- + +== from_be_bytes + +Reconstructs a safe integer value from a big-endian byte array. +The bytes are reinterpreted as the underlying type and then converted from big-endian to native byte order using `from_be`. + +[source,c++] +---- +template +constexpr auto from_be_bytes(const std::span bytes) -> T; +---- + +==== Parameters + +* `bytes` -- A span of bytes in big-endian order. May have a fixed extent or `std::dynamic_extent`. + +==== Return Value + +The reconstructed safe integer value in native byte order. + +==== Extent Matching + +The function validates that the number of input bytes matches `sizeof(T)`: + +* **Fixed extent matches `sizeof(T)`**: Compiles and executes normally. +* **Fixed extent does not match `sizeof(T)`**: Produces a `static_assert` failure at compile time. +* **Dynamic extent**: Checks at runtime and throws `std::domain_error` if the sizes do not match. + +==== Complexity + +O(1). + +==== Example + +[source,c++] +---- +using namespace boost::safe_numbers; + +// From a fixed-extent span +std::array bytes {std::byte{0x01}, std::byte{0x02}, std::byte{0x03}, std::byte{0x04}}; +auto val = from_be_bytes(std::span{bytes}); +// val == u32{0x01020304} + +// Round-trip +auto original = u32{0xDEADBEEF}; +auto reconstructed = from_be_bytes(std::span{to_be_bytes(original)}); +// reconstructed == original +---- diff --git a/include/boost/safe_numbers/byte_conversions.hpp b/include/boost/safe_numbers/byte_conversions.hpp index 0137cb7..577c745 100644 --- a/include/boost/safe_numbers/byte_conversions.hpp +++ b/include/boost/safe_numbers/byte_conversions.hpp @@ -11,7 +11,10 @@ #ifndef BOOST_SAFE_NUMBERS_BUILD_MODULE +#include #include +#include +#include #endif // BOOST_SAFE_NUMBERS_BUILD_MODULE @@ -101,6 +104,56 @@ consteval auto from_le(const detail::verified_type_basis value) noexcept -> d return to_le(value); } +template + requires (!detail::is_verified_type_v) +constexpr auto to_be_bytes(const T value) noexcept -> std::array +{ + const auto be_value {to_be(value)}; + return std::bit_cast>(be_value); +} + +template +consteval auto to_be_bytes(const detail::verified_type_basis value) noexcept -> std::array +{ + const auto be_value {to_be(value)}; + return std::bit_cast>(be_value); +} + +template +constexpr auto from_be_bytes(const std::span bytes) -> T +{ + using underlying_type = detail::underlying_type_t; + + if constexpr (N == sizeof(T)) + { + std::array arr {}; + for (std::size_t i {}; i < N; ++i) + { + arr[i] = bytes[i]; + } + return from_be(T{std::bit_cast(arr)}); + } + else if constexpr (N != std::dynamic_extent) + { + static_assert(detail::dependent_false, "The number of bytes provided, and the target type number of bytes do not match"); + return T{}; // LCOV_EXCL_LINE + } + else + { + if (bytes.size_bytes() != sizeof(T)) + { + BOOST_THROW_EXCEPTION(std::domain_error("The number of bytes provided, and the target type number of bytes do not match")); + } + + std::array arr {}; + for (std::size_t i {}; i < sizeof(T); ++i) + { + arr[i] = bytes[i]; + } + return from_be(T{std::bit_cast(arr)}); + } +} + } // namespace boost::safe_numbers #endif //BOOST_SAFE_NUMBERS_BYTE_CONVERSIONS_HPP diff --git a/test/Jamfile b/test/Jamfile index 8495d27..0000e9c 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -132,6 +132,8 @@ run test_verified_limits.cpp ; # Byte conversion tests run test_to_from_be.cpp ; run test_to_from_le.cpp ; +run test_to_from_be_bytes.cpp ; +compile-fail compile_fail_from_be_bytes_extent.cpp ; # Utility function tests run test_isqrt.cpp ; diff --git a/test/compile_fail_from_be_bytes_extent.cpp b/test/compile_fail_from_be_bytes_extent.cpp new file mode 100644 index 0000000..9066586 --- /dev/null +++ b/test/compile_fail_from_be_bytes_extent.cpp @@ -0,0 +1,22 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +// This test verifies that from_be_bytes produces a compile error when +// the span's fixed extent does not match the target type's size. + +#include +#include +#include +#include + +using namespace boost::safe_numbers; + +int main() +{ + // u32 requires 4 bytes, but we provide a 2-byte span + const std::array bytes {std::byte{0x01}, std::byte{0x02}}; + auto val = from_be_bytes(std::span{bytes}); + + return 0; +} diff --git a/test/test_to_from_be_bytes.cpp b/test/test_to_from_be_bytes.cpp new file mode 100644 index 0000000..3eef2a6 --- /dev/null +++ b/test/test_to_from_be_bytes.cpp @@ -0,0 +1,199 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include + +#ifdef BOOST_SAFE_NUMBERS_BUILD_MODULE + +import boost.safe_numbers; + +#else + +#include +#include +#include +#include +#include +#include +#include +#include + +#endif + +using namespace boost::safe_numbers; + +// ============================================================================= +// to_be_bytes: known byte patterns +// ============================================================================= + +void test_to_be_bytes_u8() +{ + const auto bytes = to_be_bytes(u8{0x42}); + BOOST_TEST_EQ(bytes.size(), std::size_t{1}); + BOOST_TEST(bytes[0] == std::byte{0x42}); +} + +void test_to_be_bytes_u16() +{ + const auto bytes = to_be_bytes(u16{static_cast(0x0102)}); + BOOST_TEST_EQ(bytes.size(), std::size_t{2}); + // Big-endian: most significant byte first + BOOST_TEST(bytes[0] == std::byte{0x01}); + BOOST_TEST(bytes[1] == std::byte{0x02}); +} + +void test_to_be_bytes_u32() +{ + const auto bytes = to_be_bytes(u32{0x01020304U}); + BOOST_TEST_EQ(bytes.size(), std::size_t{4}); + BOOST_TEST(bytes[0] == std::byte{0x01}); + BOOST_TEST(bytes[1] == std::byte{0x02}); + BOOST_TEST(bytes[2] == std::byte{0x03}); + BOOST_TEST(bytes[3] == std::byte{0x04}); +} + +void test_to_be_bytes_u64() +{ + const auto bytes = to_be_bytes(u64{0x0102030405060708ULL}); + BOOST_TEST_EQ(bytes.size(), std::size_t{8}); + BOOST_TEST(bytes[0] == std::byte{0x01}); + BOOST_TEST(bytes[1] == std::byte{0x02}); + BOOST_TEST(bytes[2] == std::byte{0x03}); + BOOST_TEST(bytes[3] == std::byte{0x04}); + BOOST_TEST(bytes[4] == std::byte{0x05}); + BOOST_TEST(bytes[5] == std::byte{0x06}); + BOOST_TEST(bytes[6] == std::byte{0x07}); + BOOST_TEST(bytes[7] == std::byte{0x08}); +} + +void test_to_be_bytes_zero() +{ + { + const auto bytes = to_be_bytes(u8{0}); + BOOST_TEST(bytes[0] == std::byte{0x00}); + } + { + const auto bytes = to_be_bytes(u32{0U}); + for (const auto& b : bytes) + { + BOOST_TEST(b == std::byte{0x00}); + } + } +} + +// ============================================================================= +// from_be_bytes: reconstruct from known byte patterns +// ============================================================================= + +void test_from_be_bytes_u8() +{ + const std::array bytes {std::byte{0x42}}; + const auto val = from_be_bytes(std::span{bytes}); + BOOST_TEST(val == u8{0x42}); +} + +void test_from_be_bytes_u16() +{ + const std::array bytes {std::byte{0x01}, std::byte{0x02}}; + const auto val = from_be_bytes(std::span{bytes}); + BOOST_TEST(val == u16{static_cast(0x0102)}); +} + +void test_from_be_bytes_u32() +{ + const std::array bytes {std::byte{0x01}, std::byte{0x02}, std::byte{0x03}, std::byte{0x04}}; + const auto val = from_be_bytes(std::span{bytes}); + BOOST_TEST(val == u32{0x01020304U}); +} + +void test_from_be_bytes_u64() +{ + const std::array bytes { + std::byte{0x01}, std::byte{0x02}, std::byte{0x03}, std::byte{0x04}, + std::byte{0x05}, std::byte{0x06}, std::byte{0x07}, std::byte{0x08} + }; + const auto val = from_be_bytes(std::span{bytes}); + BOOST_TEST(val == u64{0x0102030405060708ULL}); +} + +void test_from_be_bytes_zero() +{ + const std::array bytes {}; + const auto val = from_be_bytes(std::span{bytes}); + BOOST_TEST(val == u32{0U}); +} + +// ============================================================================= +// Round-trip: to_be_bytes -> from_be_bytes == identity +// ============================================================================= + +template +void test_round_trip(T value) +{ + const auto bytes = to_be_bytes(value); + const auto result = from_be_bytes(std::span{bytes}); + BOOST_TEST(result == value); +} + +void test_round_trips() +{ + test_round_trip(u8{0}); + test_round_trip(u8{0xFF}); + test_round_trip(u8{0x42}); + + test_round_trip(u16{0}); + test_round_trip(u16{static_cast(0xFFFF)}); + test_round_trip(u16{static_cast(0xABCD)}); + + test_round_trip(u32{0U}); + test_round_trip(u32{0xFFFFFFFFU}); + test_round_trip(u32{0xDEADBEEFU}); + + test_round_trip(u64{0ULL}); + test_round_trip(u64{0xFFFFFFFFFFFFFFFFULL}); + test_round_trip(u64{0x0123456789ABCDEFULL}); +} + +// ============================================================================= +// from_be_bytes with dynamic extent: runtime size mismatch throws +// ============================================================================= + +void test_from_be_bytes_dynamic_size_mismatch() +{ + const std::array bytes {std::byte{0x01}, std::byte{0x02}}; + std::span dynamic_span {bytes}; + + BOOST_TEST_THROWS(from_be_bytes(dynamic_span), std::domain_error); +} + +void test_from_be_bytes_dynamic_size_match() +{ + const std::array bytes {std::byte{0x01}, std::byte{0x02}, std::byte{0x03}, std::byte{0x04}}; + std::span dynamic_span {bytes}; + + const auto val = from_be_bytes(dynamic_span); + BOOST_TEST(val == u32{0x01020304U}); +} + +int main() +{ + test_to_be_bytes_u8(); + test_to_be_bytes_u16(); + test_to_be_bytes_u32(); + test_to_be_bytes_u64(); + test_to_be_bytes_zero(); + + test_from_be_bytes_u8(); + test_from_be_bytes_u16(); + test_from_be_bytes_u32(); + test_from_be_bytes_u64(); + test_from_be_bytes_zero(); + + test_round_trips(); + + test_from_be_bytes_dynamic_size_mismatch(); + test_from_be_bytes_dynamic_size_match(); + + return boost::report_errors(); +}