diff --git a/quic/common/BUCK b/quic/common/BUCK index 01e8463c7..c15705996 100644 --- a/quic/common/BUCK +++ b/quic/common/BUCK @@ -65,11 +65,14 @@ mvfst_cpp_library( name = "buf_util", srcs = [ "BufUtil.cpp", + "ChainedByteRange.cpp", ], headers = [ "BufUtil.h", + "ChainedByteRange.h", ], exported_deps = [ + "//folly:range", "//folly/io:iobuf", "//quic:constants", ], diff --git a/quic/common/BufUtil.cpp b/quic/common/BufUtil.cpp index ec1f67e5c..d560dc56a 100644 --- a/quic/common/BufUtil.cpp +++ b/quic/common/BufUtil.cpp @@ -170,6 +170,14 @@ void BufWriter::insert(const folly::IOBuf* data, size_t limit) { copy(data, limit); } +void BufWriter::insert(const ChainedByteRangeHead* data) { + insert(data, data->chainLength()); +} + +void BufWriter::insert(const ChainedByteRangeHead* data, size_t limit) { + copy(&data->head, limit); +} + void BufWriter::append(size_t len) { iobuf_.append(len); written_ += len; @@ -197,6 +205,27 @@ void BufWriter::copy(const folly::IOBuf* data, size_t limit) { CHECK_GE(limit, totalInserted); } +void BufWriter::copy(const ChainedByteRange* data, size_t limit) { + if (!limit) { + return; + } + sizeCheck(limit); + size_t totalInserted = 0; + const ChainedByteRange* curBuf = data; + auto remaining = limit; + do { + auto lenToCopy = std::min(curBuf->length(), remaining); + push(curBuf->getRange().begin(), lenToCopy); + totalInserted += lenToCopy; + remaining -= lenToCopy; + if (lenToCopy < curBuf->length()) { + break; + } + curBuf = curBuf->getNext(); + } while (remaining && curBuf != data); + CHECK_GE(limit, totalInserted); +} + void BufWriter::backFill(const uint8_t* data, size_t len, size_t destOffset) { CHECK_GE(appendCount_, len); appendCount_ -= len; diff --git a/quic/common/BufUtil.h b/quic/common/BufUtil.h index 5b10b1ba8..b238d4f07 100644 --- a/quic/common/BufUtil.h +++ b/quic/common/BufUtil.h @@ -6,8 +6,10 @@ */ #pragma once +#include #include #include +#include namespace quic { @@ -121,6 +123,9 @@ class BufWriter { void insert(const folly::IOBuf* data); void insert(const folly::IOBuf* data, size_t limit); + void insert(const ChainedByteRangeHead* data); + void insert(const ChainedByteRangeHead* data, size_t limit); + void append(size_t len); private: @@ -135,6 +140,7 @@ class BufWriter { } void copy(const folly::IOBuf* data, size_t limit); + void copy(const ChainedByteRange* data, size_t limit); private: folly::IOBuf& iobuf_; diff --git a/quic/common/CMakeLists.txt b/quic/common/CMakeLists.txt index 45da556e0..c0544ecba 100644 --- a/quic/common/CMakeLists.txt +++ b/quic/common/CMakeLists.txt @@ -55,6 +55,7 @@ target_link_libraries( add_library( mvfst_bufutil BufUtil.cpp + ChainedByteRange.cpp ) set_property(TARGET mvfst_bufutil PROPERTY VERSION ${PACKAGE_VERSION}) diff --git a/quic/common/ChainedByteRange.cpp b/quic/common/ChainedByteRange.cpp new file mode 100644 index 000000000..4dfd14e2c --- /dev/null +++ b/quic/common/ChainedByteRange.cpp @@ -0,0 +1,259 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include + +namespace quic { + +[[nodiscard]] bool ChainedByteRange::empty() const { + if (range_.size() != 0) { + return false; + } + for (auto* current = next_; current != this; current = current->next_) { + if (current->range_.size() != 0) { + return false; + } + } + return true; +} + +[[nodiscard]] std::string ChainedByteRange::toStr() const { + std::string result; + result.reserve(computeChainDataLength()); + result.append(range_.toString()); + for (auto* current = next_; current != this; current = current->next_) { + result.append(current->range_.toString()); + } + return result; +} + +[[nodiscard]] size_t ChainedByteRange::computeChainDataLength() const { + size_t fullLength = range_.size(); + for (auto* current = next_; current != this; current = current->next_) { + fullLength += current->range_.size(); + } + return fullLength; +} + +ChainedByteRangeHead::ChainedByteRangeHead(const Buf& buf) { + if (!buf || buf->empty()) { + return; + } + + auto it = buf->begin(); + while (it != buf->end() && it->empty()) { + it++; + } + + CHECK(it != buf->end()); + head.range_ = *it++; + chainLength_ += head.range_.size(); + + ChainedByteRange* cur = &head; + for (; it != buf->end(); it++) { + chainLength_ += it->size(); + auto next = std::make_unique().release(); + next->range_ = *it; + next->prev_ = cur; + cur->next_ = next; + cur = next; + } + cur->next_ = &head; + head.prev_ = cur; +} + +void ChainedByteRangeHead::append(const Buf& buf) { + if (!buf || buf->empty()) { + return; + } + + auto it = buf->begin(); + while (it != buf->end() && it->empty()) { + it++; + } + + CHECK(it != buf->end()); + // We know that *it is non-empty at this point because of the initial + // check that the chain is non-empty. + if (head.range_.empty()) { + head.range_ = *it; + chainLength_ += it->size(); + it++; + } + + ChainedByteRange* tail = head.prev_; + while (it != buf->end()) { + if (it->empty()) { + it++; + continue; + } + + auto* newElement = std::make_unique(*it).release(); + chainLength_ += it->size(); + newElement->next_ = &head; + newElement->prev_ = tail; + + tail->next_ = newElement; + tail = newElement; + head.prev_ = newElement; + + it++; + } +} + +void ChainedByteRangeHead::append(ChainedByteRangeHead&& chainHead) { + ChainedByteRange* oldTail = head.prev_; + // Since we're merging the input chain into this one, we need to create a + // ChainedByteRange for the data that's held as the first buffer in the input + // chain. + ChainedByteRange* headSubstitute = + std::make_unique(chainHead.head.getRange()).release(); + ChainedByteRange* newTail = (chainHead.head.prev_ == &chainHead.head) + ? headSubstitute + : chainHead.head.prev_; + + headSubstitute->next_ = + (newTail == &chainHead.head) ? headSubstitute : chainHead.head.next_; + chainHead.head.next_->prev_ = headSubstitute; + headSubstitute->prev_ = oldTail; + oldTail->next_ = headSubstitute; + newTail->next_ = &head; + head.prev_ = newTail; + + chainLength_ += chainHead.chainLength_; + + chainHead.head.next_ = chainHead.head.prev_ = &chainHead.head; + chainHead.chainLength_ = 0; +} + +ChainedByteRangeHead ChainedByteRangeHead::splitAtMost(size_t len) { + // entire chain requested + if (len >= chainLength_) { + return std::move(*this); + } + + ChainedByteRangeHead ret; + ret.chainLength_ = len; + if (len == 0) { + return ret; + } + + chainLength_ -= len; + + if (head.length() > len) { + // Just need to trim a little off the head. + ret.head.range_ = + folly::ByteRange(head.range_.begin(), head.range_.begin() + len); + ret.head.next_ = &ret.head; + ret.head.prev_ = &ret.head; + head.trimStart(len); + return ret; + } + + ChainedByteRange* current = &head; + /** + * Find the last ChainedByteRange containing range requested. This will + * definitively terminate without looping back to head since we know length > + * len. + */ + while (len != 0) { + if (current->length() > len) { + break; + } + len -= current->length(); + current = current->next_; + } + + if (len == 0) { + /** + * In this case, we're splitting at the boundary of two ChainedByteRanges. + * We make head take up the place of the first ChainedByteRange in the + * second chain. + */ + ChainedByteRange* tailOfSecondPart = + (head.prev_ == current) ? &head : head.prev_; + ChainedByteRange* tailOfFirstPart = + (current->prev_ == &head ? &ret.head : current->prev_); + + ret.head.range_ = head.range_; + ret.head.next_ = head.next_; + ret.head.prev_ = tailOfFirstPart; + ret.head.next_->prev_ = &ret.head; + tailOfFirstPart->next_ = &ret.head; + + head.range_ = current->range_; + head.next_ = current->next_; + head.prev_ = tailOfSecondPart; + head.next_->prev_ = &head; + tailOfSecondPart->next_ = &head; + + delete current; + } else { + /** + * In this case, we're splitting somewhere in the middle of a + * ChainedByteRange. + */ + ChainedByteRange* tailOfFirstPart = current; + ChainedByteRange* tailOfSecondPart = + (head.prev_ == tailOfFirstPart) ? &head : head.prev_; + + ret.head.range_ = head.range_; + + head.range_ = + folly::ByteRange(current->range_.begin() + len, current->range_.end()); + current->range_ = folly::ByteRange( + current->range_.begin(), current->range_.begin() + len); + + ret.head.next_ = head.next_; + ret.head.prev_ = tailOfFirstPart; + ret.head.next_->prev_ = &ret.head; + + head.next_ = tailOfFirstPart->next_; + tailOfFirstPart->next_->prev_ = &head; + head.prev_ = tailOfSecondPart; + + tailOfFirstPart->next_ = &ret.head; + } + + return ret; +} + +size_t ChainedByteRangeHead::trimStartAtMost(size_t len) { + size_t amountToSplit = std::min(len, chainLength()); + auto splitRch = splitAtMost(amountToSplit); + return amountToSplit; +} + +void ChainedByteRangeHead::resetChain() { + ChainedByteRange* curr = head.next_; + while (curr != &head) { + auto* next = curr->next_; + delete curr; + curr = next; + } + head.next_ = &head; + head.prev_ = &head; + chainLength_ = 0; +} + +void ChainedByteRangeHead::moveChain(ChainedByteRangeHead&& other) { + head.range_ = other.head.range_; + ChainedByteRange* headNext = other.head.next_; + ChainedByteRange* headPrev = other.head.prev_; + headNext->prev_ = &head; + headPrev->next_ = &head; + head.next_ = other.head.next_; + head.prev_ = other.head.prev_; + + other.head.range_ = folly::ByteRange(); + other.head.next_ = &other.head; + other.head.prev_ = &other.head; + chainLength_ = other.chainLength_; + other.chainLength_ = 0; +} + +} // namespace quic diff --git a/quic/common/ChainedByteRange.h b/quic/common/ChainedByteRange.h new file mode 100644 index 000000000..55b6f1dd8 --- /dev/null +++ b/quic/common/ChainedByteRange.h @@ -0,0 +1,127 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once +#include +#include + +namespace quic { + +/* + * The ChainedByteRange depicts one block of contiguous + * memory, and has a next_ and a prev_ pointer. It has APIs + * that can be used to trim the start or end of this specific + * contiguous memory block. + */ +class ChainedByteRange { + public: + ChainedByteRange() : next_(this), prev_(this) {} + + explicit ChainedByteRange(folly::ByteRange range) + : range_(range), next_(this), prev_(this) {} + + /** + * Returns the length only of this ChainedByteRange + */ + [[nodiscard]] size_t length() const { + return range_.size(); + } + + /** + * Check whether the entire chain is empty + */ + [[nodiscard]] bool empty() const; + + [[nodiscard]] std::string toStr() const; + + [[nodiscard]] size_t computeChainDataLength() const; + + /** + * Trim the start of this specific contiguous memory block + */ + void trimStart(size_t n) { + n = std::min(n, range_.size()); + range_.advance(n); + } + + [[nodiscard]] folly::ByteRange getRange() const { + return range_; + } + + [[nodiscard]] ChainedByteRange* getNext() const { + return next_; + } + + [[nodiscard]] ChainedByteRange* getPrev() const { + return prev_; + } + + private: + folly::ByteRange range_; + ChainedByteRange* next_{nullptr}; + ChainedByteRange* prev_{nullptr}; + + friend class ChainedByteRangeHead; +}; + +/* + * The ChainedByteRangeHead depicts the head of a chain of ChainedByteRanges. + * It caches the length of the total chain, which is useful in many cases + * because we don't want to walk the entire chain to get the length. + * Additionally, it allows us to trim or split off multiple ChainedByteRanges + * with the splitAtMost and trimStartAtMost APIs. + */ +class ChainedByteRangeHead { + public: + ChainedByteRange head; + + explicit ChainedByteRangeHead(const Buf& buf); + + ChainedByteRangeHead() = default; + + ChainedByteRangeHead(ChainedByteRangeHead&& other) noexcept { + moveChain(std::move(other)); + } + + ChainedByteRangeHead& operator=(ChainedByteRangeHead&& other) noexcept { + resetChain(); + moveChain(std::move(other)); + return *this; + } + + ~ChainedByteRangeHead() { + resetChain(); + } + + [[nodiscard]] bool empty() const { + return chainLength_ == 0; + } + + [[nodiscard]] size_t chainLength() const { + return chainLength_; + } + + /** + * Splits off the initial n bytes from the chain and returns them. + */ + ChainedByteRangeHead splitAtMost(size_t n); + + size_t trimStartAtMost(size_t len); + + void append(const Buf& buf); + + void append(ChainedByteRangeHead&& chainHead); + + private: + void resetChain(); + + void moveChain(ChainedByteRangeHead&& other); + + size_t chainLength_{0}; +}; + +} // namespace quic diff --git a/quic/common/test/BUCK b/quic/common/test/BUCK index a0fe23f7c..88788e5af 100644 --- a/quic/common/test/BUCK +++ b/quic/common/test/BUCK @@ -59,6 +59,18 @@ cpp_unittest( ], ) +cpp_unittest( + name = "ChainedByteRangeTest", + srcs = [ + "ChainedByteRangeTest.cpp", + ], + deps = [ + "//folly:string", + "//folly/io:iobuf", + "//quic/common:buf_util", + ], +) + mvfst_cpp_library( name = "test_client_utils", headers = [ diff --git a/quic/common/test/BufUtilTest.cpp b/quic/common/test/BufUtilTest.cpp index b60ce260c..35a7a23d8 100644 --- a/quic/common/test/BufUtilTest.cpp +++ b/quic/common/test/BufUtilTest.cpp @@ -561,3 +561,61 @@ TEST(BufWriterTest, TwoWriters) { EXPECT_EQ(15, outputBuffer->length()); EXPECT_EQ("Destroyer Saint", reader.readFixedString(outputBuffer->length())); } + +TEST(BufWriterTest, InsertSingleByteChainedRange) { + auto testBuffer = folly::IOBuf::create(100); + BufWriter writer(*testBuffer, 100); + auto inputBuffer = + folly::IOBuf::copyBuffer("Steady on dreaming, I sleepwalk"); + auto len = inputBuffer->computeChainDataLength(); + ChainedByteRangeHead cbrh(inputBuffer); + writer.insert(&cbrh); + folly::io::Cursor reader(testBuffer.get()); + EXPECT_EQ( + inputBuffer->computeChainDataLength(), + testBuffer->computeChainDataLength()); + EXPECT_EQ(inputBuffer->to(), reader.readFixedString(len)); +} + +TEST(BufWriterTest, InsertZeroLen) { + auto testBuffer = folly::IOBuf::create(100); + BufWriter writer(*testBuffer, 100); + auto inputBuffer = folly::IOBuf::copyBuffer(""); + ChainedByteRangeHead cbrh(inputBuffer); + writer.insert(&cbrh); + folly::io::Cursor reader(testBuffer.get()); + EXPECT_EQ(testBuffer->computeChainDataLength(), 0); +} + +TEST(BufWriterTest, InsertSingleByteChainedRangeWithLimit) { + auto testBuffer = folly::IOBuf::create(100); + BufWriter writer(*testBuffer, 100); + auto inputBuffer = + folly::IOBuf::copyBuffer("Steady on dreaming, I sleepwalk"); + ChainedByteRangeHead cbrh(inputBuffer); + writer.insert(&cbrh, 10); + folly::io::Cursor reader(testBuffer.get()); + EXPECT_EQ(testBuffer->computeChainDataLength(), 10); + EXPECT_EQ("Steady on ", reader.readFixedString(10)); +} + +TEST(BufWriterTest, InsertChainByteChainedRange) { + auto testBuffer = folly::IOBuf::create(1000); + BufWriter writer(*testBuffer, 1000); + auto inputBuffer = + folly::IOBuf::copyBuffer("Cause I lost you and now what am i to do?"); + inputBuffer->prependChain( + folly::IOBuf::copyBuffer(" Can't believe that we are through.")); + inputBuffer->prependChain( + folly::IOBuf::copyBuffer(" While the memory of you linger like a song.")); + auto len = inputBuffer->computeChainDataLength(); + ChainedByteRangeHead cbrh(inputBuffer); + writer.insert(&cbrh); + folly::io::Cursor reader(testBuffer.get()); + EXPECT_EQ(testBuffer->computeChainDataLength(), len); + EXPECT_EQ( + "Cause I lost you and now what am i to do?" + " Can't believe that we are through." + " While the memory of you linger like a song.", + reader.readFixedString(len)); +} diff --git a/quic/common/test/ChainedByteRangeTest.cpp b/quic/common/test/ChainedByteRangeTest.cpp new file mode 100644 index 000000000..922477640 --- /dev/null +++ b/quic/common/test/ChainedByteRangeTest.cpp @@ -0,0 +1,480 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include + +#include +#include +#include + +using namespace std; +using namespace folly; +using namespace quic; + +#define SCL(x) (x), sizeof(x) - 1 + +namespace { +void checkConsistency(const ChainedByteRangeHead& queue) { + size_t len = queue.head.computeChainDataLength(); + EXPECT_EQ(len, queue.chainLength()); + + ChainedByteRange* current = queue.head.getNext(); + EXPECT_EQ(queue.head.getNext()->getPrev(), &queue.head); + EXPECT_EQ(queue.head.getPrev()->getNext(), &queue.head); + while (current != &queue.head) { + EXPECT_EQ(current->getNext()->getPrev(), current); + EXPECT_EQ(current->getPrev()->getNext(), current); + current = current->getNext(); + } +} + +} // namespace + +static auto kHello = IOBuf::copyBuffer(SCL("Hello")); +static auto kCommaSpace = IOBuf::copyBuffer(SCL(", ")); +static auto kComma = IOBuf::copyBuffer(SCL(",")); +static auto kSpace = IOBuf::copyBuffer(SCL(" ")); +static auto kEmpty = IOBuf::copyBuffer(SCL("")); +static auto kWorld = IOBuf::copyBuffer(SCL("World")); + +TEST(ChainedByteRangeHead, AppendBasic) { + ChainedByteRangeHead queue; + queue.append(kHello); + queue.append(kSpace); + checkConsistency(queue); + EXPECT_EQ(queue.head.computeChainDataLength(), 6); +} + +TEST(ChainedByteRangeHead, Append) { + ChainedByteRangeHead queue; + queue.append(kHello); + ChainedByteRangeHead queue2; + queue2.append(kCommaSpace); + queue2.append(kWorld); + checkConsistency(queue); + checkConsistency(queue2); +} + +TEST(ChainedByteRangeHead, Append2) { + ChainedByteRangeHead queue; + queue.append(kHello); + ChainedByteRangeHead queue2; + queue2.append(kCommaSpace); + queue2.append(kWorld); + checkConsistency(queue); + checkConsistency(queue2); +} + +TEST(ChainedByteRangeHead, AppendHead) { + ChainedByteRangeHead queue; + queue.append(kHello); + ChainedByteRangeHead queue2; + queue2.append(kCommaSpace); + queue.append(std::move(queue2)); + checkConsistency(queue); + EXPECT_EQ(queue.head.computeChainDataLength(), 7); +} + +TEST(ChainedByteRangeHead, AppendHead2) { + ChainedByteRangeHead queue; + queue.append(kHello); + queue.append(kComma); + ChainedByteRangeHead queue2; + queue2.append(kSpace); + queue2.append(kWorld); + queue.append(std::move(queue2)); + checkConsistency(queue); + EXPECT_EQ(queue.head.computeChainDataLength(), 12); +} + +TEST(ChainedByteRangeHead, AppendHead3) { + ChainedByteRangeHead queue; + queue.append(kHello); + queue.append(kComma); + ChainedByteRangeHead queue2; + queue2.append(kSpace); + queue.append(std::move(queue2)); + checkConsistency(queue); + EXPECT_EQ(queue.head.computeChainDataLength(), 7); +} + +TEST(ChainedByteRangeHead, AppendHead4) { + ChainedByteRangeHead queue; + queue.append(kHello); + ChainedByteRangeHead queue2; + queue2.append(kComma); + queue2.append(kSpace); + queue.append(std::move(queue2)); + checkConsistency(queue); + EXPECT_EQ(queue.head.computeChainDataLength(), 7); +} + +TEST(ChainedByteRangeHead, AppendMultipleEmpty) { + auto buf = folly::IOBuf::copyBuffer(""); + buf->appendToChain(folly::IOBuf::copyBuffer("")); + buf->appendToChain(folly::IOBuf::copyBuffer("apple")); + buf->appendToChain(folly::IOBuf::copyBuffer("ball")); + buf->appendToChain(folly::IOBuf::copyBuffer("")); + buf->appendToChain(folly::IOBuf::copyBuffer("dog")); + buf->appendToChain(folly::IOBuf::copyBuffer("cat")); + + ChainedByteRangeHead chainedByteRangeHead; + chainedByteRangeHead.append(buf); + EXPECT_EQ(chainedByteRangeHead.chainLength(), 15); + EXPECT_EQ(chainedByteRangeHead.head.toStr(), "appleballdogcat"); +} + +TEST(ChainedByteRangeHead, AppendStringPiece) { + std::string s("Hello, World"); + auto helloWorld = IOBuf::copyBuffer(s); + ChainedByteRangeHead queue; + ChainedByteRangeHead queue2; + queue.append(helloWorld); + queue2.append(helloWorld); + checkConsistency(queue); + checkConsistency(queue2); + EXPECT_EQ(s.length(), queue.head.computeChainDataLength()); + EXPECT_EQ(s.length(), queue2.head.computeChainDataLength()); + EXPECT_EQ( + 0, + memcmp( + queue.head.getRange().data(), + queue2.head.getRange().data(), + s.length())); +} + +TEST(ChainedByteRangeHead, Splitttt) { + ChainedByteRangeHead queue; + queue.append(kHello); + queue.append(kComma); + queue.append(kSpace); + queue.append(kEmpty); + queue.append(kWorld); + checkConsistency(queue); + EXPECT_EQ(12, queue.head.computeChainDataLength()); + + auto prefix = queue.splitAtMost(1); + checkConsistency(queue); + EXPECT_EQ(1, prefix.head.computeChainDataLength()); + EXPECT_EQ(11, queue.head.computeChainDataLength()); + EXPECT_EQ(prefix.head.toStr(), "H"); + ChainedByteRangeHead rch1(std::move(prefix)); + + prefix = queue.splitAtMost(2); + checkConsistency(queue); + EXPECT_EQ(2, prefix.head.computeChainDataLength()); + EXPECT_EQ(9, queue.head.computeChainDataLength()); + EXPECT_EQ(prefix.head.toStr(), "el"); + ChainedByteRangeHead rch2(std::move(prefix)); + + prefix = queue.splitAtMost(3); + checkConsistency(queue); + EXPECT_EQ(3, prefix.head.computeChainDataLength()); + EXPECT_EQ(6, queue.head.computeChainDataLength()); + EXPECT_EQ(prefix.head.toStr(), "lo,"); + ChainedByteRangeHead rch3(std::move(prefix)); + + prefix = queue.splitAtMost(1); + checkConsistency(queue); + EXPECT_EQ(1, prefix.head.computeChainDataLength()); + EXPECT_EQ(5, queue.head.computeChainDataLength()); + EXPECT_EQ(prefix.head.toStr(), " "); + ChainedByteRangeHead rch4(std::move(prefix)); + + prefix = queue.splitAtMost(5); + checkConsistency(queue); + EXPECT_EQ(5, prefix.head.computeChainDataLength()); + EXPECT_TRUE(queue.empty()); + EXPECT_EQ(queue.chainLength(), 0); + EXPECT_TRUE(queue.head.getRange().empty()); + EXPECT_EQ(prefix.head.toStr(), "World"); + ChainedByteRangeHead rch5(std::move(prefix)); + + auto helloComma = IOBuf::copyBuffer(SCL("Hello,")); + queue.append(helloComma); + checkConsistency(queue); + prefix = queue.splitAtMost(3); + checkConsistency(queue); + EXPECT_EQ(3, prefix.head.computeChainDataLength()); + EXPECT_EQ(3, queue.chainLength()); + EXPECT_EQ(prefix.head.toStr(), "Hel"); + ChainedByteRangeHead rch6(std::move(prefix)); + + auto spaceWorld = IOBuf::copyBuffer(SCL(" World")); + queue.append(spaceWorld); + checkConsistency(queue); + prefix = queue.splitAtMost(13); + EXPECT_EQ(9, prefix.head.computeChainDataLength()); + EXPECT_EQ(0, queue.chainLength()); + EXPECT_EQ(prefix.head.toStr(), "lo, World"); + checkConsistency(queue); + ChainedByteRangeHead rch7(std::move(prefix)); +} + +TEST(ChainedByteRangeHead, Empty) { + ChainedByteRangeHead emptyQueue; + checkConsistency(emptyQueue); + EXPECT_TRUE(emptyQueue.empty()); + EXPECT_EQ(emptyQueue.chainLength(), 0); + + emptyQueue.append(folly::IOBuf::copyBuffer("apple")); + checkConsistency(emptyQueue); + EXPECT_FALSE(emptyQueue.head.empty()); +} + +TEST(ChainedByteRangeHead, FromIobuf) { + auto buf = folly::IOBuf::copyBuffer(""); + buf->appendToChain(folly::IOBuf::copyBuffer("")); + buf->appendToChain(folly::IOBuf::copyBuffer("apple")); + buf->appendToChain(folly::IOBuf::copyBuffer("ball")); + buf->appendToChain(folly::IOBuf::copyBuffer("")); + buf->appendToChain(folly::IOBuf::copyBuffer("dog")); + buf->appendToChain(folly::IOBuf::copyBuffer("cat")); + + ChainedByteRangeHead chainedByteRangeHead(buf); + EXPECT_EQ(chainedByteRangeHead.chainLength(), 15); + EXPECT_EQ(chainedByteRangeHead.head.toStr(), "appleballdogcat"); +} + +TEST(ChainedByteRangeHead, FromIobufEmpty) { + auto buf = folly::IOBuf::copyBuffer(""); + ChainedByteRangeHead chainedByteRangeHead(buf); + EXPECT_TRUE(chainedByteRangeHead.empty()); +} + +TEST(ChainedByteRangeHead, TrimStart) { + auto cbr = std::make_unique( + folly::ByteRange(kHello->data(), kHello->length())); + cbr->trimStart(3); + EXPECT_EQ(cbr->toStr(), "lo"); +} + +TEST(ChainedByteRangeHead, SplitHeadFromChainOfOne) { + ChainedByteRangeHead queue; + queue.append(kHello); + checkConsistency(queue); + EXPECT_EQ(5, queue.head.computeChainDataLength()); + + auto prefix = queue.splitAtMost(3); + checkConsistency(queue); + EXPECT_EQ(3, prefix.head.computeChainDataLength()); + EXPECT_EQ(2, queue.head.computeChainDataLength()); + EXPECT_EQ(prefix.head.toStr(), "Hel"); + EXPECT_EQ(queue.head.toStr(), "lo"); +} + +TEST(ChainedByteRangeHead, MoveAssignmentOperator) { + ChainedByteRangeHead queue; + queue.append(kHello); + queue.append(kWorld); + + ChainedByteRangeHead queue2 = std::move(queue); + EXPECT_EQ(queue2.chainLength(), 10); + EXPECT_TRUE(queue.empty()); +} + +TEST(ChainedByteRangeHead, SplitHeadFromChainOfTwo) { + ChainedByteRangeHead queue; + queue.append(kHello); + queue.append(kWorld); + checkConsistency(queue); + EXPECT_EQ(10, queue.head.computeChainDataLength()); + + auto prefix = queue.splitAtMost(3); + checkConsistency(queue); + EXPECT_EQ(3, prefix.head.computeChainDataLength()); + EXPECT_EQ(7, queue.head.computeChainDataLength()); + EXPECT_EQ(prefix.head.toStr(), "Hel"); + EXPECT_EQ(queue.head.toStr(), "loWorld"); +} + +TEST(ChainedByteRangeHead, SplitOneAndHalfFromChainOfTwo) { + ChainedByteRangeHead queue; + queue.append(kHello); + queue.append(kWorld); + checkConsistency(queue); + EXPECT_EQ(10, queue.head.computeChainDataLength()); + + auto prefix = queue.splitAtMost(7); + checkConsistency(queue); + EXPECT_EQ(7, prefix.head.computeChainDataLength()); + EXPECT_EQ(3, queue.head.computeChainDataLength()); + EXPECT_EQ(prefix.head.toStr(), "HelloWo"); + EXPECT_EQ(queue.head.toStr(), "rld"); +} + +TEST(ChainedByteRangeHead, SplitOneAndHalfFromChainOfThree) { + ChainedByteRangeHead queue; + queue.append(kHello); + queue.append(kWorld); + queue.append(kHello); + checkConsistency(queue); + EXPECT_EQ(15, queue.head.computeChainDataLength()); + + auto prefix = queue.splitAtMost(7); + checkConsistency(queue); + EXPECT_EQ(7, prefix.head.computeChainDataLength()); + EXPECT_EQ(8, queue.head.computeChainDataLength()); + EXPECT_EQ(prefix.head.toStr(), "HelloWo"); + EXPECT_EQ(queue.head.toStr(), "rldHello"); +} + +TEST(ChainedByteRangeHead, SplitOneAndHalfFromChainOfFour) { + ChainedByteRangeHead queue; + queue.append(kHello); + queue.append(kWorld); + queue.append(kHello); + queue.append(kWorld); + checkConsistency(queue); + EXPECT_EQ(20, queue.head.computeChainDataLength()); + + auto prefix = queue.splitAtMost(7); + checkConsistency(queue); + EXPECT_EQ(7, prefix.head.computeChainDataLength()); + EXPECT_EQ(13, queue.head.computeChainDataLength()); + EXPECT_EQ(prefix.head.toStr(), "HelloWo"); + EXPECT_EQ(queue.head.toStr(), "rldHelloWorld"); +} + +TEST(ChainedByteRangeHead, SplitZero) { + ChainedByteRangeHead queue; + auto helloWorld = IOBuf::copyBuffer(SCL("Hello world")); + queue.append(helloWorld); + auto splitRch = queue.splitAtMost(0); + EXPECT_EQ(splitRch.head.computeChainDataLength(), 0); +} + +TEST(ChainedByteRangeHead, SplitEmpty) { + ChainedByteRangeHead queue; + auto splitRch = queue.splitAtMost(0); + EXPECT_EQ(splitRch.head.computeChainDataLength(), 0); +} + +TEST(ChainedByteRangeHead, SplitEmptt) { + ChainedByteRangeHead queue; + auto splitRch = queue.splitAtMost(1); + EXPECT_EQ(splitRch.head.computeChainDataLength(), 0); +} + +TEST(ChainedByteRangeHead, TrimStartAtMost) { + ChainedByteRangeHead queue; + queue.append(kHello); + auto prefixLen = queue.trimStartAtMost(3); + EXPECT_EQ(3, prefixLen); + EXPECT_EQ(2, queue.chainLength()); + checkConsistency(queue); + + prefixLen = queue.trimStartAtMost(2); + EXPECT_EQ(2, prefixLen); + EXPECT_EQ(0, queue.chainLength()); + checkConsistency(queue); + + queue.append(kHello); + queue.append(kWorld); + prefixLen = queue.trimStartAtMost(7); + EXPECT_EQ(7, prefixLen); + EXPECT_EQ(3, queue.chainLength()); + checkConsistency(queue); + + prefixLen = queue.trimStartAtMost(10); + EXPECT_EQ(3, prefixLen); + EXPECT_EQ(0, queue.chainLength()); + checkConsistency(queue); + + queue.append(kHello); + queue.append(kWorld); + + prefixLen = queue.trimStartAtMost(12); + EXPECT_EQ(10, prefixLen); + EXPECT_EQ(0, queue.chainLength()); + checkConsistency(queue); + + queue.append(kHello); + queue.append(kWorld); + queue.append(kHello); + + prefixLen = queue.trimStartAtMost(12); + EXPECT_EQ(12, prefixLen); + EXPECT_EQ(3, queue.chainLength()); + checkConsistency(queue); +} + +TEST(ChainedByteRangeHead, TrimStartOneByte) { + ChainedByteRangeHead queue; + auto h = IOBuf::copyBuffer(SCL("H")); + queue.append(h); + checkConsistency(queue); + queue.trimStartAtMost(1); + checkConsistency(queue); +} + +TEST(ChainedByteRangeHead, TrimStartClearChain) { + ChainedByteRangeHead queue; + constexpr string_view alphabet = "abcdefghijklmnopqrstuvwxyz"; + auto buf = IOBuf::copyBuffer(alphabet); + queue.append(buf); + queue.append(buf); + // validate chain length + const size_t expectedChainLength = alphabet.size() * 2; + EXPECT_EQ(queue.chainLength(), expectedChainLength); + checkConsistency(queue); + // attempt to trim more than chainLength + queue.trimStartAtMost(expectedChainLength + 1); + checkConsistency(queue); + EXPECT_TRUE(queue.empty()); + EXPECT_EQ(queue.chainLength(), 0); + EXPECT_TRUE(queue.head.empty()); +} + +TEST(ChainedByteRangeHead, TestEmptyWithMiddleEmptyBuffer) { + ChainedByteRangeHead queue1; + queue1.append(kHello); + ChainedByteRangeHead queue2; + queue1.append(std::move(queue2)); + queue1.append(kWorld); + EXPECT_FALSE(queue1.head.getNext()->empty()); +} + +TEST(ChainedByteRangeHead, TestMove) { + auto buf = folly::IOBuf::copyBuffer("corporate america"); + buf->appendToChain(folly::IOBuf::copyBuffer("apple")); + buf->appendToChain(folly::IOBuf::copyBuffer("ball")); + buf->appendToChain(folly::IOBuf::copyBuffer("dog")); + buf->appendToChain(folly::IOBuf::copyBuffer("cat")); + + ChainedByteRangeHead queue(std::move(buf)); + checkConsistency(queue); + + ChainedByteRangeHead queue2(std::move(queue)); + checkConsistency(queue2); +} + +TEST(ChainedByteRangeHead, TestSplitAtMostRemoveFirstChunk) { + auto buf = folly::IOBuf::copyBuffer("jars"); + buf->appendToChain(folly::IOBuf::copyBuffer("apple")); + buf->appendToChain(folly::IOBuf::copyBuffer("ball")); + + ChainedByteRangeHead queue(buf); + checkConsistency(queue); + + auto prefix = queue.splitAtMost(4); + EXPECT_EQ(prefix.chainLength(), 4); + EXPECT_EQ(queue.chainLength(), 9); +} + +TEST(ChainedByteRangeHead, TestSplitAtMostRemoveAllExceptLast) { + auto buf = folly::IOBuf::copyBuffer("jars"); + buf->appendToChain(folly::IOBuf::copyBuffer("apple")); + buf->appendToChain(folly::IOBuf::copyBuffer("ball")); + + ChainedByteRangeHead queue(buf); + checkConsistency(queue); + + auto prefix = queue.splitAtMost(9); + EXPECT_EQ(prefix.chainLength(), 9); + EXPECT_EQ(queue.chainLength(), 4); +}