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.
This commit is contained in:
dgaston 2024-04-16 12:01:30 -04:00 committed by Tim Schumacher
parent 4db1712f90
commit 08aaf4fb07
Notes: sideshowbarker 2024-07-16 22:22:13 +09:00
2 changed files with 171 additions and 0 deletions

View file

@ -115,6 +115,52 @@ public:
return m_buffer.read(buffer);
}
ErrorOr<StringView> read_line_with_resize(ByteBuffer& buffer)
{
return StringView { TRY(read_until_with_resize(buffer, "\n"sv)) };
}
ErrorOr<Bytes> read_until_with_resize(ByteBuffer& buffer, StringView candidate)
{
return read_until_any_of_with_resize(buffer, Array { candidate });
}
template<size_t N>
ErrorOr<Bytes> read_until_any_of_with_resize(ByteBuffer& buffer, Array<StringView, N> 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<Bytes> read_until_any_of(Bytes buffer, Array<StringView, N> candidates) { return m_helper.read_until_any_of(move(buffer), move(candidates)); }
ErrorOr<bool> 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<StringView> read_line_with_resize(ByteBuffer& buffer) { return m_helper.read_line_with_resize(buffer); }
ErrorOr<Bytes> read_until_with_resize(ByteBuffer& buffer, StringView candidate) { return m_helper.read_until_with_resize(move(buffer), move(candidate)); }
template<size_t N>
ErrorOr<Bytes> read_until_any_of_with_resize(ByteBuffer& buffer, Array<StringView, N> 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;

View file

@ -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<u8, 24> {};
// 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<FixedMemoryStream>(array.span(), FixedMemoryStream::Mode::ReadOnly);
auto buffered_stream = TRY_OR_FAIL(InputBufferedSeekable<FixedMemoryStream>::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<u8, 24> {};
// 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<FixedMemoryStream>(array.span(), FixedMemoryStream::Mode::ReadOnly);
auto buffered_stream = TRY_OR_FAIL(InputBufferedSeekable<FixedMemoryStream>::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<u8, 24> {};
array.fill('A');
auto memory_stream = make<FixedMemoryStream>(array.span(), FixedMemoryStream::Mode::ReadOnly);
auto buffered_stream = TRY_OR_FAIL(InputBufferedSeekable<FixedMemoryStream>::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<u8, 24> {};
array.fill('A');
auto memory_stream = make<FixedMemoryStream>(array.span(), FixedMemoryStream::Mode::ReadOnly);
auto buffered_stream = TRY_OR_FAIL(InputBufferedSeekable<FixedMemoryStream>::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());
}