1  
//
1  
//
2  
// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com)
2  
// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com)
3  
//
3  
//
4  
// Distributed under the Boost Software License, Version 1.0. (See accompanying
4  
// Distributed under the Boost Software License, Version 1.0. (See accompanying
5  
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
5  
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6  
//
6  
//
7  
// Official repository: https://github.com/cppalliance/capy
7  
// Official repository: https://github.com/cppalliance/capy
8  
//
8  
//
9  

9  

10  
#ifndef BOOST_CAPY_READ_UNTIL_HPP
10  
#ifndef BOOST_CAPY_READ_UNTIL_HPP
11  
#define BOOST_CAPY_READ_UNTIL_HPP
11  
#define BOOST_CAPY_READ_UNTIL_HPP
12  

12  

13  
#include <boost/capy/detail/config.hpp>
13  
#include <boost/capy/detail/config.hpp>
14  
#include <boost/capy/buffers.hpp>
14  
#include <boost/capy/buffers.hpp>
15  
#include <boost/capy/cond.hpp>
15  
#include <boost/capy/cond.hpp>
16  
#include <boost/capy/coro.hpp>
16  
#include <boost/capy/coro.hpp>
17  
#include <boost/capy/error.hpp>
17  
#include <boost/capy/error.hpp>
18  
#include <boost/capy/io_result.hpp>
18  
#include <boost/capy/io_result.hpp>
19  
#include <boost/capy/io_task.hpp>
19  
#include <boost/capy/io_task.hpp>
20  
#include <boost/capy/concept/dynamic_buffer.hpp>
20  
#include <boost/capy/concept/dynamic_buffer.hpp>
21  
#include <boost/capy/concept/match_condition.hpp>
21  
#include <boost/capy/concept/match_condition.hpp>
22  
#include <boost/capy/concept/read_stream.hpp>
22  
#include <boost/capy/concept/read_stream.hpp>
23  
#include <boost/capy/ex/executor_ref.hpp>
23  
#include <boost/capy/ex/executor_ref.hpp>
24  

24  

25  
#include <algorithm>
25  
#include <algorithm>
26  
#include <cstddef>
26  
#include <cstddef>
27  
#include <optional>
27  
#include <optional>
28  
#include <stop_token>
28  
#include <stop_token>
29  
#include <string_view>
29  
#include <string_view>
30  
#include <type_traits>
30  
#include <type_traits>
31  

31  

32  
namespace boost {
32  
namespace boost {
33  
namespace capy {
33  
namespace capy {
34  

34  

35  
namespace detail {
35  
namespace detail {
36  

36  

37  
// Linearize a buffer sequence into a string
37  
// Linearize a buffer sequence into a string
38  
inline
38  
inline
39  
std::string
39  
std::string
40  
linearize_buffers(ConstBufferSequence auto const& data)
40  
linearize_buffers(ConstBufferSequence auto const& data)
41  
{
41  
{
42  
    std::string linear;
42  
    std::string linear;
43  
    linear.reserve(buffer_size(data));
43  
    linear.reserve(buffer_size(data));
44  
    auto const end_ = end(data);
44  
    auto const end_ = end(data);
45  
    for(auto it = begin(data); it != end_; ++it)
45  
    for(auto it = begin(data); it != end_; ++it)
46  
        linear.append(
46  
        linear.append(
47  
            static_cast<char const*>(it->data()),
47  
            static_cast<char const*>(it->data()),
48  
            it->size());
48  
            it->size());
49  
    return linear;
49  
    return linear;
50  
}
50  
}
51  

51  

52  
// Search buffer using a MatchCondition, with single-buffer optimization
52  
// Search buffer using a MatchCondition, with single-buffer optimization
53  
template<MatchCondition M>
53  
template<MatchCondition M>
54  
std::size_t
54  
std::size_t
55  
search_buffer_for_match(
55  
search_buffer_for_match(
56  
    ConstBufferSequence auto const& data,
56  
    ConstBufferSequence auto const& data,
57  
    M const& match,
57  
    M const& match,
58  
    std::size_t* hint = nullptr)
58  
    std::size_t* hint = nullptr)
59  
{
59  
{
60  
    // Fast path: single buffer - no linearization needed
60  
    // Fast path: single buffer - no linearization needed
61  
    if(buffer_length(data) == 1)
61  
    if(buffer_length(data) == 1)
62  
    {
62  
    {
63  
        auto const& buf = *begin(data);
63  
        auto const& buf = *begin(data);
64  
        return match(std::string_view(
64  
        return match(std::string_view(
65  
            static_cast<char const*>(buf.data()),
65  
            static_cast<char const*>(buf.data()),
66  
            buf.size()), hint);
66  
            buf.size()), hint);
67  
    }
67  
    }
68  
    // Multiple buffers - linearize
68  
    // Multiple buffers - linearize
69  
    return match(linearize_buffers(data), hint);
69  
    return match(linearize_buffers(data), hint);
70  
}
70  
}
71  

71  

72  
// Implementation coroutine for read_until with MatchCondition
72  
// Implementation coroutine for read_until with MatchCondition
73  
template<class Stream, class B, MatchCondition M>
73  
template<class Stream, class B, MatchCondition M>
74  
io_task<std::size_t>
74  
io_task<std::size_t>
75  
read_until_match_impl(
75  
read_until_match_impl(
76  
    Stream& stream,
76  
    Stream& stream,
77  
    B& buffers,
77  
    B& buffers,
78  
    M match,
78  
    M match,
79  
    std::size_t initial_amount)
79  
    std::size_t initial_amount)
80  
{
80  
{
81  
    std::size_t amount = initial_amount;
81  
    std::size_t amount = initial_amount;
82  

82  

83  
    for(;;)
83  
    for(;;)
84  
    {
84  
    {
85  
        // Check max_size before preparing
85  
        // Check max_size before preparing
86  
        if(buffers.size() >= buffers.max_size())
86  
        if(buffers.size() >= buffers.max_size())
87  
            co_return {error::not_found, 0};
87  
            co_return {error::not_found, 0};
88  

88  

89  
        // Prepare space, respecting max_size
89  
        // Prepare space, respecting max_size
90  
        std::size_t const available = buffers.max_size() - buffers.size();
90  
        std::size_t const available = buffers.max_size() - buffers.size();
91  
        std::size_t const to_prepare = (std::min)(amount, available);
91  
        std::size_t const to_prepare = (std::min)(amount, available);
92  
        if(to_prepare == 0)
92  
        if(to_prepare == 0)
93  
            co_return {error::not_found, 0};
93  
            co_return {error::not_found, 0};
94  

94  

95  
        auto mb = buffers.prepare(to_prepare);
95  
        auto mb = buffers.prepare(to_prepare);
96  
        auto [ec, n] = co_await stream.read_some(mb);
96  
        auto [ec, n] = co_await stream.read_some(mb);
97  
        buffers.commit(n);
97  
        buffers.commit(n);
98  

98  

99  
        if(n > 0)
99  
        if(n > 0)
100  
        {
100  
        {
101  
            auto pos = search_buffer_for_match(buffers.data(), match);
101  
            auto pos = search_buffer_for_match(buffers.data(), match);
102  
            if(pos != std::string_view::npos)
102  
            if(pos != std::string_view::npos)
103  
                co_return {{}, pos};
103  
                co_return {{}, pos};
104  
        }
104  
        }
105  

105  

106  
        if(ec == cond::eof)
106  
        if(ec == cond::eof)
107  
            co_return {error::eof, buffers.size()};
107  
            co_return {error::eof, buffers.size()};
108  
        if(ec)
108  
        if(ec)
109  
            co_return {ec, buffers.size()};
109  
            co_return {ec, buffers.size()};
110  

110  

111  
        // Grow buffer size for next iteration
111  
        // Grow buffer size for next iteration
112  
        if(n == buffer_size(mb))
112  
        if(n == buffer_size(mb))
113  
            amount = amount / 2 + amount;
113  
            amount = amount / 2 + amount;
114  
    }
114  
    }
115  
}
115  
}
116  

116  

117  
template<class Stream, class B, MatchCondition M, bool OwnsBuffer>
117  
template<class Stream, class B, MatchCondition M, bool OwnsBuffer>
118  
struct read_until_awaitable
118  
struct read_until_awaitable
119  
{
119  
{
120  
    Stream* stream_;
120  
    Stream* stream_;
121  
    M match_;
121  
    M match_;
122  
    std::size_t initial_amount_;
122  
    std::size_t initial_amount_;
123  
    std::optional<io_result<std::size_t>> immediate_;
123  
    std::optional<io_result<std::size_t>> immediate_;
124  
    std::optional<io_task<std::size_t>> inner_;
124  
    std::optional<io_task<std::size_t>> inner_;
125  

125  

126  
    using storage_type = std::conditional_t<OwnsBuffer, B, B*>;
126  
    using storage_type = std::conditional_t<OwnsBuffer, B, B*>;
127  
    storage_type buffers_storage_;
127  
    storage_type buffers_storage_;
128  

128  

129  
    B& buffers() noexcept
129  
    B& buffers() noexcept
130  
    {
130  
    {
131  
        if constexpr(OwnsBuffer)
131  
        if constexpr(OwnsBuffer)
132  
            return buffers_storage_;
132  
            return buffers_storage_;
133  
        else
133  
        else
134  
            return *buffers_storage_;
134  
            return *buffers_storage_;
135  
    }
135  
    }
136  

136  

137  
    // Constructor for lvalue (pointer storage)
137  
    // Constructor for lvalue (pointer storage)
138  
    read_until_awaitable(
138  
    read_until_awaitable(
139  
        Stream& stream,
139  
        Stream& stream,
140  
        B* buffers,
140  
        B* buffers,
141  
        M match,
141  
        M match,
142  
        std::size_t initial_amount)
142  
        std::size_t initial_amount)
143  
        requires (!OwnsBuffer)
143  
        requires (!OwnsBuffer)
144  
        : stream_(std::addressof(stream))
144  
        : stream_(std::addressof(stream))
145  
        , match_(std::move(match))
145  
        , match_(std::move(match))
146  
        , initial_amount_(initial_amount)
146  
        , initial_amount_(initial_amount)
147  
        , buffers_storage_(buffers)
147  
        , buffers_storage_(buffers)
148  
    {
148  
    {
149  
        auto pos = search_buffer_for_match(
149  
        auto pos = search_buffer_for_match(
150  
            buffers_storage_->data(), match_);
150  
            buffers_storage_->data(), match_);
151  
        if(pos != std::string_view::npos)
151  
        if(pos != std::string_view::npos)
152  
            immediate_.emplace(io_result<std::size_t>{{}, pos});
152  
            immediate_.emplace(io_result<std::size_t>{{}, pos});
153  
    }
153  
    }
154  

154  

155  
    // Constructor for rvalue adapter (owned storage)
155  
    // Constructor for rvalue adapter (owned storage)
156  
    read_until_awaitable(
156  
    read_until_awaitable(
157  
        Stream& stream,
157  
        Stream& stream,
158  
        B&& buffers,
158  
        B&& buffers,
159  
        M match,
159  
        M match,
160  
        std::size_t initial_amount)
160  
        std::size_t initial_amount)
161  
        requires OwnsBuffer
161  
        requires OwnsBuffer
162  
        : stream_(std::addressof(stream))
162  
        : stream_(std::addressof(stream))
163  
        , match_(std::move(match))
163  
        , match_(std::move(match))
164  
        , initial_amount_(initial_amount)
164  
        , initial_amount_(initial_amount)
165  
        , buffers_storage_(std::move(buffers))
165  
        , buffers_storage_(std::move(buffers))
166  
    {
166  
    {
167  
        auto pos = search_buffer_for_match(
167  
        auto pos = search_buffer_for_match(
168  
            buffers_storage_.data(), match_);
168  
            buffers_storage_.data(), match_);
169  
        if(pos != std::string_view::npos)
169  
        if(pos != std::string_view::npos)
170  
            immediate_.emplace(io_result<std::size_t>{{}, pos});
170  
            immediate_.emplace(io_result<std::size_t>{{}, pos});
171  
    }
171  
    }
172  

172  

173  
    bool
173  
    bool
174  
    await_ready() const noexcept
174  
    await_ready() const noexcept
175  
    {
175  
    {
176  
        return immediate_.has_value();
176  
        return immediate_.has_value();
177  
    }
177  
    }
178  

178  

179  
    coro
179  
    coro
180  
    await_suspend(coro h, executor_ref ex, std::stop_token token)
180  
    await_suspend(coro h, executor_ref ex, std::stop_token token)
181  
    {
181  
    {
182  
        inner_.emplace(read_until_match_impl(
182  
        inner_.emplace(read_until_match_impl(
183  
            *stream_, buffers(), match_, initial_amount_));
183  
            *stream_, buffers(), match_, initial_amount_));
184  
        return inner_->await_suspend(h, ex, token);
184  
        return inner_->await_suspend(h, ex, token);
185  
    }
185  
    }
186  

186  

187  
    io_result<std::size_t>
187  
    io_result<std::size_t>
188  
    await_resume()
188  
    await_resume()
189  
    {
189  
    {
190  
        if(immediate_)
190  
        if(immediate_)
191  
            return *immediate_;
191  
            return *immediate_;
192  
        return inner_->await_resume();
192  
        return inner_->await_resume();
193  
    }
193  
    }
194  
};
194  
};
195  

195  

196  
} // namespace detail
196  
} // namespace detail
197  

197  

198  
/** Match condition that searches for a delimiter string.
198  
/** Match condition that searches for a delimiter string.
199  

199  

200  
    Satisfies @ref MatchCondition. Returns the position after the
200  
    Satisfies @ref MatchCondition. Returns the position after the
201  
    delimiter when found, or `npos` otherwise. Provides an overlap
201  
    delimiter when found, or `npos` otherwise. Provides an overlap
202  
    hint of `delim.size() - 1` to handle delimiters spanning reads.
202  
    hint of `delim.size() - 1` to handle delimiters spanning reads.
203  

203  

204  
    @see MatchCondition, read_until
204  
    @see MatchCondition, read_until
205  
*/
205  
*/
206  
struct match_delim
206  
struct match_delim
207  
{
207  
{
208  
    std::string_view delim;
208  
    std::string_view delim;
209  

209  

210  
    std::size_t
210  
    std::size_t
211  
    operator()(
211  
    operator()(
212  
        std::string_view data,
212  
        std::string_view data,
213  
        std::size_t* hint) const noexcept
213  
        std::size_t* hint) const noexcept
214  
    {
214  
    {
215  
        if(delim.empty())
215  
        if(delim.empty())
216  
            return 0;
216  
            return 0;
217  
        auto pos = data.find(delim);
217  
        auto pos = data.find(delim);
218  
        if(pos != std::string_view::npos)
218  
        if(pos != std::string_view::npos)
219  
            return pos + delim.size();
219  
            return pos + delim.size();
220  
        if(hint)
220  
        if(hint)
221  
            *hint = delim.size() > 1 ? delim.size() - 1 : 0;
221  
            *hint = delim.size() > 1 ? delim.size() - 1 : 0;
222  
        return std::string_view::npos;
222  
        return std::string_view::npos;
223  
    }
223  
    }
224  
};
224  
};
225  

225  

226  
/** Asynchronously read until a match condition is satisfied.
226  
/** Asynchronously read until a match condition is satisfied.
227  

227  

228  
    Reads data from the stream into the dynamic buffer until the match
228  
    Reads data from the stream into the dynamic buffer until the match
229  
    condition returns a valid position. Implemented using `read_some`.
229  
    condition returns a valid position. Implemented using `read_some`.
230  
    If the match condition is already satisfied by existing buffer
230  
    If the match condition is already satisfied by existing buffer
231  
    data, returns immediately without I/O.
231  
    data, returns immediately without I/O.
232  

232  

233  
    @li The operation completes when:
233  
    @li The operation completes when:
234  
    @li The match condition returns a valid position
234  
    @li The match condition returns a valid position
235  
    @li End-of-stream is reached (`cond::eof`)
235  
    @li End-of-stream is reached (`cond::eof`)
236  
    @li The buffer's `max_size()` is reached (`cond::not_found`)
236  
    @li The buffer's `max_size()` is reached (`cond::not_found`)
237  
    @li An error occurs
237  
    @li An error occurs
238  
    @li The operation is cancelled
238  
    @li The operation is cancelled
239  

239  

240  
    @par Cancellation
240  
    @par Cancellation
241  
    Supports cancellation via `stop_token` propagated through the
241  
    Supports cancellation via `stop_token` propagated through the
242  
    IoAwaitable protocol. When cancelled, returns with `cond::canceled`.
242  
    IoAwaitable protocol. When cancelled, returns with `cond::canceled`.
243  

243  

244  
    @param stream The stream to read from. The caller retains ownership.
244  
    @param stream The stream to read from. The caller retains ownership.
245  
    @param buffers The dynamic buffer to append data to. Must remain
245  
    @param buffers The dynamic buffer to append data to. Must remain
246  
        valid until the operation completes.
246  
        valid until the operation completes.
247  
    @param match The match condition callable. Copied into the awaitable.
247  
    @param match The match condition callable. Copied into the awaitable.
248  
    @param initial_amount Initial bytes to read per iteration (default
248  
    @param initial_amount Initial bytes to read per iteration (default
249  
        2048). Grows by 1.5x when filled.
249  
        2048). Grows by 1.5x when filled.
250  

250  

251  
    @return An awaitable yielding `(error_code, std::size_t)`.
251  
    @return An awaitable yielding `(error_code, std::size_t)`.
252  
        On success, `n` is the position returned by the match condition
252  
        On success, `n` is the position returned by the match condition
253  
        (bytes up to and including the matched delimiter). Compare error
253  
        (bytes up to and including the matched delimiter). Compare error
254  
        codes to conditions:
254  
        codes to conditions:
255  
        @li `cond::eof` - EOF before match; `n` is buffer size
255  
        @li `cond::eof` - EOF before match; `n` is buffer size
256  
        @li `cond::not_found` - `max_size()` reached before match
256  
        @li `cond::not_found` - `max_size()` reached before match
257  
        @li `cond::canceled` - Operation was cancelled
257  
        @li `cond::canceled` - Operation was cancelled
258  

258  

259  
    @par Example
259  
    @par Example
260  

260  

261  
    @code
261  
    @code
262  
    task<> read_http_header( ReadStream auto& stream )
262  
    task<> read_http_header( ReadStream auto& stream )
263  
    {
263  
    {
264  
        std::string header;
264  
        std::string header;
265  
        auto [ec, n] = co_await read_until(
265  
        auto [ec, n] = co_await read_until(
266  
            stream,
266  
            stream,
267  
            string_dynamic_buffer( &header ),
267  
            string_dynamic_buffer( &header ),
268  
            []( std::string_view data, std::size_t* hint ) {
268  
            []( std::string_view data, std::size_t* hint ) {
269  
                auto pos = data.find( "\r\n\r\n" );
269  
                auto pos = data.find( "\r\n\r\n" );
270  
                if( pos != std::string_view::npos )
270  
                if( pos != std::string_view::npos )
271  
                    return pos + 4;
271  
                    return pos + 4;
272  
                if( hint )
272  
                if( hint )
273  
                    *hint = 3;  // partial "\r\n\r" possible
273  
                    *hint = 3;  // partial "\r\n\r" possible
274  
                return std::string_view::npos;
274  
                return std::string_view::npos;
275  
            } );
275  
            } );
276  
        if( ec.failed() )
276  
        if( ec.failed() )
277  
            detail::throw_system_error( ec );
277  
            detail::throw_system_error( ec );
278  
        // header contains data through "\r\n\r\n"
278  
        // header contains data through "\r\n\r\n"
279  
    }
279  
    }
280  
    @endcode
280  
    @endcode
281  

281  

282  
    @see read_some, MatchCondition, DynamicBufferParam
282  
    @see read_some, MatchCondition, DynamicBufferParam
283  
*/
283  
*/
284  
template<ReadStream Stream, class B, MatchCondition M>
284  
template<ReadStream Stream, class B, MatchCondition M>
285  
    requires DynamicBufferParam<B&&>
285  
    requires DynamicBufferParam<B&&>
286  
auto
286  
auto
287  
read_until(
287  
read_until(
288  
    Stream& stream,
288  
    Stream& stream,
289  
    B&& buffers,
289  
    B&& buffers,
290  
    M match,
290  
    M match,
291  
    std::size_t initial_amount = 2048)
291  
    std::size_t initial_amount = 2048)
292  
{
292  
{
293  
    constexpr bool is_lvalue = std::is_lvalue_reference_v<B&&>;
293  
    constexpr bool is_lvalue = std::is_lvalue_reference_v<B&&>;
294  
    using BareB = std::remove_reference_t<B>;
294  
    using BareB = std::remove_reference_t<B>;
295  

295  

296  
    if constexpr(is_lvalue)
296  
    if constexpr(is_lvalue)
297  
        return detail::read_until_awaitable<Stream, BareB, M, false>(
297  
        return detail::read_until_awaitable<Stream, BareB, M, false>(
298  
            stream, std::addressof(buffers), std::move(match), initial_amount);
298  
            stream, std::addressof(buffers), std::move(match), initial_amount);
299  
    else
299  
    else
300  
        return detail::read_until_awaitable<Stream, BareB, M, true>(
300  
        return detail::read_until_awaitable<Stream, BareB, M, true>(
301  
            stream, std::move(buffers), std::move(match), initial_amount);
301  
            stream, std::move(buffers), std::move(match), initial_amount);
302  
}
302  
}
303  

303  

304  
/** Asynchronously read until a delimiter string is found.
304  
/** Asynchronously read until a delimiter string is found.
305  

305  

306  
    Reads data from the stream until the delimiter is found. This is
306  
    Reads data from the stream until the delimiter is found. This is
307  
    a convenience overload equivalent to calling `read_until` with
307  
    a convenience overload equivalent to calling `read_until` with
308  
    `match_delim{delim}`. If the delimiter already exists in the
308  
    `match_delim{delim}`. If the delimiter already exists in the
309  
    buffer, returns immediately without I/O.
309  
    buffer, returns immediately without I/O.
310  

310  

311  
    @li The operation completes when:
311  
    @li The operation completes when:
312  
    @li The delimiter string is found
312  
    @li The delimiter string is found
313  
    @li End-of-stream is reached (`cond::eof`)
313  
    @li End-of-stream is reached (`cond::eof`)
314  
    @li The buffer's `max_size()` is reached (`cond::not_found`)
314  
    @li The buffer's `max_size()` is reached (`cond::not_found`)
315  
    @li An error occurs
315  
    @li An error occurs
316  
    @li The operation is cancelled
316  
    @li The operation is cancelled
317  

317  

318  
    @par Cancellation
318  
    @par Cancellation
319  
    Supports cancellation via `stop_token` propagated through the
319  
    Supports cancellation via `stop_token` propagated through the
320  
    IoAwaitable protocol. When cancelled, returns with `cond::canceled`.
320  
    IoAwaitable protocol. When cancelled, returns with `cond::canceled`.
321  

321  

322  
    @param stream The stream to read from. The caller retains ownership.
322  
    @param stream The stream to read from. The caller retains ownership.
323  
    @param buffers The dynamic buffer to append data to. Must remain
323  
    @param buffers The dynamic buffer to append data to. Must remain
324  
        valid until the operation completes.
324  
        valid until the operation completes.
325  
    @param delim The delimiter string to search for.
325  
    @param delim The delimiter string to search for.
326  
    @param initial_amount Initial bytes to read per iteration (default
326  
    @param initial_amount Initial bytes to read per iteration (default
327  
        2048). Grows by 1.5x when filled.
327  
        2048). Grows by 1.5x when filled.
328  

328  

329  
    @return An awaitable yielding `(error_code, std::size_t)`.
329  
    @return An awaitable yielding `(error_code, std::size_t)`.
330  
        On success, `n` is bytes up to and including the delimiter.
330  
        On success, `n` is bytes up to and including the delimiter.
331  
        Compare error codes to conditions:
331  
        Compare error codes to conditions:
332  
        @li `cond::eof` - EOF before delimiter; `n` is buffer size
332  
        @li `cond::eof` - EOF before delimiter; `n` is buffer size
333  
        @li `cond::not_found` - `max_size()` reached before delimiter
333  
        @li `cond::not_found` - `max_size()` reached before delimiter
334  
        @li `cond::canceled` - Operation was cancelled
334  
        @li `cond::canceled` - Operation was cancelled
335  

335  

336  
    @par Example
336  
    @par Example
337  

337  

338  
    @code
338  
    @code
339  
    task<std::string> read_line( ReadStream auto& stream )
339  
    task<std::string> read_line( ReadStream auto& stream )
340  
    {
340  
    {
341  
        std::string line;
341  
        std::string line;
342  
        auto [ec, n] = co_await read_until(
342  
        auto [ec, n] = co_await read_until(
343  
            stream, string_dynamic_buffer( &line ), "\r\n" );
343  
            stream, string_dynamic_buffer( &line ), "\r\n" );
344  
        if( ec == cond::eof )
344  
        if( ec == cond::eof )
345  
            co_return line;  // partial line at EOF
345  
            co_return line;  // partial line at EOF
346  
        if( ec.failed() )
346  
        if( ec.failed() )
347  
            detail::throw_system_error( ec );
347  
            detail::throw_system_error( ec );
348  
        line.resize( n - 2 );  // remove "\r\n"
348  
        line.resize( n - 2 );  // remove "\r\n"
349  
        co_return line;
349  
        co_return line;
350  
    }
350  
    }
351  
    @endcode
351  
    @endcode
352  

352  

353  
    @see read_until, match_delim, DynamicBufferParam
353  
    @see read_until, match_delim, DynamicBufferParam
354  
*/
354  
*/
355  
template<ReadStream Stream, class B>
355  
template<ReadStream Stream, class B>
356  
    requires DynamicBufferParam<B&&>
356  
    requires DynamicBufferParam<B&&>
357  
auto
357  
auto
358  
read_until(
358  
read_until(
359  
    Stream& stream,
359  
    Stream& stream,
360  
    B&& buffers,
360  
    B&& buffers,
361  
    std::string_view delim,
361  
    std::string_view delim,
362  
    std::size_t initial_amount = 2048)
362  
    std::size_t initial_amount = 2048)
363  
{
363  
{
364  
    return read_until(
364  
    return read_until(
365  
        stream,
365  
        stream,
366  
        std::forward<B>(buffers),
366  
        std::forward<B>(buffers),
367  
        match_delim{delim},
367  
        match_delim{delim},
368  
        initial_amount);
368  
        initial_amount);
369  
}
369  
}
370  

370  

371  
} // namespace capy
371  
} // namespace capy
372  
} // namespace boost
372  
} // namespace boost
373  

373  

374  
#endif
374  
#endif