Adds a ChunkedBuffer class to contain HTTP/2 wire format bytes.

This will replace a std::string used for this purpose in OgHttp2Session.

Possible future extensions:
* use QuicheBuffer or QuicheMemSlice as the memory region type in ChunkBuffer::Chunk
* implement spdy::ZeroCopyOutputBuffer or write an adapter, for direct frame serialization

PiperOrigin-RevId: 626495713
diff --git a/build/source_list.bzl b/build/source_list.bzl
index 334b02a..2e6cd92 100644
--- a/build/source_list.bzl
+++ b/build/source_list.bzl
@@ -60,6 +60,7 @@
     "common/simple_buffer_allocator.h",
     "common/structured_headers.h",
     "common/wire_serialization.h",
+    "http2/adapter/chunked_buffer.h",
     "http2/adapter/data_source.h",
     "http2/adapter/event_forwarder.h",
     "http2/adapter/header_validator.h",
@@ -418,6 +419,7 @@
     "common/quiche_text_utils.cc",
     "common/simple_buffer_allocator.cc",
     "common/structured_headers.cc",
+    "http2/adapter/chunked_buffer.cc",
     "http2/adapter/event_forwarder.cc",
     "http2/adapter/header_validator.cc",
     "http2/adapter/http2_protocol.cc",
@@ -1092,6 +1094,7 @@
     "common/test_tools/mock_streams_test.cc",
     "common/test_tools/quiche_test_utils_test.cc",
     "common/wire_serialization_test.cc",
+    "http2/adapter/chunked_buffer_test.cc",
     "http2/adapter/event_forwarder_test.cc",
     "http2/adapter/header_validator_test.cc",
     "http2/adapter/noop_header_validator_test.cc",
diff --git a/build/source_list.gni b/build/source_list.gni
index 8ee941e..5105182 100644
--- a/build/source_list.gni
+++ b/build/source_list.gni
@@ -60,6 +60,7 @@
     "src/quiche/common/simple_buffer_allocator.h",
     "src/quiche/common/structured_headers.h",
     "src/quiche/common/wire_serialization.h",
+    "src/quiche/http2/adapter/chunked_buffer.h",
     "src/quiche/http2/adapter/data_source.h",
     "src/quiche/http2/adapter/event_forwarder.h",
     "src/quiche/http2/adapter/header_validator.h",
@@ -418,6 +419,7 @@
     "src/quiche/common/quiche_text_utils.cc",
     "src/quiche/common/simple_buffer_allocator.cc",
     "src/quiche/common/structured_headers.cc",
+    "src/quiche/http2/adapter/chunked_buffer.cc",
     "src/quiche/http2/adapter/event_forwarder.cc",
     "src/quiche/http2/adapter/header_validator.cc",
     "src/quiche/http2/adapter/http2_protocol.cc",
@@ -1093,6 +1095,7 @@
     "src/quiche/common/test_tools/mock_streams_test.cc",
     "src/quiche/common/test_tools/quiche_test_utils_test.cc",
     "src/quiche/common/wire_serialization_test.cc",
+    "src/quiche/http2/adapter/chunked_buffer_test.cc",
     "src/quiche/http2/adapter/event_forwarder_test.cc",
     "src/quiche/http2/adapter/header_validator_test.cc",
     "src/quiche/http2/adapter/noop_header_validator_test.cc",
diff --git a/build/source_list.json b/build/source_list.json
index c58090b..7b749af 100644
--- a/build/source_list.json
+++ b/build/source_list.json
@@ -59,6 +59,7 @@
     "quiche/common/simple_buffer_allocator.h",
     "quiche/common/structured_headers.h",
     "quiche/common/wire_serialization.h",
+    "quiche/http2/adapter/chunked_buffer.h",
     "quiche/http2/adapter/data_source.h",
     "quiche/http2/adapter/event_forwarder.h",
     "quiche/http2/adapter/header_validator.h",
@@ -417,6 +418,7 @@
     "quiche/common/quiche_text_utils.cc",
     "quiche/common/simple_buffer_allocator.cc",
     "quiche/common/structured_headers.cc",
+    "quiche/http2/adapter/chunked_buffer.cc",
     "quiche/http2/adapter/event_forwarder.cc",
     "quiche/http2/adapter/header_validator.cc",
     "quiche/http2/adapter/http2_protocol.cc",
@@ -1092,6 +1094,7 @@
     "quiche/common/test_tools/mock_streams_test.cc",
     "quiche/common/test_tools/quiche_test_utils_test.cc",
     "quiche/common/wire_serialization_test.cc",
+    "quiche/http2/adapter/chunked_buffer_test.cc",
     "quiche/http2/adapter/event_forwarder_test.cc",
     "quiche/http2/adapter/header_validator_test.cc",
     "quiche/http2/adapter/noop_header_validator_test.cc",
diff --git a/quiche/http2/adapter/chunked_buffer.cc b/quiche/http2/adapter/chunked_buffer.cc
new file mode 100644
index 0000000..f941042
--- /dev/null
+++ b/quiche/http2/adapter/chunked_buffer.cc
@@ -0,0 +1,125 @@
+#include "quiche/http2/adapter/chunked_buffer.h"
+
+#include <algorithm>
+
+namespace http2 {
+namespace adapter {
+
+namespace {
+
+constexpr size_t kKilobyte = 1024;
+size_t RoundUpToNearestKilobyte(size_t n) {
+  // The way to think of this bit math is: it fills in all of the least
+  // significant bits less than 1024, then adds one. This guarantees that all of
+  // those bits end up as 0, hence rounding up to a multiple of 1024.
+  return ((n - 1) | (kKilobyte - 1)) + 1;
+}
+
+}  // namespace
+
+void ChunkedBuffer::Append(absl::string_view data) {
+  // Appends the data by copying it.
+  const size_t to_copy = std::min(TailBytesFree(), data.size());
+  if (to_copy > 0) {
+    chunks_.back().AppendSuffix(data.substr(0, to_copy));
+    data.remove_prefix(to_copy);
+  }
+  EnsureTailBytesFree(data.size());
+  chunks_.back().AppendSuffix(data);
+}
+
+void ChunkedBuffer::Append(std::unique_ptr<char[]> data, size_t size) {
+  if (TailBytesFree() >= size) {
+    // Copies the data into the existing last chunk, since it will fit.
+    Chunk& c = chunks_.back();
+    c.AppendSuffix(absl::string_view(data.get(), size));
+    return;
+  }
+  while (!chunks_.empty() && chunks_.front().Empty()) {
+    chunks_.pop_front();
+  }
+  // Appends the memory to the end of the deque, since it won't fit in an
+  // existing chunk.
+  absl::string_view v = {data.get(), size};
+  chunks_.push_back({std::move(data), size, v});
+}
+
+absl::string_view ChunkedBuffer::GetPrefix() const {
+  if (chunks_.empty()) {
+    return "";
+  }
+  return chunks_.front().live;
+}
+
+std::vector<absl::string_view> ChunkedBuffer::Read() const {
+  std::vector<absl::string_view> result;
+  result.reserve(chunks_.size());
+  for (const Chunk& c : chunks_) {
+    result.push_back(c.live);
+  }
+  return result;
+}
+
+void ChunkedBuffer::RemovePrefix(size_t n) {
+  while (!Empty() && n > 0) {
+    Chunk& c = chunks_.front();
+    const size_t to_remove = std::min(n, c.live.size());
+    c.RemovePrefix(to_remove);
+    n -= to_remove;
+    if (c.Empty()) {
+      TrimFirstChunk();
+    }
+  }
+}
+
+bool ChunkedBuffer::Empty() const {
+  return chunks_.empty() ||
+         (chunks_.size() == 1 && chunks_.front().live.empty());
+}
+
+void ChunkedBuffer::Chunk::RemovePrefix(size_t n) {
+  QUICHE_DCHECK_GE(live.size(), n);
+  live.remove_prefix(n);
+}
+
+void ChunkedBuffer::Chunk::AppendSuffix(absl::string_view to_append) {
+  QUICHE_DCHECK_GE(TailBytesFree(), to_append.size());
+  if (live.empty()) {
+    std::copy(to_append.begin(), to_append.end(), data.get());
+    // Live needs to be initialized, since it points to nullptr.
+    live = absl::string_view(data.get(), to_append.size());
+  } else {
+    std::copy(to_append.begin(), to_append.end(),
+              const_cast<char*>(live.data()) + live.size());
+    // Live can be extended, since it already points to valid data.
+    live = absl::string_view(live.data(), live.size() + to_append.size());
+  }
+}
+
+size_t ChunkedBuffer::TailBytesFree() const {
+  if (chunks_.empty()) {
+    return 0;
+  }
+  return chunks_.back().TailBytesFree();
+}
+
+void ChunkedBuffer::EnsureTailBytesFree(size_t n) {
+  if (TailBytesFree() >= n) {
+    return;
+  }
+  const size_t to_allocate = RoundUpToNearestKilobyte(n);
+  auto data = std::unique_ptr<char[]>(new char[to_allocate]);
+  chunks_.push_back({std::move(data), to_allocate, ""});
+}
+
+void ChunkedBuffer::TrimFirstChunk() {
+  // Leave the first chunk, if it's the only one and already the default size.
+  if (chunks_.empty() ||
+      (chunks_.size() == 1 && chunks_.front().size == kDefaultChunkSize)) {
+    return;
+  }
+  chunks_.pop_front();
+}
+
+}  // namespace adapter
+}  // namespace http2
diff --git a/quiche/http2/adapter/chunked_buffer.h b/quiche/http2/adapter/chunked_buffer.h
new file mode 100644
index 0000000..03ed189
--- /dev/null
+++ b/quiche/http2/adapter/chunked_buffer.h
@@ -0,0 +1,83 @@
+#ifndef QUICHE_HTTP2_ADAPTER_CHUNKED_BUFFER_H_
+#define QUICHE_HTTP2_ADAPTER_CHUNKED_BUFFER_H_
+
+#include <memory>
+#include <vector>
+
+#include "absl/strings/string_view.h"
+#include "quiche/common/platform/api/quiche_export.h"
+#include "quiche/common/quiche_circular_deque.h"
+
+namespace http2 {
+namespace adapter {
+
+// A simple buffer class that organizes its memory as a queue of contiguous
+// regions. Data is written to the end, and read from the beginning.
+class QUICHE_EXPORT ChunkedBuffer {
+ public:
+  ChunkedBuffer() = default;
+
+  // Appends data to the buffer.
+  void Append(absl::string_view data);
+  void Append(std::unique_ptr<char[]> data, size_t size);
+
+  // Reads data from the buffer non-destructively.
+  absl::string_view GetPrefix() const;
+  std::vector<absl::string_view> Read() const;
+
+  // Removes the first `n` bytes of the buffer. Invalidates any `string_view`s
+  // read from the buffer.
+  void RemovePrefix(size_t n);
+
+  // Returns true iff the buffer contains no data to read.
+  bool Empty() const;
+
+ private:
+  static constexpr size_t kDefaultChunkSize = 1024;
+
+  // Describes a contiguous region of memory contained in the ChunkedBuffer. In
+  // the common case, data is appended to the buffer by copying it to the final
+  // chunk, or adding a unique_ptr to the list of chunks. Data is consumed from
+  // the beginning of the buffer, so the first chunk may have a nonzero offset
+  // from the start of the memory region to the first byte of readable data.
+  struct Chunk {
+    // A contiguous region of memory.
+    std::unique_ptr<char[]> data;
+    // The size of the contiguous memory.
+    const size_t size;
+    // The region occupied by live data that can be read from the buffer. A
+    // subset of `data`.
+    absl::string_view live;
+
+    void RemovePrefix(size_t n);
+    void AppendSuffix(absl::string_view to_append);
+
+    bool Empty() const { return live.empty(); }
+
+    // Returns the offset of the live data from the beginning of the chunk.
+    size_t LiveDataOffset() const { return live.data() - data.get(); }
+    // Returns the size of the free space at the end of the chunk.
+    size_t TailBytesFree() const {
+      return size - live.size() - LiveDataOffset();
+    }
+  };
+
+  // Returns the number of tail bytes free in the last chunk in the buffer, or
+  // zero.
+  size_t TailBytesFree() const;
+
+  // Ensures that the last chunk in the buffer has at least this many tail bytes
+  // free.
+  void EnsureTailBytesFree(size_t n);
+
+  // Removes the first chunk, unless it is the last chunk in the buffer and its
+  // size is kDefaultChunkSize.
+  void TrimFirstChunk();
+
+  quiche::QuicheCircularDeque<Chunk> chunks_;
+};
+
+}  // namespace adapter
+}  // namespace http2
+
+#endif  // QUICHE_HTTP2_ADAPTER_CHUNKED_BUFFER_H_
diff --git a/quiche/http2/adapter/chunked_buffer_test.cc b/quiche/http2/adapter/chunked_buffer_test.cc
new file mode 100644
index 0000000..c7dad3c
--- /dev/null
+++ b/quiche/http2/adapter/chunked_buffer_test.cc
@@ -0,0 +1,103 @@
+#include "quiche/http2/adapter/chunked_buffer.h"
+
+#include "absl/strings/str_join.h"
+#include "absl/strings/string_view.h"
+#include "quiche/common/platform/api/quiche_test.h"
+
+namespace http2 {
+namespace adapter {
+namespace {
+
+constexpr absl::string_view kLoremIpsum =
+    "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod "
+    "tempor incididunt ut labore et dolore magna aliqua.";
+
+struct DataAndSize {
+  std::unique_ptr<char[]> data;
+  size_t size;
+};
+
+DataAndSize MakeDataAndSize(absl::string_view source) {
+  auto data = std::unique_ptr<char[]>(new char[source.size()]);
+  std::copy(source.begin(), source.end(), data.get());
+  return {std::move(data), source.size()};
+}
+
+TEST(ChunkedBufferTest, Empty) {
+  ChunkedBuffer buffer;
+  EXPECT_TRUE(buffer.Empty());
+
+  buffer.Append("some data");
+  EXPECT_FALSE(buffer.Empty());
+
+  buffer.RemovePrefix(9);
+  EXPECT_TRUE(buffer.Empty());
+}
+
+TEST(ChunkedBufferTest, ReusedAfterEmptied) {
+  ChunkedBuffer buffer;
+  buffer.Append("some data");
+  buffer.RemovePrefix(9);
+  buffer.Append("different data");
+  EXPECT_EQ("different data", buffer.GetPrefix());
+}
+
+TEST(ChunkedBufferTest, LargeAppendAfterEmptied) {
+  ChunkedBuffer buffer;
+  buffer.Append("some data");
+  EXPECT_THAT(buffer.GetPrefix(), testing::StartsWith("some data"));
+  buffer.RemovePrefix(9);
+  auto more_data =
+      MakeDataAndSize(absl::StrCat("different data", std::string(2048, 'x')));
+  buffer.Append(std::move(more_data.data), more_data.size);
+  EXPECT_THAT(buffer.GetPrefix(), testing::StartsWith("different data"));
+}
+
+TEST(ChunkedBufferTest, LargeAppends) {
+  ChunkedBuffer buffer;
+  buffer.Append(std::string(500, 'a'));
+  buffer.Append(std::string(2000, 'b'));
+  buffer.Append(std::string(10, 'c'));
+  auto more_data = MakeDataAndSize(std::string(4490, 'd'));
+  buffer.Append(std::move(more_data.data), more_data.size);
+
+  EXPECT_EQ(500 + 2000 + 10 + 4490, absl::StrJoin(buffer.Read(), "").size());
+}
+
+TEST(ChunkedBufferTest, RemovePartialPrefix) {
+  ChunkedBuffer buffer;
+  auto data_and_size = MakeDataAndSize(kLoremIpsum);
+  buffer.Append(std::move(data_and_size.data), data_and_size.size);
+  buffer.RemovePrefix(6);
+  EXPECT_THAT(buffer.GetPrefix(), testing::StartsWith("ipsum"));
+  buffer.RemovePrefix(20);
+  EXPECT_THAT(buffer.GetPrefix(), testing::StartsWith(", consectetur"));
+  buffer.Append(" Anday igpay atinlay!");
+  EXPECT_EQ(
+      absl::StrJoin({kLoremIpsum.substr(26), " Anday igpay atinlay!"}, ""),
+      absl::StrJoin(buffer.Read(), ""));
+}
+
+TEST(ChunkedBufferTest, DifferentAppends) {
+  ChunkedBuffer buffer;
+  buffer.Append("Lorem ipsum");
+
+  auto more_data = MakeDataAndSize(" dolor sit amet, ");
+  buffer.Append(std::move(more_data.data), more_data.size);
+
+  buffer.Append("consectetur adipiscing elit, ");
+
+  more_data = MakeDataAndSize("sed do eiusmod tempor incididunt ut ");
+  buffer.Append(std::move(more_data.data), more_data.size);
+
+  buffer.Append("labore et dolore magna aliqua.");
+
+  EXPECT_EQ(kLoremIpsum, absl::StrJoin(buffer.Read(), ""));
+
+  buffer.RemovePrefix(kLoremIpsum.size());
+  EXPECT_TRUE(buffer.Empty());
+}
+
+}  // namespace
+}  // namespace adapter
+}  // namespace http2