From d5f557818541b9de402275af4da29e71f018ed65 Mon Sep 17 00:00:00 2001 From: Matt Borland Date: Tue, 17 Feb 2026 14:34:11 -0500 Subject: [PATCH 1/3] Add to_le/from_le functions --- .../boost/safe_numbers/byte_conversions.hpp | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/include/boost/safe_numbers/byte_conversions.hpp b/include/boost/safe_numbers/byte_conversions.hpp index 4e0b885..0137cb7 100644 --- a/include/boost/safe_numbers/byte_conversions.hpp +++ b/include/boost/safe_numbers/byte_conversions.hpp @@ -59,6 +59,48 @@ consteval auto from_be(const detail::verified_type_basis value) noexcept -> d return to_be(value); } +template + requires (!detail::is_verified_type_v) +constexpr auto to_le(const T value) noexcept -> T +{ + if constexpr (std::endian::native == std::endian::little) + { + return value; + } + else + { + return byteswap(value); + } +} + +template +consteval auto to_le(const detail::verified_type_basis value) noexcept -> detail::verified_type_basis +{ + if constexpr (std::endian::native == std::endian::little) + { + return value; + } + else + { + return byteswap(value); + } +} + +template + requires (!detail::is_verified_type_v) +constexpr auto from_le(const T value) noexcept -> T +{ + // Self-inverse + return to_le(value); +} + +template +consteval auto from_le(const detail::verified_type_basis value) noexcept -> detail::verified_type_basis +{ + // Self-inverse + return to_le(value); +} + } // namespace boost::safe_numbers #endif //BOOST_SAFE_NUMBERS_BYTE_CONVERSIONS_HPP From aabef855c33c691dbf773b8a58e50abff5ce03ec Mon Sep 17 00:00:00 2001 From: Matt Borland Date: Tue, 17 Feb 2026 14:34:25 -0500 Subject: [PATCH 2/3] Add testing of to_/from_le --- test/Jamfile | 1 + test/test_to_from_le.cpp | 348 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 349 insertions(+) create mode 100644 test/test_to_from_le.cpp diff --git a/test/Jamfile b/test/Jamfile index daa8b7e..8495d27 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -131,6 +131,7 @@ run test_verified_limits.cpp ; # Byte conversion tests run test_to_from_be.cpp ; +run test_to_from_le.cpp ; # Utility function tests run test_isqrt.cpp ; diff --git a/test/test_to_from_le.cpp b/test/test_to_from_le.cpp new file mode 100644 index 0000000..49bf44c --- /dev/null +++ b/test/test_to_from_le.cpp @@ -0,0 +1,348 @@ +// 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 + +#endif + +using namespace boost::safe_numbers; + +// to_le followed by from_le should round-trip to the original value +template +void test_round_trip() +{ + using basis_type = detail::underlying_type_t; + + // Zero + { + const auto val = T{0U}; + BOOST_TEST(from_le(to_le(val)) == val); + } + + // One + { + const auto val = T{1U}; + BOOST_TEST(from_le(to_le(val)) == val); + } + + // Max + { + const auto val = T{std::numeric_limits::max()}; + BOOST_TEST(from_le(to_le(val)) == val); + } + + // Arbitrary values + { + const auto val = T{static_cast(0x42)}; + BOOST_TEST(from_le(to_le(val)) == val); + } + + if constexpr (sizeof(basis_type) >= 2) + { + const auto val = T{static_cast(0xABCD)}; + BOOST_TEST(from_le(to_le(val)) == val); + } + + if constexpr (sizeof(basis_type) >= 4) + { + const auto val = T{static_cast(0xDEADBEEF)}; + BOOST_TEST(from_le(to_le(val)) == val); + } + + if constexpr (sizeof(basis_type) >= 8) + { + const auto val = T{static_cast(0x0123456789ABCDEFULL)}; + BOOST_TEST(from_le(to_le(val)) == val); + } +} + +// to_le should produce the same result as manually byteswapping on big-endian, +// or be a no-op on little-endian +template +void test_to_le_matches_byteswap() +{ + using basis_type = detail::underlying_type_t; + + const auto val = T{static_cast(0x42)}; + const auto le_val = to_le(val); + + if constexpr (std::endian::native == std::endian::little) + { + BOOST_TEST(le_val == val); + } + else + { + BOOST_TEST(le_val == byteswap(val)); + } + + if constexpr (sizeof(basis_type) >= 2) + { + const auto val2 = T{static_cast(0xABCD)}; + const auto le_val2 = to_le(val2); + + if constexpr (std::endian::native == std::endian::little) + { + BOOST_TEST(le_val2 == val2); + } + else + { + BOOST_TEST(le_val2 == byteswap(val2)); + } + } + + if constexpr (sizeof(basis_type) >= 4) + { + const auto val4 = T{static_cast(0xDEADBEEF)}; + const auto le_val4 = to_le(val4); + + if constexpr (std::endian::native == std::endian::little) + { + BOOST_TEST(le_val4 == val4); + } + else + { + BOOST_TEST(le_val4 == byteswap(val4)); + } + } + + if constexpr (sizeof(basis_type) >= 8) + { + const auto val8 = T{static_cast(0x0123456789ABCDEFULL)}; + const auto le_val8 = to_le(val8); + + if constexpr (std::endian::native == std::endian::little) + { + BOOST_TEST(le_val8 == val8); + } + else + { + BOOST_TEST(le_val8 == byteswap(val8)); + } + } +} + +// from_le is the same operation as to_le (self-inverse) +template +void test_from_le_equals_to_le() +{ + using basis_type = detail::underlying_type_t; + + const auto val = T{static_cast(0x42)}; + BOOST_TEST(from_le(val) == to_le(val)); + + if constexpr (sizeof(basis_type) >= 2) + { + const auto val2 = T{static_cast(0xABCD)}; + BOOST_TEST(from_le(val2) == to_le(val2)); + } + + if constexpr (sizeof(basis_type) >= 4) + { + const auto val4 = T{static_cast(0xDEADBEEF)}; + BOOST_TEST(from_le(val4) == to_le(val4)); + } +} + +// Double application of to_le should return the original value +template +void test_double_to_le_is_identity() +{ + using basis_type = detail::underlying_type_t; + + { + const auto val = T{0U}; + BOOST_TEST(to_le(to_le(val)) == val); + } + + { + const auto val = T{std::numeric_limits::max()}; + BOOST_TEST(to_le(to_le(val)) == val); + } + + { + const auto val = T{static_cast(0x42)}; + BOOST_TEST(to_le(to_le(val)) == val); + } +} + +// Verify known byte patterns on big-endian systems, +// and identity on little-endian systems +void test_known_byte_patterns() +{ + if constexpr (std::endian::native == std::endian::little) + { + // On little-endian, to_le is identity + { + const auto val = u16{static_cast(0x0102)}; + BOOST_TEST(to_le(val) == val); + } + + { + const auto val = u32{0x01020304U}; + BOOST_TEST(to_le(val) == val); + } + + { + const auto val = u64{0x0102030405060708ULL}; + BOOST_TEST(to_le(val) == val); + } + + { + const auto val = u8{0xAB}; + BOOST_TEST(to_le(val) == val); + } + } + else + { + // On big-endian, to_le swaps bytes + // u16: 0x0102 should become 0x0201 + { + const auto val = u16{static_cast(0x0102)}; + const auto expected = u16{static_cast(0x0201)}; + BOOST_TEST(to_le(val) == expected); + } + + // u32: 0x01020304 should become 0x04030201 + { + const auto val = u32{0x01020304U}; + const auto expected = u32{0x04030201U}; + BOOST_TEST(to_le(val) == expected); + } + + // u64: 0x0102030405060708 should become 0x0807060504030201 + { + const auto val = u64{0x0102030405060708ULL}; + const auto expected = u64{0x0807060504030201ULL}; + BOOST_TEST(to_le(val) == expected); + } + + // u8: should be identity (single byte) + { + const auto val = u8{0xAB}; + BOOST_TEST(to_le(val) == val); + } + } +} + +// to_le and to_be should be inverses of each other (on non-identity platforms) +template +void test_le_be_relationship() +{ + using basis_type = detail::underlying_type_t; + + // On any platform: from_le(to_be(val)) should equal byteswap(val) + // unless both are identity (which only happens on mixed-endian, not realistic) + { + const auto val = T{static_cast(0x42)}; + BOOST_TEST(from_le(to_le(val)) == val); + BOOST_TEST(from_be(to_be(val)) == val); + } + + if constexpr (sizeof(basis_type) >= 4) + { + const auto val = T{static_cast(0xDEADBEEF)}; + BOOST_TEST(from_le(to_le(val)) == val); + BOOST_TEST(from_be(to_be(val)) == val); + } +} + +// Verified types: to_le and from_le should work at consteval +void test_verified_consteval() +{ + // verified_u8 round-trip + { + constexpr auto val = verified_u8{u8{0x42}}; + constexpr auto le_val = to_le(val); + constexpr auto rt_val = from_le(le_val); + BOOST_TEST(rt_val == val); + } + + // verified_u16 round-trip + { + constexpr auto val = verified_u16{u16{static_cast(0xABCD)}}; + constexpr auto le_val = to_le(val); + constexpr auto rt_val = from_le(le_val); + BOOST_TEST(rt_val == val); + } + + // verified_u32 round-trip + { + constexpr auto val = verified_u32{u32{0xDEADBEEFU}}; + constexpr auto le_val = to_le(val); + constexpr auto rt_val = from_le(le_val); + BOOST_TEST(rt_val == val); + } + + // verified_u64 round-trip + { + constexpr auto val = verified_u64{u64{0x0123456789ABCDEFULL}}; + constexpr auto le_val = to_le(val); + constexpr auto rt_val = from_le(le_val); + BOOST_TEST(rt_val == val); + } + + // On little-endian, to_le for verified types should be identity + if constexpr (std::endian::native == std::endian::little) + { + constexpr auto val = verified_u32{u32{0x01020304U}}; + constexpr auto le_val = to_le(val); + BOOST_TEST(le_val == val); + } +} + +int main() +{ + // Round-trip tests for all unsigned types + test_round_trip(); + test_round_trip(); + test_round_trip(); + test_round_trip(); + test_round_trip(); + + // to_le matches byteswap on big-endian, identity on little-endian + test_to_le_matches_byteswap(); + test_to_le_matches_byteswap(); + test_to_le_matches_byteswap(); + test_to_le_matches_byteswap(); + + // from_le == to_le (self-inverse property) + test_from_le_equals_to_le(); + test_from_le_equals_to_le(); + test_from_le_equals_to_le(); + test_from_le_equals_to_le(); + + // Double application is identity + test_double_to_le_is_identity(); + test_double_to_le_is_identity(); + test_double_to_le_is_identity(); + test_double_to_le_is_identity(); + test_double_to_le_is_identity(); + + // Known byte patterns + test_known_byte_patterns(); + + // Relationship between to_le and to_be + test_le_be_relationship(); + test_le_be_relationship(); + test_le_be_relationship(); + test_le_be_relationship(); + + // Verified types (consteval) + test_verified_consteval(); + + return boost::report_errors(); +} From da4ab2d5dfecbe23dcbc9128d0a211b9f7ad9d04 Mon Sep 17 00:00:00 2001 From: Matt Borland Date: Tue, 17 Feb 2026 14:34:37 -0500 Subject: [PATCH 3/3] Add to_le/from_le to documentation page --- doc/modules/ROOT/pages/api_reference.adoc | 8 +- doc/modules/ROOT/pages/byte_conversions.adoc | 127 ++++++++++++++++++- 2 files changed, 131 insertions(+), 4 deletions(-) diff --git a/doc/modules/ROOT/pages/api_reference.adoc b/doc/modules/ROOT/pages/api_reference.adoc index a413dc4..d5ac80c 100644 --- a/doc/modules/ROOT/pages/api_reference.adoc +++ b/doc/modules/ROOT/pages/api_reference.adoc @@ -196,6 +196,12 @@ https://www.boost.org/LICENSE_1_0.txt | xref:byte_conversions.adoc[`from_be`] | Converts a safe integer from big-endian to native byte order + +| xref:byte_conversions.adoc[`to_le`] +| Converts a safe integer from native byte order to little-endian + +| xref:byte_conversions.adoc[`from_le`] +| Converts a safe integer from little-endian to native byte order |=== === Arithmetic @@ -268,7 +274,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`) +| Byte order conversion functions (`to_be`, `from_be`, `to_le`, `from_le`) | `` | 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 5b0ff48..8f414a3 100644 --- a/doc/modules/ROOT/pages/byte_conversions.adoc +++ b/doc/modules/ROOT/pages/byte_conversions.adoc @@ -10,11 +10,12 @@ https://www.boost.org/LICENSE_1_0.txt == Description -The library provides functions for converting safe integer types to and from big-endian byte order. +The library provides functions for converting safe integer types to and from big-endian or little-endian byte order. These operate on the non-bounded unsigned types (`u8`, `u16`, `u32`, `u64`, `u128`) and their verified counterparts. -On big-endian platforms these are no-ops. -On little-endian platforms they delegate to `byteswap`. +For big-endian conversions (`to_be`/`from_be`): on big-endian platforms these are no-ops; on little-endian platforms they delegate to `byteswap`. + +For little-endian conversions (`to_le`/`from_le`): on little-endian platforms these are no-ops; on big-endian platforms they delegate to `byteswap`. [source,c++] ---- @@ -140,3 +141,123 @@ constexpr auto val = verified_u32{u32{0xDEADBEEF}}; constexpr auto round_tripped = from_be(to_be(val)); static_assert(round_tripped == val); ---- + +== to_le + +Converts a value from the native byte order to little-endian byte order. + +=== Runtime Overload + +[source,c++] +---- +template + requires (!is_verified_type_v) +constexpr auto to_le(const T value) noexcept -> T; +---- + +==== Parameters + +* `value` -- The value to convert. + +==== Return Value + +If `std::endian::native == std::endian::little`, returns `value` unchanged. +Otherwise, returns `byteswap(value)`. + +==== Complexity + +O(1). + +==== Example + +[source,c++] +---- +using namespace boost::safe_numbers; + +auto le_val = to_le(u32{0x01020304}); +// On little-endian: le_val == u32{0x01020304} (identity) +// On big-endian: le_val == u32{0x04030201} +---- + +=== Verified Overload + +[source,c++] +---- +template +consteval auto to_le(const verified_type_basis value) noexcept -> verified_type_basis; +---- + +Compile-time only overload for verified types. + +Since `to_le` is `consteval` for verified types, the result is guaranteed to be a compile-time constant. + +==== Example + +[source,c++] +---- +using namespace boost::safe_numbers; + +constexpr auto le_val = to_le(verified_u32{u32{0x01020304}}); +---- + +== from_le + +Converts a value from little-endian byte order to the native byte order. + +This is the inverse of `to_le`. +Since byte swapping is its own inverse, `from_le` delegates directly to `to_le`. + +=== Runtime Overload + +[source,c++] +---- +template + requires (!is_verified_type_v) +constexpr auto from_le(const T value) noexcept -> T; +---- + +==== Parameters + +* `value` -- The little-endian value to convert to native byte order. + +==== Return Value + +The value in native byte order. +Equivalent to `to_le(value)`. + +==== Complexity + +O(1). + +==== Example + +[source,c++] +---- +using namespace boost::safe_numbers; + +auto native_val = from_le(to_le(u64{0x0123456789ABCDEF})); +// native_val == u64{0x0123456789ABCDEF} (round-trip) +---- + +=== Verified Overload + +[source,c++] +---- +template +consteval auto from_le(const verified_type_basis value) noexcept -> verified_type_basis; +---- + +Compile-time only overload for verified types. + +Since `from_le` is `consteval` for verified types, the result is guaranteed to be a compile-time constant. + +==== Example + +[source,c++] +---- +using namespace boost::safe_numbers; + +constexpr auto val = verified_u32{u32{0xDEADBEEF}}; +constexpr auto round_tripped = from_le(to_le(val)); +static_assert(round_tripped == val); +----