pw_persistent_ram: Add PersistentBuffer

Adds a PersistentBuffer object that acts as a persistent form of a
MemoryWriterBuffer.

Change-Id: I61db1a77476ceececd088fd60bc10bfaf1b99e0e
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/39360
Reviewed-by: Wyatt Hepler <hepler@google.com>
Commit-Queue: Armando Montanez <amontanez@google.com>
diff --git a/pw_persistent_ram/BUILD b/pw_persistent_ram/BUILD
index 3d35525..1cde0cd 100644
--- a/pw_persistent_ram/BUILD
+++ b/pw_persistent_ram/BUILD
@@ -26,8 +26,16 @@
     name = "pw_persistent_ram",
     hdrs = [
         "public/pw_persistent_ram/persistent.h",
+        "public/pw_persistent_ram/persistent_buffer.h",
     ],
     includes = ["public"],
+    srcs = ["persistent_buffer.cc"],
+    deps = [
+        "//pw_assert",
+        "//pw_bytes",
+        "//pw_checksum",
+        "//pw_stream",
+    ],
 )
 
 pw_cc_test(
@@ -40,3 +48,15 @@
         "//pw_unit_test",
     ],
 )
+
+pw_cc_test(
+    name = "persistent_buffer_test",
+    srcs = [
+        "persistent_buffer_test.cc",
+    ],
+    deps = [
+        ":pw_persistent_ram",
+        "//pw_unit_test",
+        "//pw_random",
+    ],
+)
diff --git a/pw_persistent_ram/BUILD.gn b/pw_persistent_ram/BUILD.gn
index b58ccd5..729656f 100644
--- a/pw_persistent_ram/BUILD.gn
+++ b/pw_persistent_ram/BUILD.gn
@@ -26,15 +26,24 @@
 
 pw_source_set("pw_persistent_ram") {
   public_configs = [ ":public_include_path" ]
-  public = [ "public/pw_persistent_ram/persistent.h" ]
+  public = [
+    "public/pw_persistent_ram/persistent.h",
+    "public/pw_persistent_ram/persistent_buffer.h",
+  ]
+  sources = [ "persistent_buffer.cc" ]
   public_deps = [
     dir_pw_assert,
+    dir_pw_bytes,
     dir_pw_checksum,
+    dir_pw_stream,
   ]
 }
 
 pw_test_group("tests") {
-  tests = [ ":persistent_test" ]
+  tests = [
+    ":persistent_test",
+    ":persistent_buffer_test",
+  ]
 }
 
 pw_test("persistent_test") {
@@ -42,6 +51,14 @@
   sources = [ "persistent_test.cc" ]
 }
 
+pw_test("persistent_buffer_test") {
+  deps = [
+    ":pw_persistent_ram",
+    dir_pw_random,
+  ]
+  sources = [ "persistent_buffer_test.cc" ]
+}
+
 pw_doc_group("docs") {
   sources = [ "docs.rst" ]
   report_deps = [ ":persistent_size" ]
diff --git a/pw_persistent_ram/docs.rst b/pw_persistent_ram/docs.rst
index 75e8175..5286c86 100644
--- a/pw_persistent_ram/docs.rst
+++ b/pw_persistent_ram/docs.rst
@@ -209,6 +209,54 @@
       // ... rest of main
     }
 
+------------------------------------
+pw::persistent_ram::PersistentBuffer
+------------------------------------
+The PersistentBuffer is a persistent storage container for variable-length
+serialized data. Rather than allowing direct access to the underlying buffer for
+random-access mutations, the PersistentBuffer is mutable through a
+PersistentBufferWriter that implements the pw::stream::Writer interface. This
+removes the potential for logical errors due to RAII or open()/close() semantics
+as both the PersistentBuffer and PersistentBufferWriter can be used validly as
+long as their access is serialized.
+
+Example
+-------
+An example use case is emitting crash handler logs to a buffer for them to be
+available after a the device reboots. Once the device reboots, the logs would be
+emitted by the logging system. While this isn't always practical for plaintext
+logs, tokenized logs are small enough for this to be useful.
+
+.. code-block:: cpp
+
+    #include "pw_persistent_ram/persistent_buffer.h"
+    #include "pw_preprocessor/compiler.h"
+
+    using pw::persistent_ram::PersistentBuffer;
+    using pw::persistent_ram::PersistentBuffer::PersistentBufferWriter;
+
+    PW_KEEP_IN_SECTION(".noinit") PersistentBuffer<2048> crash_logs;
+    void CheckForCrashLogs() {
+      if (crash_logs.has_value()) {
+        // A function that dumps sequentially serialized logs using pw_log.
+        DumpRawLogs(crash_logs.written_data());
+        crash_logs.clear();
+      }
+    }
+
+    void HandleCrash(CrashInfo* crash_info) {
+      PersistentBufferWriter crash_log_writer = crash_logs.GetWriter();
+      // Sets the pw::stream::Writer that pw_log should dump logs to.
+      crash_log_writer.clear();
+      SetLogSink(crash_log_writer);
+      // Handle crash, calling PW_LOG to log useful info.
+    }
+
+    int main() {
+      void CheckForCrashLogs();
+      // ... rest of main
+    }
+
 Size Report
 -----------
 The following size report showcases the overhead for using Persistent. Note that
diff --git a/pw_persistent_ram/persistent_buffer.cc b/pw_persistent_ram/persistent_buffer.cc
new file mode 100644
index 0000000..9cee8fc
--- /dev/null
+++ b/pw_persistent_ram/persistent_buffer.cc
@@ -0,0 +1,43 @@
+// Copyright 2021 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_persistent_ram/persistent_buffer.h"
+
+#include "pw_bytes/span.h"
+#include "pw_checksum/crc16_ccitt.h"
+#include "pw_status/status.h"
+
+namespace pw::persistent_ram {
+
+Status PersistentBufferWriter::DoWrite(ConstByteSpan data) {
+  if (ConservativeWriteLimit() == 0) {
+    return Status::OutOfRange();
+  }
+  if (ConservativeWriteLimit() < data.size_bytes()) {
+    return Status::ResourceExhausted();
+  }
+  if (data.empty()) {
+    return OkStatus();
+  }
+
+  std::memcpy(buffer_.data() + size_, data.data(), data.size_bytes());
+
+  // Only checksum newly written data.
+  checksum_ = checksum::Crc16Ccitt::Calculate(
+      ByteSpan(buffer_.data() + size_, data.size_bytes()), checksum_);
+  size_ += data.size_bytes();
+
+  return OkStatus();
+}
+
+}  // namespace pw::persistent_ram
diff --git a/pw_persistent_ram/persistent_buffer_test.cc b/pw_persistent_ram/persistent_buffer_test.cc
new file mode 100644
index 0000000..cf72fc2
--- /dev/null
+++ b/pw_persistent_ram/persistent_buffer_test.cc
@@ -0,0 +1,158 @@
+// Copyright 2021 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_persistent_ram/persistent_buffer.h"
+
+#include <cstddef>
+#include <span>
+#include <type_traits>
+
+#include "gtest/gtest.h"
+#include "pw_bytes/span.h"
+#include "pw_random/xor_shift.h"
+
+namespace pw::persistent_ram {
+namespace {
+
+class PersistentTest : public ::testing::Test {
+ protected:
+  static constexpr size_t kBufferSize = 256;
+  PersistentTest() { ZeroPersistentMemory(); }
+
+  // Emulate invalidation of persistent section(s).
+  void ZeroPersistentMemory() { memset(buffer_, 0, sizeof(buffer_)); }
+  void RandomFillMemory() {
+    random::XorShiftStarRng64 rng(0x9ad75);
+    StatusWithSize sws = rng.Get(buffer_);
+    ASSERT_TRUE(sws.ok());
+    ASSERT_EQ(sws.size(), sizeof(buffer_));
+  }
+
+  PersistentBuffer<kBufferSize>& GetPersistentBuffer() {
+    return *(new (buffer_) PersistentBuffer<kBufferSize>());
+  }
+
+  // Allocate a chunk of aligned storage that can be independently controlled.
+  alignas(PersistentBuffer<kBufferSize>)
+      std::byte buffer_[sizeof(PersistentBuffer<kBufferSize>)];
+};
+
+TEST_F(PersistentTest, DefaultConstructionAndDestruction) {
+  constexpr uint32_t kExpectedNumber = 0x6C2C6582;
+  {
+    // Emulate a boot where the persistent sections were invalidated.
+    // Although the fixture always does this, we do this an extra time to be
+    // 100% confident that an integrity check cannot be accidentally selected
+    // which results in reporting there is valid data when zero'd.
+    ZeroPersistentMemory();
+    auto& persistent = GetPersistentBuffer();
+    auto writer = persistent.GetWriter();
+    EXPECT_EQ(persistent.size(), 0u);
+
+    writer.Write(std::as_bytes(std::span(&kExpectedNumber, 1)));
+    ASSERT_TRUE(persistent.has_value());
+
+    persistent.~PersistentBuffer();  // Emulate shutdown / global destructors.
+  }
+
+  {  // Emulate a boot where persistent memory was kept as is.
+    auto& persistent = GetPersistentBuffer();
+    ASSERT_TRUE(persistent.has_value());
+    EXPECT_EQ(persistent.size(), sizeof(kExpectedNumber));
+
+    uint32_t temp = 0;
+    memcpy(&temp, persistent.data(), sizeof(temp));
+    EXPECT_EQ(temp, kExpectedNumber);
+  }
+}
+
+TEST_F(PersistentTest, LongData) {
+  constexpr std::string_view kTestString(
+      "A nice string should remain valid even if written incrementally!");
+  constexpr size_t kWriteSize = 5;
+
+  {  // Initialize the buffer.
+    RandomFillMemory();
+    auto& persistent = GetPersistentBuffer();
+    ASSERT_FALSE(persistent.has_value());
+
+    auto writer = persistent.GetWriter();
+    for (size_t i = 0; i < kTestString.length(); i += kWriteSize) {
+      writer.Write(kTestString.data() + i,
+                   std::min(kWriteSize, kTestString.length() - i));
+    }
+    // Need to manually write a null terminator since std::string_view doesn't
+    // include one in the string length.
+    writer.Write(std::byte(0));
+
+    persistent.~PersistentBuffer();  // Emulate shutdown / global destructors.
+  }
+
+  {  // Ensure data is valid.
+    auto& persistent = GetPersistentBuffer();
+    ASSERT_TRUE(persistent.has_value());
+    ASSERT_STREQ(kTestString.data(),
+                 reinterpret_cast<const char*>(persistent.data()));
+  }
+}
+
+TEST_F(PersistentTest, ZeroDataIsNoValue) {
+  ZeroPersistentMemory();
+  auto& persistent = GetPersistentBuffer();
+  EXPECT_FALSE(persistent.has_value());
+}
+
+TEST_F(PersistentTest, RandomDataIsInvalid) {
+  RandomFillMemory();
+  auto& persistent = GetPersistentBuffer();
+  ASSERT_FALSE(persistent.has_value());
+}
+
+TEST_F(PersistentTest, AppendingData) {
+  constexpr std::string_view kTestString("Test string one!");
+  constexpr uint32_t kTestNumber = 42;
+
+  {  // Initialize the buffer.
+    RandomFillMemory();
+    auto& persistent = GetPersistentBuffer();
+    auto writer = persistent.GetWriter();
+    EXPECT_EQ(persistent.size(), 0u);
+
+    // Write an integer.
+    writer.Write(std::as_bytes(std::span(&kTestNumber, 1)));
+    ASSERT_TRUE(persistent.has_value());
+
+    persistent.~PersistentBuffer();  // Emulate shutdown / global destructors.
+  }
+
+  {  // Get a pointer to the buffer and validate the contents.
+    auto& persistent = GetPersistentBuffer();
+    ASSERT_TRUE(persistent.has_value());
+    EXPECT_EQ(persistent.size(), sizeof(kTestNumber));
+
+    // Write more data.
+    auto writer = persistent.GetWriter();
+    EXPECT_EQ(persistent.size(), sizeof(kTestNumber));
+    writer.Write(std::as_bytes(std::span<const char>(kTestString)));
+
+    persistent.~PersistentBuffer();  // Emulate shutdown / global destructors.
+  }
+  {  // Ensure data was appended.
+    auto& persistent = GetPersistentBuffer();
+    ASSERT_TRUE(persistent.has_value());
+    EXPECT_EQ(persistent.size(), sizeof(kTestNumber) + kTestString.length());
+  }
+}
+
+}  // namespace
+}  // namespace pw::persistent_ram
diff --git a/pw_persistent_ram/public/pw_persistent_ram/persistent_buffer.h b/pw_persistent_ram/public/pw_persistent_ram/persistent_buffer.h
new file mode 100644
index 0000000..033d244
--- /dev/null
+++ b/pw_persistent_ram/public/pw_persistent_ram/persistent_buffer.h
@@ -0,0 +1,150 @@
+// Copyright 2021 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.
+#pragma once
+
+#include <cstdint>
+#include <cstring>
+#include <span>
+#include <type_traits>
+#include <utility>
+
+#include "pw_bytes/span.h"
+#include "pw_checksum/crc16_ccitt.h"
+#include "pw_status/status.h"
+#include "pw_stream/stream.h"
+
+namespace pw::persistent_ram {
+
+// A PersistentBufferWriter implements the pw::stream::Writer interface and
+// provides handles to mutate and access the underlying data of a
+// PersistentBuffer. This object should NOT be stored in persistent RAM.
+//
+// Only one writer should be open at a given time.
+class PersistentBufferWriter : public stream::Writer {
+ public:
+  PersistentBufferWriter() = delete;
+
+  size_t ConservativeWriteLimit() const override {
+    return buffer_.size_bytes() - size_;
+  }
+
+ private:
+  template <size_t>
+  friend class PersistentBuffer;
+
+  PersistentBufferWriter(ByteSpan buffer,
+                         volatile size_t& size,
+                         volatile uint16_t& checksum)
+      : buffer_(buffer), size_(size), checksum_(checksum) {}
+
+  // Implementation for writing data to this stream.
+  Status DoWrite(ConstByteSpan data) override;
+
+  ByteSpan buffer_;
+  volatile size_t& size_;
+  volatile uint16_t& checksum_;
+};
+
+#if defined(__clang__)
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wuninitialized"
+#elif defined(__GNUC__)
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wuninitialized"
+#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
+#endif
+
+// When a PersistentBuffer is statically allocated in persistent memory, its
+// state will persist across soft resets in accordance with the expected
+// behavior of the underlying RAM. This object is completely safe to use before
+// static constructors are called as its constructor is effectively a no-op.
+//
+// While the stored data can be read by PersistentBuffer's public functions,
+// each public function must validate the integrity of the stored data. It's
+// typically more performant to get a handle to a PersistentBufferWriter
+// instead, as data is validated on creation of the PersistentBufferWriter,
+// which allows access to the underlying data without needing to validate the
+// data's integrity with each call to PersistentBufferWriter functions.
+template <size_t kMaxSizeBytes>
+class PersistentBuffer {
+ public:
+  // The default constructor intentionally does not initialize anything. This
+  // allows a persistent buffer statically allocated in persistent RAM to be
+  // highly available.
+  //
+  // Explicitly declaring an empty constructor rather than using the default
+  // constructor prevents the object from being zero-initialized when the object
+  // is value initialized. If this was left as a default constructor,
+  // PersistentBuffer objects declared as value-initialized would be
+  // zero-initialized.
+  //
+  //   // Value initialization:
+  //   PersistentBuffer<256> persistent_buffer();
+  //
+  //   // Default initialization:
+  //   PersistentBuffer<256> persistent_buffer;
+  PersistentBuffer() {}
+  // Disable copy and move constructors.
+  PersistentBuffer(const PersistentBuffer&) = delete;
+  PersistentBuffer(PersistentBuffer&&) = delete;
+  // Explicit no-op destructor.
+  ~PersistentBuffer() {}
+
+  PersistentBufferWriter GetWriter() {
+    if (!has_value()) {
+      clear();
+    }
+    return PersistentBufferWriter(
+        ByteSpan(const_cast<std::byte*>(buffer_), kMaxSizeBytes),
+        size_,
+        checksum_);
+  }
+
+  size_t size() const {
+    if (has_value()) {
+      return size_;
+    }
+    return 0;
+  }
+
+  const std::byte* data() const { return const_cast<std::byte*>(buffer_); }
+
+  void clear() {
+    size_ = 0;
+    checksum_ = checksum::Crc16Ccitt::kInitialValue;
+  }
+
+  bool has_value() const {
+    if (size_ > kMaxSizeBytes || size_ == 0) {
+      return false;
+    }
+
+    // Check checksum. This is more costly.
+    return checksum_ == checksum::Crc16Ccitt::Calculate(ConstByteSpan(
+                            const_cast<std::byte*>(buffer_), size_));
+  }
+
+ private:
+  // None of these members are initialized by the constructor by design.
+  volatile uint16_t checksum_;
+  volatile size_t size_;
+  volatile std::byte buffer_[kMaxSizeBytes];
+};
+
+#if defined(__clang__)
+#pragma clang diagnostic pop
+#elif defined(__GNUC__)
+#pragma GCC diagnostic pop
+#endif
+}  // namespace pw::persistent_ram