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()); +}