1  
//
1  
//
2  
// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
2  
// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.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 <coroutine>
16  
#include <coroutine>
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/io_env.hpp>
23  
#include <boost/capy/ex/io_env.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(!ec)
99  
        if(!ec)
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  
    std::coroutine_handle<>
179  
    std::coroutine_handle<>
180  
    await_suspend(std::coroutine_handle<> h, io_env const* env)
180  
    await_suspend(std::coroutine_handle<> h, io_env const* env)
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, env);
184  
        return inner_->await_suspend(h, env);
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 -
    /** The delimiter string to search for.
 
208 -

 
209 -
        @note The referenced characters must remain valid
 
210 -
            for the lifetime of this object and any pending
 
211 -
            read operation.
 
212 -
    */
 
213  
{
207  
{
214  
    std::string_view delim;
208  
    std::string_view delim;
215 -
    /** Search for the delimiter in `data`.
 
216 -

 
217 -
        @param data The data to search.
 
218 -
        @param hint If non-null, receives the overlap hint
 
219 -
            on miss.
 
220 -
        @return `0` if `delim` is empty; otherwise the position
 
221 -
            just past the delimiter, or `npos` if not found.
 
222 -
    */
 
223  

209  

224  
    std::size_t
210  
    std::size_t
225  
    operator()(
211  
    operator()(
226  
        std::string_view data,
212  
        std::string_view data,
227  
        std::size_t* hint) const noexcept
213  
        std::size_t* hint) const noexcept
228  
    {
214  
    {
229  
        if(delim.empty())
215  
        if(delim.empty())
230  
            return 0;
216  
            return 0;
231  
        auto pos = data.find(delim);
217  
        auto pos = data.find(delim);
232  
        if(pos != std::string_view::npos)
218  
        if(pos != std::string_view::npos)
233  
            return pos + delim.size();
219  
            return pos + delim.size();
234  
        if(hint)
220  
        if(hint)
235  
            *hint = delim.size() > 1 ? delim.size() - 1 : 0;
221  
            *hint = delim.size() > 1 ? delim.size() - 1 : 0;
236  
        return std::string_view::npos;
222  
        return std::string_view::npos;
237  
    }
223  
    }
238  
};
224  
};
239  

225  

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

227  

242  
    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
243  
    condition returns a valid position. Implemented using `read_some`.
229  
    condition returns a valid position. Implemented using `read_some`.
244  
    If the match condition is already satisfied by existing buffer
230  
    If the match condition is already satisfied by existing buffer
245  
    data, returns immediately without I/O.
231  
    data, returns immediately without I/O.
246  

232  

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

239  

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

243  

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

250  

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

258  

273  
    @par Example
259  
    @par Example
274  

260  

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

281  

296  
    @see read_some, MatchCondition, DynamicBufferParam
282  
    @see read_some, MatchCondition, DynamicBufferParam
297  
*/
283  
*/
298  
template<ReadStream Stream, class B, MatchCondition M>
284  
template<ReadStream Stream, class B, MatchCondition M>
299  
    requires DynamicBufferParam<B&&>
285  
    requires DynamicBufferParam<B&&>
300  
auto
286  
auto
301  
read_until(
287  
read_until(
302  
    Stream& stream,
288  
    Stream& stream,
303  
    B&& buffers,
289  
    B&& buffers,
304  
    M match,
290  
    M match,
305  
    std::size_t initial_amount = 2048)
291  
    std::size_t initial_amount = 2048)
306  
{
292  
{
307  
    constexpr bool is_lvalue = std::is_lvalue_reference_v<B&&>;
293  
    constexpr bool is_lvalue = std::is_lvalue_reference_v<B&&>;
308  
    using BareB = std::remove_reference_t<B>;
294  
    using BareB = std::remove_reference_t<B>;
309  

295  

310  
    if constexpr(is_lvalue)
296  
    if constexpr(is_lvalue)
311  
        return detail::read_until_awaitable<Stream, BareB, M, false>(
297  
        return detail::read_until_awaitable<Stream, BareB, M, false>(
312  
            stream, std::addressof(buffers), std::move(match), initial_amount);
298  
            stream, std::addressof(buffers), std::move(match), initial_amount);
313  
    else
299  
    else
314  
        return detail::read_until_awaitable<Stream, BareB, M, true>(
300  
        return detail::read_until_awaitable<Stream, BareB, M, true>(
315  
            stream, std::move(buffers), std::move(match), initial_amount);
301  
            stream, std::move(buffers), std::move(match), initial_amount);
316  
}
302  
}
317  

303  

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

305  

320  
    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
321  
    a convenience overload equivalent to calling `read_until` with
307  
    a convenience overload equivalent to calling `read_until` with
322  
    `match_delim{delim}`. If the delimiter already exists in the
308  
    `match_delim{delim}`. If the delimiter already exists in the
323  
    buffer, returns immediately without I/O.
309  
    buffer, returns immediately without I/O.
324  

310  

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

317  

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

321  

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

328  

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

335  

350  
    @par Example
336  
    @par Example
351  

337  

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

352  

367  
    @see read_until, match_delim, DynamicBufferParam
353  
    @see read_until, match_delim, DynamicBufferParam
368  
*/
354  
*/
369  
template<ReadStream Stream, class B>
355  
template<ReadStream Stream, class B>
370  
    requires DynamicBufferParam<B&&>
356  
    requires DynamicBufferParam<B&&>
371  
auto
357  
auto
372  
read_until(
358  
read_until(
373  
    Stream& stream,
359  
    Stream& stream,
374  
    B&& buffers,
360  
    B&& buffers,
375  
    std::string_view delim,
361  
    std::string_view delim,
376  
    std::size_t initial_amount = 2048)
362  
    std::size_t initial_amount = 2048)
377  
{
363  
{
378  
    return read_until(
364  
    return read_until(
379  
        stream,
365  
        stream,
380  
        std::forward<B>(buffers),
366  
        std::forward<B>(buffers),
381  
        match_delim{delim},
367  
        match_delim{delim},
382  
        initial_amount);
368  
        initial_amount);
383  
}
369  
}
384  

370  

385  
} // namespace capy
371  
} // namespace capy
386  
} // namespace boost
372  
} // namespace boost
387  

373  

388  
#endif
374  
#endif