diff --git a/doc/modules/ROOT/pages/api_reference.adoc b/doc/modules/ROOT/pages/api_reference.adoc index 600eb6f..ee05a89 100644 --- a/doc/modules/ROOT/pages/api_reference.adoc +++ b/doc/modules/ROOT/pages/api_reference.adoc @@ -218,6 +218,12 @@ https://www.boost.org/LICENSE_1_0.txt | xref:byte_conversions.adoc[`from_be_bytes`] | Reconstructs a safe integer from a big-endian byte array + +| xref:byte_conversions.adoc[`to_le_bytes`] +| Converts a safe integer to a little-endian byte array + +| xref:byte_conversions.adoc[`from_le_bytes`] +| Reconstructs a safe integer from a little-endian byte array |=== === Arithmetic @@ -290,7 +296,7 @@ This header is not included in the convenience header since it requires external | Bounded unsigned integer type (`bounded_uint`) | `` -| Byte order conversion functions (`to_be`, `from_be`, `to_le`, `from_le`) +| Byte order conversion functions (`to_be`, `from_be`, `to_le`, `from_le`, `to_be_bytes`, `from_be_bytes`, `to_le_bytes`, `from_le_bytes`) | `` | Verified integer types (`verified_u8`, `verified_u16`, `verified_u32`, `verified_u64`, `verified_u128`, `verified_bounded_integer`) diff --git a/doc/modules/ROOT/pages/byte_conversions.adoc b/doc/modules/ROOT/pages/byte_conversions.adoc index f911d0c..17e4d29 100644 --- a/doc/modules/ROOT/pages/byte_conversions.adoc +++ b/doc/modules/ROOT/pages/byte_conversions.adoc @@ -365,3 +365,107 @@ auto original = u32{0xDEADBEEF}; auto reconstructed = from_be_bytes(std::span{to_be_bytes(original)}); // reconstructed == original ---- + +== to_le_bytes + +Converts a safe integer value to a little-endian byte array. +The value is first converted to little-endian byte order using `to_le`, then reinterpreted as an array of `std::byte`. + +=== Runtime Overload + +[source,c++] +---- +template + requires (!is_verified_type_v) +constexpr auto to_le_bytes(const T value) noexcept -> std::array; +---- + +==== Parameters + +* `value` -- The value to convert. + +==== Return Value + +A `std::array` containing the value's bytes in little-endian order (least significant byte first). + +==== Complexity + +O(1). + +==== Example + +[source,c++] +---- +using namespace boost::safe_numbers; + +auto bytes = to_le_bytes(u32{0x01020304}); +// bytes == {0x04, 0x03, 0x02, 0x01} +---- + +=== Verified Overload + +[source,c++] +---- +template +consteval auto to_le_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_le_bytes(verified_u32{u32{0x01020304}}); +// bytes == {0x04, 0x03, 0x02, 0x01} +---- + +== from_le_bytes + +Reconstructs a safe integer value from a little-endian byte array. +The bytes are reinterpreted as the underlying type and then converted from little-endian to native byte order using `from_le`. + +[source,c++] +---- +template +constexpr auto from_le_bytes(const std::span bytes) -> T; +---- + +==== Parameters + +* `bytes` -- A span of bytes in little-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{0x04}, std::byte{0x03}, std::byte{0x02}, std::byte{0x01}}; +auto val = from_le_bytes(std::span{bytes}); +// val == u32{0x01020304} + +// Round-trip +auto original = u32{0xDEADBEEF}; +auto reconstructed = from_le_bytes(std::span{to_le_bytes(original)}); +// reconstructed == original +---- diff --git a/doc/modules/ROOT/pages/overview.adoc b/doc/modules/ROOT/pages/overview.adoc index fdc7bdd..d356fba 100644 --- a/doc/modules/ROOT/pages/overview.adoc +++ b/doc/modules/ROOT/pages/overview.adoc @@ -6,13 +6,13 @@ https://www.boost.org/LICENSE_1_0.txt [#overview] -= Safe_numbers: Safer fundamental numeric types in C++20 += SafeNumbers: Safer fundamental numeric types in C++20 Matt Borland == Description -Safe_numbers strives to enhance the type safety of fundamental numeric types, with an acceptable performance degradation. +SafeNumbers strives to enhance the type safety of fundamental numeric types, with an acceptable performance degradation. The library is header-only, minimal dependencies (Boost.Assert, Config, and Throw_Exception), and requires only C++20. @@ -27,7 +27,7 @@ Safety critical applications == Supported Compilers -Boost.safe_numbers is tested natively on Ubuntu (x86_64, x86_32, s390x, aarch64, ARM32v7), macOS (x86_64, and Apple Silicon), and Windows (x86_64, x86_32, and ARM64); +Boost.SafeNumbers is tested natively on Ubuntu (x86_64, x86_32, s390x, aarch64, ARM32v7), macOS (x86_64, and Apple Silicon), and Windows (x86_64, x86_32, and ARM64); as well as emulated PPC64LE using QEMU with the following compilers: * GCC 11 and later diff --git a/include/boost/safe_numbers/byte_conversions.hpp b/include/boost/safe_numbers/byte_conversions.hpp index 577c745..7cf3df1 100644 --- a/include/boost/safe_numbers/byte_conversions.hpp +++ b/include/boost/safe_numbers/byte_conversions.hpp @@ -120,6 +120,7 @@ consteval auto to_be_bytes(const detail::verified_type_basis value) noexcept } template + requires (!detail::is_verified_type_v) constexpr auto from_be_bytes(const std::span bytes) -> T { using underlying_type = detail::underlying_type_t; @@ -154,6 +155,85 @@ constexpr auto from_be_bytes(const std::span bytes) -> T } } +template +consteval auto from_be_bytes(const std::span bytes) -> T +{ + using underlying_type = detail::underlying_type_t; + static_assert(N == sizeof(T), "The number of bytes provided, and the target type number of bytes do not match"); + + std::array arr {}; + for (std::size_t i {}; i < N; ++i) + { + arr[i] = bytes[i]; + } + return from_be(T{std::bit_cast(arr)}); +} + +template + requires (!detail::is_verified_type_v) +constexpr auto to_le_bytes(const T value) noexcept -> std::array +{ + const auto le_value {to_le(value)}; + return std::bit_cast>(le_value); +} + +template +consteval auto to_le_bytes(const detail::verified_type_basis value) noexcept -> std::array +{ + const auto le_value {to_le(value)}; + return std::bit_cast>(le_value); +} + +template + requires (!detail::is_verified_type_v) +constexpr auto from_le_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_le(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_le(T{std::bit_cast(arr)}); + } +} + +template +consteval auto from_le_bytes(const std::span bytes) -> T +{ + using underlying_type = detail::underlying_type_t; + static_assert(N == sizeof(T), "The number of bytes provided, and the target type number of bytes do not match"); + + std::array arr {}; + for (std::size_t i {}; i < N; ++i) + { + arr[i] = bytes[i]; + } + return from_le(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 0000e9c..8948f7f 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -134,6 +134,8 @@ 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 ; +run test_to_from_le_bytes.cpp ; +compile-fail compile_fail_from_le_bytes_extent.cpp ; # Utility function tests run test_isqrt.cpp ; diff --git a/test/compile_fail_from_le_bytes_extent.cpp b/test/compile_fail_from_le_bytes_extent.cpp new file mode 100644 index 0000000..f3cc63c --- /dev/null +++ b/test/compile_fail_from_le_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_le_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_le_bytes(std::span{bytes}); + + return 0; +} diff --git a/test/test_to_from_be_bytes.cpp b/test/test_to_from_be_bytes.cpp index 3eef2a6..5284969 100644 --- a/test/test_to_from_be_bytes.cpp +++ b/test/test_to_from_be_bytes.cpp @@ -159,6 +159,83 @@ void test_round_trips() // from_be_bytes with dynamic extent: runtime size mismatch throws // ============================================================================= +// ============================================================================= +// Verified overloads: to_be_bytes and from_be_bytes at compile time +// ============================================================================= + +void test_verified_to_be_bytes() +{ + { + constexpr auto bytes = to_be_bytes(verified_u8{u8{0x42}}); + static_assert(bytes[0] == std::byte{0x42}); + } + { + constexpr auto bytes = to_be_bytes(verified_u16{u16{static_cast(0x0102)}}); + static_assert(bytes[0] == std::byte{0x01}); + static_assert(bytes[1] == std::byte{0x02}); + } + { + constexpr auto bytes = to_be_bytes(verified_u32{u32{0x01020304U}}); + static_assert(bytes[0] == std::byte{0x01}); + static_assert(bytes[1] == std::byte{0x02}); + static_assert(bytes[2] == std::byte{0x03}); + static_assert(bytes[3] == std::byte{0x04}); + } + { + constexpr auto bytes = to_be_bytes(verified_u64{u64{0x0102030405060708ULL}}); + static_assert(bytes[0] == std::byte{0x01}); + static_assert(bytes[1] == std::byte{0x02}); + static_assert(bytes[2] == std::byte{0x03}); + static_assert(bytes[3] == std::byte{0x04}); + static_assert(bytes[4] == std::byte{0x05}); + static_assert(bytes[5] == std::byte{0x06}); + static_assert(bytes[6] == std::byte{0x07}); + static_assert(bytes[7] == std::byte{0x08}); + } +} + +void test_verified_from_be_bytes() +{ + { + constexpr std::array bytes {std::byte{0x42}}; + constexpr auto val = from_be_bytes(std::span{bytes}); + static_assert(static_cast(val) == u8{0x42}); + } + { + constexpr std::array bytes {std::byte{0x01}, std::byte{0x02}, std::byte{0x03}, std::byte{0x04}}; + constexpr auto val = from_be_bytes(std::span{bytes}); + static_assert(static_cast(val) == u32{0x01020304U}); + } + { + constexpr 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} + }; + constexpr auto val = from_be_bytes(std::span{bytes}); + static_assert(static_cast(val) == u64{0x0102030405060708ULL}); + } +} + +void test_verified_be_bytes_round_trip() +{ + { + constexpr auto original = verified_u32{u32{0xDEADBEEFU}}; + constexpr auto bytes = to_be_bytes(original); + constexpr auto reconstructed = from_be_bytes(std::span{bytes}); + static_assert(reconstructed == original); + } + { + constexpr auto original = verified_u64{u64{0x0123456789ABCDEFULL}}; + constexpr auto bytes = to_be_bytes(original); + constexpr auto reconstructed = from_be_bytes(std::span{bytes}); + static_assert(reconstructed == original); + } +} + +// ============================================================================= +// 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}}; @@ -192,6 +269,10 @@ int main() test_round_trips(); + test_verified_to_be_bytes(); + test_verified_from_be_bytes(); + test_verified_be_bytes_round_trip(); + test_from_be_bytes_dynamic_size_mismatch(); test_from_be_bytes_dynamic_size_match(); diff --git a/test/test_to_from_le_bytes.cpp b/test/test_to_from_le_bytes.cpp new file mode 100644 index 0000000..9e4f05f --- /dev/null +++ b/test/test_to_from_le_bytes.cpp @@ -0,0 +1,281 @@ +// 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_le_bytes: known byte patterns +// ============================================================================= + +void test_to_le_bytes_u8() +{ + const auto bytes = to_le_bytes(u8{0x42}); + BOOST_TEST_EQ(bytes.size(), std::size_t{1}); + BOOST_TEST(bytes[0] == std::byte{0x42}); +} + +void test_to_le_bytes_u16() +{ + const auto bytes = to_le_bytes(u16{static_cast(0x0102)}); + BOOST_TEST_EQ(bytes.size(), std::size_t{2}); + // Little-endian: least significant byte first + BOOST_TEST(bytes[0] == std::byte{0x02}); + BOOST_TEST(bytes[1] == std::byte{0x01}); +} + +void test_to_le_bytes_u32() +{ + const auto bytes = to_le_bytes(u32{0x01020304U}); + BOOST_TEST_EQ(bytes.size(), std::size_t{4}); + BOOST_TEST(bytes[0] == std::byte{0x04}); + BOOST_TEST(bytes[1] == std::byte{0x03}); + BOOST_TEST(bytes[2] == std::byte{0x02}); + BOOST_TEST(bytes[3] == std::byte{0x01}); +} + +void test_to_le_bytes_u64() +{ + const auto bytes = to_le_bytes(u64{0x0102030405060708ULL}); + BOOST_TEST_EQ(bytes.size(), std::size_t{8}); + BOOST_TEST(bytes[0] == std::byte{0x08}); + BOOST_TEST(bytes[1] == std::byte{0x07}); + BOOST_TEST(bytes[2] == std::byte{0x06}); + BOOST_TEST(bytes[3] == std::byte{0x05}); + BOOST_TEST(bytes[4] == std::byte{0x04}); + BOOST_TEST(bytes[5] == std::byte{0x03}); + BOOST_TEST(bytes[6] == std::byte{0x02}); + BOOST_TEST(bytes[7] == std::byte{0x01}); +} + +void test_to_le_bytes_zero() +{ + { + const auto bytes = to_le_bytes(u8{0}); + BOOST_TEST(bytes[0] == std::byte{0x00}); + } + { + const auto bytes = to_le_bytes(u32{0U}); + for (const auto& b : bytes) + { + BOOST_TEST(b == std::byte{0x00}); + } + } +} + +// ============================================================================= +// from_le_bytes: reconstruct from known byte patterns +// ============================================================================= + +void test_from_le_bytes_u8() +{ + const std::array bytes {std::byte{0x42}}; + const auto val = from_le_bytes(std::span{bytes}); + BOOST_TEST(val == u8{0x42}); +} + +void test_from_le_bytes_u16() +{ + // Little-endian: least significant byte first + const std::array bytes {std::byte{0x02}, std::byte{0x01}}; + const auto val = from_le_bytes(std::span{bytes}); + BOOST_TEST(val == u16{static_cast(0x0102)}); +} + +void test_from_le_bytes_u32() +{ + const std::array bytes {std::byte{0x04}, std::byte{0x03}, std::byte{0x02}, std::byte{0x01}}; + const auto val = from_le_bytes(std::span{bytes}); + BOOST_TEST(val == u32{0x01020304U}); +} + +void test_from_le_bytes_u64() +{ + const std::array bytes { + std::byte{0x08}, std::byte{0x07}, std::byte{0x06}, std::byte{0x05}, + std::byte{0x04}, std::byte{0x03}, std::byte{0x02}, std::byte{0x01} + }; + const auto val = from_le_bytes(std::span{bytes}); + BOOST_TEST(val == u64{0x0102030405060708ULL}); +} + +void test_from_le_bytes_zero() +{ + const std::array bytes {}; + const auto val = from_le_bytes(std::span{bytes}); + BOOST_TEST(val == u32{0U}); +} + +// ============================================================================= +// Round-trip: to_le_bytes -> from_le_bytes == identity +// ============================================================================= + +template +void test_round_trip(T value) +{ + const auto bytes = to_le_bytes(value); + const auto result = from_le_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_le_bytes with dynamic extent: runtime size mismatch throws +// ============================================================================= + +// ============================================================================= +// Verified overloads: to_le_bytes and from_le_bytes at compile time +// ============================================================================= + +void test_verified_to_le_bytes() +{ + { + constexpr auto bytes = to_le_bytes(verified_u8{u8{0x42}}); + static_assert(bytes[0] == std::byte{0x42}); + } + { + constexpr auto bytes = to_le_bytes(verified_u16{u16{static_cast(0x0102)}}); + static_assert(bytes[0] == std::byte{0x02}); + static_assert(bytes[1] == std::byte{0x01}); + } + { + constexpr auto bytes = to_le_bytes(verified_u32{u32{0x01020304U}}); + static_assert(bytes[0] == std::byte{0x04}); + static_assert(bytes[1] == std::byte{0x03}); + static_assert(bytes[2] == std::byte{0x02}); + static_assert(bytes[3] == std::byte{0x01}); + } + { + constexpr auto bytes = to_le_bytes(verified_u64{u64{0x0102030405060708ULL}}); + static_assert(bytes[0] == std::byte{0x08}); + static_assert(bytes[1] == std::byte{0x07}); + static_assert(bytes[2] == std::byte{0x06}); + static_assert(bytes[3] == std::byte{0x05}); + static_assert(bytes[4] == std::byte{0x04}); + static_assert(bytes[5] == std::byte{0x03}); + static_assert(bytes[6] == std::byte{0x02}); + static_assert(bytes[7] == std::byte{0x01}); + } +} + +void test_verified_from_le_bytes() +{ + { + constexpr std::array bytes {std::byte{0x42}}; + constexpr auto val = from_le_bytes(std::span{bytes}); + static_assert(static_cast(val) == u8{0x42}); + } + { + constexpr std::array bytes {std::byte{0x04}, std::byte{0x03}, std::byte{0x02}, std::byte{0x01}}; + constexpr auto val = from_le_bytes(std::span{bytes}); + static_assert(static_cast(val) == u32{0x01020304U}); + } + { + constexpr std::array bytes { + std::byte{0x08}, std::byte{0x07}, std::byte{0x06}, std::byte{0x05}, + std::byte{0x04}, std::byte{0x03}, std::byte{0x02}, std::byte{0x01} + }; + constexpr auto val = from_le_bytes(std::span{bytes}); + static_assert(static_cast(val) == u64{0x0102030405060708ULL}); + } +} + +void test_verified_le_bytes_round_trip() +{ + { + constexpr auto original = verified_u32{u32{0xDEADBEEFU}}; + constexpr auto bytes = to_le_bytes(original); + constexpr auto reconstructed = from_le_bytes(std::span{bytes}); + static_assert(reconstructed == original); + } + { + constexpr auto original = verified_u64{u64{0x0123456789ABCDEFULL}}; + constexpr auto bytes = to_le_bytes(original); + constexpr auto reconstructed = from_le_bytes(std::span{bytes}); + static_assert(reconstructed == original); + } +} + +// ============================================================================= +// from_le_bytes with dynamic extent: runtime size mismatch throws +// ============================================================================= + +void test_from_le_bytes_dynamic_size_mismatch() +{ + const std::array bytes {std::byte{0x01}, std::byte{0x02}}; + std::span dynamic_span {bytes}; + + BOOST_TEST_THROWS(from_le_bytes(dynamic_span), std::domain_error); +} + +void test_from_le_bytes_dynamic_size_match() +{ + const std::array bytes {std::byte{0x04}, std::byte{0x03}, std::byte{0x02}, std::byte{0x01}}; + std::span dynamic_span {bytes}; + + const auto val = from_le_bytes(dynamic_span); + BOOST_TEST(val == u32{0x01020304U}); +} + +int main() +{ + test_to_le_bytes_u8(); + test_to_le_bytes_u16(); + test_to_le_bytes_u32(); + test_to_le_bytes_u64(); + test_to_le_bytes_zero(); + + test_from_le_bytes_u8(); + test_from_le_bytes_u16(); + test_from_le_bytes_u32(); + test_from_le_bytes_u64(); + test_from_le_bytes_zero(); + + test_round_trips(); + + test_verified_to_le_bytes(); + test_verified_from_le_bytes(); + test_verified_le_bytes_round_trip(); + + test_from_le_bytes_dynamic_size_mismatch(); + test_from_le_bytes_dynamic_size_match(); + + return boost::report_errors(); +}