From 08aaf4fb0701bfb17340548d0d722f6f2dda4a0a Mon Sep 17 00:00:00 2001 From: dgaston Date: Tue, 16 Apr 2024 12:01:30 -0400 Subject: [PATCH] AK: Add methods to BufferedStream to resize the user supplied buffer These changes allow lines of arbitrary length to be read with BufferedStream. When the user supplied buffer is smaller than the line, it will be resized to fit the line. When the internal buffer in BufferedStream is smaller than the line, it will be read into the user supplied buffer chunk by chunk with the buffer growing accordingly. Other behaviors match the behavior of the existing read_line method. --- AK/BufferedStream.h | 52 +++++++++++++++ Tests/AK/TestMemoryStream.cpp | 119 ++++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+) diff --git a/AK/BufferedStream.h b/AK/BufferedStream.h index 49c6df84ca3..3c72f45540a 100644 --- a/AK/BufferedStream.h +++ b/AK/BufferedStream.h @@ -115,6 +115,52 @@ public: return m_buffer.read(buffer); } + ErrorOr read_line_with_resize(ByteBuffer& buffer) + { + return StringView { TRY(read_until_with_resize(buffer, "\n"sv)) }; + } + + ErrorOr read_until_with_resize(ByteBuffer& buffer, StringView candidate) + { + return read_until_any_of_with_resize(buffer, Array { candidate }); + } + + template + ErrorOr read_until_any_of_with_resize(ByteBuffer& buffer, Array candidates) + { + if (!stream().is_open()) + return Error::from_errno(ENOTCONN); + + auto candidate = TRY(find_and_populate_until_any_of(candidates)); + + size_t bytes_read_to_user_buffer = 0; + while (!candidate.has_value()) { + if (m_buffer.used_space() == 0 && stream().is_eof()) { + // If we read to the very end of the buffered and unbuffered data, + // then treat the remainder as a full line (the last one), even if it + // doesn't end in the delimiter. + return buffer.span().trim(bytes_read_to_user_buffer); + } + + if (buffer.size() - bytes_read_to_user_buffer < m_buffer.used_space()) { + // Resize the user supplied buffer because it cannot fit + // the contents of m_buffer. + TRY(buffer.try_resize(buffer.size() + m_buffer.used_space())); + } + + // Read bytes into the buffer starting from the offset of how many bytes have previously been read. + bytes_read_to_user_buffer += m_buffer.read(buffer.span().slice(bytes_read_to_user_buffer)).size(); + candidate = TRY(find_and_populate_until_any_of(candidates)); + } + + // Once the candidate has been found, read the contents of m_buffer into the buffer, + // offset by how many bytes have already been read in. + TRY(buffer.try_resize(bytes_read_to_user_buffer + candidate->offset)); + m_buffer.read(buffer.span().slice(bytes_read_to_user_buffer)); + TRY(m_buffer.discard(candidate->size)); + return buffer.span(); + } + struct Match { size_t offset {}; size_t size {}; @@ -308,6 +354,12 @@ public: ErrorOr read_until_any_of(Bytes buffer, Array candidates) { return m_helper.read_until_any_of(move(buffer), move(candidates)); } ErrorOr can_read_up_to_delimiter(ReadonlyBytes delimiter) { return m_helper.can_read_up_to_delimiter(delimiter); } + // Methods for reading stream into an auto-adjusting buffer + ErrorOr read_line_with_resize(ByteBuffer& buffer) { return m_helper.read_line_with_resize(buffer); } + ErrorOr read_until_with_resize(ByteBuffer& buffer, StringView candidate) { return m_helper.read_until_with_resize(move(buffer), move(candidate)); } + template + ErrorOr read_until_any_of_with_resize(ByteBuffer& buffer, Array candidates) { return m_helper.read_until_any_of_with_resize(move(buffer), move(candidates)); } + size_t buffer_size() const { return m_helper.buffer_size(); } virtual ~InputBufferedSeekable() override = default; diff --git a/Tests/AK/TestMemoryStream.cpp b/Tests/AK/TestMemoryStream.cpp index 0b7784b4a5b..ce8aa90dcaa 100644 --- a/Tests/AK/TestMemoryStream.cpp +++ b/Tests/AK/TestMemoryStream.cpp @@ -298,3 +298,122 @@ TEST_CASE(buffered_memory_stream_read_line) EXPECT(read_or_error.is_error()); EXPECT_EQ(read_or_error.error().code(), EMSGSIZE); } + +TEST_CASE(buffered_memory_stream_read_line_with_resizing_where_stream_buffer_is_sufficient) +{ + auto array = Array {}; + + // The first line is 8 A's, the second line is 14 A's, two bytes are newline characters. + array.fill('A'); + array[8] = '\n'; + array[23] = '\n'; + + auto memory_stream = make(array.span(), FixedMemoryStream::Mode::ReadOnly); + + auto buffered_stream = TRY_OR_FAIL(InputBufferedSeekable::create(move(memory_stream), 64)); + + size_t initial_buffer_size = 4; + auto buffer = TRY_OR_FAIL(ByteBuffer::create_zeroed(initial_buffer_size)); + + auto read_bytes = TRY_OR_FAIL(buffered_stream->read_line_with_resize(buffer)); + + // The first line, which is 8 A's, should be read in. + EXPECT_EQ(read_bytes, "AAAAAAAA"sv); + + read_bytes = TRY_OR_FAIL(buffered_stream->read_line_with_resize(buffer)); + + // The second line, which is 14 A's, should be read in. + EXPECT_EQ(read_bytes, "AAAAAAAAAAAAAA"sv); + + // A resize should have happened because the user supplied buffer was too small. + EXPECT(buffer.size() > initial_buffer_size); + + // Reading from the stream again should return an empty StringView. + read_bytes = TRY_OR_FAIL(buffered_stream->read_line_with_resize(buffer)); + EXPECT(read_bytes.is_empty()); +} + +TEST_CASE(buffered_memory_stream_read_line_with_resizing_where_stream_buffer_is_not_sufficient) +{ + // This is the same as "buffered_memory_stream_read_line_with_resizing_where_stream_buffer_is_sufficient" + // but with a smaller stream buffer, meaning that the line must be read into the user supplied + // buffer in chunks. All assertions and invariants should remain unchanged. + auto array = Array {}; + + // The first line is 8 A's, the second line is 14 A's, two bytes are newline characters. + array.fill('A'); + array[8] = '\n'; + array[23] = '\n'; + + auto memory_stream = make(array.span(), FixedMemoryStream::Mode::ReadOnly); + + auto buffered_stream = TRY_OR_FAIL(InputBufferedSeekable::create(move(memory_stream), 6)); + + size_t initial_buffer_size = 4; + auto buffer = TRY_OR_FAIL(ByteBuffer::create_zeroed(initial_buffer_size)); + + auto read_bytes = TRY_OR_FAIL(buffered_stream->read_line_with_resize(buffer)); + + // The first line, which is 8 A's, should be read in. + EXPECT_EQ(read_bytes, "AAAAAAAA"sv); + + read_bytes = TRY_OR_FAIL(buffered_stream->read_line_with_resize(buffer)); + + // The second line, which is 14 A's, should be read in. + EXPECT_EQ(read_bytes, "AAAAAAAAAAAAAA"sv); + + // A resize should have happened because the user supplied buffer was too small. + EXPECT(buffer.size() > initial_buffer_size); + + // Reading from the stream again should return an empty StringView. + read_bytes = TRY_OR_FAIL(buffered_stream->read_line_with_resize(buffer)); + EXPECT(read_bytes.is_empty()); +} + +TEST_CASE(buffered_memory_stream_read_line_with_resizing_with_no_newline_where_stream_buffer_is_sufficient) +{ + auto array = Array {}; + + array.fill('A'); + + auto memory_stream = make(array.span(), FixedMemoryStream::Mode::ReadOnly); + + auto buffered_stream = TRY_OR_FAIL(InputBufferedSeekable::create(move(memory_stream), 64)); + + size_t initial_buffer_size = 4; + auto buffer = TRY_OR_FAIL(ByteBuffer::create_zeroed(initial_buffer_size)); + + auto read_bytes = TRY_OR_FAIL(buffered_stream->read_line_with_resize(buffer)); + + // All the contents of the buffer should have been read in. + EXPECT_EQ(read_bytes.length(), array.size()); + + // Reading from the stream again should return an empty StringView. + read_bytes = TRY_OR_FAIL(buffered_stream->read_line_with_resize(buffer)); + EXPECT(read_bytes.is_empty()); +} + +TEST_CASE(buffered_memory_stream_read_line_with_resizing_with_no_newline_where_stream_buffer_is_not_sufficient) +{ + // This should behave as buffered_memory_stream_read_line_with_resizing_with_no_newline_where_stream_buffer_is_sufficient + // but the internal buffer of the stream must be copied over in chunks. + auto array = Array {}; + + array.fill('A'); + + auto memory_stream = make(array.span(), FixedMemoryStream::Mode::ReadOnly); + + auto buffered_stream = TRY_OR_FAIL(InputBufferedSeekable::create(move(memory_stream), 6)); + + size_t initial_buffer_size = 4; + auto buffer = TRY_OR_FAIL(ByteBuffer::create_zeroed(initial_buffer_size)); + + auto read_bytes = TRY_OR_FAIL(buffered_stream->read_line_with_resize(buffer)); + + // All the contents of the buffer should have been read in. + EXPECT_EQ(read_bytes.length(), array.size()); + + // Reading from the stream again should return an empty StringView. + read_bytes = TRY_OR_FAIL(buffered_stream->read_line_with_resize(buffer)); + EXPECT(read_bytes.is_empty()); +}