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