Add functions to interop between absl::Cord and QuicheMemSlice. PiperOrigin-RevId: 911445172
diff --git a/build/source_list.bzl b/build/source_list.bzl index 3c8fcbe..b2b27f6 100644 --- a/build/source_list.bzl +++ b/build/source_list.bzl
@@ -44,6 +44,7 @@ "common/quiche_buffer_allocator.h", "common/quiche_callbacks.h", "common/quiche_circular_deque.h", + "common/quiche_cord_utils.h", "common/quiche_crypto_logging.h", "common/quiche_data_reader.h", "common/quiche_data_writer.h", @@ -432,6 +433,7 @@ "common/moq_varint.cc", "common/platform/api/quiche_hostname_utils.cc", "common/quiche_buffer_allocator.cc", + "common/quiche_cord_utils.cc", "common/quiche_crypto_logging.cc", "common/quiche_data_reader.cc", "common/quiche_data_writer.cc", @@ -1143,6 +1145,7 @@ "common/quiche_buffer_allocator_test.cc", "common/quiche_callbacks_test.cc", "common/quiche_circular_deque_test.cc", + "common/quiche_cord_utils_test.cc", "common/quiche_data_reader_test.cc", "common/quiche_data_writer_fuzz_test.cc", "common/quiche_data_writer_test.cc",
diff --git a/build/source_list.gni b/build/source_list.gni index b926f30..c68b800 100644 --- a/build/source_list.gni +++ b/build/source_list.gni
@@ -44,6 +44,7 @@ "src/quiche/common/quiche_buffer_allocator.h", "src/quiche/common/quiche_callbacks.h", "src/quiche/common/quiche_circular_deque.h", + "src/quiche/common/quiche_cord_utils.h", "src/quiche/common/quiche_crypto_logging.h", "src/quiche/common/quiche_data_reader.h", "src/quiche/common/quiche_data_writer.h", @@ -432,6 +433,7 @@ "src/quiche/common/moq_varint.cc", "src/quiche/common/platform/api/quiche_hostname_utils.cc", "src/quiche/common/quiche_buffer_allocator.cc", + "src/quiche/common/quiche_cord_utils.cc", "src/quiche/common/quiche_crypto_logging.cc", "src/quiche/common/quiche_data_reader.cc", "src/quiche/common/quiche_data_writer.cc", @@ -1144,6 +1146,7 @@ "src/quiche/common/quiche_buffer_allocator_test.cc", "src/quiche/common/quiche_callbacks_test.cc", "src/quiche/common/quiche_circular_deque_test.cc", + "src/quiche/common/quiche_cord_utils_test.cc", "src/quiche/common/quiche_data_reader_test.cc", "src/quiche/common/quiche_data_writer_fuzz_test.cc", "src/quiche/common/quiche_data_writer_test.cc",
diff --git a/build/source_list.json b/build/source_list.json index 6f39024..215e90a 100644 --- a/build/source_list.json +++ b/build/source_list.json
@@ -43,6 +43,7 @@ "quiche/common/quiche_buffer_allocator.h", "quiche/common/quiche_callbacks.h", "quiche/common/quiche_circular_deque.h", + "quiche/common/quiche_cord_utils.h", "quiche/common/quiche_crypto_logging.h", "quiche/common/quiche_data_reader.h", "quiche/common/quiche_data_writer.h", @@ -431,6 +432,7 @@ "quiche/common/moq_varint.cc", "quiche/common/platform/api/quiche_hostname_utils.cc", "quiche/common/quiche_buffer_allocator.cc", + "quiche/common/quiche_cord_utils.cc", "quiche/common/quiche_crypto_logging.cc", "quiche/common/quiche_data_reader.cc", "quiche/common/quiche_data_writer.cc", @@ -1143,6 +1145,7 @@ "quiche/common/quiche_buffer_allocator_test.cc", "quiche/common/quiche_callbacks_test.cc", "quiche/common/quiche_circular_deque_test.cc", + "quiche/common/quiche_cord_utils_test.cc", "quiche/common/quiche_data_reader_test.cc", "quiche/common/quiche_data_writer_fuzz_test.cc", "quiche/common/quiche_data_writer_test.cc",
diff --git a/quiche/common/quiche_cord_utils.cc b/quiche/common/quiche_cord_utils.cc new file mode 100644 index 0000000..51d9403 --- /dev/null +++ b/quiche/common/quiche_cord_utils.cc
@@ -0,0 +1,79 @@ +// Copyright 2026 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "quiche/common/quiche_cord_utils.h" + +#include <cstddef> +#include <cstdint> +#include <memory> +#include <utility> + +#include "absl/strings/cord.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "quiche/common/platform/api/quiche_logging.h" +#include "quiche/common/quiche_callbacks.h" +#include "quiche/common/quiche_mem_slice.h" + +namespace quiche { + +namespace { +// Explicitly stated in the absl::Cord documentation. +constexpr size_t kMaxInlinedCordSize = 15; +} // namespace + +absl::Cord MemSliceToCord(QuicheMemSlice slice) { + if (slice.empty()) { + return absl::Cord(); + } + QuicheMemSlice::ReleasedSlice released_slice = std::move(slice).Release(); + if (released_slice.callback == nullptr) { + released_slice.callback = [](absl::string_view) {}; + } + return absl::MakeCordFromExternal(released_slice.data, + std::move(released_slice.callback)); +} + +absl::Cord MemSliceSpanToCord(absl::Span<QuicheMemSlice> slices) { + absl::Cord cord; + for (QuicheMemSlice& slice : slices) { + cord.Append(MemSliceToCord(std::move(slice))); + } + return cord; +} + +void CordToMemSlices(const absl::Cord& cord, + UnretainedCallback<void(QuicheMemSlice)> sink) { + size_t current_offset = 0; + for (absl::string_view chunk : cord.Chunks()) { + // absl::Cord does not provide any API to access individual chunks or to + // extract the release callback from a chunk. To side-step this issue, + // allocate an instance of absl::Cord on the heap, and delete it from the + // QuicheMemSlice release callback. The heap allocation is necessary since + // absl::Cord supports small string inlining, meaning the resulting + // absl::string_view is not guaranteed to point to a stable heap-allocated + // address otherwise. + auto subcord = std::make_unique<absl::Cord>( + cord.Subcord(current_offset, chunk.size())); + QUICHE_DCHECK(subcord->TryFlat().has_value()); + // Despite the QUICHE_DCHECK above, the production code below still uses + // `Flatten()` as a fail-safe. + absl::string_view stored_chunk = subcord->Flatten(); + + QUICHE_DCHECK_EQ(stored_chunk, chunk); + if (chunk.size() > kMaxInlinedCordSize) { + // absl::Cord has a documented inlining threshold; ensure that if it is + // exceeded, the data address does not change, since the goal of this API + // is to avoid copies. + QUICHE_DCHECK_EQ(reinterpret_cast<uintptr_t>(stored_chunk.data()), + reinterpret_cast<uintptr_t>(chunk.data())); + } + sink(QuicheMemSlice( + stored_chunk.data(), stored_chunk.size(), + [ptr = subcord.release()](absl::string_view) { delete ptr; })); + current_offset += chunk.size(); + } +} + +} // namespace quiche
diff --git a/quiche/common/quiche_cord_utils.h b/quiche/common/quiche_cord_utils.h new file mode 100644 index 0000000..87ed18c --- /dev/null +++ b/quiche/common/quiche_cord_utils.h
@@ -0,0 +1,44 @@ +// Copyright 2026 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef QUICHE_COMMON_QUICHE_CORD_UTILS_H_ +#define QUICHE_COMMON_QUICHE_CORD_UTILS_H_ + +#include <utility> + +#include "absl/strings/cord.h" +#include "absl/types/span.h" +#include "quiche/common/platform/api/quiche_export.h" +#include "quiche/common/quiche_callbacks.h" +#include "quiche/common/quiche_mem_slice.h" + +namespace quiche { + +// Converts an instance of QuicheMemSlice into an instance of absl::Cord that +// owns the underlying MemSlice. +absl::Cord QUICHE_EXPORT MemSliceToCord(QuicheMemSlice slice); +// Converts a span of MemSlices into a single absl::Cord instance. All of the +// slices in `slices` are moved into the Cord, and are no longer valid. +absl::Cord QUICHE_EXPORT MemSliceSpanToCord(absl::Span<QuicheMemSlice> slices); + +// Converts an absl::Cord into a sequence of QuicheMemSlice objects without +// copying any of the data in the Cord. Calls `sink` for every QuicheMemSlice +// object generated. +void QUICHE_EXPORT CordToMemSlices( + const absl::Cord& cord, UnretainedCallback<void(QuicheMemSlice)> sink); + +// Converts an absl::Cord into a sequence of QuicheMemSlice objects, and appends +// them to the provided container. The container has to provide a `push_back` +// method. +template <typename Container> +void QUICHE_NO_EXPORT CordToMemSlicesTo(const absl::Cord& cord, + Container& container) { + CordToMemSlices(cord, [&](QuicheMemSlice slice) { + container.push_back(std::move(slice)); + }); +} + +} // namespace quiche + +#endif // QUICHE_COMMON_QUICHE_CORD_UTILS_H_
diff --git a/quiche/common/quiche_cord_utils_test.cc b/quiche/common/quiche_cord_utils_test.cc new file mode 100644 index 0000000..120afb4 --- /dev/null +++ b/quiche/common/quiche_cord_utils_test.cc
@@ -0,0 +1,148 @@ +// Copyright 2026 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "quiche/common/quiche_cord_utils.h" + +#include <array> +#include <cstddef> +#include <cstdint> +#include <cstring> +#include <memory> +#include <string> +#include <utility> +#include <vector> + +#include "absl/strings/cord.h" +#include "absl/strings/str_cat.h" +#include "absl/strings/string_view.h" +#include "absl/types/span.h" +#include "quiche/common/platform/api/quiche_test.h" +#include "quiche/common/quiche_mem_slice.h" + +namespace quiche::test { +namespace { + +TEST(QuicheCordUtilsTest, MemSliceToCord) { + QuicheMemSlice slice1 = QuicheMemSlice::Copy("foo"); + QuicheMemSlice slice2 = QuicheMemSlice::Copy("bar"); + absl::Cord cord = MemSliceToCord(std::move(slice1)); + cord.Append(MemSliceToCord(std::move(slice2))); + EXPECT_EQ(cord, "foobar"); +} + +TEST(QuicheCordUtilsTest, MemSliceToCordEmpty) { + absl::Cord cord = MemSliceToCord(QuicheMemSlice()); + EXPECT_TRUE(cord.empty()); +} + +TEST(QuicheCordUtilsTest, MemSliceToCordNullDeleter) { + absl::string_view kText = "test"; + absl::Cord cord = + MemSliceToCord(QuicheMemSlice(kText.data(), kText.size(), nullptr)); + EXPECT_EQ(cord, kText); +} + +TEST(QuicheCordUtilsTest, MemSliceSpanToCord) { + std::array<QuicheMemSlice, 2> slices = {QuicheMemSlice::Copy("foo"), + QuicheMemSlice::Copy("bar")}; + absl::Cord cord = MemSliceSpanToCord(absl::MakeSpan(slices)); + EXPECT_EQ(cord, "foobar"); +} + +TEST(QuicheCordUtilsTest, CordToMemSlicesInlined) { + absl::Cord cord("test"); + std::vector<QuicheMemSlice> slices; + CordToMemSlicesTo(cord, slices); + ASSERT_EQ(slices.size(), 1); + EXPECT_EQ(slices[0].AsStringView(), "test"); +} + +TEST(QuicheCordUtilsTest, CordToMemSlicesNotInlined) { + constexpr size_t kSize = 8192; + auto buffer = std::make_unique<char[]>(kSize); + uintptr_t original_address = reinterpret_cast<uintptr_t>(buffer.get()); + memset(buffer.get(), 'a', kSize); + absl::Cord cord = absl::MakeCordFromExternal( + absl::string_view(buffer.release(), kSize), + [](absl::string_view data) { delete[] data.data(); }); + std::vector<QuicheMemSlice> slices; + CordToMemSlicesTo(cord, slices); + cord.Clear(); + + ASSERT_EQ(slices.size(), 1); + EXPECT_EQ(slices[0].length(), kSize); + EXPECT_EQ(reinterpret_cast<uintptr_t>(slices[0].data()), original_address); + EXPECT_EQ(slices[0].AsStringView(), std::string(kSize, 'a')); +} + +TEST(QuicheCordUtilsTest, CordToMemSlicesLarge) { + const std::string kBlock(1024, 'a'); + constexpr size_t kBlockCount = 128; + absl::Cord cord; + for (size_t i = 0; i < kBlockCount; ++i) { + cord.Append(kBlock); + } + + std::vector<QuicheMemSlice> slices; + CordToMemSlicesTo(cord, slices); + cord.Clear(); + + ASSERT_GT(slices.size(), 1); + size_t total_size = 0; + for (const QuicheMemSlice& slice : slices) { + total_size += slice.length(); + } + EXPECT_EQ(total_size, kBlock.size() * kBlockCount); + for (const QuicheMemSlice& slice : slices) { + ASSERT_THAT(slice.AsStringView(), testing::Each('a')); + } +} + +TEST(QuicheCordUtilsTest, CordWithMixedTypes) { + absl::Cord cord("bar"); + cord.Prepend("foo"); + + auto block_before = std::make_unique<std::string>(8192, 'a'); + absl::string_view block_before_view(*block_before); + cord.Prepend(absl::MakeCordFromExternal( + block_before_view, + [ptr = block_before.release()](absl::string_view) { delete ptr; })); + + auto block_after = std::make_unique<std::string>(8192, 'b'); + absl::string_view block_after_view(*block_after); + cord.Append(absl::MakeCordFromExternal( + block_after_view, + [ptr = block_after.release()](absl::string_view) { delete ptr; })); + + std::vector<QuicheMemSlice> slices; + CordToMemSlicesTo(cord, slices); + cord.Clear(); + + ASSERT_GT(slices.size(), 1); + std::string concatenated; + for (const QuicheMemSlice& slice : slices) { + absl::StrAppend(&concatenated, slice.AsStringView()); + } + EXPECT_EQ(concatenated, absl::StrCat(std::string(8192, 'a'), "foobar", + std::string(8192, 'b'))); +} + +TEST(QuicheCordUtilsTest, Subcord) { + constexpr size_t kCount = 100; + absl::Cord cord; + for (size_t i = 0; i < kCount; ++i) { + cord.Append("foobar"); + } + absl::Cord subcord = cord.Subcord(99, 6); + std::vector<QuicheMemSlice> slices; + CordToMemSlicesTo(subcord, slices); + ASSERT_EQ(slices.size(), 1); + EXPECT_EQ(slices[0].AsStringView(), "barfoo"); // 99 = 16 * 6 + 3 + cord.Clear(); + subcord.Clear(); + EXPECT_EQ(slices[0].AsStringView(), "barfoo"); +} + +} // namespace +} // namespace quiche::test
diff --git a/quiche/common/quiche_mem_slice.cc b/quiche/common/quiche_mem_slice.cc index 84d1214..7979d87 100644 --- a/quiche/common/quiche_mem_slice.cc +++ b/quiche/common/quiche_mem_slice.cc
@@ -64,6 +64,15 @@ done_callback_ = nullptr; } +QuicheMemSlice::ReleasedSlice QuicheMemSlice::Release() && { + ReleasedSlice slice{.data = AsStringView(), + .callback = std::move(done_callback_)}; + data_ = nullptr; + size_ = 0; + done_callback_ = nullptr; + return slice; +} + QuicheMemSlice QuicheMemSlice::Copy(absl::string_view data) { if (data.empty()) { return QuicheMemSlice();
diff --git a/quiche/common/quiche_mem_slice.h b/quiche/common/quiche_mem_slice.h index cf2d173..4fb5035 100644 --- a/quiche/common/quiche_mem_slice.h +++ b/quiche/common/quiche_mem_slice.h
@@ -19,6 +19,11 @@ class QUICHE_EXPORT QuicheMemSlice { public: using ReleaseCallback = SingleUseCallback<void(absl::string_view)>; + // QuicheMemSlice released into the underlying components. + struct ReleasedSlice { + absl::string_view data; + ReleaseCallback callback; // Can be nullptr. + }; // Creates a QuicheMemSlice by allocating memory on heap and copying the // specified bytes. @@ -54,6 +59,11 @@ // undefined behavior. void Reset(); + // Releases the underlying deleter callback, and clears the state of the + // object. Returns both the callback and the view of the data owned by the + // slice. + ReleasedSlice Release() &&; + // Returns a const char pointer to underlying data buffer. const char* data() const { return data_; } // Returns the length of underlying data buffer.
diff --git a/quiche/common/quiche_mem_slice_test.cc b/quiche/common/quiche_mem_slice_test.cc index e891195..5abe46d 100644 --- a/quiche/common/quiche_mem_slice_test.cc +++ b/quiche/common/quiche_mem_slice_test.cc
@@ -140,6 +140,26 @@ EXPECT_EQ(slice.length(), 0u); } +TEST_F(QuicheMemSliceTest, Release) { + constexpr absl::string_view kData = "test"; + bool deleted = false; + QuicheMemSlice slice(kData.data(), kData.size(), [&](absl::string_view data) { + EXPECT_EQ(kData, data); + deleted = true; + }); + EXPECT_FALSE(slice.empty()); + QuicheMemSlice::ReleasedSlice released = std::move(slice).Release(); + EXPECT_TRUE(slice.empty()); // NOLINT(bugprone-use-after-move) + EXPECT_EQ(released.data, kData); + EXPECT_FALSE(deleted); + std::move(released.callback)(kData); + EXPECT_TRUE(deleted); + + released = std::move(slice).Release(); // NOLINT(bugprone-use-after-move) + EXPECT_TRUE(released.data.empty()); + EXPECT_EQ(released.callback, nullptr); +} + } // namespace } // namespace test } // namespace quiche