From 85f54c62daf1eafcaf1d1ce1eed4b827a5f014f5 Mon Sep 17 00:00:00 2001 From: altman08 <936396749@qq.com> Date: Wed, 28 Oct 2026 14:46:53 +0800 Subject: [PATCH] Add non-consuming fetch and buffer access to IOBufCutter --- src/butil/iobuf.h | 14 +++++ src/butil/iobuf_inl.h | 36 ++++++++++++ test/iobuf_unittest.cpp | 127 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 177 insertions(+) diff --git a/src/butil/iobuf.h b/src/butil/iobuf.h index 724ee02d2e..726b38b9ae 100644 --- a/src/butil/iobuf.h +++ b/src/butil/iobuf.h @@ -534,6 +534,20 @@ class IOBufCutter { // Returns pointer to the character, nullptr on empty const void* fetch1(); + // Peek the unconsumed part of the current reference without copying. + // Load the next reference if needed; return empty on exhausted input. + StringPiece front_block(); + + // Peek n bytes without consuming them. Return a pointer into the backing + // block when contiguous, otherwise copy into aux_buffer (at least n bytes). + // Return nullptr on insufficient data, leaving aux_buffer unchanged. + // For n == 0, return aux_buffer without accessing the buffer. + const void* fetch(void* aux_buffer, size_t n) const; + + // Access the underlying IOBuf without copying or synchronizing the cursor. + // Its front block may still include bytes already consumed by this cutter. + const IOBuf& buf() const { return *_buf; } + // Pop n bytes from front side // Returns bytes popped. size_t pop_front(size_t n); diff --git a/src/butil/iobuf_inl.h b/src/butil/iobuf_inl.h index 71a449d282..578242813c 100644 --- a/src/butil/iobuf_inl.h +++ b/src/butil/iobuf_inl.h @@ -241,6 +241,42 @@ inline const void* IOBufCutter::fetch1() { return _data; } +inline StringPiece IOBufCutter::front_block() { + const char* data = static_cast(fetch1()); + if (data == nullptr) { + return StringPiece(); + } + const char* end = static_cast(_data_end); + return StringPiece(data, static_cast(end - data)); +} + +inline const void* IOBufCutter::fetch(void* aux_buffer, size_t n) const { + if (n == 0) { + return aux_buffer; + } + if (_block == nullptr) { + return _buf->fetch(aux_buffer, n); + } + const size_t size = (const char*)_data_end - (const char*)_data; + if (n <= size) { + return _data; + } + if (n > remaining_bytes()) { + return nullptr; + } + // The exhausted front block has not been removed yet. Peeking into the + // next block must also remain zero-copy when all n bytes fit there. + if (size == 0) { + const StringPiece next = _buf->backing_block(1); + if (n <= next.size()) { + return next.data(); + } + } + const size_t offset = _buf->_front_ref().length - size; + _buf->copy_to(aux_buffer, n, offset); + return aux_buffer; +} + inline size_t IOBufCutter::copy_to(void* out, size_t n) { size_t size = (char*)_data_end - (char*)_data; if (n <= size) { diff --git a/test/iobuf_unittest.cpp b/test/iobuf_unittest.cpp index 9f9cc38722..50af539f28 100644 --- a/test/iobuf_unittest.cpp +++ b/test/iobuf_unittest.cpp @@ -2030,6 +2030,133 @@ static void append_as_separate_blocks(butil::IOBuf* buf, } } +TEST_F(IOBufTest, cutter_front_block_empty_and_append) { + butil::IOBuf buf; + butil::IOBufCutter cutter(&buf); + ASSERT_TRUE(cutter.front_block().empty()); + ASSERT_TRUE(cutter.front_block().empty()); + ASSERT_EQ(0u, cutter.remaining_bytes()); + + append_as_separate_blocks(&buf, "abc", 3); + ASSERT_EQ(butil::StringPiece("abc"), cutter.front_block()); + ASSERT_EQ(buf.backing_block(0).data(), cutter.front_block().data()); + ASSERT_EQ(3u, cutter.remaining_bytes()); + ASSERT_EQ(3u, cutter.pop_front(3)); + + // Append before the exhausted reference has been removed. + append_as_separate_blocks(&buf, "def", 3); + const char* next = buf.backing_block(1).data(); + ASSERT_EQ(butil::StringPiece("def"), cutter.front_block()); + ASSERT_EQ(next, cutter.front_block().data()); + ASSERT_EQ(3u, cutter.remaining_bytes()); + ASSERT_EQ(3u, cutter.pop_front(3)); + ASSERT_TRUE(cutter.front_block().empty()); + ASSERT_TRUE(cutter.front_block().empty()); + ASSERT_EQ(0u, cutter.remaining_bytes()); + + // Appending also works after front_block() has observed empty input. + append_as_separate_blocks(&buf, "ghi", 3); + ASSERT_EQ(butil::StringPiece("ghi"), cutter.front_block()); + ASSERT_EQ(buf.backing_block(0).data(), cutter.front_block().data()); + ASSERT_EQ(3u, cutter.remaining_bytes()); +} + +TEST_F(IOBufTest, cutter_front_block_partial_and_next) { + butil::IOBuf buf; + append_as_separate_blocks(&buf, "abcdefghi", 3); + const butil::StringPiece blocks[] = { + buf.backing_block(0), buf.backing_block(1), buf.backing_block(2) + }; + butil::IOBufCutter cutter(&buf); + for (size_t i = 0; i < arraysize(blocks); ++i) { + ASSERT_EQ(blocks[i], cutter.front_block()); + ASSERT_EQ(blocks[i].data(), cutter.front_block().data()); + ASSERT_EQ(9u - i * 3, cutter.remaining_bytes()); + + ASSERT_EQ(1u, cutter.pop_front(1)); + const butil::StringPiece rest(blocks[i].data() + 1, 2); + ASSERT_EQ(rest, cutter.front_block()); + ASSERT_EQ(rest.data(), cutter.front_block().data()); + ASSERT_EQ(8u - i * 3, cutter.remaining_bytes()); + std::string consumed; + ASSERT_EQ(2u, cutter.cutn(&consumed, 2)); + ASSERT_EQ(rest, butil::StringPiece(consumed)); + } + ASSERT_TRUE(cutter.front_block().empty()); + ASSERT_EQ(0u, cutter.remaining_bytes()); + ASSERT_TRUE(buf.empty()); +} + +TEST_F(IOBufTest, cutter_fetch_empty_and_append) { + butil::IOBuf buf; + { + const butil::IOBufCutter cutter(&buf); + char aux[4] = "xyz"; + ASSERT_EQ(&buf, &cutter.buf()); + ASSERT_EQ(aux, cutter.fetch(aux, 0)); + ASSERT_EQ(nullptr, cutter.fetch(nullptr, 0)); + ASSERT_EQ(nullptr, cutter.fetch(aux, 1)); + ASSERT_STREQ("xyz", aux); + + ASSERT_EQ(0, buf.append("abc")); + ASSERT_EQ(buf.backing_block(0).data(), cutter.fetch(aux, 3)); + ASSERT_EQ(nullptr, cutter.fetch(aux, 4)); + ASSERT_STREQ("xyz", aux); + ASSERT_EQ(3u, cutter.remaining_bytes()); + } + ASSERT_EQ("abc", buf.to_string()); +} + +TEST_F(IOBufTest, cutter_fetch_at_every_offset) { + const std::string payload = "abcdefghijkl"; + const size_t chunk = 3; + for (size_t offset = 0; offset <= payload.size(); ++offset) { + butil::IOBuf buf; + append_as_separate_blocks(&buf, payload, chunk); + { + butil::IOBufCutter cutter(&buf); + ASSERT_EQ(offset, cutter.pop_front(offset)); + const butil::IOBufCutter& reader = cutter; + const std::string underlying = buf.to_string(); + const size_t block_count = buf.backing_block_num(); + const size_t remaining = payload.size() - offset; + for (size_t n = 0; n <= remaining + 1; ++n) { + char aux[16]; + memset(aux, '#', sizeof(aux)); + const void* data = reader.fetch(aux, n); + if (n == 0) { + ASSERT_EQ(aux, data); + } else if (n > remaining) { + ASSERT_EQ(nullptr, data); + } else { + ASSERT_NE(nullptr, data); + ASSERT_EQ(payload.substr(offset, n), + std::string(static_cast(data), n)); + if (n <= chunk - offset % chunk) { + const size_t index = offset - (payload.size() - buf.size()); + const butil::StringPiece block = buf.backing_block(index / chunk); + ASSERT_EQ(block.data() + index % chunk, data); + } else { + ASSERT_EQ(aux, data); + } + } + if (data != aux || n == 0) { + ASSERT_EQ(std::string(sizeof(aux), '#'), + std::string(aux, sizeof(aux))); + } + ASSERT_EQ(remaining, reader.remaining_bytes()); + ASSERT_EQ(&buf, &reader.buf()); + ASSERT_EQ(underlying, reader.buf().to_string()); + ASSERT_EQ(block_count, reader.buf().backing_block_num()); + } + std::string rest; + ASSERT_EQ(remaining, cutter.cutn(&rest, remaining)); + ASSERT_EQ(payload.substr(offset), rest); + } + ASSERT_TRUE(buf.empty()); + } +} + TEST_F(IOBufTest, as_input_stream_multi_block_read) { butil::IOBuf buf; const std::string payload = "the quick brown fox jumps over the lazy dog";