pw_blob_store: Add write support

Add BlobStore write implementation
Add initial BlobStore tests.
Add missing CMake build files in dependency modules to unbreak CMake
build.

Still missing:
Cleanup validation checking
KVS handling/usage
Some better testing

Change-Id: Ie6b711598a917d7553b5a682d8ef36cdcc137303
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/16040
Commit-Queue: David Rogers <davidrogers@google.com>
Reviewed-by: Ewout van Bekkum <ewout@google.com>
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 18e45c4..7f75c53 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -34,8 +34,11 @@
 add_subdirectory(pw_minimal_cpp_stdlib)
 add_subdirectory(pw_polyfill)
 add_subdirectory(pw_preprocessor)
+add_subdirectory(pw_random)
+add_subdirectory(pw_result)
 add_subdirectory(pw_span)
 add_subdirectory(pw_status)
+add_subdirectory(pw_stream)
 add_subdirectory(pw_string)
 add_subdirectory(pw_sys_io)
 add_subdirectory(pw_sys_io_stdio)
diff --git a/pw_blob_store/BUILD b/pw_blob_store/BUILD
index e45ee1c..dc8689f 100644
--- a/pw_blob_store/BUILD
+++ b/pw_blob_store/BUILD
@@ -24,6 +24,7 @@
 
 pw_cc_library(
     name = "pw_blob_store",
+    srcs = [ "blob_store.cc" ],
     hdrs = [
         "public/pw_blob_store/blob_store.h",
     ],
@@ -32,8 +33,23 @@
         "//pw_checksum",
         "//pw_containers",
         "//pw_log",
-        "//pw_log:facade",
         "//pw_span",
         "//pw_status",
     ],
 )
+
+pw_cc_test(
+    name = "blob_store_test",
+    srcs = [
+        "blob_store_test.cc",
+    ],
+    deps = [
+        ":pw_blob_store",
+        "//pw_kvs:crc16",
+        "//pw_kvs:fake_flash",
+        "//pw_kvs:fake_flash_test_key_value_store",
+        "//pw_log",
+        "//pw_random",
+        "//pw_unit_test",
+    ],
+)
diff --git a/pw_blob_store/BUILD.gn b/pw_blob_store/BUILD.gn
index 3022b97..4576ff4 100644
--- a/pw_blob_store/BUILD.gn
+++ b/pw_blob_store/BUILD.gn
@@ -25,11 +25,17 @@
 pw_source_set("pw_blob_store") {
   public_configs = [ ":default_config" ]
   public = [ "public/pw_blob_store/blob_store.h" ]
+  sources = [ "blob_store.cc" ]
   public_deps = [
     dir_pw_kvs,
     dir_pw_status,
     dir_pw_stream,
   ]
+  deps = [
+    dir_pw_assert,
+    dir_pw_checksum,
+    dir_pw_log,
+  ]
 }
 
 pw_test_group("tests") {
@@ -37,7 +43,15 @@
 }
 
 pw_test("blob_store_test") {
-  deps = [ ":pw_blob_store" ]
+  deps = [
+    ":pw_blob_store",
+    "$dir_pw_kvs:crc16",
+    "$dir_pw_kvs:fake_flash",
+    "$dir_pw_kvs:fake_flash_test_key_value_store",
+    dir_pw_log,
+    dir_pw_random,
+  ]
+  sources = [ "blob_store_test.cc" ]
 }
 
 pw_doc_group("docs") {
diff --git a/pw_blob_store/CMakeLists.txt b/pw_blob_store/CMakeLists.txt
index 181eae4..a61888e 100644
--- a/pw_blob_store/CMakeLists.txt
+++ b/pw_blob_store/CMakeLists.txt
@@ -15,11 +15,14 @@
 pw_auto_add_simple_module(pw_blob_store
   PUBLIC_DEPS
     pw_containers
+    pw_kvs
     pw_span
     pw_status
+    pw_stream
   PRIVATE_DEPS
     pw_assert
     pw_checksum
     pw_log
+    pw_random
     pw_string
 )
diff --git a/pw_blob_store/blob_store.cc b/pw_blob_store/blob_store.cc
new file mode 100644
index 0000000..27da48a
--- /dev/null
+++ b/pw_blob_store/blob_store.cc
@@ -0,0 +1,349 @@
+// 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_blob_store/blob_store.h"
+
+#include <algorithm>
+
+#include "pw_log/log.h"
+
+namespace pw::blob_store {
+
+Status BlobStore::Init() {
+  if (initialized_) {
+    return Status::OK;
+  }
+
+  const size_t write_buffer_size_alignment =
+      write_buffer_.size_bytes() % partition_.alignment_bytes();
+  PW_CHECK_UINT_EQ((write_buffer_size_alignment), 0);
+  PW_CHECK_UINT_GE(write_buffer_.size_bytes(), partition_.alignment_bytes());
+  PW_CHECK_UINT_LE(write_buffer_.size_bytes(), partition_.sector_size_bytes());
+
+  bool erased = false;
+  if (partition_.IsErased(&erased).ok() && erased) {
+    flash_erased_ = true;
+  } else {
+    flash_erased_ = false;
+  }
+
+  ResetChecksum();
+
+  // TODO: Add checking for already written valid blob.
+
+  initialized_ = true;
+  return Status::OK;
+}
+
+size_t BlobStore::MaxDataSizeBytes() const { return partition_.size_bytes(); }
+
+Status BlobStore::OpenWrite() {
+  // TODO: should Open auto initialize?
+  if (!initialized_) {
+    return Status::FAILED_PRECONDITION;
+  }
+
+  // Writer can only be opened if there are no other writer or readers already
+  // open.
+  if (writer_open_ || readers_open_ != 0) {
+    return Status::UNAVAILABLE;
+  }
+
+  writer_open_ = true;
+
+  write_address_ = 0;
+  flash_address_ = 0;
+
+  ResetChecksum();
+
+  return Status::OK;
+}
+
+Status BlobStore::OpenRead() {
+  if (!initialized_) {
+    return Status::FAILED_PRECONDITION;
+  }
+
+  // If there is no open writer, then once the reader opens there can not be
+  // one. So check that there is actully valid data.
+  // TODO: Move this validation to Init and CloseWrite
+  if (!writer_open_) {
+    if (!ValidateChecksum().ok()) {
+      PW_LOG_ERROR("Validation check failed, invalidate blob");
+      Invalidate();
+      return Status::FAILED_PRECONDITION;
+    }
+  }
+
+  readers_open_++;
+  return Status::OK;
+}
+
+Status BlobStore::CloseWrite() {
+  PW_DCHECK(writer_open_);
+  Status status = Status::OK;
+
+  // Only need to finish a blob if the blob has any bytes.
+  if (write_address_ > 0) {
+    // Flush bytes in buffer.
+    if (write_address_ != flash_address_) {
+      PW_CHECK_UINT_GE(write_address_, flash_address_);
+      size_t bytes_in_buffer = write_address_ - flash_address_;
+
+      // Zero out the remainder of the buffer.
+      auto zero_span = write_buffer_.subspan(bytes_in_buffer);
+      std::memset(zero_span.data(), 0, zero_span.size_bytes());
+
+      status = CommitToFlash(write_buffer_, bytes_in_buffer);
+    }
+
+    if (status.ok()) {
+      // Buffer should be clear unless there was a write error.
+      PW_DCHECK_UINT_EQ(flash_address_, write_address_);
+      // Should not be completing a blob write when it has completed metadata.
+      PW_DCHECK(!metadata_.complete);
+
+      metadata_ = {
+          .checksum = 0, .data_size_bytes = flash_address_, .complete = true};
+
+      if (checksum_algo_ != nullptr) {
+        ConstByteSpan checksum = checksum_algo_->Finish();
+        std::memcpy(&metadata_.checksum,
+                    checksum.data(),
+                    std::min(checksum.size(), sizeof(metadata_.checksum)));
+      }
+
+      status = kvs_.Put(MetadataKey(), metadata_);
+    }
+  }
+  writer_open_ = false;
+
+  if (status.ok()) {
+    return ValidateChecksum();
+  }
+  return Status::DATA_LOSS;
+}
+
+Status BlobStore::CloseRead() {
+  PW_CHECK_UINT_GT(readers_open_, 0);
+  readers_open_--;
+  return Status::OK;
+}
+
+Status BlobStore::Write(ConstByteSpan data) {
+  PW_DCHECK(writer_open_);
+
+  if (data.size_bytes() == 0) {
+    return Status::OK;
+  }
+  if (WriteBytesRemaining() == 0) {
+    return Status::OUT_OF_RANGE;
+  }
+  if (WriteBytesRemaining() < data.size_bytes()) {
+    return Status::RESOURCE_EXHAUSTED;
+  }
+
+  if (write_address_ == 0 && !erased()) {
+    Erase();
+  }
+
+  // Write in (up to) 3 steps:
+  // 1) Finish filling write buffer and if full write it to flash.
+  // 2) Write as many whole block-sized chunks as the data has remaining
+  //    after 1.
+  // 3) Put any remaining bytes less than flash write size in the write buffer.
+
+  // Step 1) If there is any data in the write buffer, finish filling write
+  //         buffer and if full write it to flash.
+  const size_t write_chunk_bytes = write_buffer_.size_bytes();
+  if (!WriteBufferEmpty()) {
+    PW_CHECK_UINT_GE(write_address_, flash_address_);
+    size_t bytes_in_buffer = write_address_ - flash_address_;
+    PW_CHECK_UINT_GE(write_chunk_bytes, bytes_in_buffer);
+
+    size_t buffer_remaining = write_chunk_bytes - bytes_in_buffer;
+
+    // Add bytes up to filling the write buffer.
+    size_t add_bytes = std::min(buffer_remaining, data.size_bytes());
+    std::memcpy(write_buffer_.data() + bytes_in_buffer, data.data(), add_bytes);
+    write_address_ += add_bytes;
+    bytes_in_buffer += add_bytes;
+    data = data.subspan(add_bytes);
+
+    if (bytes_in_buffer != write_chunk_bytes) {
+      // If there was not enough bytes to finish filling the write buffer, there
+      // should not be any bytes left.
+      PW_DCHECK(data.size_bytes() == 0);
+      return Status::OK;
+    }
+
+    // The write buffer is full, flush to flash.
+    Status status = CommitToFlash(write_buffer_);
+    if (!status.ok()) {
+      return status;
+    }
+
+    PW_DCHECK(WriteBufferEmpty());
+  }
+
+  // At this point, if data.size_bytes() > 0, the write buffer should be empty.
+  // This invariant is checked as part of of steps 2 & 3.
+
+  // Step 2) Write as many block-sized chunks as the data has remaining after
+  //         step 1.
+  // TODO: Decouple write buffer size from optimal bulk write size.
+  while (data.size_bytes() >= write_chunk_bytes) {
+    PW_DCHECK(WriteBufferEmpty());
+
+    write_address_ += write_chunk_bytes;
+    Status status = CommitToFlash(data.first(write_chunk_bytes));
+    if (!status.ok()) {
+      return status;
+    }
+
+    data = data.subspan(write_chunk_bytes);
+  }
+
+  // step 3) Put any remaining bytes to the buffer. Put the bytes starting at
+  //         the begining of the buffer, since it must be empty if there are
+  //         still bytes due to step 1 either cleaned out the buffer or didn't
+  //         have any more data to write.
+  if (data.size_bytes() > 0) {
+    PW_DCHECK(WriteBufferEmpty());
+    std::memcpy(write_buffer_.data(), data.data(), data.size_bytes());
+    write_address_ += data.size_bytes();
+  }
+
+  return Status::OK;
+}
+
+StatusWithSize BlobStore::Read(size_t offset, ByteSpan dest) {
+  (void)offset;
+  (void)dest;
+  return StatusWithSize::UNIMPLEMENTED;
+}
+
+Result<ByteSpan> BlobStore::GetMemoryMappedBlob() const {
+  if (write_address_ == 0 || !valid_data_) {
+    return Status::FAILED_PRECONDITION;
+  }
+
+  std::byte* mcu_address = partition_.PartitionAddressToMcuAddress(0);
+  if (mcu_address == nullptr) {
+    return Status::UNIMPLEMENTED;
+  }
+  return std::span(mcu_address, ReadableDataBytes());
+}
+
+size_t BlobStore::ReadableDataBytes() const { return flash_address_; }
+
+Status BlobStore::Erase() {
+  PW_DCHECK(writer_open_);
+
+  // Can't erase if any readers are open, since this would erase flash right out
+  // from under them.
+  if (readers_open_ != 0) {
+    return Status::UNAVAILABLE;
+  }
+
+  // If already erased our work here is done.
+  if (flash_erased_) {
+    PW_DCHECK_UINT_EQ(write_address_, 0);
+    PW_DCHECK_UINT_EQ(flash_address_, 0);
+    PW_DCHECK(!valid_data_);
+    return Status::OK;
+  }
+
+  Invalidate();
+
+  Status status = partition_.Erase();
+
+  if (status.ok()) {
+    flash_erased_ = true;
+  }
+  return status;
+}
+
+Status BlobStore::Invalidate() {
+  if (readers_open_ != 0) {
+    return Status::UNAVAILABLE;
+  }
+
+  Status status = kvs_.Delete(MetadataKey());
+  valid_data_ = false;
+  ResetChecksum();
+  write_address_ = 0;
+  flash_address_ = 0;
+
+  return status;
+}
+
+Status BlobStore::ValidateChecksum() {
+  if (!metadata_.complete) {
+    return Status::UNAVAILABLE;
+  }
+
+  if (checksum_algo_ == nullptr) {
+    if (metadata_.checksum != 0) {
+      return Status::DATA_LOSS;
+    }
+
+    return Status::OK;
+  }
+
+  PW_LOG_DEBUG("Validate checksum of 0x%08x in flash for blob of %u bytes",
+               unsigned(metadata_.checksum),
+               unsigned(metadata_.data_size_bytes));
+  auto result = CalculateChecksumFromFlash(metadata_.data_size_bytes);
+  if (!result.ok()) {
+    return result.status();
+  }
+
+  return checksum_algo_->Verify(as_bytes(std::span(&metadata_.checksum, 1)));
+}
+
+Result<BlobStore::ChecksumValue> BlobStore::CalculateChecksumFromFlash(
+    size_t bytes_to_check) {
+  if (checksum_algo_ == nullptr) {
+    return Status::OK;
+  }
+
+  checksum_algo_->Reset();
+
+  kvs::FlashPartition::Address address = 0;
+  const kvs::FlashPartition::Address end = bytes_to_check;
+
+  constexpr size_t kReadBufferSizeBytes = 32;
+  std::array<std::byte, kReadBufferSizeBytes> buffer;
+  while (address < end) {
+    const size_t read_size = std::min(size_t(end - address), buffer.size());
+    StatusWithSize status =
+        partition_.Read(address, std::span(buffer).first(read_size));
+    if (!status.ok()) {
+      return status.status();
+    }
+
+    checksum_algo_->Update(buffer.data(), read_size);
+    address += read_size;
+  }
+
+  ChecksumValue checksum_value;
+  std::span checksum = checksum_algo_->Finish();
+  std::memcpy(&checksum_value,
+              checksum.data(),
+              std::min(checksum.size(), sizeof(checksum_value)));
+  return checksum_value;
+}
+
+}  // namespace pw::blob_store
diff --git a/pw_blob_store/blob_store_test.cc b/pw_blob_store/blob_store_test.cc
new file mode 100644
index 0000000..396a54b
--- /dev/null
+++ b/pw_blob_store/blob_store_test.cc
@@ -0,0 +1,135 @@
+// 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_blob_store/blob_store.h"
+
+#include <array>
+#include <cstddef>
+#include <cstring>
+#include <span>
+
+#include "gtest/gtest.h"
+#include "pw_kvs/crc16_checksum.h"
+#include "pw_kvs/fake_flash_memory.h"
+#include "pw_kvs/flash_memory.h"
+#include "pw_kvs/test_key_value_store.h"
+#include "pw_log/log.h"
+#include "pw_random/xor_shift.h"
+
+namespace pw::blob_store {
+namespace {
+
+class BlobStoreTest : public ::testing::Test {
+ protected:
+  BlobStoreTest() : flash_(kFlashAlignment), partition_(&flash_) {}
+
+  void InitFlashTo(std::span<const std::byte> contents) {
+    partition_.Erase();
+    std::memcpy(flash_.buffer().data(), contents.data(), contents.size());
+  }
+
+  void InitBufferToRandom(uint64_t seed) {
+    partition_.Erase();
+    random::XorShiftStarRng64 rng(seed);
+    rng.Get(buffer_);
+  }
+
+  // Fill the source buffer with random pattern based on given seed, written to
+  // BlobStore in specified chunk size.
+  void ChunkWriteTest(size_t chunk_size, uint64_t seed) {
+    InitBufferToRandom(seed);
+
+    constexpr size_t kBufferSize = 256;
+    kvs::ChecksumCrc16 checksum;
+
+    char name[16] = {};
+    snprintf(name, sizeof(name), "Blob%u", unsigned(chunk_size));
+
+    BlobStoreBuffer<kBufferSize> blob(
+        name, &partition_, &checksum, kvs::TestKvs());
+    EXPECT_EQ(Status::OK, blob.Init());
+
+    BlobStore::BlobWriter writer(blob);
+    EXPECT_EQ(Status::OK, writer.Open());
+
+    ByteSpan source = buffer_;
+    while (source.size_bytes() > 0) {
+      const size_t write_size = std::min(source.size_bytes(), chunk_size);
+
+      PW_LOG_DEBUG("Do write of %u bytes, %u bytes remain",
+                   unsigned(write_size),
+                   unsigned(source.size_bytes()));
+
+      EXPECT_EQ(Status::OK, writer.Write(source.first(write_size)));
+
+      source = source.subspan(write_size);
+    }
+
+    EXPECT_EQ(Status::OK, writer.Close());
+
+    // Use reader to check for valid data.
+    BlobStore::BlobReader reader(blob, 0);
+    EXPECT_EQ(Status::OK, reader.Open());
+    Result<ByteSpan> result = reader.GetMemoryMappedBlob();
+    ASSERT_TRUE(result.ok());
+    VerifyFlash(result.value());
+    EXPECT_EQ(Status::OK, reader.Close());
+  }
+
+  void VerifyFlash(ByteSpan verify_bytes) {
+    // Should be defined as same size.
+    EXPECT_EQ(buffer_.size(), flash_.buffer().size_bytes());
+
+    // Can't allow it to march off the end of buffer_.
+    ASSERT_LE(verify_bytes.size_bytes(), buffer_.size());
+
+    for (size_t i = 0; i < verify_bytes.size_bytes(); i++) {
+      EXPECT_EQ(buffer_[i], verify_bytes[i]);
+    }
+  }
+
+  static constexpr size_t kFlashAlignment = 16;
+  static constexpr size_t kSectorSize = 2048;
+  static constexpr size_t kSectorCount = 2;
+
+  kvs::FakeFlashMemoryBuffer<kSectorSize, kSectorCount> flash_;
+  kvs::FlashPartition partition_;
+  std::array<std::byte, kSectorCount * kSectorSize> buffer_;
+};
+
+TEST_F(BlobStoreTest, Init_Ok) {
+  BlobStoreBuffer<256> blob("Blob_OK", &partition_, nullptr, kvs::TestKvs());
+  EXPECT_EQ(Status::OK, blob.Init());
+}
+
+TEST_F(BlobStoreTest, ChunkWrite1) { ChunkWriteTest(1, 0x8675309); }
+
+TEST_F(BlobStoreTest, ChunkWrite2) { ChunkWriteTest(2, 0x8675); }
+
+TEST_F(BlobStoreTest, ChunkWrite16) { ChunkWriteTest(16, 0x86); }
+
+TEST_F(BlobStoreTest, ChunkWrite64) { ChunkWriteTest(64, 0x9); }
+
+TEST_F(BlobStoreTest, ChunkWrite256) { ChunkWriteTest(256, 0x12345678); }
+
+TEST_F(BlobStoreTest, ChunkWrite512) { ChunkWriteTest(512, 0x42); }
+
+TEST_F(BlobStoreTest, ChunkWrite4096) { ChunkWriteTest(4096, 0x89); }
+
+TEST_F(BlobStoreTest, ChunkWriteSingleFull) {
+  ChunkWriteTest((kSectorCount * kSectorSize), 0x98765);
+}
+
+}  // namespace
+}  // namespace pw::blob_store
diff --git a/pw_blob_store/public/pw_blob_store/blob_store.h b/pw_blob_store/public/pw_blob_store/blob_store.h
index bffafa1..87433f6 100644
--- a/pw_blob_store/public/pw_blob_store/blob_store.h
+++ b/pw_blob_store/public/pw_blob_store/blob_store.h
@@ -17,6 +17,7 @@
 
 #include "pw_kvs/checksum.h"
 #include "pw_kvs/flash_memory.h"
+#include "pw_kvs/key_value_store.h"
 #include "pw_status/status.h"
 #include "pw_stream/stream.h"
 
@@ -50,77 +51,128 @@
   // already erased, the Write will do any needed erase.
   class BlobWriter final : public stream::Writer {
    public:
-    constexpr BlobWriter(BlobStore& store) : store_(store) {}
+    constexpr BlobWriter(BlobStore& store) : store_(store), open_(false) {}
     BlobWriter(const BlobWriter&) = delete;
     BlobWriter& operator=(const BlobWriter&) = delete;
-    ~BlobWriter() { Close(); }
+    ~BlobWriter() {
+      if (open_) {
+        Close();
+      }
+    }
 
-    // Open a blob for writing/erasing. Returns:
+    // Open a blob for writing/erasing. Can not open when already open. Only one
+    // writer is allowed to be open at a time. Returns:
     //
     // OK - success.
-    // UNAVAILABLE - Unable to open, already open.
-    Status Open() { return Status::UNIMPLEMENTED; }
+    // UNAVAILABLE - Unable to open, another writer or reader instance is
+    //     already open.
+    Status Open() {
+      PW_DCHECK(!open_);
+      Status status = store_.OpenWrite();
+      if (status.ok()) {
+        open_ = true;
+      }
+      return status;
+    }
 
     // Finalize a blob write. Flush all remaining buffered data to storage and
-    // store blob metadata. Returns:
+    // store blob metadata. Close fails in the closed state, do NOT retry Close
+    // on error. An error may or may not result in an invalid blob stored.
+    // Returns:
     //
     // OK - success.
-    // FAILED_PRECONDITION - blob is not open.
-    Status Close() { return Status::UNIMPLEMENTED; }
+    // DATA_LOSS - Error writing data or fail to verify written data.
+    Status Close() {
+      PW_DCHECK(open_);
+      open_ = false;
+      return store_.CloseWrite();
+    }
 
     // Erase the partition and reset state for a new blob. Returns:
     //
     // OK - success.
-    // UNAVAILABLE - Unable to erase, already open.
+    // UNAVAILABLE - Unable to erase while reader is open.
     // [error status] - flash erase failed.
-    Status Erase() { return Status::UNIMPLEMENTED; }
+    Status Erase() {
+      PW_DCHECK(open_);
+      return Status::UNIMPLEMENTED;
+    }
 
     // Discard blob (in-progress or valid stored). Any written bytes are
     // considered invalid. Returns:
     //
     // OK - success.
     // FAILED_PRECONDITION - not open.
-    Status Discard() { return Status::UNIMPLEMENTED; }
+    Status Discard() {
+      PW_DCHECK(open_);
+      return store_.Invalidate();
+    }
 
     // Probable (not guaranteed) minimum number of bytes at this time that can
     // be written. Returns zero if, in the current state, Write would return
     // status other than OK. See stream.h for additional details.
     size_t ConservativeWriteLimit() const override {
-      return store_.MaxDataSizeBytes() - store_.ReadableDataBytes();
+      PW_DCHECK(open_);
+      return store_.WriteBytesRemaining();
+    }
+
+    size_t CurrentSizeBytes() {
+      PW_DCHECK(open_);
+      return store_.write_address_;
     }
 
    private:
-    Status DoWrite(ConstByteSpan data) override { return store_.Write(data); }
+    Status DoWrite(ConstByteSpan data) override {
+      PW_DCHECK(open_);
+      return store_.Write(data);
+    }
 
     BlobStore& store_;
+    bool open_;
   };
 
   // Implement stream::Reader interface for BlobStore.
   class BlobReader final : public stream::Reader {
    public:
     constexpr BlobReader(BlobStore& store, size_t offset)
-        : store_(store), offset_(offset) {}
+        : store_(store), open_(false), offset_(offset) {}
     BlobReader(const BlobReader&) = delete;
     BlobReader& operator=(const BlobReader&) = delete;
-    ~BlobReader() { Close(); }
+    ~BlobReader() {
+      if (open_) {
+        Close();
+      }
+    }
 
-    // Open to do a blob read. Currently only a single reader at a time is
-    // supported. Returns:
+    // Open to do a blob read. Can not open when already open. Multiple readers
+    // can be open at the same time. Returns:
     //
     // OK - success.
     // UNAVAILABLE - Unable to open, already open.
-    Status Open() { return Status::UNIMPLEMENTED; }
+    Status Open() {
+      PW_DCHECK(!open_);
+      Status status = store_.OpenRead();
+      if (status.ok()) {
+        open_ = true;
+      }
+      return status;
+    }
 
-    // Finish reading a blob. Returns:
+    // Finish reading a blob. Close fails in the closed state, do NOT retry
+    // Close on error. Returns:
     //
     // OK - success.
-    // FAILED_PRECONDITION - blob is not open.
-    Status Close() { return Status::UNIMPLEMENTED; }
+    Status Close() {
+      PW_DCHECK(open_);
+      open_ = false;
+      return store_.CloseRead();
+    }
 
     // Probable (not guaranteed) minimum number of bytes at this time that can
     // be read. Returns zero if, in the current state, Read would return status
     // other than OK. See stream.h for additional details.
     size_t ConservativeReadLimit() const override {
+      PW_DCHECK(open_);
       return store_.ReadableDataBytes() - offset_;
     }
 
@@ -130,27 +182,39 @@
     // FAILED_PRECONDITION - Reader not open.
     // UNIMPLEMENTED - Memory mapped access not supported for this blob.
     Result<ByteSpan> GetMemoryMappedBlob() {
+      PW_DCHECK(open_);
       return store_.GetMemoryMappedBlob();
     }
 
    private:
     StatusWithSize DoRead(ByteSpan dest) override {
+      PW_DCHECK(open_);
       return store_.Read(offset_, dest);
     }
 
     BlobStore& store_;
+    bool open_;
     size_t offset_;
   };
 
-  BlobStore(kvs::FlashPartition* partition,
+  BlobStore(std::string_view name,
+            kvs::FlashPartition* partition,
             kvs::ChecksumAlgorithm* checksum_algo,
+            kvs::KeyValueStore& kvs,
             ByteSpan write_buffer)
-      : partition_(*partition),
+      : name_(name),
+        partition_(*partition),
         checksum_algo_(checksum_algo),
+        kvs_(kvs),
         write_buffer_(write_buffer),
-        state_(BlobStore::State::kInvalidData),
+        initialized_(false),
+        valid_data_(false),
+        flash_erased_(false),
+        writer_open_(false),
+        readers_open_(0),
+        metadata_({}),
         write_address_(0),
-        flush_address_(0) {}
+        flash_address_(0) {}
 
   BlobStore(const BlobStore&) = delete;
   BlobStore& operator=(const BlobStore&) = delete;
@@ -165,31 +229,31 @@
   size_t MaxDataSizeBytes() const;
 
  private:
-  enum class OpenType {
-    // Open for doing read operations.
-    kRead,
-
-    // Open for Write operations.
-    kWrite,
-  };
+  typedef uint32_t ChecksumValue;
 
   // Is the blob erased and ready to write.
-  bool erased() const { return state_ == State::kErased; }
+  bool erased() const { return flash_erased_; }
 
-  bool IsOpen();
-
-  // Open to do a blob read or write. Returns:
+  // Open to do a blob write. Returns:
   //
   // OK - success.
-  // UNAVAILABLE - Unable to open, already open.
-  Status Open(BlobStore::OpenType type);
+  // UNAVAILABLE - Unable to open writer, another writer or reader instance is
+  //     already open.
+  Status OpenWrite();
+
+  // Open to do a blob read. Returns:
+  //
+  // OK - success.
+  // FAILED_PRECONDITION - Unable to open, no valid blob available.
+  Status OpenRead();
 
   // Finalize a blob write. Flush all remaining buffered data to storage and
   // store blob metadata. Returns:
   //
   // OK - success.
   // FAILED_PRECONDITION - blob is not open.
-  Status Close();
+  Status CloseWrite();
+  Status CloseRead();
 
   // Write/append data to the in-progress blob write. Data is written
   // sequentially, with each append added directly after the previous. Data is
@@ -203,6 +267,27 @@
   //     no more will be written.
   Status Write(ConstByteSpan data);
 
+  // Commit data to flash and update flash_address_ with data bytes written. The
+  // only time data_bytes should be manually specified is for a CloseWrite with
+  // an unaligned-size chunk remaining in the buffer that has been zero padded
+  // to alignment.
+  Status CommitToFlash(ConstByteSpan source, size_t data_bytes = 0) {
+    if (data_bytes == 0) {
+      data_bytes = source.size_bytes();
+    }
+    flash_erased_ = false;
+    valid_data_ = true;
+    StatusWithSize result = partition_.Write(flash_address_, source);
+    flash_address_ += data_bytes;
+    if (checksum_algo_ != nullptr) {
+      checksum_algo_->Update(source.first(data_bytes));
+    }
+
+    return result.status();
+  }
+
+  bool WriteBufferEmpty() { return flash_address_ == write_address_; }
+
   // Read valid data. Attempts to read the lesser of output.size_bytes() or
   // available bytes worth of data. Returns:
   //
@@ -221,22 +306,96 @@
   // OK with span - Valid span respresenting the blob data
   // FAILED_PRECONDITION - Blob not in a state to read data
   // UNIMPLEMENTED - Memory mapped access not supported for this blob.
-  Result<ByteSpan> GetMemoryMappedBlob();
+  Result<ByteSpan> GetMemoryMappedBlob() const;
 
   // Current size of blob/readable data, in bytes. For a completed write this is
   // the size of the data blob. For all other cases this is zero bytes.
   size_t ReadableDataBytes() const;
 
-  Status EraseInternal();
+  size_t WriteBytesRemaining() const {
+    return MaxDataSizeBytes() - write_address_;
+  }
 
-  Status InvalidateInternal();
+  Status Erase();
 
+  Status Invalidate();
+
+  void ResetChecksum() {
+    if (checksum_algo_ != nullptr) {
+      checksum_algo_->Reset();
+    }
+  }
+
+  Status ValidateChecksum();
+
+  Result<BlobStore::ChecksumValue> CalculateChecksumFromFlash(
+      size_t bytes_to_check);
+
+  const std::string_view MetadataKey() { return name_; }
+
+  // Changes to the metadata format should also get a different key signature to
+  // avoid new code improperly reading old format metadata.
+  struct BlobMetadata {
+    // The checksum of the blob data stored in flash.
+    ChecksumValue checksum;
+
+    // Number of blob data bytes stored in flash.
+    size_t data_size_bytes;
+
+    // This blob is complete (the write was properly closed).
+    bool complete;
+  };
+
+  std::string_view name_;
   kvs::FlashPartition& partition_;
   kvs::ChecksumAlgorithm* const checksum_algo_;
+  kvs::KeyValueStore& kvs_;
   ByteSpan write_buffer_;
-  State state_;
+
+  //
+  // Internal state for Blob store
+  //
+  // TODO: Consolidate blob state to a single struct
+
+  // Initialization has been done.
+  bool initialized_;
+
+  // Bytes stored are valid and good.
+  bool valid_data_;
+
+  // Blob partition is currently erased and ready to write a new blob.
+  bool flash_erased_;
+
+  // BlobWriter instance is currently open
+  bool writer_open_;
+
+  // Count of open BlobReader instances
+  size_t readers_open_;
+
+  // Metadata for the blob.
+  BlobMetadata metadata_;
+
+  // Current index for end of overal blob data. Represents current byte size of
+  // blob data since the FlashPartition starts at address 0.
   kvs::FlashPartition::Address write_address_;
-  kvs::FlashPartition::Address flush_address_;
+
+  // Current index of end of data written to flash. Number of buffered data
+  // bytes is write_address_ - flash_address_.
+  kvs::FlashPartition::Address flash_address_;
+};
+
+// Creates a BlobStore with the buffer of kBufferSizeBytes.
+template <size_t kBufferSizeBytes>
+class BlobStoreBuffer : public BlobStore {
+ public:
+  explicit BlobStoreBuffer(std::string_view name,
+                           kvs::FlashPartition* partition,
+                           kvs::ChecksumAlgorithm* checksum_algo,
+                           kvs::KeyValueStore& kvs)
+      : BlobStore(name, partition, checksum_algo, kvs, buffer_) {}
+
+ private:
+  std::array<std::byte, kBufferSizeBytes> buffer_;
 };
 
 }  // namespace pw::blob_store
diff --git a/pw_random/CMakeLists.txt b/pw_random/CMakeLists.txt
new file mode 100644
index 0000000..b297f32
--- /dev/null
+++ b/pw_random/CMakeLists.txt
@@ -0,0 +1,20 @@
+# 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.
+
+pw_auto_add_simple_module(pw_random
+  PUBLIC_DEPS
+    pw_bytes
+    pw_span
+    pw_status
+)
diff --git a/pw_result/CMakeLists.txt b/pw_result/CMakeLists.txt
new file mode 100644
index 0000000..9bf8f5f
--- /dev/null
+++ b/pw_result/CMakeLists.txt
@@ -0,0 +1,20 @@
+# 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.
+
+pw_auto_add_simple_module(pw_result
+  PUBLIC_DEPS
+    pw_status
+  PRIVATE_DEPS
+    pw_assert
+)
diff --git a/pw_stream/CMakeLists.txt b/pw_stream/CMakeLists.txt
new file mode 100644
index 0000000..1d17437
--- /dev/null
+++ b/pw_stream/CMakeLists.txt
@@ -0,0 +1,26 @@
+# 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.
+
+pw_auto_add_simple_module(pw_stream
+  PUBLIC_DEPS
+    pw_bytes
+    pw_containers
+    pw_log
+    pw_result
+    pw_span
+    pw_status
+  PRIVATE_DEPS
+    pw_assert
+    pw_string
+)