diff --git a/doc/modules/ROOT/pages/api_reference.adoc b/doc/modules/ROOT/pages/api_reference.adoc index ee05a89..aa77884 100644 --- a/doc/modules/ROOT/pages/api_reference.adoc +++ b/doc/modules/ROOT/pages/api_reference.adoc @@ -224,6 +224,12 @@ https://www.boost.org/LICENSE_1_0.txt | xref:byte_conversions.adoc[`from_le_bytes`] | Reconstructs a safe integer from a little-endian byte array + +| xref:byte_conversions.adoc[`to_ne_bytes`] +| Converts a safe integer to a native-endian byte array + +| xref:byte_conversions.adoc[`from_ne_bytes`] +| Reconstructs a safe integer from a native-endian byte array |=== === Arithmetic @@ -296,7 +302,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`, `to_be_bytes`, `from_be_bytes`, `to_le_bytes`, `from_le_bytes`) +| Byte order conversion functions (`to_be`, `from_be`, `to_le`, `from_le`, `to_be_bytes`, `from_be_bytes`, `to_le_bytes`, `from_le_bytes`, `to_ne_bytes`, `from_ne_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 17e4d29..3c7b754 100644 --- a/doc/modules/ROOT/pages/byte_conversions.adoc +++ b/doc/modules/ROOT/pages/byte_conversions.adoc @@ -323,9 +323,12 @@ constexpr auto bytes = to_be_bytes(verified_u32{u32{0x01020304}}); 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`. +=== Runtime Overload + [source,c++] ---- template + requires (!is_verified_type_v) constexpr auto from_be_bytes(const std::span bytes) -> T; ---- @@ -366,6 +369,33 @@ auto reconstructed = from_be_bytes(std::span{to_be_byte // reconstructed == original ---- +=== Verified Overload + +[source,c++] +---- +template +consteval auto from_be_bytes(const std::span bytes) -> T; +---- + +Compile-time only overload for verified types. +The span must have a fixed extent matching `sizeof(T)`; a mismatched extent produces a `static_assert` failure. + +==== Example + +[source,c++] +---- +using namespace boost::safe_numbers; + +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}); +// val == verified_u32{u32{0x01020304}} + +// Round-trip +constexpr auto original = verified_u32{u32{0xDEADBEEF}}; +constexpr auto reconstructed = from_be_bytes(std::span{to_be_bytes(original)}); +static_assert(reconstructed == original); +---- + == to_le_bytes Converts a safe integer value to a little-endian byte array. @@ -427,9 +457,12 @@ constexpr auto bytes = to_le_bytes(verified_u32{u32{0x01020304}}); 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`. +=== Runtime Overload + [source,c++] ---- template + requires (!is_verified_type_v) constexpr auto from_le_bytes(const std::span bytes) -> T; ---- @@ -469,3 +502,160 @@ auto original = u32{0xDEADBEEF}; auto reconstructed = from_le_bytes(std::span{to_le_bytes(original)}); // reconstructed == original ---- + +=== Verified Overload + +[source,c++] +---- +template +consteval auto from_le_bytes(const std::span bytes) -> T; +---- + +Compile-time only overload for verified types. +The span must have a fixed extent matching `sizeof(T)`; a mismatched extent produces a `static_assert` failure. + +==== Example + +[source,c++] +---- +using namespace boost::safe_numbers; + +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}); +// val == verified_u32{u32{0x01020304}} + +// Round-trip +constexpr auto original = verified_u32{u32{0xDEADBEEF}}; +constexpr auto reconstructed = from_le_bytes(std::span{to_le_bytes(original)}); +static_assert(reconstructed == original); +---- + +== to_ne_bytes + +Converts a safe integer value to a native-endian byte array. +Delegates to `to_le_bytes` on little-endian platforms and `to_be_bytes` on big-endian platforms. + +The result is equivalent to `std::bit_cast>(value)` -- i.e., the raw in-memory representation. + +=== Runtime Overload + +[source,c++] +---- +template + requires (!is_verified_type_v) +constexpr auto to_ne_bytes(const T value) noexcept -> std::array; +---- + +==== Parameters + +* `value` -- The value to convert. + +==== Return Value + +A `std::array` containing the value's bytes in native byte order. + +==== Complexity + +O(1). + +==== Example + +[source,c++] +---- +using namespace boost::safe_numbers; + +auto bytes = to_ne_bytes(u32{0x01020304}); +// On little-endian: bytes == {0x04, 0x03, 0x02, 0x01} +// On big-endian: bytes == {0x01, 0x02, 0x03, 0x04} +---- + +=== Verified Overload + +[source,c++] +---- +template +consteval auto to_ne_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_ne_bytes(verified_u32{u32{0x01020304}}); +---- + +== from_ne_bytes + +Reconstructs a safe integer value from a native-endian byte array. +Delegates to `from_le_bytes` on little-endian platforms and `from_be_bytes` on big-endian platforms. + +=== Runtime Overload + +[source,c++] +---- +template + requires (!is_verified_type_v) +constexpr auto from_ne_bytes(const std::span bytes) -> T; +---- + +==== Parameters + +* `bytes` -- A span of bytes in native byte order. May have a fixed extent or `std::dynamic_extent`. + +==== Return Value + +The reconstructed safe integer value. + +==== 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; + +// Round-trip +auto original = u32{0xDEADBEEF}; +auto reconstructed = from_ne_bytes(std::span{to_ne_bytes(original)}); +// reconstructed == original +---- + +=== Verified Overload + +[source,c++] +---- +template +consteval auto from_ne_bytes(const std::span bytes) -> T; +---- + +Compile-time only overload for verified types. +The span must have a fixed extent matching `sizeof(T)`; a mismatched extent produces a `static_assert` failure. + +==== Example + +[source,c++] +---- +using namespace boost::safe_numbers; + +constexpr auto original = verified_u32{u32{0xDEADBEEF}}; +constexpr auto reconstructed = from_ne_bytes(std::span{to_ne_bytes(original)}); +static_assert(reconstructed == original); +---- + +== Example + +For a complete example demonstrating all byte conversion functions, see xref:examples.adoc#examples_byte_conversions[Byte Conversions]. diff --git a/doc/modules/ROOT/pages/examples.adoc b/doc/modules/ROOT/pages/examples.adoc index f4a414d..6973ea2 100644 --- a/doc/modules/ROOT/pages/examples.adoc +++ b/doc/modules/ROOT/pages/examples.adoc @@ -414,6 +414,51 @@ shr(u32(8), 1) = 4 ---- ==== +[#examples_byte_conversions] +== Byte Conversions + +The library provides functions for converting safe integers to and from byte arrays in big-endian, little-endian, or native byte order. +Verified types support the same conversions at compile time via `consteval` overloads. + +.This https://github.com/boostorg/safe_numbers/blob/develop/examples/byte_conversions.cpp[example] demonstrates byte conversion functions with all safe integer types. +==== +[source, c++] +---- +include::example$byte_conversions.cpp[] +---- + +Output: +---- +=== to_be_bytes === +u32(0x01020304) -> BE bytes: 01 02 03 04 +u16(0xABCD) -> BE bytes: ab cd + +=== from_be_bytes === +BE bytes {01,02,03,04} -> u32: 0x1020304 + +=== to_le_bytes === +u32(0x01020304) -> LE bytes: 04 03 02 01 +u64(0x01..08) -> LE bytes: 08 07 06 05 04 03 02 01 + +=== from_le_bytes === +LE bytes {04,03,02,01} -> u32: 0x1020304 + +=== to_ne_bytes / from_ne_bytes (native endian) === +u32(0xDEADBEEF) -> NE bytes: ef be ad de +Round-trip: -> u32: 0xdeadbeef + +=== u8 round-trip === +u8(0x42) -> BE: 42 +u8(0x42) -> LE: 42 + +=== Verified types (compile-time) === +verified_u32 to_be_bytes: 01 02 03 04 +verified_u32 BE round-trip: 0xdeadbeef +verified_u64 LE round-trip: 0x123456789abcdef +verified_u32 NE round-trip: 0xcafebabe +---- +==== + [#examples_verified_construction] == Verified Types: Construction and Runtime Usage diff --git a/examples/byte_conversions.cpp b/examples/byte_conversions.cpp new file mode 100644 index 0000000..125af6d --- /dev/null +++ b/examples/byte_conversions.cpp @@ -0,0 +1,155 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include +#include +#include +#include +#include +#include +#include + +int main() +{ + using namespace boost::safe_numbers; + + // ---- to_be_bytes: convert to big-endian byte array ---- + std::cout << "=== to_be_bytes ===\n"; + { + const auto bytes = to_be_bytes(u32{0x01020304U}); + std::cout << "u32(0x01020304) -> BE bytes: "; + for (const auto& b : bytes) + { + std::cout << std::hex << std::setfill('0') << std::setw(2) + << static_cast(b) << ' '; + } + std::cout << '\n'; + } + { + const auto bytes = to_be_bytes(u16{static_cast(0xABCD)}); + std::cout << "u16(0xABCD) -> BE bytes: "; + for (const auto& b : bytes) + { + std::cout << std::hex << std::setfill('0') << std::setw(2) + << static_cast(b) << ' '; + } + std::cout << '\n'; + } + + // ---- from_be_bytes: reconstruct from big-endian bytes ---- + std::cout << "\n=== from_be_bytes ===\n"; + { + 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}); + std::cout << "BE bytes {01,02,03,04} -> u32: 0x" + << std::hex << static_cast(val) << '\n'; + } + + // ---- to_le_bytes: convert to little-endian byte array ---- + std::cout << "\n=== to_le_bytes ===\n"; + { + const auto bytes = to_le_bytes(u32{0x01020304U}); + std::cout << "u32(0x01020304) -> LE bytes: "; + for (const auto& b : bytes) + { + std::cout << std::hex << std::setfill('0') << std::setw(2) + << static_cast(b) << ' '; + } + std::cout << '\n'; + } + { + const auto bytes = to_le_bytes(u64{0x0102030405060708ULL}); + std::cout << "u64(0x01..08) -> LE bytes: "; + for (const auto& b : bytes) + { + std::cout << std::hex << std::setfill('0') << std::setw(2) + << static_cast(b) << ' '; + } + std::cout << '\n'; + } + + // ---- from_le_bytes: reconstruct from little-endian bytes ---- + std::cout << "\n=== from_le_bytes ===\n"; + { + 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}); + std::cout << "LE bytes {04,03,02,01} -> u32: 0x" + << std::hex << static_cast(val) << '\n'; + } + + // ---- to_ne_bytes / from_ne_bytes: native endian round-trip ---- + std::cout << "\n=== to_ne_bytes / from_ne_bytes (native endian) ===\n"; + { + const auto original = u32{0xDEADBEEFU}; + const auto bytes = to_ne_bytes(original); + std::cout << "u32(0xDEADBEEF) -> NE bytes: "; + for (const auto& b : bytes) + { + std::cout << std::hex << std::setfill('0') << std::setw(2) + << static_cast(b) << ' '; + } + std::cout << '\n'; + + const auto reconstructed = from_ne_bytes(std::span{bytes}); + std::cout << "Round-trip: -> u32: 0x" + << std::hex << static_cast(reconstructed) << '\n'; + } + + // ---- BE round-trip with u8 ---- + std::cout << "\n=== u8 round-trip ===\n"; + { + const auto original = u8{0x42}; + const auto be = to_be_bytes(original); + const auto le = to_le_bytes(original); + std::cout << "u8(0x42) -> BE: " << std::hex << std::setfill('0') << std::setw(2) + << static_cast(be[0]) << '\n'; + std::cout << "u8(0x42) -> LE: " << std::hex << std::setfill('0') << std::setw(2) + << static_cast(le[0]) << '\n'; + } + + // ---- Verified types (compile-time) ---- + std::cout << "\n=== Verified types (compile-time) ===\n"; + { + constexpr auto bytes = to_be_bytes(verified_u32{u32{0x01020304U}}); + static_assert(bytes[0] == std::byte{0x01}); + static_assert(bytes[3] == std::byte{0x04}); + std::cout << "verified_u32 to_be_bytes: "; + for (const auto& b : bytes) + { + std::cout << std::hex << std::setfill('0') << std::setw(2) + << static_cast(b) << ' '; + } + std::cout << '\n'; + } + { + 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); + std::cout << "verified_u32 BE round-trip: 0x" << std::hex + << static_cast(static_cast(reconstructed)) << '\n'; + } + { + 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); + std::cout << "verified_u64 LE round-trip: 0x" << std::hex + << static_cast(static_cast(reconstructed)) << '\n'; + } + { + constexpr auto original = verified_u32{u32{0xCAFEBABEU}}; + constexpr auto bytes = to_ne_bytes(original); + constexpr auto reconstructed = from_ne_bytes(std::span{bytes}); + static_assert(reconstructed == original); + std::cout << "verified_u32 NE round-trip: 0x" << std::hex + << static_cast(static_cast(reconstructed)) << '\n'; + } + + return 0; +} diff --git a/include/boost/safe_numbers/byte_conversions.hpp b/include/boost/safe_numbers/byte_conversions.hpp index 7cf3df1..d8b55ff 100644 --- a/include/boost/safe_numbers/byte_conversions.hpp +++ b/include/boost/safe_numbers/byte_conversions.hpp @@ -234,6 +234,60 @@ consteval auto from_le_bytes(const std::span bytes) -> T return from_le(T{std::bit_cast(arr)}); } +template + requires (!detail::is_verified_type_v) +constexpr auto to_ne_bytes(const T value) noexcept -> std::array +{ + if constexpr (std::endian::native == std::endian::little) + { + return to_le_bytes(value); + } + else + { + return to_be_bytes(value); + } +} + +template +consteval auto to_ne_bytes(const detail::verified_type_basis value) noexcept -> std::array +{ + if constexpr (std::endian::native == std::endian::little) + { + return to_le_bytes(value); + } + else + { + return to_be_bytes(value); + } +} + +template + requires (!detail::is_verified_type_v) +constexpr auto from_ne_bytes(const std::span bytes) -> T +{ + if constexpr (std::endian::native == std::endian::little) + { + return from_le_bytes(bytes); + } + else + { + return from_be_bytes(bytes); + } +} + +template +consteval auto from_ne_bytes(const std::span bytes) -> T +{ + if constexpr (std::endian::native == std::endian::little) + { + return from_le_bytes(bytes); + } + else + { + return from_be_bytes(bytes); + } +} + } // namespace boost::safe_numbers #endif //BOOST_SAFE_NUMBERS_BYTE_CONVERSIONS_HPP diff --git a/test/Jamfile b/test/Jamfile index 8948f7f..2c05578 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -136,6 +136,7 @@ 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 ; +run test_to_from_ne_bytes.cpp ; # Utility function tests run test_isqrt.cpp ; @@ -179,3 +180,4 @@ run ../examples/verified_runtime_usage.cpp ; run ../examples/verified_mixed_ops.cpp ; compile-fail ../examples/compile_fail_verified_overflow.cpp ; run ../examples/safe_numerics_comparison.cpp ; +run ../examples/byte_conversions.cpp ; diff --git a/test/test_to_from_ne_bytes.cpp b/test/test_to_from_ne_bytes.cpp new file mode 100644 index 0000000..ac36321 --- /dev/null +++ b/test/test_to_from_ne_bytes.cpp @@ -0,0 +1,211 @@ +// 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_ne_bytes: the byte representation matches the platform's native layout +// ============================================================================= + +void test_to_ne_bytes_matches_platform() +{ + // On little-endian, to_ne_bytes should match to_le_bytes + // On big-endian, to_ne_bytes should match to_be_bytes + { + const auto ne_bytes = to_ne_bytes(u32{0x01020304U}); + if constexpr (std::endian::native == std::endian::little) + { + const auto le_bytes = to_le_bytes(u32{0x01020304U}); + BOOST_TEST(ne_bytes == le_bytes); + } + else + { + const auto be_bytes = to_be_bytes(u32{0x01020304U}); + BOOST_TEST(ne_bytes == be_bytes); + } + } + { + const auto ne_bytes = to_ne_bytes(u64{0x0102030405060708ULL}); + if constexpr (std::endian::native == std::endian::little) + { + const auto le_bytes = to_le_bytes(u64{0x0102030405060708ULL}); + BOOST_TEST(ne_bytes == le_bytes); + } + else + { + const auto be_bytes = to_be_bytes(u64{0x0102030405060708ULL}); + BOOST_TEST(ne_bytes == be_bytes); + } + } +} + +void test_to_ne_bytes_is_identity() +{ + // to_ne_bytes should produce the same bytes as a bit_cast of the value + { + const auto val = u32{0xDEADBEEFU}; + const auto ne_bytes = to_ne_bytes(val); + const auto raw_bytes = std::bit_cast>(val); + BOOST_TEST(ne_bytes == raw_bytes); + } + { + const auto val = u16{static_cast(0xABCD)}; + const auto ne_bytes = to_ne_bytes(val); + const auto raw_bytes = std::bit_cast>(val); + BOOST_TEST(ne_bytes == raw_bytes); + } + { + const auto val = u64{0x0123456789ABCDEFULL}; + const auto ne_bytes = to_ne_bytes(val); + const auto raw_bytes = std::bit_cast>(val); + BOOST_TEST(ne_bytes == raw_bytes); + } +} + +// ============================================================================= +// from_ne_bytes: reconstruct from native byte patterns +// ============================================================================= + +void test_from_ne_bytes_matches_platform() +{ + { + const std::array bytes {std::byte{0x01}, std::byte{0x02}, std::byte{0x03}, std::byte{0x04}}; + const auto ne_val = from_ne_bytes(std::span{bytes}); + if constexpr (std::endian::native == std::endian::little) + { + const auto le_val = from_le_bytes(std::span{bytes}); + BOOST_TEST(ne_val == le_val); + } + else + { + const auto be_val = from_be_bytes(std::span{bytes}); + BOOST_TEST(ne_val == be_val); + } + } +} + +// ============================================================================= +// Round-trip: to_ne_bytes -> from_ne_bytes == identity +// ============================================================================= + +template +void test_round_trip(T value) +{ + const auto bytes = to_ne_bytes(value); + const auto result = from_ne_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_ne_bytes with dynamic extent +// ============================================================================= + +void test_from_ne_bytes_dynamic_size_mismatch() +{ + const std::array bytes {std::byte{0x01}, std::byte{0x02}}; + std::span dynamic_span {bytes}; + + BOOST_TEST_THROWS(from_ne_bytes(dynamic_span), std::domain_error); +} + +void test_from_ne_bytes_dynamic_size_match() +{ + const auto val = u32{0x01020304U}; + const auto bytes = to_ne_bytes(val); + std::span dynamic_span {bytes}; + + const auto result = from_ne_bytes(dynamic_span); + BOOST_TEST(result == val); +} + +// ============================================================================= +// Verified overloads +// ============================================================================= + +void test_verified_to_ne_bytes() +{ + // Verify consteval works and produces correct bytes via round-trip + { + constexpr auto original = verified_u32{u32{0x01020304U}}; + constexpr auto bytes = to_ne_bytes(original); + static_assert(bytes.size() == 4); + } + { + constexpr auto original = verified_u64{u64{0x0102030405060708ULL}}; + constexpr auto bytes = to_ne_bytes(original); + static_assert(bytes.size() == 8); + } +} + +void test_verified_from_ne_bytes() +{ + { + constexpr auto original = verified_u32{u32{0xDEADBEEFU}}; + constexpr auto bytes = to_ne_bytes(original); + constexpr auto reconstructed = from_ne_bytes(std::span{bytes}); + static_assert(reconstructed == original); + } + { + constexpr auto original = verified_u64{u64{0x0123456789ABCDEFULL}}; + constexpr auto bytes = to_ne_bytes(original); + constexpr auto reconstructed = from_ne_bytes(std::span{bytes}); + static_assert(reconstructed == original); + } +} + +int main() +{ + test_to_ne_bytes_matches_platform(); + test_to_ne_bytes_is_identity(); + + test_from_ne_bytes_matches_platform(); + + test_round_trips(); + + test_from_ne_bytes_dynamic_size_mismatch(); + test_from_ne_bytes_dynamic_size_match(); + + test_verified_to_ne_bytes(); + test_verified_from_ne_bytes(); + + return boost::report_errors(); +}