pw_kvs: FlashMemory updates; test utilities
- Update comments for FlashMemory class.
- Split InMemoryFakeFlash to move the fixed-size buffer into the
derived FakeFlashBuffer class.
- Support initializing FakeFlashBuffer to data provided at construction.
- Allow direct access to the underlying fake flash buffer for testing.
- Create utilities for working with byte arrays in byte_utils.h.
Change-Id: I90d33621cb91da079d7213fe7d33823494120e48
diff --git a/pw_kvs/BUILD b/pw_kvs/BUILD
index 052216e..1f12d67 100644
--- a/pw_kvs/BUILD
+++ b/pw_kvs/BUILD
@@ -62,12 +62,18 @@
)
pw_cc_library(
- name = "in_memory_fake_flash",
+ name = "test_utils",
+ srcs = [
+ "in_memory_fake_flash.cc",
+ ],
hdrs = [
"public/pw_kvs/in_memory_fake_flash.h",
+ "pw_kvs_private/byte_utils.h",
],
+ includes = ["public"],
visibility = ["//visibility:private"],
deps = [
+ "//pw_kvs",
"//pw_log",
"//pw_status",
],
@@ -100,8 +106,8 @@
"entry_test.cc",
],
deps = [
- ":in_memory_fake_flash",
":pw_kvs",
+ ":test_utils",
],
)
@@ -110,8 +116,8 @@
srcs = ["key_value_store_test.cc"],
deps = [
":crc16",
- ":in_memory_fake_flash",
":pw_kvs",
+ ":test_utils",
"//pw_checksum",
"//pw_log",
],
@@ -122,8 +128,8 @@
srcs = ["key_value_store_fuzz_test.cc"],
deps = [
":crc16",
- ":in_memory_fake_flash",
":pw_kvs",
+ ":test_utils",
"//pw_checksum",
],
)
@@ -133,19 +139,26 @@
srcs = ["key_value_store_map_test.cc"],
deps = [
":crc16",
- ":in_memory_fake_flash",
":pw_kvs",
+ ":test_utils",
"//pw_checksum",
],
)
-cc_binary(
+# TODO: This binary is not building due to a linker error. The error does not occur in GN Builds.
+# A filegroup is used below so that the file is included in the Bazel build.
+# cc_binary(
+# name = "debug_cli",
+# srcs = ["debug_cli.cc"],
+# copts = ["-std=c++17"],
+# deps = [
+# ":crc16",
+# ":pw_kvs",
+# ":test_utils",
+# ],
+# )
+
+filegroup(
name = "debug_cli",
srcs = ["debug_cli.cc"],
- copts = ["-std=c++17"],
- deps = [
- ":crc16",
- ":in_memory_fake_flash",
- ":pw_kvs",
- ],
)
diff --git a/pw_kvs/BUILD.gn b/pw_kvs/BUILD.gn
index 1edd0d2..c2552c8 100644
--- a/pw_kvs/BUILD.gn
+++ b/pw_kvs/BUILD.gn
@@ -64,13 +64,16 @@
]
}
-source_set("in_memory_fake_flash") {
+source_set("test_utils") {
+ public_configs = [ ":default_config" ]
public = [
"public/pw_kvs/in_memory_fake_flash.h",
+ "pw_kvs_private/byte_utils.h",
]
- sources = public
+ sources = [ "in_memory_fake_flash.cc" ] + public
visibility = [ ":*" ]
public_deps = [
+ dir_pw_kvs,
dir_pw_log,
]
}
@@ -81,8 +84,8 @@
]
deps = [
":crc16",
- ":in_memory_fake_flash",
":pw_kvs",
+ ":test_utils",
]
}
@@ -119,8 +122,9 @@
pw_test("entry_test") {
deps = [
- ":in_memory_fake_flash",
+ ":crc16",
":pw_kvs",
+ ":test_utils",
]
sources = [
"entry_test.cc",
@@ -130,8 +134,8 @@
pw_test("key_value_store_test") {
deps = [
":crc16",
- ":in_memory_fake_flash",
":pw_kvs",
+ ":test_utils",
dir_pw_checksum,
dir_pw_log,
]
@@ -143,8 +147,8 @@
pw_test("key_value_store_fuzz_test") {
deps = [
":crc16",
- ":in_memory_fake_flash",
":pw_kvs",
+ ":test_utils",
]
sources = [
"key_value_store_fuzz_test.cc",
@@ -154,8 +158,8 @@
pw_test("key_value_store_map_test") {
deps = [
":crc16",
- ":in_memory_fake_flash",
":pw_kvs",
+ ":test_utils",
dir_pw_checksum,
]
sources = [
diff --git a/pw_kvs/debug_cli.cc b/pw_kvs/debug_cli.cc
index 473e80a..8f5baeb 100644
--- a/pw_kvs/debug_cli.cc
+++ b/pw_kvs/debug_cli.cc
@@ -30,7 +30,7 @@
void Run() {
// 4 x 4k sectors, 16 byte alignment
- InMemoryFakeFlash<4 * 1024, 4> test_flash(16);
+ FakeFlashBuffer<4 * 1024, 4> test_flash(16);
FlashPartition test_partition(&test_flash, 0, test_flash.sector_count());
test_partition.Erase(0, test_partition.sector_count());
diff --git a/pw_kvs/entry_test.cc b/pw_kvs/entry_test.cc
index d181926..26c44a3 100644
--- a/pw_kvs/entry_test.cc
+++ b/pw_kvs/entry_test.cc
@@ -24,7 +24,7 @@
// TODO(hepler): expand these tests
-InMemoryFakeFlash<128, 4> test_flash(16);
+FakeFlashBuffer<128, 4> test_flash(16);
FlashPartition test_partition(&test_flash, 0, test_flash.sector_count());
TEST(Entry, Size_RoundsUpToAlignment) {
diff --git a/pw_kvs/in_memory_fake_flash.cc b/pw_kvs/in_memory_fake_flash.cc
new file mode 100644
index 0000000..70f3890
--- /dev/null
+++ b/pw_kvs/in_memory_fake_flash.cc
@@ -0,0 +1,90 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_kvs/in_memory_fake_flash.h"
+
+#include "pw_log/log.h"
+
+namespace pw::kvs {
+
+Status InMemoryFakeFlash::Erase(Address address, size_t num_sectors) {
+ if (address % sector_size_bytes() != 0) {
+ PW_LOG_ERROR(
+ "Attempted to erase sector at non-sector aligned boundary; address %zx",
+ size_t(address));
+ return Status::INVALID_ARGUMENT;
+ }
+ const size_t sector_id = address / sector_size_bytes();
+ if (address / sector_size_bytes() + num_sectors > sector_count()) {
+ PW_LOG_ERROR(
+ "Tried to erase a sector at an address past flash end; "
+ "address: %zx, sector implied: %zu",
+ size_t(address),
+ sector_id);
+ return Status::OUT_OF_RANGE;
+ }
+
+ std::memset(
+ &buffer_[address], int(kErasedValue), sector_size_bytes() * num_sectors);
+ return Status::OK;
+}
+
+StatusWithSize InMemoryFakeFlash::Read(Address address,
+ span<std::byte> output) {
+ if (address + output.size() >= sector_count() * size_bytes()) {
+ return StatusWithSize(Status::OUT_OF_RANGE);
+ }
+ std::memcpy(output.data(), &buffer_[address], output.size());
+ return StatusWithSize(output.size());
+}
+
+StatusWithSize InMemoryFakeFlash::Write(Address address,
+ span<const std::byte> data) {
+ if (address % alignment_bytes() != 0 ||
+ data.size() % alignment_bytes() != 0) {
+ PW_LOG_ERROR("Unaligned write; address %zx, size %zu B, alignment %zu",
+ size_t(address),
+ data.size(),
+ alignment_bytes());
+ return StatusWithSize(Status::INVALID_ARGUMENT);
+ }
+
+ if (data.size() > sector_size_bytes() - (address % sector_size_bytes())) {
+ PW_LOG_ERROR("Write crosses sector boundary; address %zx, size %zu B",
+ size_t(address),
+ data.size());
+ return StatusWithSize(Status::INVALID_ARGUMENT);
+ }
+
+ if (address + data.size() > sector_count() * sector_size_bytes()) {
+ PW_LOG_ERROR(
+ "Write beyond end of memory; address %zx, size %zu B, max address %zx",
+ size_t(address),
+ data.size(),
+ sector_count() * sector_size_bytes());
+ return StatusWithSize(Status::OUT_OF_RANGE);
+ }
+
+ // Check in erased state
+ for (unsigned i = 0; i < data.size(); i++) {
+ if (buffer_[address + i] != kErasedValue) {
+ PW_LOG_ERROR("Writing to previously written address: %zx",
+ size_t(address));
+ return StatusWithSize(Status::UNKNOWN);
+ }
+ }
+ std::memcpy(&buffer_[address], data.data(), data.size());
+ return StatusWithSize(data.size());
+}
+} // namespace pw::kvs
diff --git a/pw_kvs/key_value_store_fuzz_test.cc b/pw_kvs/key_value_store_fuzz_test.cc
index 3ffaaa5..b751bce 100644
--- a/pw_kvs/key_value_store_fuzz_test.cc
+++ b/pw_kvs/key_value_store_fuzz_test.cc
@@ -22,8 +22,8 @@
using std::byte;
-InMemoryFakeFlash<4 * 1024, 4> test_flash(
- 16); // 4 x 4k sectors, 16 byte alignment
+// 4 x 4k sectors, 16 byte alignment
+FakeFlashBuffer<4 * 1024, 4> test_flash(16);
FlashPartition test_partition(&test_flash, 0, test_flash.sector_count());
ChecksumCrc16 checksum;
diff --git a/pw_kvs/key_value_store_map_test.cc b/pw_kvs/key_value_store_map_test.cc
index 6643256..542493d 100644
--- a/pw_kvs/key_value_store_map_test.cc
+++ b/pw_kvs/key_value_store_map_test.cc
@@ -292,7 +292,7 @@
static constexpr size_t kMaxValueLength = 64;
- static InMemoryFakeFlash<kParams.sector_size, kParams.sector_count> flash_;
+ static FakeFlashBuffer<kParams.sector_size, kParams.sector_count> flash_;
FlashPartition partition_;
KeyValueStore kvs_;
@@ -302,9 +302,9 @@
};
template <const TestParameters& kParams>
-InMemoryFakeFlash<kParams.sector_size, kParams.sector_count>
+FakeFlashBuffer<kParams.sector_size, kParams.sector_count>
KvsTester<kParams>::flash_ =
- InMemoryFakeFlash<kParams.sector_size, kParams.sector_count>(
+ FakeFlashBuffer<kParams.sector_size, kParams.sector_count>(
kParams.sector_alignment);
#define _TEST(fixture, test, ...) \
diff --git a/pw_kvs/key_value_store_test.cc b/pw_kvs/key_value_store_test.cc
index b608b8f..4b00ed5 100644
--- a/pw_kvs/key_value_store_test.cc
+++ b/pw_kvs/key_value_store_test.cc
@@ -30,6 +30,7 @@
#include "pw_checksum/ccitt_crc16.h"
#include "pw_kvs/crc16_checksum.h"
#include "pw_kvs/flash_memory.h"
+#include "pw_kvs_private/byte_utils.h"
#include "pw_kvs_private/entry.h"
#include "pw_kvs_private/macros.h"
#include "pw_log/log.h"
@@ -46,10 +47,29 @@
using std::byte;
-template <typename... Args>
-constexpr auto ByteArray(Args... args) {
- return std::array<byte, sizeof...(args)>{static_cast<byte>(args)...};
-}
+// Test the functions in byte_utils.h. Create a byte array with AsBytes and
+// ByteStr and check that its contents are correct.
+inline constexpr std::array<char, 2> kTestArray = {'a', 'b'};
+
+inline constexpr auto kAsBytesTest = AsBytes(
+ 'a', uint16_t(1), uint8_t(23), kTestArray, ByteStr("c"), uint64_t(-1));
+
+static_assert(kAsBytesTest.size() == 15);
+static_assert(kAsBytesTest[0] == std::byte{'a'});
+static_assert(kAsBytesTest[1] == std::byte{1});
+static_assert(kAsBytesTest[2] == std::byte{0});
+static_assert(kAsBytesTest[3] == std::byte{23});
+static_assert(kAsBytesTest[4] == std::byte{'a'});
+static_assert(kAsBytesTest[5] == std::byte{'b'});
+static_assert(kAsBytesTest[6] == std::byte{'c'});
+static_assert(kAsBytesTest[7] == std::byte{0xff});
+static_assert(kAsBytesTest[8] == std::byte{0xff});
+static_assert(kAsBytesTest[9] == std::byte{0xff});
+static_assert(kAsBytesTest[10] == std::byte{0xff});
+static_assert(kAsBytesTest[11] == std::byte{0xff});
+static_assert(kAsBytesTest[12] == std::byte{0xff});
+static_assert(kAsBytesTest[13] == std::byte{0xff});
+static_assert(kAsBytesTest[14] == std::byte{0xff});
// Test that the ConvertsToSpan trait correctly idenitifies types that convert
// to span.
@@ -84,7 +104,7 @@
FlashWithPartitionFake(size_t alignment_bytes)
: memory(alignment_bytes), partition(&memory, 0, memory.sector_count()) {}
- InMemoryFakeFlash<sector_size_bytes, sector_count> memory;
+ FakeFlashBuffer<sector_size_bytes, sector_count> memory;
FlashPartition partition;
public:
@@ -128,10 +148,10 @@
#if USE_MEMORY_BUFFER
// Although it might be useful to test other configurations, some tests require
// at least 3 sectors; therfore it should have this when checked in.
-InMemoryFakeFlash<4 * 1024, 4> test_flash(
+FakeFlashBuffer<4 * 1024, 4> test_flash(
16); // 4 x 4k sectors, 16 byte alignment
FlashPartition test_partition(&test_flash, 0, test_flash.sector_count());
-InMemoryFakeFlash<1024, 60> large_test_flash(8);
+FakeFlashBuffer<1024, 60> large_test_flash(8);
FlashPartition large_test_partition(&large_test_flash,
0,
large_test_flash.sector_count());
@@ -177,7 +197,7 @@
class EmptyInitializedKvs : public ::testing::Test {
protected:
EmptyInitializedKvs() : kvs_(&test_partition, format) {
- test_partition.Erase(0, test_partition.sector_count());
+ test_partition.Erase();
ASSERT_EQ(Status::OK, kvs_.Init());
}
@@ -1040,7 +1060,7 @@
}
TEST_F(EmptyInitializedKvs, ValueSize_Positive) {
- constexpr auto kData = ByteArray('h', 'i', '!');
+ constexpr auto kData = AsBytes('h', 'i', '!');
ASSERT_EQ(Status::OK, kvs_.Put("TheKey", kData));
auto result = kvs_.ValueSize("TheKey");
diff --git a/pw_kvs/public/pw_kvs/flash_memory.h b/pw_kvs/public/pw_kvs/flash_memory.h
index 5d8f558..93ee1eb 100644
--- a/pw_kvs/public/pw_kvs/flash_memory.h
+++ b/pw_kvs/public/pw_kvs/flash_memory.h
@@ -58,17 +58,19 @@
// Erase num_sectors starting at a given address. Blocking call.
// Address should be on a sector boundary.
- // Returns: OK, on success.
- // TIMEOUT, on timeout.
- // INVALID_ARGUMENT, if address or sector count is invalid.
- // UNKNOWN, on HAL error
+ //
+ // OK: success
+ // DEADLINE_EXCEEDED: timeout
+ // INVALID_ARGUMENT: address is not sector-aligned
+ // OUT_OF_RANGE: erases past the end of the memory
+ //
virtual Status Erase(Address flash_address, size_t num_sectors) = 0;
// Reads bytes from flash into buffer. Blocking call.
- // Returns: OK, on success.
- // TIMEOUT, on timeout.
- // INVALID_ARGUMENT, if address or length is invalid.
- // UNKNOWN, on HAL error
+ //
+ // OK: success
+ // DEADLINE_EXCEEDED: timeout
+ // OUT_OF_RANGE: write does not fit in the flash memory
virtual StatusWithSize Read(Address address, span<std::byte> output) = 0;
StatusWithSize Read(Address address, void* buffer, size_t len) {
@@ -76,10 +78,12 @@
}
// Writes bytes to flash. Blocking call.
- // Returns: OK, on success.
- // TIMEOUT, on timeout.
- // INVALID_ARGUMENT, if address or length is invalid.
- // UNKNOWN, on HAL error
+ //
+ // OK: success
+ // DEADLINE_EXCEEDED: timeout
+ // INVALID_ARGUMENT: address or data size are not aligned
+ // OUT_OF_RANGE: write does not fit in the memory
+ //
virtual StatusWithSize Write(Address destination_flash_address,
span<const std::byte> data) = 0;
@@ -150,6 +154,11 @@
: alignment_bytes),
permission_(permission) {}
+ // Creates a FlashPartition that uses the entire flash with its alignment.
+ constexpr FlashPartition(FlashMemory* flash)
+ : FlashPartition(
+ flash, 0, flash->sector_count(), flash->alignment_bytes()) {}
+
FlashPartition(const FlashPartition&) = delete;
FlashPartition& operator=(const FlashPartition&) = delete;
diff --git a/pw_kvs/public/pw_kvs/in_memory_fake_flash.h b/pw_kvs/public/pw_kvs/in_memory_fake_flash.h
index 4037161..761e57c 100644
--- a/pw_kvs/public/pw_kvs/in_memory_fake_flash.h
+++ b/pw_kvs/public/pw_kvs/in_memory_fake_flash.h
@@ -13,94 +13,80 @@
// the License.
#pragma once
+#include <algorithm>
#include <array>
+#include <cstddef>
#include <cstring>
-// TODO: Push/pop log module name due to logging in header.
-// Alternately: Push implementation into .cc
#include "pw_kvs/flash_memory.h"
-#include "pw_log/log.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
namespace pw::kvs {
-// This creates a buffer which mimics the behaviour of flash (requires erase,
-// before write, checks alignments, and is addressed in sectors).
-template <uint32_t kSectorSize, uint16_t kSectorCount>
+// This uses a buffer to mimic the behaviour of flash (requires erase before
+// write, checks alignments, and is addressed in sectors). The underlying buffer
+// is not initialized.
class InMemoryFakeFlash : public FlashMemory {
public:
- InMemoryFakeFlash(uint8_t alignment_bytes = 1) // default 8 bit alignment
- : FlashMemory(kSectorSize, kSectorCount, alignment_bytes) {}
+ // Default to 8-bit alignment.
+ static constexpr size_t kDefaultAlignmentBytes = 1;
- // Always enabled
+ static constexpr std::byte kErasedValue = std::byte{0xff};
+
+ InMemoryFakeFlash(span<std::byte> buffer,
+ size_t sector_size,
+ size_t sector_count,
+ size_t alignment_bytes = kDefaultAlignmentBytes)
+ : FlashMemory(sector_size, sector_count, alignment_bytes),
+ buffer_(buffer) {}
+
+ // The fake flash is always enabled.
Status Enable() override { return Status::OK; }
+
Status Disable() override { return Status::OK; }
+
bool IsEnabled() const override { return true; }
- // Erase num_sectors starting at a given address. Blocking call.
- // Address should be on a sector boundary.
- // Returns: OK, on success.
- // INVALID_ARGUMENT, if address or sector count is invalid.
- // UNKNOWN, on HAL error
- Status Erase(Address address, size_t num_sectors) override {
- if (address % sector_size_bytes() != 0) {
- PW_LOG_ERROR(
- "Attempted to erase sector at non-sector aligned boundary: %zx",
- size_t(address));
- return Status::INVALID_ARGUMENT;
- }
- size_t sector_id = address / sector_size_bytes();
- if (address / sector_size_bytes() + num_sectors > sector_count()) {
- PW_LOG_ERROR(
- "Tried to erase a sector at an address past partition end; "
- "address: %zx, sector implied: %zu",
- size_t(address),
- sector_id);
- return Status::UNKNOWN;
- }
- if (address % alignment_bytes() != 0) {
- return Status::INVALID_ARGUMENT;
- }
- std::memset(&buffer_[address], 0xFF, sector_size_bytes() * num_sectors);
- return Status::OK;
- }
+ // Erase num_sectors starting at a given address.
+ Status Erase(Address address, size_t num_sectors) override;
- // Reads bytes from flash into buffer. Blocking call.
- // Returns: OK, on success.
- // INVALID_ARGUMENT, if address or length is invalid.
- // UNKNOWN, on HAL error
- StatusWithSize Read(Address address, span<std::byte> output) override {
- if (address + output.size() >= sector_count() * size_bytes()) {
- return StatusWithSize(Status::INVALID_ARGUMENT);
- }
- std::memcpy(output.data(), &buffer_[address], output.size());
- return StatusWithSize(output.size());
- }
+ // Reads bytes from flash into buffer.
+ StatusWithSize Read(Address address, span<std::byte> output) override;
- // Writes bytes to flash. Blocking call.
- // Returns: OK, on success.
- // INVALID_ARGUMENT, if address or length is invalid.
- // UNKNOWN, on HAL error
- StatusWithSize Write(Address address, span<const std::byte> data) override {
- if ((address + data.size()) >= sector_count() * size_bytes() ||
- address % alignment_bytes() != 0 ||
- data.size() % alignment_bytes() != 0) {
- return StatusWithSize(Status::INVALID_ARGUMENT);
- }
- // Check in erased state
- for (unsigned i = 0; i < data.size(); i++) {
- if (buffer_[address + i] != 0xFF) {
- PW_LOG_ERROR("Writing to previously written address: %zx",
- size_t(address));
- return StatusWithSize(Status::UNKNOWN);
- }
- }
- std::memcpy(&buffer_[address], data.data(), data.size());
- return StatusWithSize(data.size());
+ // Writes bytes to flash.
+ StatusWithSize Write(Address address, span<const std::byte> data) override;
+
+ // Access the underlying buffer for testing purposes. Not part of the
+ // FlashMemory API.
+ span<std::byte> buffer() const { return buffer_; }
+
+ private:
+ const span<std::byte> buffer_;
+};
+
+// Creates an InMemoryFakeFlash backed by a std::array. The array is initialized
+// to the erased value. A byte array to which to initialize the memory may be
+// provided.
+template <size_t kSectorSize, size_t kSectorCount>
+class FakeFlashBuffer : public InMemoryFakeFlash {
+ public:
+ // Creates a flash memory with no data written.
+ FakeFlashBuffer(size_t alignment_bytes = kDefaultAlignmentBytes)
+ : FakeFlashBuffer(std::array<std::byte, 0>{}, alignment_bytes) {}
+
+ // Creates a flash memory initialized to the provided contents.
+ FakeFlashBuffer(span<const std::byte> contents,
+ size_t alignment_bytes = kDefaultAlignmentBytes)
+ : InMemoryFakeFlash(buffer_, kSectorSize, kSectorCount, alignment_bytes) {
+ std::memset(buffer_.data(), int(kErasedValue), buffer_.size());
+ std::memcpy(buffer_.data(),
+ contents.data(),
+ std::min(contents.size(), buffer_.size()));
}
private:
- std::array<uint8_t, kSectorCount * kSectorSize> buffer_;
+ std::array<std::byte, kSectorCount * kSectorSize> buffer_;
};
} // namespace pw::kvs
diff --git a/pw_kvs/pw_kvs_private/byte_utils.h b/pw_kvs/pw_kvs_private/byte_utils.h
new file mode 100644
index 0000000..8eeafbc
--- /dev/null
+++ b/pw_kvs/pw_kvs_private/byte_utils.h
@@ -0,0 +1,72 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// Utilities for building std::byte arrays from strings or integer values.
+#pragma once
+
+#include <array>
+#include <cstddef>
+
+namespace pw {
+
+template <typename T, typename... Args>
+constexpr void CopyBytes(std::byte* array, T value, Args... args) {
+ if constexpr (std::is_integral_v<T>) {
+ if constexpr (sizeof(T) == 1u) {
+ *array++ = static_cast<std::byte>(value);
+ } else {
+ for (size_t i = 0; i < sizeof(T); ++i) {
+ *array++ = static_cast<std::byte>(value & 0xFF);
+ value >>= 8;
+ }
+ }
+ } else {
+ static_assert(sizeof(value[0]) == sizeof(std::byte));
+ for (auto b : value) {
+ *array++ = static_cast<std::byte>(b);
+ }
+ }
+
+ if constexpr (sizeof...(args) > 0u) {
+ CopyBytes(array, args...);
+ }
+}
+
+// Converts a series of integers to a std::byte array at compile time.
+template <typename... Args>
+constexpr auto AsBytes(Args... args) {
+ std::array<std::byte, (sizeof(args) + ...)> bytes{};
+
+ auto iterator = bytes.begin();
+ CopyBytes(iterator, args...);
+
+ return bytes;
+}
+
+namespace internal {
+
+template <typename T, size_t... kIndex>
+constexpr auto ByteStr(const T& array, std::index_sequence<kIndex...>) {
+ return std::array{static_cast<std::byte>(array[kIndex])...};
+}
+
+} // namespace internal
+
+// Converts a string literal to a byte array, without the trailing '\0'.
+template <size_t kSize, typename Indices = std::make_index_sequence<kSize - 1>>
+constexpr auto ByteStr(const char (&str)[kSize]) {
+ return internal::ByteStr(str, Indices{});
+}
+
+} // namespace pw