Repository: randombit/botan
Pull request #3869 (2024-01-03 10:14:07)

Refactor: Final iteration on load/store
### Description

This should be the final iteration on the big/little endian helpers for now (modulo the eventual removal of the legacy ptr-based overloads).

The patch fulfills three main objectives:

1. **Remove the inherent code duplication** of the load/store overloads for big-endian and little endian
   This is achieved by introducing `detail::load_any<>` and `detail::store_any<>` with a template parameter `Endianness`. By passing this all the way to the lowest-level overload, we can do the distinction *once* and implement convenience overloads without duplicating them each time (+more duplication for legacy overloads)
2. **Support strong types and enums that wrap unsigned integers**
   This allows for things like: `load_be<MyIntStrongType>(some_bytes)`, or `store_be(some_enum_value)`. It makes working with strong types and enums more convenient when loading/storing them.
3. **Allow loading/storing collections of unsigned integers**
   Before, one could load/store multiple variables at once (passed as variadic parameters). Now, its possible to pass ranges of integers (similarly to `typecast_copy()`). That is useful initializing and extracting internal hash states (that are collections of `uint64_t`), for instance.
4. **constexpr**
   The whole range of features described above may also be used at compile-time. For instance, to initialize algorithm parameters that need a certain endianness.

### Quick and Dirty Benchmark

Running `./botan speed` both on the feature branch and on the `master` commit it is based on, both compiled with clang-15 on ubuntu 22.04 (running in WSL). **Is that a reasonable smoke test?**

Many algorithms actually seem to benefit from this patch (without modifying the used load/store overloads). For instance, AES encryption/decryption (using NI extensions) clocks in at a 7% speedup. **Is that even realistic?** 😲 Keccak seems to suffer slightly. For instance, the SHAKE throughput is about 2% less.

Here's the full data set: [bench_loadstore_refactoring.txt](https://github.com/randombit/botan/files/13906243/bench_loadstore_refactoring.txt)


coveralls: 
[![Coverage Status](https://coveralls.io/builds/64856099/badge)](https://coveralls.io/builds/64856099)

coverage: 92.074% (+0.01%) from 92.062%
when pulling **5ad0f311ddb318fc3df3564cfb4f2beab1ef5f1d on Rohde-Schwarz:chore/more_loadstore_convenience**
into **541a29f6f7e49a7eb2e84730471ef129b1347557 on randombit:master**.

reneme: Rebased after #3888.

Patch:
diff --git a/src/lib/utils/bswap.h b/src/lib/utils/bswap.h
index a010063a7..2a7045a43 100644
--- a/src/lib/utils/bswap.h
+++ b/src/lib/utils/bswap.h
@@ -3,6 +3,8 @@
 * (C) 1999-2011,2018 Jack Lloyd
 * (C) 2007 Yves Jerschow
 *
+* TODO: C++23: replace this entire implementation with std::byteswap
+*
 * Botan is released under the Simplified BSD License (see license.txt)
 */
 
diff --git a/src/lib/utils/concepts.h b/src/lib/utils/concepts.h
index 07e2af445..78366041a 100644
--- a/src/lib/utils/concepts.h
+++ b/src/lib/utils/concepts.h
@@ -67,6 +67,17 @@ concept contiguous_output_range = contiguous_range<T, ValueT> && std::ranges::ou
 template <typename T>
 concept spanable_range = std::constructible_from<std::span<const std::ranges::range_value_t<T>>, T>;
 
+/**
+ * Models a range that can be turned into a std::span<> with a static extent.
+ * Typically, this is a std::array or a std::span derived from an array.
+ */
+// clang-format off
+template <typename T>
+concept statically_spanable_range = spanable_range<T> &&
+                                    decltype(std::span{std::declval<T&>()})::extent != std::dynamic_extent;
+
+// clang-format on
+
 /**
  * Find the length in bytes of a given contiguous range @p r.
  */
@@ -85,7 +96,7 @@ inline constexpr size_t size_bytes(spanable_range auto&& r) {
 template <size_t expected, spanable_range R>
 inline constexpr void assert_exact_byte_length(R&& r) {
    const std::span s{r};
-   if constexpr(decltype(s)::extent != std::dynamic_extent) {
+   if constexpr(statically_spanable_range<R>) {
       static_assert(s.size_bytes() == expected, "memory region does not have expected byte lengths");
    } else {
       BOTAN_ASSERT(s.size_bytes() == expected, "memory region does not have expected byte lengths");
@@ -107,7 +118,7 @@ inline constexpr void assert_equal_byte_lengths(R0&& r0, Rs&&... rs)
 {
    const std::span s0{r0};
 
-   if constexpr(decltype(s0)::extent != std::dynamic_extent) {
+   if constexpr(statically_spanable_range<R0>) {
       constexpr size_t expected_size = s0.size_bytes();
       (assert_exact_byte_length<expected_size>(rs), ...);
    } else {
@@ -172,6 +183,9 @@ concept strong_type = is_strong_type_v<T>;
 template <class T>
 concept contiguous_strong_type = strong_type<T> && contiguous_container<T>;
 
+template <class T>
+concept unsigned_integral_strong_type = strong_type<T> && std::unsigned_integral<typename T::wrapped_type>;
+
 }  // namespace concepts
 
 }  // namespace Botan
diff --git a/src/lib/utils/loadstor.h b/src/lib/utils/loadstor.h
index fd5d28755..09c372fd2 100644
--- a/src/lib/utils/loadstor.h
+++ b/src/lib/utils/loadstor.h
@@ -2,6 +2,7 @@
 * Load/Store Operators
 * (C) 1999-2007,2015,2017 Jack Lloyd
 *     2007 Yves Jerschow
+*     2023-2024 Fabian Albert, René Meusel - Rohde & Schwarz Cybersecurity
 *
 * Botan is released under the Simplified BSD License (see license.txt)
 */
@@ -9,12 +10,49 @@
 #ifndef BOTAN_LOAD_STORE_H_
 #define BOTAN_LOAD_STORE_H_
 
+#include <botan/concepts.h>
 #include <botan/mem_ops.h>
 #include <botan/types.h>
 #include <botan/internal/bswap.h>
-#include <concepts>
 #include <vector>
 
+/**
+ * @file loadstor.h
+ *
+ * @brief This header contains various helper functions to load and store
+ *        unsigned integers in big- or little-endian byte order.
+ *
+ * Storing integer values in various ways (same for BE and LE):
+ * @code {.cpp}
+ *
+ *   std::array<uint8_t, 8> bytes = store_le(some_uint64);
+ *   std::array<uint8_t, 12> bytes = store_le(some_uint32_1, some_uint32_2, some_uint32_3, ...);
+ *   auto bytes = store_le<std::vector<uint8_t>>(some_uint64);
+ *   auto bytes = store_le<MyContainerStrongType>(some_uint64);
+ *   auto bytes = store_le<std::vector<uint8_t>>(vector_of_ints);
+ *   auto bytes = store_le<secure_vector<uint8_t>>(some_uint32_1, some_uint32_2, some_uint32_3, ...);
+ *   store_le(bytes, some_uint64);
+ *   store_le(concatenated_bytes, some_uint64_1, some_uint64_2, some_uint64_3, ...);
+ *   store_le(concatenated_bytes, vector_of_ints);
+ *   copy_out_le(short_concated_bytes, vector_of_ints); // stores as many bytes as required in the output buffer
+ *
+ * @endcode
+ *
+ * Loading integer values in various ways (same for BE and LE):
+ * @code {.cpp}
+ *
+ *   uint64_t some_uint64 = load_le(bytes_8);
+ *   auto some_int32s = load_le<std::vector<uint32_t>>(concatenated_bytes);
+ *   auto some_int32s = load_le<std::vector<MyIntStrongType>>(concatenated_bytes);
+ *   auto some_int32s = load_le(some_strong_typed_bytes);
+ *   auto strong_int  = load_le<MyStrongTypedInteger>(concatenated_bytes);
+ *   load_le(concatenated_bytes, out_some_uint64);
+ *   load_le(concatenated_bytes, out_some_uint64_1, out_some_uint64_2, out_some_uint64_3, ...);
+ *   load_le(out_vector_of_ints, concatenated_bytes);
+ *
+ * @endcode
+ */
+
 namespace Botan {
 
 /**
@@ -83,174 +121,185 @@ inline constexpr uint64_t make_uint64(
            (static_cast<uint64_t>(i6) << 8) | (static_cast<uint64_t>(i7)));
 }
 
+namespace detail {
+
+enum class Endianness : bool {
+   Big,
+   Little,
+};
+
+struct AutoDetect {
+      constexpr AutoDetect() = delete;
+};
+
 /**
-* Load a big-endian unsigned integer
-* @param in_range a fixed-length span with some bytes
-* @return T loaded from in, as a big-endian value
-*/
-template <std::unsigned_integral T, ranges::contiguous_range<uint8_t> InR>
-inline constexpr T load_be(InR&& in_range) {
-   ranges::assert_exact_byte_length<sizeof(T)>(in_range);
-   std::span in{in_range};
-   if constexpr(sizeof(T) == 1) {
-      return static_cast<T>(in[0]);
-   } else {
+ * @warning This function may return false if the native endianness is unknown
+ * @returns true iff the native endianness matches the given endianness
+ */
+constexpr bool is_native(Endianness endianness) {
 #if defined(BOTAN_TARGET_CPU_IS_BIG_ENDIAN)
-      return typecast_copy<T>(in);
+   return endianness == Endianness::Big;
 #elif defined(BOTAN_TARGET_CPU_IS_LITTLE_ENDIAN)
-      return reverse_bytes(typecast_copy<T>(in));
+   return endianness == Endianness::Little;
 #else
-      return [&]<size_t... i>(std::index_sequence<i...>) {
-         return ((static_cast<T>(in[i]) << ((sizeof(T) - i - 1) * 8)) | ...);
-      }
-      (std::make_index_sequence<sizeof(T)>());
+   return false;
 #endif
-   }
 }
 
 /**
-* Load a little-endian unsigned integer
-* @param in_range a fixed-length span with some bytes
-* @return T loaded from in, as a little-endian value
-*/
-template <std::unsigned_integral T, ranges::contiguous_range<uint8_t> InR>
-inline constexpr T load_le(InR&& in_range) {
-   ranges::assert_exact_byte_length<sizeof(T)>(in_range);
-   std::span in{in_range};
-   if constexpr(sizeof(T) == 1) {
-      return static_cast<T>(in[0]);
-   } else {
+ * @warning This function may return false if the native endianness is unknown
+ * @returns true iff the native endianness does not match the given endianness
+ */
+constexpr bool is_opposite(Endianness endianness) {
 #if defined(BOTAN_TARGET_CPU_IS_BIG_ENDIAN)
-      return reverse_bytes(typecast_copy<T>(in));
+   return endianness == Endianness::Little;
 #elif defined(BOTAN_TARGET_CPU_IS_LITTLE_ENDIAN)
-      return typecast_copy<T>(in);
+   return endianness == Endianness::Big;
 #else
-      return [&]<size_t... i>(std::index_sequence<i...>) {
-         return ((static_cast<T>(in[i]) << (i * 8)) | ...);
-      }
-      (std::make_index_sequence<sizeof(T)>());
+   return false;
 #endif
-   }
 }
 
-namespace details {
-
-/**
-* Given a statically sized range @p r of size 1, 2, 4, or 8, this finds the
-* corresponding unsigned integer type to hold the value of @p r. Note, that
-* this only maps the type, and does not perform any loading.
-*/
-template <ranges::contiguous_range<uint8_t> InR>
-consteval auto default_load_type(InR&& r) {
-   std::span s{r};
-   static_assert(decltype(s)::extent != std::dynamic_extent,
-                 "Cannot determine the output type based on a dynamically sized bytearray");
-   constexpr size_t e = decltype(s)::extent;
-
-   // clang-format off
-   using type =
-      std::conditional_t<e == 1, uint8_t,
-      std::conditional_t<e == 2, uint16_t,
-      std::conditional_t<e == 4, uint32_t,
-      std::conditional_t<e == 8, uint64_t, void>>>>;
-   // clang-format on
-
-   static_assert(
-      !std::is_void_v<type>,
-      "Cannot determine the output type based on a statically sized bytearray with length other than those: 1, 2, 4, 8");
-
-   return type{};
+template <Endianness endianness>
+constexpr bool native_endianness_is_unknown() {
+#if defined(BOTAN_TARGET_CPU_IS_BIG_ENDIAN) || defined(BOTAN_TARGET_CPU_IS_LITTLE_ENDIAN)
+   return false;
+#else
+   return true;
+#endif
 }
 
-}  // namespace details
-
 /**
-* Load a little-endian unsigned integer, auto-detect the output type
-* @param in_range a statically-sized range with some bytes
-* @return T loaded from in, as a little-endian value
-*/
-template <ranges::contiguous_range<uint8_t> InR>
-inline constexpr auto load_le(InR&& in_range) {
-   return load_le<decltype(details::default_load_type(in_range)), InR>(std::forward<InR>(in_range));
-}
+ * Models a type that can be loaded/stored from/to a byte range.
+ */
+template <typename T>
+concept unsigned_integralish = std::unsigned_integral<T> || concepts::unsigned_integral_strong_type<T> ||
+                               (std::is_enum_v<T> && std::unsigned_integral<std::underlying_type_t<T>>);
 
 /**
-* Load a big-endian unsigned integer, auto-detect the output type
-* @param in_range a statically-sized range with some bytes
-* @return T loaded from in, as a big-endian value
-*/
-template <ranges::contiguous_range<uint8_t> InR>
-inline constexpr auto load_be(InR&& in_range) {
-   return load_be<decltype(details::default_load_type(in_range)), InR>(std::forward<InR>(in_range));
+ * Manually load a word from a range in either big or little endian byte order.
+ * This will be used only if the endianness of the target platform is unknown at
+ * compile time.
+ */
+template <Endianness endianness, std::unsigned_integral OutT, ranges::contiguous_range<uint8_t> InR>
+inline constexpr OutT fallback_load_any(InR&& in_range) {
+   std::span in{in_range};
+   // clang-format off
+   if constexpr(endianness == Endianness::Big) {
+      return [&]<size_t... i>(std::index_sequence<i...>) {
+         return static_cast<OutT>(((static_cast<OutT>(in[i]) << ((sizeof(OutT) - i - 1) * 8)) | ...));
+      } (std::make_index_sequence<sizeof(OutT)>());
+   } else {
+      static_assert(endianness == Endianness::Little);
+      return [&]<size_t... i>(std::index_sequence<i...>) {
+         return static_cast<OutT>(((static_cast<OutT>(in[i]) << (i * 8)) | ...));
+      } (std::make_index_sequence<sizeof(OutT)>());
+   }
+   // clang-format on
 }
 
 /**
-* Load a big-endian word
-* @param in a pointer to some bytes
-* @param off an offset into the array
-* @return off'th T of in, as a big-endian value
-*/
-template <typename T>
-inline constexpr T load_be(const uint8_t in[], size_t off) {
-   in += off * sizeof(T);
-   T out = 0;
-   for(size_t i = 0; i != sizeof(T); ++i) {
-      out = static_cast<T>((out << 8) | in[i]);
+ * Manually store a word into a range in either big or little endian byte order.
+ * This will be used only if the endianness of the target platform is unknown at
+ * compile time.
+ */
+template <Endianness endianness, std::unsigned_integral InT, ranges::contiguous_output_range<uint8_t> OutR>
+inline constexpr void fallback_store_any(InT in, OutR&& out_range) {
+   std::span out{out_range};
+   // clang-format off
+   if constexpr(endianness == Endianness::Big) {
+      [&]<size_t... i>(std::index_sequence<i...>) {
+         ((out[i] = get_byte<i>(in)), ...);
+      } (std::make_index_sequence<sizeof(InT)>());
+   } else {
+      static_assert(endianness == Endianness::Little);
+      [&]<size_t... i>(std::index_sequence<i...>) {
+         ((out[i] = get_byte<sizeof(InT) - i - 1>(in)), ...);
+      } (std::make_index_sequence<sizeof(InT)>());
    }
-   return out;
+   // clang-format on
 }
 
 /**
-* Load a little-endian word
-* @param in a pointer to some bytes
-* @param off an offset into the array
-* @return off'th T of in, as a litte-endian value
-*/
-template <typename T>
-inline constexpr T load_le(const uint8_t in[], size_t off) {
-   in += off * sizeof(T);
-   T out = 0;
-   for(size_t i = 0; i != sizeof(T); ++i) {
-      out = (out << 8) | in[sizeof(T) - 1 - i];
+ * Load a word from a range in either big or little endian byte order
+ *
+ * This is the base implementation, all other overloads are just convenience
+ * wrappers. It is assumed that the range has the correct size for the word.
+ *
+ * Template arguments of all overloads of load_any() share the same semantics:
+ *
+ *   1.  Endianness      Either `Endianness::Big` or `Endianness::Little`, that
+ *                       will eventually select the byte order translation mode
+ *                       implemented in this base function.
+ *
+ *   2.  Output type     Either `AutoDetect`, an unsigned integer or a container
+ *                       holding an unsigned integer type. `AutoDetect` means
+ *                       that the caller did not explicitly specify the type and
+ *                       expects the type to be inferred from the input.
+ *
+ *   3+. Argument types  Typically, those are input and output ranges of bytes
+ *                       or unsigned integers. Or one or more unsigned integers
+ *                       acting as output parameters.
+ *
+ * @param in_range a fixed-length byte range
+ * @return T loaded from @p in_range, as a big-endian value
+ */
+template <Endianness endianness, std::unsigned_integral OutT, ranges::contiguous_range<uint8_t> InR>
+inline constexpr OutT load_any(InR&& in_range) {
+   ranges::assert_exact_byte_length<sizeof(OutT)>(in_range);
+   std::span in{in_range};
+
+   // At compile time we cannot use `typecast_copy` as it uses `std::memcpy`
+   // internally to copy ranges on a byte-by-byte basis, which is not allowed
+   // in a `constexpr` context.
+   if(std::is_constant_evaluated()) /* TODO: C++23: if consteval {} */ {
+      return fallback_load_any<endianness, OutT>(std::forward<InR>(in_range));
+   } else {
+      if constexpr(sizeof(OutT) == 1) {
+         return static_cast<OutT>(in[0]);
+      } else if constexpr(is_native(endianness)) {
+         return typecast_copy<OutT>(in);
+      } else if constexpr(is_opposite(endianness)) {
+         return reverse_bytes(typecast_copy<OutT>(in));
+      } else {
+         static_assert(native_endianness_is_unknown<endianness>());
+         return fallback_load_any<endianness, OutT>(std::forward<InR>(in_range));
+      }
    }
-   return out;
 }
 
 /**
-* Load a big-endian unsigned integer
-* @param in a pointer to some bytes
-* @param off an offset into the array
-* @return off'th unsigned integer of in, as a big-endian value
-*/
-template <std::unsigned_integral T>
-inline constexpr T load_be(const uint8_t in[], size_t off) {
-   // asserts that *in points to the correct amount of memory
-   return load_be<T>(std::span<const uint8_t, sizeof(T)>(in + off * sizeof(T), sizeof(T)));
+ * Overload for loading into a strong type holding an unsigned integer
+ */
+template <Endianness endianness, concepts::unsigned_integral_strong_type OutT, ranges::contiguous_range<uint8_t> InR>
+inline constexpr OutT load_any(InR&& in_range) {
+   using underlying_type = typename OutT::wrapped_type;
+   return OutT{load_any<endianness, underlying_type>(std::forward<InR>(in_range))};
 }
 
 /**
-* Load a little-endian unsigned integer
-* @param in a pointer to some bytes
-* @param off an offset into the array
-* @return off'th unsigned integer of in, as a little-endian value
-*/
-template <std::unsigned_integral T>
-inline constexpr T load_le(const uint8_t in[], size_t off) {
-   // asserts that *in points to the correct amount of memory
-   return load_le<T>(std::span<const uint8_t, sizeof(T)>(in + off * sizeof(T), sizeof(T)));
+ * Overload for loading into an enum type that uses an unsigned integer as its
+ * underlying type.
+ */
+template <Endianness endianness, typename OutT, ranges::contiguous_range<uint8_t> InR>
+   requires(std::is_enum_v<OutT> && std::unsigned_integral<std::underlying_type_t<OutT>>)
+inline constexpr OutT load_any(InR&& in_range) {
+   using underlying_type = std::underlying_type_t<OutT>;
+   return static_cast<OutT>(load_any<endianness, underlying_type>(std::forward<InR>(in_range)));
 }
 
 /**
-* Load many big-endian unsigned integers
-* @param in   a fixed-length span to some bytes
-* @param outs a arbitrary-length parameter list of unsigned integers to be loaded
-*/
-template <ranges::contiguous_range<uint8_t> InR, std::unsigned_integral... Ts>
-   requires(sizeof...(Ts) > 0) && all_same_v<Ts...>
-inline constexpr void load_be(InR&& in, Ts&... outs) {
+ * Load many unsigned integers
+ * @param in   a fixed-length span to some bytes
+ * @param outs a arbitrary-length parameter list of unsigned integers to be loaded
+ */
+template <Endianness endianness, typename OutT, ranges::contiguous_range<uint8_t> InR, unsigned_integralish... Ts>
+   requires(sizeof...(Ts) > 0) && ((std::same_as<AutoDetect, OutT> && all_same_v<Ts...>) ||
+                                   (unsigned_integralish<OutT> && all_same_v<OutT, Ts...>))
+inline constexpr void load_any(InR&& in, Ts&... outs) {
    ranges::assert_exact_byte_length<(sizeof(Ts) + ...)>(in);
    auto load_one = [off = 0]<typename T>(auto i, T& o) mutable {
-      o = load_be<T>(i.subspan(off).template first<sizeof(T)>());
+      o = load_any<endianness, T>(i.subspan(off).template first<sizeof(T)>());
       off += sizeof(T);
    };
 
@@ -258,178 +307,248 @@ inline constexpr void load_be(InR&& in, Ts&... outs) {
 }
 
 /**
-* Load many little-endian unsigned integers
-* @param in   a fixed-length span to some bytes
-* @param outs a arbitrary-length parameter list of unsigned integers to be loaded
-*/
-template <ranges::contiguous_range<uint8_t> InR, std::unsigned_integral... Ts>
-   requires(sizeof...(Ts) > 0) && all_same_v<Ts...>
-inline constexpr void load_le(InR&& in, Ts&... outs) {
-   ranges::assert_exact_byte_length<(sizeof(Ts) + ...)>(in);
-   auto load_one = [off = 0]<typename T>(auto i, T& o) mutable {
-      o = load_le<T>(i.subspan(off).template first<sizeof(T)>());
-      off += sizeof(T);
+ * Load a variable number of words from @p in into @p out.
+ * The byte length of the @p out and @p in ranges must match.
+ *
+ * @param out the output range of words
+ * @param in the input range of bytes
+ */
+template <Endianness endianness,
+          typename OutT,
+          ranges::contiguous_output_range OutR,
+          ranges::contiguous_range<uint8_t> InR>
+   requires(unsigned_integralish<std::ranges::range_value_t<OutR>> &&
+            (std::same_as<AutoDetect, OutT> || std::same_as<OutT, std::ranges::range_value_t<OutR>>))
+inline constexpr void load_any(OutR&& out, InR&& in) {
+   ranges::assert_equal_byte_lengths(out, in);
+
+   auto load_elementwise = [&] {
+      using element_type = std::ranges::range_value_t<OutR>;
+      constexpr size_t bytes_per_element = sizeof(element_type);
+      std::span<const uint8_t> in_s(in);
+      for(auto& out_elem : out) {
+         out_elem = load_any<endianness, element_type>(in_s.template first<bytes_per_element>());
+         in_s = in_s.subspan(bytes_per_element);
+      }
    };
 
-   (load_one(std::span{in}, outs), ...);
+   // At compile time we cannot use `typecast_copy` as it uses `std::memcpy`
+   // internally to copy ranges on a byte-by-byte basis, which is not allowed
+   // in a `constexpr` context.
+   if(std::is_constant_evaluated()) /* TODO: C++23: if consteval {} */ {
+      load_elementwise();
+   } else {
+      if constexpr(is_native(endianness)) {
+         typecast_copy(out, in);
+      } else {
+         load_elementwise();
+      }
+   }
 }
 
+//
+// Type inference overloads
+//
+
 /**
-* Load many big-endian unsigned integers
-* @param in   a pointer to some bytes
-* @param outs a arbitrary-length parameter list of unsigned integers to be loaded
-*/
-template <std::unsigned_integral... Ts>
-   requires(sizeof...(Ts) > 0) && all_same_v<Ts...>
-inline constexpr void load_be(const uint8_t in[], Ts&... outs) {
-   constexpr auto bytes = (sizeof(outs) + ...);
-   // asserts that *in points to the correct amount of memory
-   load_be(std::span<const uint8_t, bytes>(in, bytes), outs...);
+ * Load one or more unsigned integers, auto-detect the output type if
+ * possible. Otherwise, use the specified integer or integer container type.
+ *
+ * @param in_range a statically-sized range with some bytes
+ * @return T loaded from in
+ */
+template <Endianness endianness, typename OutT, ranges::contiguous_range<uint8_t> InR>
+   requires(std::same_as<AutoDetect, OutT> ||
+            ((ranges::statically_spanable_range<OutT> ||
+              concepts::resizable_container<OutT>)&&unsigned_integralish<typename OutT::value_type>))
+inline constexpr auto load_any(InR&& in_range) {
+   auto out = []([[maybe_unused]] const auto& in) {
+      if constexpr(std::same_as<AutoDetect, OutT>) {
+         if constexpr(ranges::statically_spanable_range<InR>) {
+            constexpr size_t extent = decltype(std::span{in})::extent;
+
+            // clang-format off
+            using type =
+               std::conditional_t<extent == 1, uint8_t,
+               std::conditional_t<extent == 2, uint16_t,
+               std::conditional_t<extent == 4, uint32_t,
+               std::conditional_t<extent == 8, uint64_t, void>>>>;
+            // clang-format on
+
+            static_assert(
+               !std::is_void_v<type>,
+               "Cannot determine the output type based on a statically sized bytearray with length other than those: 1, 2, 4, 8");
+
+            return type{};
+         } else {
+            static_assert(
+               !std::same_as<AutoDetect, OutT>,
+               "cannot infer return type from a dynamic range at compile time, please specify it explicitly");
+         }
+      } else if constexpr(concepts::resizable_container<OutT>) {
+         const size_t in_bytes = std::span{in}.size_bytes();
+         constexpr size_t out_elem_bytes = sizeof(typename OutT::value_type);
+         BOTAN_ARG_CHECK(in_bytes % out_elem_bytes == 0,
+                         "Input range is not word-aligned with the requested output range");
+         return OutT(in_bytes / out_elem_bytes);
+      } else {
+         return OutT{};
+      }
+   }(in_range);
+
+   using out_type = decltype(out);
+   if constexpr(unsigned_integralish<out_type>) {
+      out = load_any<endianness, out_type>(std::forward<InR>(in_range));
+   } else {
+      static_assert(ranges::contiguous_range<out_type>);
+      using out_range_type = std::ranges::range_value_t<out_type>;
+      load_any<endianness, out_range_type>(out, std::forward<InR>(in_range));
+   }
+   return out;
 }
 
+//
+// Legacy load functions that work on raw pointers and arrays
+//
+
 /**
-* Load many little-endian unsigned integers
-* @param in   a pointer to some bytes
-* @param outs a arbitrary-length parameter list of unsigned integers to be loaded
-*/
-template <std::unsigned_integral... Ts>
-   requires(sizeof...(Ts) > 0) && all_same_v<Ts...>
-inline constexpr void load_le(const uint8_t in[], Ts&... outs) {
-   constexpr auto bytes = (sizeof(outs) + ...);
-   // asserts that *in points to the correct amount of memory
-   load_le(std::span<const uint8_t, bytes>(in, bytes), outs...);
+ * Load a word from @p in at some offset @p off
+ * @param in a pointer to some bytes
+ * @param off an offset into the array
+ * @return off'th T of in, as a big-endian value
+ */
+template <Endianness endianness, unsigned_integralish OutT>
+inline constexpr OutT load_any(const uint8_t in[], size_t off) {
+   // asserts that *in points to enough bytes to read at offset off
+   constexpr size_t out_size = sizeof(OutT);
+   return load_any<endianness, OutT>(std::span<const uint8_t, out_size>(in + off * out_size, out_size));
 }
 
 /**
-* Load a variable number of little-endian words
-* @param out the output array of words
-* @param in the input array of bytes
-* @param count how many words are in in
-*/
-template <typename T>
-inline constexpr void load_le(T out[], const uint8_t in[], size_t count) {
-   if(count > 0) {
-#if defined(BOTAN_TARGET_CPU_IS_LITTLE_ENDIAN)
-      typecast_copy(out, in, count);
-
-#elif defined(BOTAN_TARGET_CPU_IS_BIG_ENDIAN)
-      typecast_copy(out, in, count);
-
-      for(size_t i = 0; i != count; ++i)
-         out[i] = reverse_bytes(out[i]);
-#else
-      for(size_t i = 0; i != count; ++i)
-         out[i] = load_le<T>(in, i);
-#endif
-   }
+ * Load many words from @p in
+ * @param in   a pointer to some bytes
+ * @param outs a arbitrary-length parameter list of unsigned integers to be loaded
+ */
+template <Endianness endianness, typename OutT, unsigned_integralish... Ts>
+   requires(sizeof...(Ts) > 0 && all_same_v<Ts...> &&
+            ((std::same_as<AutoDetect, OutT> && all_same_v<Ts...>) ||
+             (unsigned_integralish<OutT> && all_same_v<OutT, Ts...>)))
+inline constexpr void load_any(const uint8_t in[], Ts&... outs) {
+   constexpr auto bytes = (sizeof(outs) + ...);
+   // asserts that *in points to the correct amount of memory
+   load_any<endianness, OutT>(std::span<const uint8_t, bytes>(in, bytes), outs...);
 }
 
 /**
-* Load a variable number of big-endian words
-* @param out the output array of words
-* @param in the input array of bytes
-* @param count how many words are in in
-*/
-template <typename T>
-inline constexpr void load_be(T out[], const uint8_t in[], size_t count) {
-   if(count > 0) {
-#if defined(BOTAN_TARGET_CPU_IS_BIG_ENDIAN)
-      typecast_copy(out, in, count);
+ * Load a variable number of words from @p in into @p out.
+ * @param out the output array of words
+ * @param in the input array of bytes
+ * @param count how many words are in in
+ */
+template <Endianness endianness, typename OutT, unsigned_integralish T>
+   requires(std::same_as<AutoDetect, OutT> || std::same_as<T, OutT>)
+inline constexpr void load_any(T out[], const uint8_t in[], size_t count) {
+   // asserts that *in and *out point to the correct amount of memory
+   load_any<endianness, OutT>(std::span<T>(out, count), std::span<const uint8_t>(in, count * sizeof(T)));
+}
 
-#elif defined(BOTAN_TARGET_CPU_IS_LITTLE_ENDIAN)
-      typecast_copy(out, in, count);
+}  // namespace detail
 
-      for(size_t i = 0; i != count; ++i) {
-         out[i] = reverse_bytes(out[i]);
-      }
-#else
-      for(size_t i = 0; i != count; ++i)
-         out[i] = load_be<T>(in, i);
-#endif
-   }
+/**
+ * Load "something" in little endian byte order
+ * See the documentation of this file for more details.
+ */
+template <typename OutT = detail::AutoDetect, typename... ParamTs>
+inline constexpr auto load_le(ParamTs&&... params) {
+   return detail::load_any<detail::Endianness::Little, OutT>(std::forward<ParamTs>(params)...);
 }
 
 /**
-* Store a big-endian unsigned integer
-* @param in the input unsigned integer
-* @param out_range the fixed-length span to write to
-*/
-template <std::unsigned_integral T, ranges::contiguous_output_range<uint8_t> OutR>
-inline constexpr void store_be(T in, OutR&& out_range) {
-   ranges::assert_exact_byte_length<sizeof(T)>(out_range);
-   std::span out{out_range};
-   if constexpr(sizeof(T) == 1) {
-      out[0] = static_cast<uint8_t>(in);
-   } else {
-#if defined(BOTAN_TARGET_CPU_IS_BIG_ENDIAN)
-      typecast_copy(out, in);
-#elif defined(BOTAN_TARGET_CPU_IS_LITTLE_ENDIAN)
-      typecast_copy(out, reverse_bytes(in));
-#else
-      [&]<size_t... i>(std::index_sequence<i...>) {
-         ((out[i] = get_byte<i>(in)), ...);
-      }
-      (std::make_index_sequence<sizeof(T)>());
-#endif
-   }
+ * Load "something" in big endian byte order
+ * See the documentation of this file for more details.
+ */
+template <typename OutT = detail::AutoDetect, typename... ParamTs>
+inline constexpr auto load_be(ParamTs&&... params) {
+   return detail::load_any<detail::Endianness::Big, OutT>(std::forward<ParamTs>(params)...);
 }
 
+namespace detail {
+
 /**
-* Store a little-endian unsigned integer
-* @param in the input unsigned integer
-* @param out_range the fixed-length span to write to
-*/
-template <std::unsigned_integral T, ranges::contiguous_output_range<uint8_t> OutR>
-inline constexpr void store_le(T in, OutR&& out_range) {
-   ranges::assert_exact_byte_length<sizeof(T)>(out_range);
+ * Store a word in either big or little endian byte order into a range
+ *
+ * This is the base implementation, all other overloads are just convenience
+ * wrappers. It is assumed that the range has the correct size for the word.
+ *
+ * Template arguments of all overloads of store_any() share the same semantics
+ * as those of load_any(). See the documentation of this function for more
+ * details.
+ *
+ * @param in an unsigned integral to be stored
+ * @param out_range a byte range to store the word into
+ */
+template <Endianness endianness, std::unsigned_integral InT, ranges::contiguous_output_range<uint8_t> OutR>
+inline constexpr void store_any(InT in, OutR&& out_range) {
+   ranges::assert_exact_byte_length<sizeof(InT)>(out_range);
    std::span out{out_range};
-   if constexpr(sizeof(T) == 1) {
-      out[0] = static_cast<uint8_t>(in);
+
+   // At compile time we cannot use `typecast_copy` as it uses `std::memcpy`
+   // internally to copy ranges on a byte-by-byte basis, which is not allowed
+   // in a `constexpr` context.
+   if(std::is_constant_evaluated()) /* TODO: C++23: if consteval {} */ {
+      return fallback_store_any<endianness, InT>(in, std::forward<OutR>(out_range));
    } else {
-#if defined(BOTAN_TARGET_CPU_IS_BIG_ENDIAN)
-      typecast_copy(out, reverse_bytes(in));
-#elif defined(BOTAN_TARGET_CPU_IS_LITTLE_ENDIAN)
-      typecast_copy(out, in);
-#else
-      [&]<size_t... i>(std::index_sequence<i...>) {
-         ((out[i] = get_byte<sizeof(T) - i - 1>(in)), ...);
+      if constexpr(sizeof(InT) == 1) {
+         out[0] = static_cast<uint8_t>(in);
+      } else if constexpr(is_native(endianness)) {
+         typecast_copy(out, in);
+      } else if constexpr(is_opposite(endianness)) {
+         typecast_copy(out, reverse_bytes(in));
+      } else {
+         static_assert(native_endianness_is_unknown<endianness>());
+         return fallback_store_any<endianness, InT>(in, std::forward<OutR>(out_range));
       }
-      (std::make_index_sequence<sizeof(T)>());
-#endif
    }
 }
 
 /**
-* Store a big-endian unsigned integer
-* @param in the input unsigned integer
-* @param out the byte array to write to
-*/
-template <std::unsigned_integral T>
-inline constexpr void store_be(T in, uint8_t out[sizeof(T)]) {
-   store_be(in, std::span<uint8_t, sizeof(T)>(out, sizeof(T)));
+ * Overload for loading into a strong type holding an unsigned integer
+ */
+template <Endianness endianness,
+          concepts::unsigned_integral_strong_type InT,
+          ranges::contiguous_output_range<uint8_t> OutR>
+inline constexpr void store_any(InT in, OutR&& out_range) {
+   using underlying_type = typename InT::wrapped_type;
+   store_any<endianness, underlying_type>(in.get(), std::forward<OutR>(out_range));
 }
 
 /**
-* Store a little-endian unsigned integer
-* @param in the input unsigned integer
-* @param out the byte array to write to
-*/
-template <std::unsigned_integral T>
-inline constexpr void store_le(T in, uint8_t out[sizeof(T)]) {
-   store_le(in, std::span<uint8_t, sizeof(T)>(out, sizeof(T)));
+ * Overload for storing an enum type that uses an unsigned integer as its
+ * underlying type.
+ */
+template <Endianness endianness, typename InT, ranges::contiguous_output_range<uint8_t> OutR>
+   requires(std::is_enum_v<InT> && std::unsigned_integral<std::underlying_type_t<InT>>)
+inline constexpr void store_any(InT in, OutR&& out_range) {
+   using underlying_type = std::underlying_type_t<InT>;
+   // TODO: C++23: use std::to_underlying(in) instead
+   store_any<endianness, underlying_type>(static_cast<underlying_type>(in), std::forward<OutR>(out_range));
 }
 
 /**
-* Store many big-endian unsigned integers
-* @param out a fixed-length span to some bytes
-* @param ins a arbitrary-length parameter list of unsigned integers to be stored
-*/
-template <ranges::contiguous_output_range<uint8_t> OutR, std::unsigned_integral... Ts>
-   requires(sizeof...(Ts) > 0) && all_same_v<Ts...>
-inline constexpr void store_be(OutR&& out, Ts... ins) {
+ * Store many unsigned integers words into a byte range
+ * @param out a sized range of some bytes
+ * @param ins a arbitrary-length parameter list of unsigned integers to be stored
+ */
+template <Endianness endianness,
+          typename InT,
+          ranges::contiguous_output_range<uint8_t> OutR,
+          unsigned_integralish... Ts>
+   requires(sizeof...(Ts) > 0) && ((std::same_as<AutoDetect, InT> && all_same_v<Ts...>) ||
+                                   (unsigned_integralish<InT> && all_same_v<InT, Ts...>))
+inline constexpr void store_any(OutR&& out, Ts... ins) {
    ranges::assert_exact_byte_length<(sizeof(Ts) + ...)>(out);
    auto store_one = [off = 0]<typename T>(auto o, T i) mutable {
-      store_be<T>(i, o.subspan(off).template first<sizeof(T)>());
+      store_any<endianness, T>(i, o.subspan(off).template first<sizeof(T)>());
       off += sizeof(T);
    };
 
@@ -437,70 +556,160 @@ inline constexpr void store_be(OutR&& out, Ts... ins) {
 }
 
 /**
-* Store many little-endian unsigned integers
-* @param out a fixed-length span to some bytes
-* @param ins a arbitrary-length parameter list of unsigned integers to be stored
-*/
-template <ranges::contiguous_output_range<uint8_t> OutR, std::unsigned_integral... Ts>
-   requires(sizeof...(Ts) > 0) && all_same_v<Ts...>
-inline constexpr void store_le(OutR&& out, Ts... ins) {
-   ranges::assert_exact_byte_length<(sizeof(Ts) + ...)>(out);
-   auto store_one = [off = 0]<typename T>(auto o, T i) mutable {
-      store_le<T>(i, o.subspan(off).template first<sizeof(T)>());
-      off += sizeof(T);
+ * Store a variable number of words given in @p in into @p out.
+ * The byte lengths of @p in and @p out must be consistent.
+ * @param out the output range of bytes
+ * @param in the input range of words
+ */
+template <Endianness endianness,
+          typename InT,
+          ranges::contiguous_output_range<uint8_t> OutR,
+          ranges::spanable_range InR>
+   requires(std::same_as<AutoDetect, InT> || std::same_as<InT, std::ranges::range_value_t<InR>>)
+inline constexpr void store_any(OutR&& out, InR&& in) {
+   ranges::assert_equal_byte_lengths(out, in);
+
+   auto store_elementwise = [&] {
+      using element_type = std::ranges::range_value_t<InR>;
+      constexpr size_t bytes_per_element = sizeof(element_type);
+      std::span<uint8_t> out_s(out);
+      for(auto in_elem : in) {
+         store_any<endianness, element_type>(out_s.template first<bytes_per_element>(), in_elem);
+         out_s = out_s.subspan(bytes_per_element);
+      }
    };
 
-   (store_one(std::span{out}, ins), ...);
+   // At compile time we cannot use `typecast_copy` as it uses `std::memcpy`
+   // internally to copy ranges on a byte-by-byte basis, which is not allowed
+   // in a `constexpr` context.
+   if(std::is_constant_evaluated()) /* TODO: C++23: if consteval {} */ {
+      store_elementwise();
+   } else {
+      if constexpr(is_native(endianness)) {
+         typecast_copy(out, in);
+      } else {
+         store_elementwise();
+      }
+   }
+}
+
+//
+// Type inference overloads
+//
+
+/**
+ * Infer InT from a single unsigned integer input parameter.
+ *
+ * TODO: we might consider dropping this overload (i.e. out-range as second
+ *       parameter) and make this a "special case" of the overload below, that
+ *       takes a variadic number of input parameters.
+ *
+ * @param in an unsigned integer to be stored
+ * @param out_range a range of bytes to store the word into
+ */
+template <Endianness endianness, typename InT, unsigned_integralish T, ranges::contiguous_output_range<uint8_t> OutR>
+   requires std::same_as<AutoDetect, InT>
+inline constexpr void store_any(T in, OutR&& out_range) {
+   store_any<endianness, T>(in, std::forward<OutR>(out_range));
 }
 
 /**
-* Store many big-endian unsigned integers
-* @param ins a pointer to some bytes to be written
-* @param out a arbitrary-length parameter list of unsigned integers to be stored
-*/
-template <std::unsigned_integral... Ts>
-   requires(sizeof...(Ts) > 0) && all_same_v<Ts...>
-inline constexpr void store_be(uint8_t out[], Ts... ins) {
-   constexpr auto bytes = (sizeof(ins) + ...);
-   // asserts that *out points to the correct amount of memory
-   store_be(std::span<uint8_t, bytes>(out, bytes), ins...);
+ * The caller provided some integer values in a collection but did not provide
+ * the output container. Let's create one for them, fill it with one of the
+ * overloads above and return it. This will default to a std::array if the
+ * caller did not specify the desired output container type.
+ *
+ * @param in_range a range of words that should be stored
+ * @return a container of bytes that contains the stored words
+ */
+template <Endianness endianness, typename OutR, ranges::spanable_range InR>
+   requires(std::same_as<AutoDetect, OutR> ||
+            (ranges::statically_spanable_range<OutR> && std::default_initializable<OutR>) ||
+            concepts::resizable_byte_buffer<OutR>)
+inline constexpr auto store_any(InR&& in_range) {
+   auto out = []([[maybe_unused]] const auto& in) {
+      if constexpr(std::same_as<AutoDetect, OutR>) {
+         if constexpr(ranges::statically_spanable_range<InR>) {
+            constexpr size_t bytes = decltype(std::span{in})::extent * sizeof(std::ranges::range_value_t<InR>);
+            return std::array<uint8_t, bytes>();
+         } else {
+            static_assert(
+               !std::same_as<AutoDetect, OutR>,
+               "cannot infer a suitable result container type from the given parameters at compile time, please specify it explicitly");
+         }
+      } else if constexpr(concepts::resizable_byte_buffer<OutR>) {
+         return OutR(std::span{in}.size_bytes());
+      } else {
+         return OutR{};
+      }
+   }(in_range);
+
+   store_any<endianness, std::ranges::range_value_t<InR>>(out, std::forward<InR>(in_range));
+   return out;
 }
 
 /**
-* Store many little-endian unsigned integers
-* @param ins a pointer to some bytes to be written
-* @param out a arbitrary-length parameter list of unsigned integers to be stored
-*/
-template <std::unsigned_integral... Ts>
-   requires(sizeof...(Ts) > 0) && all_same_v<Ts...>
-inline constexpr void store_le(uint8_t out[], Ts... ins) {
-   constexpr auto bytes = (sizeof(ins) + ...);
+ * The caller provided some integer values but did not provide the output
+ * container. Let's create one for them, fill it with one of the overloads above
+ * and return it. This will default to a std::array if the caller did not
+ * specify the desired output container type.
+ *
+ * @param ins some words that should be stored
+ * @return a container of bytes that contains the stored words
+ */
+template <Endianness endianness, typename OutR, unsigned_integralish... Ts>
+   requires all_same_v<Ts...>
+inline constexpr auto store_any(Ts... ins) {
+   return store_any<endianness, OutR>(std::array{ins...});
+}
+
+//
+// Legacy store functions that work on raw pointers and arrays
+//
+
+/**
+ * Store a single unsigned integer into a raw pointer
+ * @param in the input unsigned integer
+ * @param out the byte array to write to
+ */
+template <Endianness endianness, typename InT, unsigned_integralish T>
+   requires(std::same_as<AutoDetect, InT> || std::same_as<T, InT>)
+inline constexpr void store_any(T in, uint8_t out[]) {
+   // asserts that *out points to enough bytes to write into
+   store_any<endianness, InT>(in, std::span<uint8_t, sizeof(T)>(out, sizeof(T)));
+}
+
+/**
+ * Store many unsigned integers words into a raw pointer
+ * @param ins a arbitrary-length parameter list of unsigned integers to be stored
+ * @param out the byte array to write to
+ */
+template <Endianness endianness, typename InT, unsigned_integralish T0, unsigned_integralish... Ts>
+   requires(std::same_as<AutoDetect, InT> || std::same_as<T0, InT>) && all_same_v<T0, Ts...>
+inline constexpr void store_any(uint8_t out[], T0 in0, Ts... ins) {
+   constexpr auto bytes = sizeof(in0) + (sizeof(ins) + ... + 0);
    // asserts that *out points to the correct amount of memory
-   store_le(std::span<uint8_t, bytes>(out, bytes), ins...);
+   store_any<endianness, T0>(std::span<uint8_t, bytes>(out, bytes), in0, ins...);
 }
 
+}  // namespace detail
+
 /**
-* Store a big-endian unsigned integer
-* @param in the input unsigned integer
-* @return a byte array holding the integer value in big-endian byte order
-*/
-template <std::unsigned_integral T>
-inline constexpr auto store_be(T in) {
-   std::array<uint8_t, sizeof(T)> out;
-   store_be(in, out);
-   return out;
+ * Store "something" in little endian byte order
+ * See the documentation of this file for more details.
+ */
+template <typename ModifierT = detail::AutoDetect, typename... ParamTs>
+inline constexpr auto store_le(ParamTs&&... params) {
+   return detail::store_any<detail::Endianness::Little, ModifierT>(std::forward<ParamTs>(params)...);
 }
 
 /**
-* Store a little-endian unsigned integer
-* @param in the input unsigned integer
-* @return a byte array holding the integer value in little-endian byte order
-*/
-template <std::unsigned_integral T>
-inline constexpr auto store_le(T in) {
-   std::array<uint8_t, sizeof(T)> out;
-   store_le(in, out);
-   return out;
+ * Store "something" in big endian byte order
+ * See the documentation of this file for more details.
+ */
+template <typename ModifierT = detail::AutoDetect, typename... ParamTs>
+inline constexpr auto store_be(ParamTs&&... params) {
+   return detail::store_any<detail::Endianness::Big, ModifierT>(std::forward<ParamTs>(params)...);
 }
 
 template <typename T>


Test patch:
diff --git a/src/tests/test_utils.cpp b/src/tests/test_utils.cpp
index 604bd782f..1758c0b5f 100644
--- a/src/tests/test_utils.cpp
+++ b/src/tests/test_utils.cpp
@@ -17,6 +17,7 @@
 #include <botan/internal/loadstor.h>
 #include <botan/internal/parsing.h>
 #include <botan/internal/rounding.h>
+#include <botan/internal/stl_util.h>
 #include <ctime>
 #include <functional>
 
@@ -64,10 +65,26 @@ class Utility_Function_Tests final : public Text_Based_Test {
          std::vector<Test::Result> results;
 
          results.push_back(test_loadstore());
-
+         results.push_back(test_loadstore_fallback());
+         results.push_back(test_loadstore_constexpr());
          return results;
       }
 
+   private:
+      using TestInt64 = Botan::Strong<uint64_t, struct TestInt64_>;
+      using TestInt32 = Botan::Strong<uint32_t, struct TestInt64_>;
+      using TestVectorSink = Botan::Strong<std::vector<uint8_t>, struct TestVectorSink_>;
+
+      enum class TestEnum64 : uint64_t {
+         _1 = 0x1234567890ABCDEF,
+         _2 = 0xEFCDAB9078563412,
+      };
+
+      enum class TestEnum32 : uint32_t {
+         _1 = 0x12345678,
+         _2 = 0x78563412,
+      };
+
       static Test::Result test_loadstore() {
          Test::Result result("Util load/store");
 
@@ -199,6 +216,48 @@ class Utility_Function_Tests final : public Text_Based_Test {
          result.test_is_eq<uint16_t>(i2, 0x01EF);
          result.test_is_eq<uint16_t>(i3, 0xABCD);  // remains unchanged
 
+         i0 = 0xAA11;
+         i1 = 0xBB22;
+         i2 = 0xCC33;
+         i3 = 0xDD44;
+         Botan::store_be(outarr, i0, i1, i2, i3);
+         result.test_is_eq(outarr, {0xAA, 0x11, 0xBB, 0x22, 0xCC, 0x33, 0xDD, 0x44});
+         std::vector<uint8_t> outvec(8);
+         Botan::store_be(outvec, i0, i1, i2, i3);
+         result.test_is_eq(outvec, Botan::hex_decode("AA11BB22CC33DD44"));
+
+         Botan::store_le(outarr, i0, i1, i2, i3);
+         result.test_is_eq(outarr, {0x11, 0xAA, 0x22, 0xBB, 0x33, 0xCC, 0x44, 0xDD});
+         Botan::store_le(outvec, i0, i1, i2, i3);
+         result.test_is_eq(outvec, Botan::hex_decode("11AA22BB33CC44DD"));
+
+#if !defined(BOTAN_TERMINATE_ON_ASSERTS)
+         std::vector<uint8_t> sink56bits(7);
+         std::vector<uint8_t> sink72bits(9);
+         result.test_throws("store_le with a buffer that is too small",
+                            [&] { Botan::store_le(sink56bits, i0, i1, i2, i3); });
+         result.test_throws("store_le with a buffer that is too big",
+                            [&] { Botan::store_le(sink72bits, i0, i1, i2, i3); });
+         result.test_throws("store_be with a buffer that is too small",
+                            [&] { Botan::store_be(sink56bits, i0, i1, i2, i3); });
+         result.test_throws("store_be with a buffer that is too big",
+                            [&] { Botan::store_be(sink72bits, i0, i1, i2, i3); });
+#endif
+
+         // can store multiple values straight into a collection
+         auto out64_array_be = Botan::store_be(i0, i1, i2, i3);
+         auto out64_vec_be = Botan::store_be<std::vector<uint8_t>>(i0, i1, i2, i3);
+         auto out64_strong_be = Botan::store_be<TestVectorSink>(i0, i1, i2, i3);
+         result.test_is_eq(out64_array_be, {0xAA, 0x11, 0xBB, 0x22, 0xCC, 0x33, 0xDD, 0x44});
+         result.test_is_eq(out64_vec_be, Botan::hex_decode("AA11BB22CC33DD44"));
+         result.test_is_eq(out64_strong_be, TestVectorSink(Botan::hex_decode("AA11BB22CC33DD44")));
+         auto out64_array_le = Botan::store_le(i0, i1, i2, i3);
+         auto out64_vec_le = Botan::store_le<std::vector<uint8_t>>(i0, i1, i2, i3);
+         auto out64_strong_le = Botan::store_le<TestVectorSink>(i0, i1, i2, i3);
+         result.test_is_eq(out64_array_le, {0x11, 0xAA, 0x22, 0xBB, 0x33, 0xCC, 0x44, 0xDD});
+         result.test_is_eq(out64_vec_le, Botan::hex_decode("11AA22BB33CC44DD"));
+         result.test_is_eq(out64_strong_le, TestVectorSink(Botan::hex_decode("11AA22BB33CC44DD")));
+
          result.test_is_eq(in16, Botan::load_be(Botan::store_be(in16)));
          result.test_is_eq(in32, Botan::load_be(Botan::store_be(in32)));
          result.test_is_eq(in64, Botan::load_be(Botan::store_be(in64)));
@@ -207,6 +266,315 @@ class Utility_Function_Tests final : public Text_Based_Test {
          result.test_is_eq(in32, Botan::load_le(Botan::store_le(in32)));
          result.test_is_eq(in64, Botan::load_le(Botan::store_le(in64)));
 
+         // Test that the runtime detects incompatible range sizes
+#if !defined(BOTAN_TERMINATE_ON_ASSERTS)
+         std::vector<uint16_t> too_big16(4);
+         std::vector<uint16_t> too_small16(1);
+         result.test_throws("load_le with incompatible buffers",
+                            [&] { Botan::load_le(too_big16, Botan::hex_decode("BAADB00B")); });
+         result.test_throws("load_le with incompatible buffers",
+                            [&] { Botan::load_le(too_small16, Botan::hex_decode("BAADB00B")); });
+         result.test_throws("load_be with incompatible buffers",
+                            [&] { Botan::load_be(too_big16, Botan::hex_decode("BAADB00B")); });
+         result.test_throws("load_be with incompatible buffers",
+                            [&] { Botan::load_be(too_small16, Botan::hex_decode("BAADB00B")); });
+
+         std::vector<uint8_t> too_big8(4);
+         std::vector<uint8_t> too_small8(1);
+         result.test_throws("store_le with incompatible buffers",
+                            [&] { Botan::store_le(too_big8, std::array<uint16_t, 1>{}); });
+         result.test_throws("store_le with incompatible buffers",
+                            [&] { Botan::store_le(too_small8, std::array<uint16_t, 1>{}); });
+         result.test_throws("store_be with incompatible buffers",
+                            [&] { Botan::store_be(too_big8, std::array<uint16_t, 1>{}); });
+         result.test_throws("store_be with incompatible buffers",
+                            [&] { Botan::store_be(too_small8, std::array<uint16_t, 1>{}); });
+#endif
+
+         // Test store of entire ranges
+         std::array<uint16_t, 2> in16_array = {0x0A0B, 0x0C0D};
+         result.test_is_eq(Botan::store_be<std::vector<uint8_t>>(in16_array), Botan::hex_decode("0A0B0C0D"));
+         result.test_is_eq(Botan::store_le<std::vector<uint8_t>>(in16_array), Botan::hex_decode("0B0A0D0C"));
+
+         std::vector<uint16_t> in16_vector = {0x0A0B, 0x0C0D};
+         result.test_is_eq(Botan::store_be<std::vector<uint8_t>>(in16_vector), Botan::hex_decode("0A0B0C0D"));
+         result.test_is_eq(Botan::store_le<std::vector<uint8_t>>(in16_vector), Botan::hex_decode("0B0A0D0C"));
+
+         std::array<uint8_t, 4> out_array;
+         Botan::store_be(out_array, in16_array);
+         result.test_is_eq(out_array, std::array<uint8_t, 4>{0x0A, 0x0B, 0x0C, 0x0D});
+         Botan::store_le(out_array, in16_array);
+         result.test_is_eq(out_array, std::array<uint8_t, 4>{0x0B, 0x0A, 0x0D, 0x0C});
+
+         const auto be_inferred = Botan::store_be(in16_array);
+         result.test_is_eq(be_inferred, std::array<uint8_t, 4>{0x0A, 0x0B, 0x0C, 0x0D});
+         const auto le_inferred = Botan::store_le(in16_array);
+         result.test_is_eq(le_inferred, std::array<uint8_t, 4>{0x0B, 0x0A, 0x0D, 0x0C});
+
+         // Test load of entire ranges
+         const auto in_buffer = Botan::hex_decode("AABBCCDD");
+         auto out16_array_be = Botan::load_be<std::array<uint16_t, 2>>(in_buffer);
+         result.test_is_eq<uint16_t>(out16_array_be[0], 0xAABB);
+         result.test_is_eq<uint16_t>(out16_array_be[1], 0xCCDD);
+         auto out16_vec_be = Botan::load_be<std::vector<uint16_t>>(in_buffer);
+         result.test_eq_sz("be-vector has expected size", out16_vec_be.size(), 2);
+         result.test_is_eq<uint16_t>(out16_vec_be[0], 0xAABB);
+         result.test_is_eq<uint16_t>(out16_vec_be[1], 0xCCDD);
+
+         auto out16_array_le = Botan::load_le<std::array<uint16_t, 2>>(in_buffer);
+         result.test_is_eq<uint16_t>(out16_array_le[0], 0xBBAA);
+         result.test_is_eq<uint16_t>(out16_array_le[1], 0xDDCC);
+         auto out16_vec_le = Botan::load_le<Botan::secure_vector<uint16_t>>(in_buffer);
+         result.test_eq_sz("le-vector has expected size", out16_vec_be.size(), 2);
+         result.test_is_eq<uint16_t>(out16_vec_le[0], 0xBBAA);
+         result.test_is_eq<uint16_t>(out16_vec_le[1], 0xDDCC);
+
+         // Test loading/storing of strong type integers
+         const TestInt64 in64_strong{0xABCDEF0123456789};
+         const TestInt32 in32_strong{0xABCDEF01};
+
+         result.test_is_eq(Botan::store_be<std::vector<uint8_t>>(in64_strong), Botan::hex_decode("ABCDEF0123456789"));
+         result.test_is_eq(Botan::store_le<std::vector<uint8_t>>(in64_strong), Botan::hex_decode("8967452301EFCDAB"));
+         result.test_is_eq(Botan::store_be<std::vector<uint8_t>>(in32_strong), Botan::hex_decode("ABCDEF01"));
+         result.test_is_eq(Botan::store_le<std::vector<uint8_t>>(in32_strong), Botan::hex_decode("01EFCDAB"));
+
+         result.test_is_eq(Botan::load_be<TestInt64>(Botan::hex_decode("ABCDEF0123456789")), in64_strong);
+         result.test_is_eq(Botan::load_le<TestInt64>(Botan::hex_decode("8967452301EFCDAB")), in64_strong);
+         result.test_is_eq(Botan::load_be<TestInt32>(Botan::hex_decode("ABCDEF01")), in32_strong);
+         result.test_is_eq(Botan::load_le<TestInt32>(Botan::hex_decode("01EFCDAB")), in32_strong);
+
+         std::vector<TestInt64> some_in64_strongs{TestInt64{0xABCDEF0123456789}, TestInt64{0x0123456789ABCDEF}};
+         result.test_is_eq(Botan::store_be<std::vector<uint8_t>>(some_in64_strongs),
+                           Botan::hex_decode("ABCDEF01234567890123456789ABCDEF"));
+         result.test_is_eq(Botan::store_le<std::vector<uint8_t>>(some_in64_strongs),
+                           Botan::hex_decode("8967452301EFCDABEFCDAB8967452301"));
+
+         const auto in64_strongs_le =
+            Botan::load_le<std::array<TestInt64, 2>>(Botan::hex_decode("8967452301EFCDABEFCDAB8967452301"));
+         result.test_is_eq(in64_strongs_le[0], TestInt64{0xABCDEF0123456789});
+         result.test_is_eq(in64_strongs_le[1], TestInt64{0x0123456789ABCDEF});
+
+         const auto in64_strongs_be =
+            Botan::load_be<std::vector<TestInt64>>(Botan::hex_decode("ABCDEF01234567890123456789ABCDEF"));
+         result.test_is_eq(in64_strongs_be[0], TestInt64{0xABCDEF0123456789});
+         result.test_is_eq(in64_strongs_be[1], TestInt64{0x0123456789ABCDEF});
+
+         // Test loading/storing of enum types with different endianness
+         const auto in64_enum_le = Botan::load_le<TestEnum64>(Botan::hex_decode("1234567890ABCDEF"));
+         result.test_is_eq(in64_enum_le, TestEnum64::_2);
+         const auto in64_enum_be = Botan::load_be<TestEnum64>(Botan::hex_decode("1234567890ABCDEF"));
+         result.test_is_eq(in64_enum_be, TestEnum64::_1);
+         result.test_is_eq(Botan::store_le<std::vector<uint8_t>>(TestEnum64::_1),
+                           Botan::hex_decode("EFCDAB9078563412"));
+         result.test_is_eq<std::array<uint8_t, 8>>(Botan::store_be(TestEnum64::_2),
+                                                   {0xEF, 0xCD, 0xAB, 0x90, 0x78, 0x56, 0x34, 0x12});
+
+         const auto in32_enum_le = Botan::load_le<TestEnum32>(Botan::hex_decode("78563412"));
+         result.test_is_eq(in32_enum_le, TestEnum32::_1);
+         const auto in32_enum_be = Botan::load_be<TestEnum32>(Botan::hex_decode("78563412"));
+         result.test_is_eq(in32_enum_be, TestEnum32::_2);
+         result.test_is_eq(Botan::store_le<std::vector<uint8_t>>(TestEnum32::_1), Botan::hex_decode("78563412"));
+         result.test_is_eq<std::array<uint8_t, 4>>(Botan::store_be(TestEnum32::_2), {0x78, 0x56, 0x34, 0x12});
+
+         return result;
+      }
+
+      template <std::unsigned_integral T>
+      static T fb_load_be(std::array<const uint8_t, sizeof(T)> in) {
+         return Botan::detail::fallback_load_any<Botan::detail::Endianness::Big, T>(in);
+      }
+
+      template <std::unsigned_integral T>
+      static T fb_load_le(std::array<const uint8_t, sizeof(T)> in) {
+         return Botan::detail::fallback_load_any<Botan::detail::Endianness::Little, T>(in);
+      }
+
+      template <std::unsigned_integral T>
+      static decltype(auto) fb_store_be(const T in) {
+         std::array<uint8_t, sizeof(T)> out;
+         Botan::detail::fallback_store_any<Botan::detail::Endianness::Big, T>(in, out);
+         return out;
+      }
+
+      template <std::unsigned_integral T>
+      static decltype(auto) fb_store_le(const T in) {
+         std::array<uint8_t, sizeof(T)> out;
+         Botan::detail::fallback_store_any<Botan::detail::Endianness::Little, T>(in, out);
+         return out;
+      }
+
+      template <size_t N>
+      using a = std::array<uint8_t, N>;
+
+      static Test::Result test_loadstore_fallback() {
+         // The fallback implementation is only used if we don't know the
+         // endianness of the target at compile time. This makes sure that the
+         // fallback implementation is correct. On all typical platforms it
+         // won't be called in production.
+         Test::Result result("Util load/store fallback");
+
+         result.test_is_eq<uint16_t>("lLE 16", fb_load_le<uint16_t>({1, 2}), 0x0201);
+         result.test_is_eq<uint32_t>("lLE 32", fb_load_le<uint32_t>({1, 2, 3, 4}), 0x04030201);
+         result.test_is_eq<uint64_t>("lLE 64", fb_load_le<uint64_t>({1, 2, 3, 4, 5, 6, 7, 8}), 0x0807060504030201);
+
+         result.test_is_eq<uint16_t>("lBE 16", fb_load_be<uint16_t>({1, 2}), 0x0102);
+         result.test_is_eq<uint32_t>("lBE 32", fb_load_be<uint32_t>({1, 2, 3, 4}), 0x01020304);
+         result.test_is_eq<uint64_t>("lBE 64", fb_load_be<uint64_t>({1, 2, 3, 4, 5, 6, 7, 8}), 0x0102030405060708);
+
+         result.test_is_eq<a<2>>("sLE 16", fb_store_le<uint16_t>(0x0201), {1, 2});
+         result.test_is_eq<a<4>>("sLE 32", fb_store_le<uint32_t>(0x04030201), {1, 2, 3, 4});
+         result.test_is_eq<a<8>>("sLE 64", fb_store_le<uint64_t>(0x0807060504030201), {1, 2, 3, 4, 5, 6, 7, 8});
+
+         result.test_is_eq<a<2>>("sBE 16", fb_store_be<uint16_t>(0x0102), {1, 2});
+         result.test_is_eq<a<4>>("sBE 32", fb_store_be<uint32_t>(0x01020304), {1, 2, 3, 4});
+         result.test_is_eq<a<8>>("sBE 64", fb_store_be<uint64_t>(0x0102030405060708), {1, 2, 3, 4, 5, 6, 7, 8});
+
+         return result;
+      }
+
+      static Test::Result test_loadstore_constexpr() {
+         Test::Result result("Util load/store constexpr");
+
+         constexpr uint16_t in16 = 0x1234;
+         constexpr uint32_t in32 = 0xA0B0C0D0;
+         constexpr uint64_t in64 = 0xABCDEF0123456789;
+
+         // clang-format off
+         constexpr std::array<uint8_t, 16> cex_mem = {
+            0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF,
+         };
+         // clang-format on
+
+         // get_byte<> w/ 16bit
+         constexpr auto cex_byte_16_0 = Botan::get_byte<0>(in16);
+         result.test_is_eq<uint8_t>(cex_byte_16_0, 0x12);
+         constexpr auto cex_byte_16_1 = Botan::get_byte<1>(in16);
+         result.test_is_eq<uint8_t>(cex_byte_16_1, 0x34);
+
+         // get_byte<> w/ 32bit
+         constexpr auto cex_byte_32_0 = Botan::get_byte<0>(in32);
+         result.test_is_eq<uint8_t>(cex_byte_32_0, 0xA0);
+         constexpr auto cex_byte_32_1 = Botan::get_byte<1>(in32);
+         result.test_is_eq<uint8_t>(cex_byte_32_1, 0xB0);
+         constexpr auto cex_byte_32_2 = Botan::get_byte<2>(in32);
+         result.test_is_eq<uint8_t>(cex_byte_32_2, 0xC0);
+         constexpr auto cex_byte_32_3 = Botan::get_byte<3>(in32);
+         result.test_is_eq<uint8_t>(cex_byte_32_3, 0xD0);
+
+         // get_byte<> w/ 64bit
+         constexpr auto cex_byte_64_0 = Botan::get_byte<0>(in64);
+         result.test_is_eq<uint8_t>(cex_byte_64_0, 0xAB);
+         constexpr auto cex_byte_64_1 = Botan::get_byte<1>(in64);
+         result.test_is_eq<uint8_t>(cex_byte_64_1, 0xCD);
+         constexpr auto cex_byte_64_2 = Botan::get_byte<2>(in64);
+         result.test_is_eq<uint8_t>(cex_byte_64_2, 0xEF);
+         constexpr auto cex_byte_64_3 = Botan::get_byte<3>(in64);
+         result.test_is_eq<uint8_t>(cex_byte_64_3, 0x01);
+         constexpr auto cex_byte_64_4 = Botan::get_byte<4>(in64);
+         result.test_is_eq<uint8_t>(cex_byte_64_4, 0x23);
+         constexpr auto cex_byte_64_5 = Botan::get_byte<5>(in64);
+         result.test_is_eq<uint8_t>(cex_byte_64_5, 0x45);
+         constexpr auto cex_byte_64_6 = Botan::get_byte<6>(in64);
+         result.test_is_eq<uint8_t>(cex_byte_64_6, 0x67);
+         constexpr auto cex_byte_64_7 = Botan::get_byte<7>(in64);
+         result.test_is_eq<uint8_t>(cex_byte_64_7, 0x89);
+
+         // make_uintXX()
+         constexpr auto cex_uint16_t = Botan::make_uint16(0x12, 0x34);
+         result.test_is_eq<uint16_t>(cex_uint16_t, in16);
+         constexpr auto cex_uint32_t = Botan::make_uint32(0xA0, 0xB0, 0xC0, 0xD0);
+         result.test_is_eq<uint32_t>(cex_uint32_t, in32);
+         constexpr auto cex_uint64_t = Botan::make_uint64(0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x89);
+         result.test_is_eq<uint64_t>(cex_uint64_t, in64);
+
+         // store_le/be with a single integer
+         constexpr std::array<uint8_t, 2> cex_store_le16 = Botan::store_le(in16);
+         result.test_is_eq(cex_store_le16, std::array<uint8_t, 2>{0x34, 0x12});
+         constexpr std::array<uint8_t, 4> cex_store_le32 = Botan::store_le(in32);
+         result.test_is_eq(cex_store_le32, std::array<uint8_t, 4>{0xD0, 0xC0, 0xB0, 0xA0});
+         constexpr std::array<uint8_t, 8> cex_store_le64 = Botan::store_le(in64);
+         result.test_is_eq(cex_store_le64, std::array<uint8_t, 8>{0x89, 0x67, 0x45, 0x23, 0x01, 0xEF, 0xCD, 0xAB});
+
+         constexpr std::array<uint8_t, 2> cex_store_be16 = Botan::store_be(in16);
+         result.test_is_eq(cex_store_be16, std::array<uint8_t, 2>{0x12, 0x34});
+         constexpr std::array<uint8_t, 4> cex_store_be32 = Botan::store_be(in32);
+         result.test_is_eq(cex_store_be32, std::array<uint8_t, 4>{0xA0, 0xB0, 0xC0, 0xD0});
+         constexpr std::array<uint8_t, 8> cex_store_be64 = Botan::store_be(in64);
+         result.test_is_eq(cex_store_be64, std::array<uint8_t, 8>{0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x89});
+
+         // store_le/be with multiple integers, both as a parameter pack and a range (std::array for constexpr)
+         constexpr std::array<uint8_t, 16> cex_store_le16s =
+            Botan::store_le(in16, in16, in16, in16, in16, in16, in16, in16);
+         constexpr std::array<uint8_t, 16> cex_store_le16s2 =
+            Botan::store_le(std::array{in16, in16, in16, in16, in16, in16, in16, in16});
+         result.test_is_eq(
+            cex_store_le16s,
+            {0x34, 0x12, 0x34, 0x12, 0x34, 0x12, 0x34, 0x12, 0x34, 0x12, 0x34, 0x12, 0x34, 0x12, 0x34, 0x12});
+         result.test_is_eq(cex_store_le16s, cex_store_le16s2);
+         constexpr std::array<uint8_t, 16> cex_store_le32s = Botan::store_le(in32, in32, in32, in32);
+         constexpr std::array<uint8_t, 16> cex_store_le32s2 = Botan::store_le(std::array{in32, in32, in32, in32});
+         result.test_is_eq(
+            cex_store_le32s,
+            {0xD0, 0xC0, 0xB0, 0xA0, 0xD0, 0xC0, 0xB0, 0xA0, 0xD0, 0xC0, 0xB0, 0xA0, 0xD0, 0xC0, 0xB0, 0xA0});
+         result.test_is_eq(cex_store_le32s, cex_store_le32s2);
+         constexpr std::array<uint8_t, 16> cex_store_le64s = Botan::store_le(in64, in64);
+         constexpr std::array<uint8_t, 16> cex_store_le64s2 = Botan::store_le(std::array{in64, in64});
+         result.test_is_eq(
+            cex_store_le64s,
+            {0x89, 0x67, 0x45, 0x23, 0x01, 0xEF, 0xCD, 0xAB, 0x89, 0x67, 0x45, 0x23, 0x01, 0xEF, 0xCD, 0xAB});
+         result.test_is_eq(cex_store_le64s, cex_store_le64s2);
+
+         constexpr std::array<uint8_t, 16> cex_store_be16s =
+            Botan::store_be(in16, in16, in16, in16, in16, in16, in16, in16);
+         constexpr std::array<uint8_t, 16> cex_store_be16s2 =
+            Botan::store_be(std::array{in16, in16, in16, in16, in16, in16, in16, in16});
+         result.test_is_eq(
+            cex_store_be16s,
+            {0x12, 0x34, 0x12, 0x34, 0x12, 0x34, 0x12, 0x34, 0x12, 0x34, 0x12, 0x34, 0x12, 0x34, 0x12, 0x34});
+         result.test_is_eq(cex_store_be16s, cex_store_be16s2);
+         constexpr std::array<uint8_t, 16> cex_store_be32s = Botan::store_be(in32, in32, in32, in32);
+         constexpr std::array<uint8_t, 16> cex_store_be32s2 = Botan::store_be(std::array{in32, in32, in32, in32});
+         result.test_is_eq(
+            cex_store_be32s,
+            {0xA0, 0xB0, 0xC0, 0xD0, 0xA0, 0xB0, 0xC0, 0xD0, 0xA0, 0xB0, 0xC0, 0xD0, 0xA0, 0xB0, 0xC0, 0xD0});
+         result.test_is_eq(cex_store_be32s, cex_store_be32s2);
+         constexpr std::array<uint8_t, 16> cex_store_be64s = Botan::store_be(in64, in64);
+         constexpr std::array<uint8_t, 16> cex_store_be64s2 = Botan::store_be(std::array{in64, in64});
+         result.test_is_eq(
+            cex_store_be64s,
+            {0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x89});
+         result.test_is_eq(cex_store_be64s, cex_store_be64s2);
+
+         // load_le/be a single integer
+         constexpr uint16_t cex_load_le16 = Botan::load_le<uint16_t>(cex_store_le16);
+         result.test_is_eq(cex_load_le16, in16);
+         constexpr uint32_t cex_load_le32 = Botan::load_le<uint32_t>(cex_store_le32);
+         result.test_is_eq(cex_load_le32, in32);
+         constexpr uint64_t cex_load_le64 = Botan::load_le<uint64_t>(cex_store_le64);
+         result.test_is_eq(cex_load_le64, in64);
+
+         constexpr uint16_t cex_load_be16 = Botan::load_be<uint16_t>(cex_store_be16);
+         result.test_is_eq(cex_load_be16, in16);
+         constexpr uint32_t cex_load_be32 = Botan::load_be<uint32_t>(cex_store_be32);
+         result.test_is_eq(cex_load_be32, in32);
+         constexpr uint64_t cex_load_be64 = Botan::load_be<uint64_t>(cex_store_be64);
+         result.test_is_eq(cex_load_be64, in64);
+
+         // load_le/be multiple integers into a std::array for constexpr
+         constexpr auto cex_load_le16s = Botan::load_le<std::array<uint16_t, cex_mem.size() / 2>>(cex_mem);
+         result.test_is_eq(cex_load_le16s, {0x1100, 0x3322, 0x5544, 0x7766, 0x9988, 0xBBAA, 0xDDCC, 0xFFEE});
+         constexpr auto cex_load_le32s = Botan::load_le<std::array<uint32_t, cex_mem.size() / 4>>(cex_mem);
+         result.test_is_eq(cex_load_le32s, {0x33221100, 0x77665544, 0xBBAA9988, 0xFFEEDDCC});
+         constexpr auto cex_load_le64s = Botan::load_le<std::array<uint64_t, cex_mem.size() / 8>>(cex_mem);
+         result.test_is_eq(cex_load_le64s, {0x7766554433221100, 0xFFEEDDCCBBAA9988});
+
+         constexpr auto cex_load_be16s = Botan::load_be<std::array<uint16_t, cex_mem.size() / 2>>(cex_mem);
+         result.test_is_eq(cex_load_be16s, {0x0011, 0x2233, 0x4455, 0x6677, 0x8899, 0xAABB, 0xCCDD, 0xEEFF});
+         constexpr auto cex_load_be32s = Botan::load_be<std::array<uint32_t, cex_mem.size() / 4>>(cex_mem);
+         result.test_is_eq(cex_load_be32s, {0x00112233, 0x44556677, 0x8899AABB, 0xCCDDEEFF});
+         constexpr auto cex_load_be64s = Botan::load_be<std::array<uint64_t, cex_mem.size() / 8>>(cex_mem);
+         result.test_is_eq(cex_load_be64s, {0x0011223344556677, 0x8899AABBCCDDEEFF});
+
          return result;
       }
 };