pw_multibuf: Add basic MultiBuf operations

This change introduces the essential operators for creating, inserting
chunks into, removing chunks from, and iterating over MultiBufs.

Later changes will add higher-level convenience functions for inserting
new blocks of bytes within a MultiBuf, as well as the MultiBufAllocator
interface.

Change-Id: I9c9af1d887df5145482a74c7f6dacfd987c86a4d
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/178036
Commit-Queue: Taylor Cramer <cramertj@google.com>
Reviewed-by: Carlos Chinchilla <cachinchilla@google.com>
Presubmit-Verified: CQ Bot Account <pigweed-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Alexei Frolov <frolv@google.com>
diff --git a/pw_multibuf/BUILD.bazel b/pw_multibuf/BUILD.bazel
index 8a37f50..c078e60 100644
--- a/pw_multibuf/BUILD.bazel
+++ b/pw_multibuf/BUILD.bazel
@@ -36,13 +36,24 @@
     ],
 )
 
+pw_cc_library(
+    name = "test_utils",
+    hdrs = ["public/pw_multibuf/internal/test_utils.h"],
+    includes = ["public"],
+    visibility = [":__subpackages__"],
+    deps = [
+        ":chunk",
+        "//pw_allocator:allocator_metric_proxy",
+        "//pw_allocator:split_free_list_allocator",
+    ],
+)
+
 pw_cc_test(
     name = "chunk_test",
     srcs = ["chunk_test.cc"],
     deps = [
         ":chunk",
-        "//pw_allocator:allocator_metric_proxy",
-        "//pw_allocator:split_free_list_allocator",
+        ":test_utils",
         "//pw_unit_test",
     ],
 )
@@ -59,6 +70,7 @@
     srcs = ["multibuf_test.cc"],
     deps = [
         ":pw_multibuf",
+        ":test_utils",
         "//pw_unit_test",
     ],
 )
diff --git a/pw_multibuf/BUILD.gn b/pw_multibuf/BUILD.gn
index bfdc05f..41a9f20 100644
--- a/pw_multibuf/BUILD.gn
+++ b/pw_multibuf/BUILD.gn
@@ -37,12 +37,21 @@
   deps = [ "$dir_pw_assert:check" ]
 }
 
-pw_test("chunk_test") {
-  deps = [
+pw_source_set("test_utils") {
+  public_configs = [ ":public_include_path" ]
+  public = [ "public/pw_multibuf/internal/test_utils.h" ]
+  public_deps = [
     ":chunk",
     "$dir_pw_allocator:allocator_metric_proxy",
     "$dir_pw_allocator:split_free_list_allocator",
   ]
+}
+
+pw_test("chunk_test") {
+  deps = [
+    ":chunk",
+    ":test_utils",
+  ]
   sources = [ "chunk_test.cc" ]
 }
 
@@ -54,7 +63,10 @@
 }
 
 pw_test("multibuf_test") {
-  deps = [ ":pw_multibuf" ]
+  deps = [
+    ":pw_multibuf",
+    ":test_utils",
+  ]
   sources = [ "multibuf_test.cc" ]
 }
 
diff --git a/pw_multibuf/CMakeLists.txt b/pw_multibuf/CMakeLists.txt
index 4c20592..217b70a 100644
--- a/pw_multibuf/CMakeLists.txt
+++ b/pw_multibuf/CMakeLists.txt
@@ -31,12 +31,22 @@
     chunk.cc
 )
 
-pw_add_test(pw_multibuf.chunk
+pw_add_library(pw_multibuf.test_utils STATIC
+  HEADERS
+    public/pw_multibuf/internal/test_utils.h
+  PUBLIC_INCLUDES
+    public
+  PUBLIC_DEPS
+    pw_multibuf.chunk
+    pw_allocator.allocator_metric_proxy
+    pw_allocator.split_free_list_allocator
+
+pw_add_test(pw_multibuf.chunk_test STATIC
   SOURCES
     chunk_test.cc
   PRIVATE_DEPS
-    pw_allocator.allocator_metric_proxy
-    pw_allocator.split_free_list_allocator
+    pw_multibuf.chunk
+    pw_multibuf.test_utils
   GROUPS
     modules
     pw_multibuf
@@ -58,6 +68,7 @@
     multibuf_test.cc
   PRIVATE_DEPS
     pw_multibuf.multibuf
+    pw_multibuf.test_utils
   GROUPS
     modules
     pw_multibuf
diff --git a/pw_multibuf/chunk_test.cc b/pw_multibuf/chunk_test.cc
index e4f2c08..23ff0e3 100644
--- a/pw_multibuf/chunk_test.cc
+++ b/pw_multibuf/chunk_test.cc
@@ -21,126 +21,13 @@
 #endif  // __cplusplus >= 202002L
 
 #include "gtest/gtest.h"
-#include "pw_allocator/allocator_metric_proxy.h"
-#include "pw_allocator/split_free_list_allocator.h"
+#include "pw_multibuf/internal/test_utils.h"
 
 namespace pw::multibuf {
 namespace {
 
-class TrackingAllocator : public pw::allocator::Allocator {
- public:
-  TrackingAllocator(ByteSpan span) : alloc_stats_(kFakeToken) {
-    Status status = alloc_.Init(span, kFakeThreshold);
-    EXPECT_EQ(status, OkStatus());
-    alloc_stats_.Initialize(alloc_);
-  }
-
-  size_t count() const { return alloc_stats_.count(); }
-  size_t used() const { return alloc_stats_.used(); }
-
- protected:
-  void* DoAllocate(size_t size, size_t alignment) override {
-    return alloc_stats_.AllocateUnchecked(size, alignment);
-  }
-  bool DoResize(void* ptr,
-                size_t old_size,
-                size_t old_align,
-                size_t new_size) override {
-    return alloc_stats_.ResizeUnchecked(ptr, old_size, old_align, new_size);
-  }
-  void DoDeallocate(void* ptr, size_t size, size_t alignment) override {
-    alloc_stats_.DeallocateUnchecked(ptr, size, alignment);
-  }
-
- private:
-  const size_t kFakeThreshold = 0;
-  const int32_t kFakeToken = 0;
-
-  pw::allocator::SplitFreeListAllocator<> alloc_;
-  pw::allocator::AllocatorMetricProxy alloc_stats_;
-};
-
-template <auto NumBytes>
-class TrackingAllocatorWithMemory : public pw::allocator::Allocator {
- public:
-  TrackingAllocatorWithMemory() : mem_(), alloc_(mem_) {}
-  size_t count() const { return alloc_.count(); }
-  size_t used() const { return alloc_.used(); }
-  void* DoAllocate(size_t size, size_t alignment) override {
-    return alloc_.AllocateUnchecked(size, alignment);
-  }
-  bool DoResize(void* ptr,
-                size_t old_size,
-                size_t old_align,
-                size_t new_size) override {
-    return alloc_.ResizeUnchecked(ptr, old_size, old_align, new_size);
-  }
-  void DoDeallocate(void* ptr, size_t size, size_t alignment) override {
-    alloc_.DeallocateUnchecked(ptr, size, alignment);
-  }
-
- private:
-  std::array<std::byte, NumBytes> mem_;
-  TrackingAllocator alloc_;
-};
-
-class HeaderChunkRegionTracker final : public ChunkRegionTracker {
- public:
-  static std::optional<OwnedChunk> AllocateRegionAsChunk(
-      pw::allocator::Allocator* alloc, size_t size) {
-    HeaderChunkRegionTracker* tracker = AllocateRegion(alloc, size);
-    if (tracker == nullptr) {
-      return std::nullopt;
-    }
-    std::optional<OwnedChunk> chunk = Chunk::CreateFirstForRegion(*tracker);
-    if (!chunk.has_value()) {
-      tracker->Destroy();
-      return std::nullopt;
-    }
-    return chunk;
-  }
-
-  static HeaderChunkRegionTracker* AllocateRegion(
-      pw::allocator::Allocator* alloc, size_t size) {
-    size_t alloc_size = size + sizeof(HeaderChunkRegionTracker);
-    size_t align = alignof(HeaderChunkRegionTracker);
-    void* ptr = alloc->AllocateUnchecked(alloc_size, align);
-    if (ptr == nullptr) {
-      return nullptr;
-    }
-    std::byte* data =
-        reinterpret_cast<std::byte*>(ptr) + sizeof(HeaderChunkRegionTracker);
-    return new (ptr) HeaderChunkRegionTracker(ByteSpan(data, size), alloc);
-  }
-
-  ByteSpan Region() const final { return region_; }
-  ~HeaderChunkRegionTracker() final {}
-
- protected:
-  void Destroy() final {
-    std::byte* ptr = reinterpret_cast<std::byte*>(this);
-    size_t size = sizeof(HeaderChunkRegionTracker) + region_.size();
-    size_t align = alignof(HeaderChunkRegionTracker);
-    auto alloc = alloc_;
-    std::destroy_at(this);
-    alloc->DeallocateUnchecked(ptr, size, align);
-  }
-  void* AllocateChunkClass() final {
-    return alloc_->Allocate(pw::allocator::Layout::Of<Chunk>());
-  }
-  void DeallocateChunkClass(void* ptr) final {
-    alloc_->Deallocate(ptr, pw::allocator::Layout::Of<Chunk>());
-  }
-
- private:
-  ByteSpan region_;
-  pw::allocator::Allocator* alloc_;
-
-  // NOTE: `region` must directly follow this `FakeChunkRegionTracker`
-  // in memory allocated by allocated by `alloc`.
-  HeaderChunkRegionTracker(ByteSpan region, pw::allocator::Allocator* alloc)
-      : region_(region), alloc_(alloc) {}
-};
+using ::pw::multibuf::internal::HeaderChunkRegionTracker;
+using ::pw::multibuf::internal::TrackingAllocatorWithMemory;
 
 /// Returns literal with ``_size`` suffix as a ``size_t``.
 ///
diff --git a/pw_multibuf/docs.rst b/pw_multibuf/docs.rst
index 78d8919..e19f8e7 100644
--- a/pw_multibuf/docs.rst
+++ b/pw_multibuf/docs.rst
@@ -55,7 +55,8 @@
 
 ``MultiBuf`` s consist of a number of ``Chunk`` s of contiguous memory.
 These ``Chunk`` s can be grown, shrunk, modified, or extracted from the
-``MultiBuf``.
+``MultiBuf``. ``MultiBuf`` exposes an ``std::byte`` iterator interface as well
+as a ``Chunk`` iterator available through the ``Chunks()`` method.
 
 An RAII-style ``OwnedChunk`` is also provided, and manages the lifetime of
 ``Chunk`` s which are not currently stored inside of a ``MultiBuf``.
diff --git a/pw_multibuf/multibuf.cc b/pw_multibuf/multibuf.cc
index e69de29..b1eb805 100644
--- a/pw_multibuf/multibuf.cc
+++ b/pw_multibuf/multibuf.cc
@@ -0,0 +1,129 @@
+// Copyright 2023 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_multibuf/multibuf.h"
+
+#include "pw_assert/check.h"
+
+namespace pw::multibuf {
+
+void MultiBuf::Release() noexcept {
+  while (first_ != nullptr) {
+    Chunk* removed = first_;
+    first_ = first_->next_in_buf_;
+    removed->Free();
+  }
+}
+
+size_t MultiBuf::size() const {
+  size_t len = 0;
+  for (const auto& chunk : Chunks()) {
+    len += chunk.size();
+  }
+  return len;
+}
+
+void MultiBuf::PushFrontChunk(OwnedChunk chunk) {
+  PW_DCHECK(chunk->next_in_buf_ == nullptr);
+  Chunk* new_chunk = std::move(chunk).Take();
+  Chunk* old_first = first_;
+  new_chunk->next_in_buf_ = old_first;
+  first_ = new_chunk;
+}
+
+MultiBuf::ChunkIterator MultiBuf::InsertChunk(ChunkIterator position,
+                                              OwnedChunk chunk) {
+  // Note: this also catches the cases where ``first_ == nullptr``
+  PW_DCHECK(chunk->next_in_buf_ == nullptr);
+  if (position == ChunkBegin()) {
+    PushFrontChunk(std::move(chunk));
+    return ChunkIterator(first_);
+  }
+  Chunk* previous = Previous(position.chunk_);
+  Chunk* old_next = previous->next_in_buf_;
+  Chunk* new_chunk = std::move(chunk).Take();
+  new_chunk->next_in_buf_ = old_next;
+  previous->next_in_buf_ = new_chunk;
+  return ChunkIterator(new_chunk);
+}
+
+std::tuple<MultiBuf::ChunkIterator, OwnedChunk> MultiBuf::TakeChunk(
+    ChunkIterator position) {
+  Chunk* chunk = position.chunk_;
+  if (position == ChunkBegin()) {
+    Chunk* old_first = first_;
+    first_ = old_first->next_in_buf_;
+    old_first->next_in_buf_ = nullptr;
+    return std::make_tuple(ChunkIterator(first_), OwnedChunk(old_first));
+  }
+  Chunk* previous = Previous(chunk);
+  previous->next_in_buf_ = chunk->next_in_buf_;
+  chunk->next_in_buf_ = nullptr;
+  return std::make_tuple(ChunkIterator(previous->next_in_buf_),
+                         OwnedChunk(chunk));
+}
+
+Chunk* MultiBuf::Previous(Chunk* chunk) const {
+  Chunk* previous = first_;
+  while (previous != nullptr && previous->next_in_buf_ != chunk) {
+    previous = previous->next_in_buf_;
+  }
+  return previous;
+}
+
+MultiBuf::iterator& MultiBuf::iterator::operator++() {
+  if (byte_index_ + 1 == chunk_->size()) {
+    chunk_ = chunk_->next_in_buf_;
+    byte_index_ = 0;
+    AdvanceToData();
+  } else {
+    ++byte_index_;
+  }
+  return *this;
+}
+
+void MultiBuf::iterator::AdvanceToData() {
+  while (chunk_ != nullptr && chunk_->size() == 0) {
+    chunk_ = chunk_->next_in_buf_;
+  }
+}
+
+MultiBuf::const_iterator& MultiBuf::const_iterator::operator++() {
+  if (byte_index_ + 1 == chunk_->size()) {
+    chunk_ = chunk_->next_in_buf_;
+    byte_index_ = 0;
+    AdvanceToData();
+  } else {
+    ++byte_index_;
+  }
+  return *this;
+}
+
+void MultiBuf::const_iterator::AdvanceToData() {
+  while (chunk_ != nullptr && chunk_->size() == 0) {
+    chunk_ = chunk_->next_in_buf_;
+  }
+}
+
+size_t MultiBuf::ChunkIterable::size() const {
+  Chunk* current = first_;
+  size_t i = 0;
+  while (current != nullptr) {
+    ++i;
+    current = current->next_in_buf_;
+  }
+  return i;
+}
+
+}  // namespace pw::multibuf
diff --git a/pw_multibuf/multibuf_test.cc b/pw_multibuf/multibuf_test.cc
index 285b572..d1bfe96 100644
--- a/pw_multibuf/multibuf_test.cc
+++ b/pw_multibuf/multibuf_test.cc
@@ -15,4 +15,200 @@
 #include "pw_multibuf/multibuf.h"
 
 #include "gtest/gtest.h"
-#include "pw_multibuf/chunk.h"
+#include "pw_bytes/suffix.h"
+#include "pw_multibuf/internal/test_utils.h"
+
+namespace pw::multibuf {
+namespace {
+
+using ::pw::multibuf::internal::HeaderChunkRegionTracker;
+using ::pw::multibuf::internal::TrackingAllocatorWithMemory;
+
+const size_t kArbitraryAllocatorSize = 1024;
+const size_t kArbitraryChunkSize = 32;
+
+#if __cplusplus >= 202002L
+static_assert(std::forward_iterator<MultiBuf::iterator>);
+static_assert(std::forward_iterator<MultiBuf::const_iterator>);
+static_assert(std::forward_iterator<MultiBuf::ChunkIterator>);
+static_assert(std::forward_iterator<MultiBuf::ConstChunkIterator>);
+#endif  // __cplusplus >= 202002L
+
+OwnedChunk MakeChunk(pw::allocator::Allocator& alloc, size_t size) {
+  std::optional<OwnedChunk> chunk =
+      HeaderChunkRegionTracker::AllocateRegionAsChunk(&alloc, size);
+  assert(chunk.has_value());
+  return std::move(*chunk);
+}
+
+TEST(MultiBuf, IsDefaultConstructible) { [[maybe_unused]] MultiBuf buf; }
+
+TEST(MultiBuf, WithOneChunkReleases) {
+  TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+  MultiBuf buf;
+  buf.PushFrontChunk(MakeChunk(alloc, kArbitraryChunkSize));
+  EXPECT_EQ(alloc.count(), 2U);
+  buf.Release();
+  EXPECT_EQ(alloc.count(), 0U);
+}
+
+TEST(MultiBuf, WithOneChunkReleasesOnDestruction) {
+  TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+  {
+    MultiBuf buf;
+    buf.PushFrontChunk(MakeChunk(alloc, kArbitraryChunkSize));
+    EXPECT_EQ(alloc.count(), 2U);
+  }
+  EXPECT_EQ(alloc.count(), 0U);
+}
+
+TEST(MultiBuf, WithMultipleChunksReleasesAllOnDestruction) {
+  TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+  {
+    MultiBuf buf;
+    buf.PushFrontChunk(MakeChunk(alloc, kArbitraryChunkSize));
+    buf.PushFrontChunk(MakeChunk(alloc, kArbitraryChunkSize));
+    EXPECT_EQ(alloc.count(), 4U);
+  }
+  EXPECT_EQ(alloc.count(), 0U);
+}
+
+TEST(MultiBuf, SizeReturnsNumberOfBytes) {
+  TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+  MultiBuf buf;
+  EXPECT_EQ(buf.size(), 0U);
+  buf.PushFrontChunk(MakeChunk(alloc, kArbitraryChunkSize));
+  EXPECT_EQ(buf.size(), kArbitraryChunkSize);
+  buf.PushFrontChunk(MakeChunk(alloc, kArbitraryChunkSize));
+  EXPECT_EQ(buf.size(), kArbitraryChunkSize * 2);
+}
+
+TEST(MultiBuf, PushFrontChunkAddsBytesToFront) {
+  TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+  MultiBuf buf;
+
+  const std::array<std::byte, 3> kBytesOne = {0_b, 1_b, 2_b};
+  auto chunk_one = MakeChunk(alloc, kBytesOne.size());
+  std::copy(kBytesOne.begin(), kBytesOne.end(), chunk_one->begin());
+  buf.PushFrontChunk(std::move(chunk_one));
+
+  size_t i = 0;
+  auto buf_iter = buf.begin();
+  for (; i < kBytesOne.size(); i++, buf_iter++) {
+    ASSERT_NE(buf_iter, buf.end());
+    EXPECT_EQ(*buf_iter, kBytesOne[i]);
+  }
+
+  const std::array<std::byte, 4> kBytesTwo = {9_b, 10_b, 11_b, 12_b};
+  auto chunk_two = MakeChunk(alloc, kBytesTwo.size());
+  std::copy(kBytesTwo.begin(), kBytesTwo.end(), chunk_two->begin());
+  buf.PushFrontChunk(std::move(chunk_two));
+
+  std::array<std::byte, 7> expected = {
+      9_b,
+      10_b,
+      11_b,
+      12_b,
+      0_b,
+      1_b,
+      2_b,
+  };
+  i = 0;
+  buf_iter = buf.begin();
+  for (; i < expected.size(); i++, buf_iter++) {
+    ASSERT_NE(buf_iter, buf.end());
+    EXPECT_EQ(*buf_iter, expected[i]);
+  }
+}
+
+TEST(MultiBuf, InsertChunkOnEmptyBufAddsFirstChunk) {
+  TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+  MultiBuf buf;
+
+  const std::array<std::byte, 3> kBytes = {0_b, 1_b, 2_b};
+  auto chunk = MakeChunk(alloc, kBytes.size());
+  std::copy(kBytes.begin(), kBytes.end(), chunk->begin());
+  auto inserted_iter = buf.InsertChunk(buf.Chunks().begin(), std::move(chunk));
+  EXPECT_EQ(inserted_iter, buf.Chunks().begin());
+
+  size_t i = 0;
+  auto buf_iter = buf.begin();
+  for (; i < kBytes.size(); i++, buf_iter++) {
+    ASSERT_NE(buf_iter, buf.end());
+    EXPECT_EQ(*buf_iter, kBytes[i]);
+  }
+
+  EXPECT_EQ(++inserted_iter, buf.Chunks().end());
+}
+
+TEST(MultiBuf, InsertChunkAtEndOfBufAddsLastChunk) {
+  TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+  MultiBuf buf;
+
+  // Add a chunk to the beginning
+  buf.PushFrontChunk(MakeChunk(alloc, kArbitraryChunkSize));
+
+  const std::array<std::byte, 3> kBytes = {0_b, 1_b, 2_b};
+  auto chunk = MakeChunk(alloc, kBytes.size());
+  std::copy(kBytes.begin(), kBytes.end(), chunk->begin());
+  auto inserted_iter = buf.InsertChunk(buf.Chunks().end(), std::move(chunk));
+  EXPECT_EQ(inserted_iter, ++buf.Chunks().begin());
+  EXPECT_EQ(++inserted_iter, buf.Chunks().end());
+
+  size_t i = 0;
+  auto buf_iter = buf.Chunks().begin();
+  buf_iter++;
+  auto chunk_iter = buf_iter->begin();
+  for (; i < kBytes.size(); i++, chunk_iter++) {
+    ASSERT_NE(chunk_iter, buf_iter->end());
+    EXPECT_EQ(*chunk_iter, kBytes[i]);
+  }
+}
+
+TEST(MultiBuf, TakeChunkAtBeginRemovesAndReturnsFirstChunk) {
+  TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+  MultiBuf buf;
+  auto insert_iter = buf.Chunks().begin();
+  insert_iter = buf.InsertChunk(insert_iter, MakeChunk(alloc, 2));
+  insert_iter = buf.InsertChunk(++insert_iter, MakeChunk(alloc, 4));
+
+  auto [chunk_iter, chunk] = buf.TakeChunk(buf.Chunks().begin());
+  EXPECT_EQ(chunk.size(), 2U);
+  EXPECT_EQ(chunk_iter->size(), 4U);
+  chunk_iter++;
+  EXPECT_EQ(chunk_iter, buf.Chunks().end());
+}
+
+TEST(MultiBuf, TakeChunkOnLastInsertedIterReturnsLastInserted) {
+  TrackingAllocatorWithMemory<kArbitraryAllocatorSize> alloc;
+  MultiBuf buf;
+  auto iter = buf.Chunks().begin();
+  iter = buf.InsertChunk(iter, MakeChunk(alloc, 42));
+  iter = buf.InsertChunk(++iter, MakeChunk(alloc, 11));
+  iter = buf.InsertChunk(++iter, MakeChunk(alloc, 65));
+  OwnedChunk chunk;
+  std::tie(iter, chunk) = buf.TakeChunk(iter);
+  EXPECT_EQ(iter, buf.Chunks().end());
+  EXPECT_EQ(chunk.size(), 65U);
+}
+
+TEST(MultiBuf, RangeBasedForLoopsCompile) {
+  MultiBuf buf;
+  for ([[maybe_unused]] std::byte& byte : buf) {
+  }
+  for ([[maybe_unused]] const std::byte& byte : buf) {
+  }
+  for ([[maybe_unused]] Chunk& chunk : buf.Chunks()) {
+  }
+  for ([[maybe_unused]] const Chunk& chunk : buf.Chunks()) {
+  }
+
+  const MultiBuf const_buf;
+  for ([[maybe_unused]] const std::byte& byte : const_buf) {
+  }
+  for ([[maybe_unused]] const Chunk& chunk : const_buf.Chunks()) {
+  }
+}
+
+}  // namespace
+}  // namespace pw::multibuf
diff --git a/pw_multibuf/public/pw_multibuf/chunk.h b/pw_multibuf/public/pw_multibuf/chunk.h
index 7dd7ed6..9747b94 100644
--- a/pw_multibuf/public/pw_multibuf/chunk.h
+++ b/pw_multibuf/public/pw_multibuf/chunk.h
@@ -66,9 +66,9 @@
   ConstByteSpan span() const { return span_; }
 
   std::byte& operator[](size_t index) { return span_[index]; }
-  std::byte operator[](size_t index) const { return span_[index]; }
+  const std::byte& operator[](size_t index) const { return span_[index]; }
 
-  // Iterator declarations
+  // Container declarations
   using element_type = std::byte;
   using value_type = std::byte;
   using size_type = size_t;
@@ -223,7 +223,7 @@
   /// last element of a ``MultiBuf``.
   //
   // reserved for use by the MultiBuf class.
-  // Chunk* next_in_buf_;
+  Chunk* next_in_buf_;
 
   /// Pointer to the sub-region to which this chunk has exclusive access.
   ///
@@ -240,6 +240,7 @@
   ByteSpan span_;
 
   friend class OwnedChunk;  // for ``Free``.
+  friend class MultiBuf;    // for ``Free`` and ``next_in_buf_``.
 };
 
 /// An object that manages a single allocated region which is referenced by one
@@ -347,6 +348,14 @@
   /// This method will acquire a mutex and is not IRQ safe.
   void Release();
 
+  /// Returns the contained ``Chunk*`` and empties this ``OwnedChunk`` without
+  /// releasing the underlying ``Chunk``.
+  Chunk* Take() && {
+    Chunk* result = inner_;
+    inner_ = nullptr;
+    return result;
+  }
+
  private:
   /// Constructs a new ``OwnedChunk`` from an existing ``Chunk*``.
   OwnedChunk(Chunk* inner) : inner_(inner) {}
@@ -354,9 +363,10 @@
   /// A pointer to the owned ``Chunk``.
   Chunk* inner_;
 
-  /// Allow ``Chunk`` to create an ``OwnedChunk`` using the private constructor
-  /// above.
+  /// Allow ``Chunk`` and ``MultiBuf`` to create ``OwnedChunk``s using the
+  /// private constructor above.
   friend class Chunk;
+  friend class MultiBuf;
 };
 
 }  // namespace pw::multibuf
diff --git a/pw_multibuf/public/pw_multibuf/internal/test_utils.h b/pw_multibuf/public/pw_multibuf/internal/test_utils.h
new file mode 100644
index 0000000..3e713ff
--- /dev/null
+++ b/pw_multibuf/public/pw_multibuf/internal/test_utils.h
@@ -0,0 +1,164 @@
+// Copyright 2023 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 <memory>
+
+#include "pw_allocator/allocator_metric_proxy.h"
+#include "pw_allocator/split_free_list_allocator.h"
+#include "pw_multibuf/chunk.h"
+
+namespace pw::multibuf::internal {
+
+/// A basic ``Allocator`` implementation that reports the number and size of
+/// allocations.
+class TrackingAllocator : public pw::allocator::Allocator {
+ public:
+  /// Constructs a new ``TrackingAllocator`` which allocates from the provided
+  /// region of memory.
+  TrackingAllocator(ByteSpan span) : alloc_stats_(kFakeToken) {
+    Status status = alloc_.Init(span, kFakeThreshold);
+    EXPECT_EQ(status, OkStatus());
+    alloc_stats_.Initialize(alloc_);
+  }
+
+  /// Returns the number of current allocations.
+  size_t count() const { return alloc_stats_.count(); }
+
+  /// Returns the combined size in bytes of all current allocations.
+  size_t used() const { return alloc_stats_.used(); }
+
+ protected:
+  void* DoAllocate(size_t size, size_t alignment) override {
+    return alloc_stats_.AllocateUnchecked(size, alignment);
+  }
+  bool DoResize(void* ptr,
+                size_t old_size,
+                size_t old_align,
+                size_t new_size) override {
+    return alloc_stats_.ResizeUnchecked(ptr, old_size, old_align, new_size);
+  }
+  void DoDeallocate(void* ptr, size_t size, size_t alignment) override {
+    alloc_stats_.DeallocateUnchecked(ptr, size, alignment);
+  }
+
+ private:
+  const size_t kFakeThreshold = 0;
+  const int32_t kFakeToken = 0;
+
+  pw::allocator::SplitFreeListAllocator<> alloc_;
+  pw::allocator::AllocatorMetricProxy alloc_stats_;
+};
+
+/// A ``TrackingAllocator`` which holds an internal buffer of size `num_buffer`
+/// for its allocations.
+template <auto num_bytes>
+class TrackingAllocatorWithMemory : public pw::allocator::Allocator {
+ public:
+  TrackingAllocatorWithMemory() : mem_(), alloc_(mem_) {}
+  size_t count() const { return alloc_.count(); }
+  size_t used() const { return alloc_.used(); }
+  void* DoAllocate(size_t size, size_t alignment) override {
+    return alloc_.AllocateUnchecked(size, alignment);
+  }
+  bool DoResize(void* ptr,
+                size_t old_size,
+                size_t old_align,
+                size_t new_size) override {
+    return alloc_.ResizeUnchecked(ptr, old_size, old_align, new_size);
+  }
+  void DoDeallocate(void* ptr, size_t size, size_t alignment) override {
+    alloc_.DeallocateUnchecked(ptr, size, alignment);
+  }
+
+ private:
+  std::array<std::byte, num_bytes> mem_;
+  TrackingAllocator alloc_;
+};
+
+/// A ``ChunkRegionTracker`` which stores its ``Chunk`` and region metadata
+/// in a ``pw::allocator::Allocator`` allocation alongside the data.
+class HeaderChunkRegionTracker final : public ChunkRegionTracker {
+ public:
+  /// Allocates a new ``Chunk`` region of ``size`` bytes  in ``alloc``.
+  ///
+  /// The underlyiing allocation will also store the
+  /// ``HeaderChunkRegionTracker`` itself.
+  ///
+  /// Returns the newly-created ``OwnedChunk`` if successful.
+  static std::optional<OwnedChunk> AllocateRegionAsChunk(
+      pw::allocator::Allocator* alloc, size_t size) {
+    HeaderChunkRegionTracker* tracker = AllocateRegion(alloc, size);
+    if (tracker == nullptr) {
+      return std::nullopt;
+    }
+    std::optional<OwnedChunk> chunk = Chunk::CreateFirstForRegion(*tracker);
+    if (!chunk.has_value()) {
+      tracker->Destroy();
+      return std::nullopt;
+    }
+    return chunk;
+  }
+
+  /// Allocates a new ``Chunk`` region of ``size`` bytes  in ``alloc``.
+  ///
+  /// The underlyiing allocation will also store the
+  /// ``HeaderChunkRegionTracker`` itself.
+  ///
+  /// Returns a pointer to the newly-created ``HeaderChunkRegionTracker``
+  /// or ``nullptr`` if the allocation failed.
+  static HeaderChunkRegionTracker* AllocateRegion(
+      pw::allocator::Allocator* alloc, size_t size) {
+    const size_t alloc_size = size + sizeof(HeaderChunkRegionTracker);
+    const size_t align = alignof(HeaderChunkRegionTracker);
+    void* ptr = alloc->AllocateUnchecked(alloc_size, align);
+    if (ptr == nullptr) {
+      return nullptr;
+    }
+    std::byte* data =
+        reinterpret_cast<std::byte*>(ptr) + sizeof(HeaderChunkRegionTracker);
+    return new (ptr) HeaderChunkRegionTracker(ByteSpan(data, size), alloc);
+  }
+
+  ByteSpan Region() const final { return region_; }
+  ~HeaderChunkRegionTracker() final {}
+
+ protected:
+  void Destroy() final {
+    std::byte* ptr = reinterpret_cast<std::byte*>(this);
+    const size_t size = sizeof(HeaderChunkRegionTracker) + region_.size();
+    const size_t align = alignof(HeaderChunkRegionTracker);
+    auto alloc = alloc_;
+    std::destroy_at(this);
+    alloc->DeallocateUnchecked(ptr, size, align);
+  }
+  void* AllocateChunkClass() final {
+    return alloc_->Allocate(pw::allocator::Layout::Of<Chunk>());
+  }
+  void DeallocateChunkClass(void* ptr) final {
+    alloc_->Deallocate(ptr, pw::allocator::Layout::Of<Chunk>());
+  }
+
+ private:
+  ByteSpan region_;
+  pw::allocator::Allocator* alloc_;
+
+  // NOTE: `region` must directly follow this `FakeChunkRegionTracker`
+  // in memory allocated by allocated by `alloc`.
+  HeaderChunkRegionTracker(ByteSpan region, pw::allocator::Allocator* alloc)
+      : region_(region), alloc_(alloc) {}
+};
+
+}  // namespace pw::multibuf::internal
diff --git a/pw_multibuf/public/pw_multibuf/multibuf.h b/pw_multibuf/public/pw_multibuf/multibuf.h
index 3d037e1..a48ce6f 100644
--- a/pw_multibuf/public/pw_multibuf/multibuf.h
+++ b/pw_multibuf/public/pw_multibuf/multibuf.h
@@ -13,17 +13,359 @@
 // the License.
 #pragma once
 
+#include <tuple>
+
 #include "pw_multibuf/chunk.h"
 
 namespace pw::multibuf {
 
 /// A buffer optimized for zero-copy data transfer.
 ///
-/// A ``MultiBuf`` consists of one or more ``Chunk`` s of data.
+/// A ``MultiBuf`` consists of multiple ``Chunk`` s of data.
 class MultiBuf {
  public:
+  class iterator;
+  class const_iterator;
+  class ChunkIterator;
+  class ConstChunkIterator;
+  class ChunkIterable;
+  class ConstChunkIterable;
+
+  constexpr MultiBuf() : first_(nullptr) {}
+  constexpr MultiBuf(MultiBuf&& other) noexcept : first_(other.first_) {
+    other.first_ = nullptr;
+  }
+  MultiBuf& operator=(MultiBuf&& other) noexcept {
+    Release();
+    first_ = other.first_;
+    other.first_ = nullptr;
+    return *this;
+  }
+
+  /// Decrements the reference count on the underlying chunks of data and
+  /// empties this ``MultiBuf`` so that ``size() == 0``.
+  ///
+  /// Does not modify the underlying data, but may cause it to be deallocated
+  ///
+  /// This method is equivalent to ``{ MultiBuf _unused = std::move(multibuf);
+  /// }``
+  ///
+  /// This method will acquire a mutex and is not IRQ safe.
+  void Release() noexcept;
+
+  /// This destructor will acquire a mutex and is not IRQ safe.
+  ~MultiBuf() { Release(); }
+
+  /// Returns the number of bytes in this container.
+  ///
+  /// This method's complexity is ``O(Chunks().size())``.
+  [[nodiscard]] size_t size() const;
+
+  /// Pushes ``Chunk`` onto the front of the ``MultiBuf``.
+  ///
+  /// This operation does not move any data and is ``O(1)``.
+  void PushFrontChunk(OwnedChunk chunk);
+
+  /// Inserts ``chunk`` into the specified position in the ``MultiBuf``.
+  ///
+  /// This operation does not move any data and is ``O(Chunks().size())``.
+  ///
+  /// Returns an iterator pointing to the newly-inserted ``Chunk``.
+  //
+  // Implementation note: ``Chunks().size()`` should be remain relatively
+  // small, but this could be made ``O(1)`` in the future by adding a ``prev``
+  // pointer to the ``ChunkIterator``.
+  ChunkIterator InsertChunk(ChunkIterator position, OwnedChunk chunk);
+
+  /// Removes a ``Chunk`` from the specified position.
+  ///
+  /// This operation does not move any data and is ``O(Chunks().size())``.
+  ///
+  /// Returns an iterator pointing to the ``Chunk`` after the removed
+  /// ``Chunk``, or ``Chunks().end()`` if this was the last ``Chunk`` in the
+  /// ``MultiBuf``.
+  //
+  // Implementation note: ``Chunks().size()`` should be remain relatively
+  // small, but this could be made ``O(1)`` in the future by adding a ``prev``
+  // pointer to the ``ChunkIterator``.
+  std::tuple<ChunkIterator, OwnedChunk> TakeChunk(ChunkIterator position);
+
+  /// Returns an iterator pointing to the first byte of this ``MultiBuf`.
+  iterator begin() { return iterator(first_, 0); }
+  /// Returns a const iterator pointing to the first byte of this ``MultiBuf`.
+  const_iterator begin() const { return const_iterator(first_, 0); }
+  /// Returns a const iterator pointing to the first byte of this ``MultiBuf`.
+  const_iterator cbegin() const { return const_iterator(first_, 0); }
+
+  /// Returns an iterator pointing to the end of this ``MultiBuf``.
+  iterator end() { return iterator::end(); }
+  /// Returns a const iterator pointing to the end of this ``MultiBuf``.
+  const_iterator end() const { return const_iterator::end(); }
+  /// Returns a const iterator pointing to the end of this ``MultiBuf``.
+  const_iterator cend() const { return const_iterator::end(); }
+
+  /// Returns an iterable container which yields the ``Chunk``s in this
+  /// ``MultiBuf``.
+  constexpr ChunkIterable Chunks() { return ChunkIterable(first_); }
+
+  /// Returns an iterable container which yields the ``const Chunk``s in
+  /// this ``MultiBuf``.
+  constexpr const ChunkIterable Chunks() const { return ChunkIterable(first_); }
+
+  /// Returns an iterator pointing to the first ``Chunk`` in this ``MultiBuf``.
+  constexpr ChunkIterator ChunkBegin() { return ChunkIterator(first_); }
+  /// Returns an iterator pointing to the end of the ``Chunk``s in this
+  /// ``MultiBuf``.
+  constexpr ChunkIterator ChunkEnd() { return ChunkIterator::end(); }
+  /// Returns a const iterator pointing to the first ``Chunk`` in this
+  /// ``MultiBuf``.
+  constexpr ConstChunkIterator ConstChunkBegin() {
+    return ConstChunkIterator(first_);
+  }
+  /// Returns a const iterator pointing to the end of the ``Chunk``s in this
+  /// ``MultiBuf``.
+  constexpr ConstChunkIterator ConstChunkEnd() {
+    return ConstChunkIterator::end();
+  }
+
+  ///////////////////////////////////////////////////////////////////
+  //--------------------- Iterator details ------------------------//
+  ///////////////////////////////////////////////////////////////////
+
+  using element_type = std::byte;
+  using value_type = std::byte;
+  using pointer = std::byte*;
+  using const_pointer = const std::byte*;
+  using reference = std::byte&;
+  using const_reference = const std::byte&;
+  using difference_type = std::ptrdiff_t;
+  using size_type = std::size_t;
+
+  /// An ``std::forward_iterator`` over the bytes of a ``MultiBuf``.
+  class iterator {
+   public:
+    using value_type = std::byte;
+    using difference_type = std::ptrdiff_t;
+    using reference = std::byte&;
+    using pointer = std::byte*;
+    using iterator_category = std::forward_iterator_tag;
+
+    constexpr iterator() = default;
+    iterator(Chunk* chunk, size_t byte_index)
+        : chunk_(chunk), byte_index_(byte_index) {
+      AdvanceToData();
+    }
+    static iterator end() { return iterator(nullptr, 0); }
+
+    reference operator*() const { return (*chunk_)[byte_index_]; }
+    pointer operator->() const { return &(*chunk_)[byte_index_]; }
+
+    iterator& operator++();
+    iterator operator++(int) {
+      iterator tmp = *this;
+      ++(*this);
+      return tmp;
+    }
+    constexpr bool operator==(const iterator& other) const {
+      return chunk_ == other.chunk_ && byte_index_ == other.byte_index_;
+    }
+    constexpr bool operator!=(const iterator& other) const {
+      return chunk_ != other.chunk_ || byte_index_ != other.byte_index_;
+    }
+
+    /// Returns the current ``Chunk`` pointed to by this `iterator`.
+    constexpr Chunk* chunk() const { return chunk_; }
+
+    /// Returns the index of the byte pointed to by this `iterator` within the
+    /// current ``Chunk``.
+    constexpr size_t byte_index() const { return byte_index_; }
+
+   private:
+    void AdvanceToData();
+
+    Chunk* chunk_ = nullptr;
+    size_t byte_index_ = 0;
+  };
+
+  /// A const ``std::forward_iterator`` over the bytes of a ``MultiBuf``.
+  class const_iterator {
+   public:
+    using value_type = std::byte;
+    using difference_type = std::ptrdiff_t;
+    using reference = const std::byte&;
+    using pointer = const std::byte*;
+    using iterator_category = std::forward_iterator_tag;
+
+    constexpr const_iterator() = default;
+    const_iterator(const Chunk* chunk, size_t byte_index)
+        : chunk_(chunk), byte_index_(byte_index) {
+      AdvanceToData();
+    }
+    static const_iterator end() { return const_iterator(nullptr, 0); }
+
+    reference operator*() const { return (*chunk_)[byte_index_]; }
+    pointer operator->() const { return &(*chunk_)[byte_index_]; }
+
+    const_iterator& operator++();
+    const_iterator operator++(int) {
+      const_iterator tmp = *this;
+      ++(*this);
+      return tmp;
+    }
+
+    constexpr bool operator==(const const_iterator& other) const {
+      return chunk_ == other.chunk_ && byte_index_ == other.byte_index_;
+    }
+
+    constexpr bool operator!=(const const_iterator& other) const {
+      return chunk_ != other.chunk_ || byte_index_ != other.byte_index_;
+    }
+
+    /// Returns the current ``Chunk`` pointed to by this `iterator`.
+    constexpr const Chunk* chunk() const { return chunk_; }
+
+    /// Returns the index of the byte pointed to by this `iterator` within the
+    /// current ``Chunk``.
+    constexpr size_t byte_index() const { return byte_index_; }
+
+   private:
+    void AdvanceToData();
+
+    const Chunk* chunk_ = nullptr;
+    size_t byte_index_ = 0;
+  };
+
+  /// An iterable containing the ``Chunk`` s of a ``MultiBuf``.
+  class ChunkIterable {
+   public:
+    using element_type = Chunk;
+    using value_type = Chunk;
+    using pointer = Chunk*;
+    using reference = Chunk&;
+    using const_pointer = const Chunk*;
+    using difference_type = std::ptrdiff_t;
+    using const_reference = const Chunk&;
+    using size_type = std::size_t;
+
+    constexpr ChunkIterator begin() { return ChunkIterator(first_); }
+    constexpr ConstChunkIterator begin() const { return cbegin(); }
+    constexpr ConstChunkIterator cbegin() const {
+      return ConstChunkIterator(first_);
+    }
+    constexpr ChunkIterator end() { return ChunkIterator::end(); }
+    constexpr ConstChunkIterator end() const { return cend(); }
+    constexpr ConstChunkIterator cend() const {
+      return ConstChunkIterator::end();
+    }
+
+    /// Returns the number of ``Chunk``s in this iterable.
+    size_t size() const;
+
+   private:
+    Chunk* first_ = nullptr;
+    constexpr ChunkIterable(Chunk* chunk) : first_(chunk) {}
+    friend class MultiBuf;
+  };
+
+  /// A ``std::forward_iterator`` over the ``Chunk``s of a ``MultiBuf``.
+  class ChunkIterator {
+   public:
+    using value_type = Chunk;
+    using difference_type = std::ptrdiff_t;
+    using reference = Chunk&;
+    using pointer = Chunk*;
+    using iterator_category = std::forward_iterator_tag;
+
+    constexpr ChunkIterator() = default;
+
+    constexpr reference operator*() const { return *chunk_; }
+    constexpr pointer operator->() const { return chunk_; }
+
+    constexpr ChunkIterator& operator++() {
+      chunk_ = chunk_->next_in_buf_;
+      return *this;
+    }
+
+    constexpr ChunkIterator operator++(int) {
+      ChunkIterator tmp = *this;
+      ++(*this);
+      return tmp;
+    }
+
+    constexpr bool operator==(const ChunkIterator& other) const {
+      return chunk_ == other.chunk_;
+    }
+
+    constexpr bool operator!=(const ChunkIterator& other) const {
+      return chunk_ != other.chunk_;
+    }
+
+    constexpr Chunk* chunk() const { return chunk_; }
+
+   private:
+    constexpr ChunkIterator(Chunk* chunk) : chunk_(chunk) {}
+    static constexpr ChunkIterator end() { return ChunkIterator(nullptr); }
+    Chunk* chunk_ = nullptr;
+    friend class MultiBuf;
+    friend class ChunkIterable;
+  };
+
+  /// A const ``std::forward_iterator`` over the ``Chunk``s of a ``MultiBuf``.
+  class ConstChunkIterator {
+   public:
+    using value_type = const Chunk;
+    using difference_type = std::ptrdiff_t;
+    using reference = const Chunk&;
+    using pointer = const Chunk*;
+    using iterator_category = std::forward_iterator_tag;
+
+    constexpr ConstChunkIterator() = default;
+
+    constexpr reference operator*() const { return *chunk_; }
+    constexpr pointer operator->() const { return chunk_; }
+
+    constexpr ConstChunkIterator& operator++() {
+      chunk_ = chunk_->next_in_buf_;
+      return *this;
+    }
+
+    constexpr ConstChunkIterator operator++(int) {
+      ConstChunkIterator tmp = *this;
+      ++(*this);
+      return tmp;
+    }
+
+    constexpr bool operator==(const ConstChunkIterator& other) const {
+      return chunk_ == other.chunk_;
+    }
+
+    constexpr bool operator!=(const ConstChunkIterator& other) const {
+      return chunk_ != other.chunk_;
+    }
+
+    constexpr const Chunk* chunk() const { return chunk_; }
+
+   private:
+    constexpr ConstChunkIterator(const Chunk* chunk) : chunk_(chunk) {}
+    static constexpr ConstChunkIterator end() {
+      return ConstChunkIterator(nullptr);
+    }
+    const Chunk* chunk_ = nullptr;
+    friend class MultiBuf;
+    friend class ChunkIterable;
+  };
+
  private:
-  OwnedChunk chunk_;
+  /// Returns the ``Chunk`` preceding ``chunk`` in this ``MultiBuf``.
+  ///
+  /// Requires that this ``MultiBuf`` is not empty, and that ``chunk``
+  /// is either in ``MultiBuf`` or is ``nullptr``, in which case the last
+  /// ``Chunk`` in ``MultiBuf`` will be returned.
+  ///
+  /// This operation is ``O(Chunks().size())``.
+  Chunk* Previous(Chunk* chunk) const;
+
+  Chunk* first_;
 };
 
 class MultiBufAllocator {};