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