100.00% Lines (65/65) 100.00% Functions (12/12)
TLA Baseline Branch
Line Hits Code Line Hits Code
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
HITCBC 40   2 linearize_buffers(ConstBufferSequence auto const& data) 40   2 linearize_buffers(ConstBufferSequence auto const& data)
41   { 41   {
HITCBC 42   2 std::string linear; 42   2 std::string linear;
HITCBC 43   2 linear.reserve(buffer_size(data)); 43   2 linear.reserve(buffer_size(data));
HITCBC 44   2 auto const end_ = end(data); 44   2 auto const end_ = end(data);
DCB 45 - 6 {  
46 - const_buffer b = *it;  
HITCBC 47   4 for(auto it = begin(data); it != end_; ++it) 45   6 for(auto it = begin(data); it != end_; ++it)
HITCBC 48   8 linear.append( 46   8 linear.append(
HITCBC 49 - 4 static_cast<char const*>(b.data()), 47 + 4 static_cast<char const*>(it->data()),
50 - b.size()); 48 + it->size());
51 - }  
HITCBC 52   4 return linear; 49   4 return linear;
53   } // LCOV_EXCL_LINE gcov brace artifact (linearize_buffers is exercised) 50   } // LCOV_EXCL_LINE gcov brace artifact (linearize_buffers is exercised)
54   51  
55   // Search buffer using a MatchCondition, with single-buffer optimization 52   // Search buffer using a MatchCondition, with single-buffer optimization
56   template<MatchCondition M> 53   template<MatchCondition M>
57   std::size_t 54   std::size_t
HITCBC 58   277 search_buffer_for_match( 55   263 search_buffer_for_match(
59   ConstBufferSequence auto const& data, 56   ConstBufferSequence auto const& data,
60   M const& match, 57   M const& match,
61   std::size_t* hint = nullptr) 58   std::size_t* hint = nullptr)
62   { 59   {
63   // Fast path: single buffer - no linearization needed 60   // Fast path: single buffer - no linearization needed
HITCBC 64   277 if(buffer_length(data) == 1) 61   263 if(buffer_length(data) == 1)
65   { 62   {
HITCBC 66   276 auto const& buf = *begin(data); 63   262 auto const& buf = *begin(data);
HITCBC 67   828 return match(std::string_view( 64   786 return match(std::string_view(
HITCBC 68   276 static_cast<char const*>(buf.data()), 65   262 static_cast<char const*>(buf.data()),
HITCBC 69   276 buf.size()), hint); 66   262 buf.size()), hint);
70   } 67   }
71   // Multiple buffers - linearize 68   // Multiple buffers - linearize
HITCBC 72   1 return match(linearize_buffers(data), hint); 69   1 return match(linearize_buffers(data), hint);
73   } 70   }
74   71  
75   // Implementation coroutine for read_until with MatchCondition 72   // Implementation coroutine for read_until with MatchCondition
76   template<class Stream, class B, MatchCondition M> 73   template<class Stream, class B, MatchCondition M>
77   io_task<std::size_t> 74   io_task<std::size_t>
HITCBC 78   144 read_until_match_impl( 75   136 read_until_match_impl(
79   Stream& stream, 76   Stream& stream,
80   B& buffers, 77   B& buffers,
81   M match, 78   M match,
82   std::size_t initial_amount) 79   std::size_t initial_amount)
83   { 80   {
84   std::size_t amount = initial_amount; 81   std::size_t amount = initial_amount;
85   82  
86   for(;;) 83   for(;;)
87   { 84   {
88   // Check max_size before preparing 85   // Check max_size before preparing
89   if(buffers.size() >= buffers.max_size()) 86   if(buffers.size() >= buffers.max_size())
90   co_return {error::not_found, 0}; 87   co_return {error::not_found, 0};
91   88  
92   // Prepare space, respecting max_size 89   // Prepare space, respecting max_size
93   std::size_t const available = buffers.max_size() - buffers.size(); 90   std::size_t const available = buffers.max_size() - buffers.size();
94   std::size_t const to_prepare = (std::min)(amount, available); 91   std::size_t const to_prepare = (std::min)(amount, available);
95   if(to_prepare == 0) 92   if(to_prepare == 0)
96   co_return {error::not_found, 0}; 93   co_return {error::not_found, 0};
97   94  
98   auto mb = buffers.prepare(to_prepare); 95   auto mb = buffers.prepare(to_prepare);
99   auto [ec, n] = co_await stream.read_some(mb); 96   auto [ec, n] = co_await stream.read_some(mb);
100   buffers.commit(n); 97   buffers.commit(n);
101   98  
102   if(!ec) 99   if(!ec)
103   { 100   {
104   auto pos = search_buffer_for_match(buffers.data(), match); 101   auto pos = search_buffer_for_match(buffers.data(), match);
105   if(pos != std::string_view::npos) 102   if(pos != std::string_view::npos)
106   co_return {{}, pos}; 103   co_return {{}, pos};
107   } 104   }
108   105  
109   if(ec == cond::eof) 106   if(ec == cond::eof)
110   co_return {error::eof, buffers.size()}; 107   co_return {error::eof, buffers.size()};
111   if(ec) 108   if(ec)
112   co_return {ec, buffers.size()}; 109   co_return {ec, buffers.size()};
113   110  
114   // Grow buffer size for next iteration 111   // Grow buffer size for next iteration
115   if(n == buffer_size(mb)) 112   if(n == buffer_size(mb))
116   amount = amount / 2 + amount; 113   amount = amount / 2 + amount;
117   } 114   }
HITCBC 118   288 } 115   272 }
119   116  
120   template<class Stream, class B, MatchCondition M, bool OwnsBuffer> 117   template<class Stream, class B, MatchCondition M, bool OwnsBuffer>
121   struct read_until_awaitable 118   struct read_until_awaitable
122   { 119   {
123   Stream* stream_; 120   Stream* stream_;
124   M match_; 121   M match_;
125   std::size_t initial_amount_; 122   std::size_t initial_amount_;
126   std::optional<io_result<std::size_t>> immediate_; 123   std::optional<io_result<std::size_t>> immediate_;
127   std::optional<io_task<std::size_t>> inner_; 124   std::optional<io_task<std::size_t>> inner_;
128   125  
129   using storage_type = std::conditional_t<OwnsBuffer, B, B*>; 126   using storage_type = std::conditional_t<OwnsBuffer, B, B*>;
130   storage_type buffers_storage_; 127   storage_type buffers_storage_;
131   128  
HITCBC 132   144 B& buffers() noexcept 129   136 B& buffers() noexcept
133   { 130   {
134   if constexpr(OwnsBuffer) 131   if constexpr(OwnsBuffer)
HITCBC 135   134 return buffers_storage_; 132   126 return buffers_storage_;
136   else 133   else
HITCBC 137   10 return *buffers_storage_; 134   10 return *buffers_storage_;
138   } 135   }
139   136  
140   // Constructor for lvalue (pointer storage) 137   // Constructor for lvalue (pointer storage)
HITCBC 141   14 read_until_awaitable( 138   14 read_until_awaitable(
142   Stream& stream, 139   Stream& stream,
143   B* buffers, 140   B* buffers,
144   M match, 141   M match,
145   std::size_t initial_amount) 142   std::size_t initial_amount)
146   requires (!OwnsBuffer) 143   requires (!OwnsBuffer)
HITCBC 147   14 : stream_(std::addressof(stream)) 144   14 : stream_(std::addressof(stream))
HITCBC 148   14 , match_(std::move(match)) 145   14 , match_(std::move(match))
HITCBC 149   14 , initial_amount_(initial_amount) 146   14 , initial_amount_(initial_amount)
HITCBC 150   14 , buffers_storage_(buffers) 147   14 , buffers_storage_(buffers)
151   { 148   {
HITCBC 152   14 auto pos = search_buffer_for_match( 149   14 auto pos = search_buffer_for_match(
HITCBC 153   14 buffers_storage_->data(), match_); 150   14 buffers_storage_->data(), match_);
HITCBC 154   14 if(pos != std::string_view::npos) 151   14 if(pos != std::string_view::npos)
HITCBC 155   4 immediate_.emplace(io_result<std::size_t>{{}, pos}); 152   4 immediate_.emplace(io_result<std::size_t>{{}, pos});
HITCBC 156   14 } 153   14 }
157   154  
158   // Constructor for rvalue adapter (owned storage) 155   // Constructor for rvalue adapter (owned storage)
HITCBC 159   140 read_until_awaitable( 156   132 read_until_awaitable(
160   Stream& stream, 157   Stream& stream,
161   B&& buffers, 158   B&& buffers,
162   M match, 159   M match,
163   std::size_t initial_amount) 160   std::size_t initial_amount)
164   requires OwnsBuffer 161   requires OwnsBuffer
HITCBC 165   140 : stream_(std::addressof(stream)) 162   132 : stream_(std::addressof(stream))
HITCBC 166   140 , match_(std::move(match)) 163   132 , match_(std::move(match))
HITCBC 167   140 , initial_amount_(initial_amount) 164   132 , initial_amount_(initial_amount)
HITCBC 168   140 , buffers_storage_(std::move(buffers)) 165   132 , buffers_storage_(std::move(buffers))
169   { 166   {
HITCBC 170   140 auto pos = search_buffer_for_match( 167   132 auto pos = search_buffer_for_match(
HITCBC 171   140 buffers_storage_.data(), match_); 168   132 buffers_storage_.data(), match_);
HITCBC 172   140 if(pos != std::string_view::npos) 169   132 if(pos != std::string_view::npos)
HITCBC 173   6 immediate_.emplace(io_result<std::size_t>{{}, pos}); 170   6 immediate_.emplace(io_result<std::size_t>{{}, pos});
HITCBC 174   140 } 171   132 }
175   172  
176   bool 173   bool
HITCBC 177   154 await_ready() const noexcept 174   146 await_ready() const noexcept
178   { 175   {
HITCBC 179   154 return immediate_.has_value(); 176   146 return immediate_.has_value();
180   } 177   }
181   178  
182   std::coroutine_handle<> 179   std::coroutine_handle<>
HITCBC 183   144 await_suspend(std::coroutine_handle<> h, io_env const* env) 180   136 await_suspend(std::coroutine_handle<> h, io_env const* env)
184   { 181   {
HITCBC 185   288 inner_.emplace(read_until_match_impl( 182   272 inner_.emplace(read_until_match_impl(
HITCBC 186 - 288 *stream_, buffers(), std::move(match_), initial_amount_)); 183 + 136 *stream_, buffers(), match_, initial_amount_));
HITCBC 187   144 return inner_->await_suspend(h, env); 184   136 return inner_->await_suspend(h, env);
188   } 185   }
189   186  
190   io_result<std::size_t> 187   io_result<std::size_t>
HITCBC 191   154 await_resume() 188   146 await_resume()
192   { 189   {
HITCBC 193   154 if(immediate_) 190   146 if(immediate_)
HITCBC 194   10 return *immediate_; 191   10 return *immediate_;
HITCBC 195   144 return inner_->await_resume(); 192   136 return inner_->await_resume();
196   } 193   }
197   }; 194   };
198   195  
199   template<ReadStream Stream, class B, MatchCondition M> 196   template<ReadStream Stream, class B, MatchCondition M>
200   using read_until_return_t = read_until_awaitable< 197   using read_until_return_t = read_until_awaitable<
201   Stream, 198   Stream,
202   std::remove_reference_t<B>, 199   std::remove_reference_t<B>,
203   M, 200   M,
204   !std::is_lvalue_reference_v<B&&>>; 201   !std::is_lvalue_reference_v<B&&>>;
205   202  
206   } // namespace detail 203   } // namespace detail
207   204  
208   /** Match condition that searches for a delimiter string. 205   /** Match condition that searches for a delimiter string.
209   206  
210   Satisfies @ref MatchCondition. Returns the position after the 207   Satisfies @ref MatchCondition. Returns the position after the
211   delimiter when found, or `npos` otherwise. Provides an overlap 208   delimiter when found, or `npos` otherwise. Provides an overlap
212   hint of `delim.size() - 1` to handle delimiters spanning reads. 209   hint of `delim.size() - 1` to handle delimiters spanning reads.
213   210  
214   @see MatchCondition, read_until 211   @see MatchCondition, read_until
215   */ 212   */
216   struct match_delim 213   struct match_delim
217   { 214   {
218   /** The delimiter string to search for. 215   /** The delimiter string to search for.
219   216  
220   @note The referenced characters must remain valid 217   @note The referenced characters must remain valid
221   for the lifetime of this object and any pending 218   for the lifetime of this object and any pending
222   read operation. 219   read operation.
223   */ 220   */
224   std::string_view delim; 221   std::string_view delim;
225   222  
226   /** Search for the delimiter in `data`. 223   /** Search for the delimiter in `data`.
227   224  
228   @param data The data to search. 225   @param data The data to search.
229   @param hint If non-null, receives the overlap hint 226   @param hint If non-null, receives the overlap hint
230   on miss. 227   on miss.
231   @return `0` if `delim` is empty; otherwise the position 228   @return `0` if `delim` is empty; otherwise the position
232   just past the delimiter, or `npos` if not found. 229   just past the delimiter, or `npos` if not found.
233   */ 230   */
234   std::size_t 231   std::size_t
HITCBC 235   226 operator()( 232   226 operator()(
236   std::string_view data, 233   std::string_view data,
237   std::size_t* hint) const noexcept 234   std::size_t* hint) const noexcept
238   { 235   {
HITCBC 239   226 if(delim.empty()) 236   226 if(delim.empty())
HITCBC 240   2 return 0; 237   2 return 0;
HITCBC 241   224 auto pos = data.find(delim); 238   224 auto pos = data.find(delim);
HITCBC 242   224 if(pos != std::string_view::npos) 239   224 if(pos != std::string_view::npos)
HITCBC 243   27 return pos + delim.size(); 240   27 return pos + delim.size();
HITCBC 244   197 if(hint) 241   197 if(hint)
HITCBC 245   1 *hint = delim.size() > 1 ? delim.size() - 1 : 0; 242   1 *hint = delim.size() > 1 ? delim.size() - 1 : 0;
HITCBC 246   197 return std::string_view::npos; 243   197 return std::string_view::npos;
247   } 244   }
248   }; 245   };
249   246  
250   /** Asynchronously read until a match condition is satisfied. 247   /** Asynchronously read until a match condition is satisfied.
251   248  
252   Reads data from `stream` and appends it to `dynbuf` via calling 249   Reads data from `stream` and appends it to `dynbuf` via calling
253   `stream.read_some` zero or more times and using the prepare/commit 250   `stream.read_some` zero or more times and using the prepare/commit
254   interface until: 251   interface until:
255   252  
256   @li either @c match returns a valid position, 253   @li either @c match returns a valid position,
257   @li or @c dynbuf.size() == @c dynbuf.max_size() , 254   @li or @c dynbuf.size() == @c dynbuf.max_size() ,
258   @li or a contingency on @c stream.read_some occurs. 255   @li or a contingency on @c stream.read_some occurs.
259   256  
260   If the match condition is satisfied by data in `dynbuf.data()` upon entry, 257   If the match condition is satisfied by data in `dynbuf.data()` upon entry,
261   no call to `stream.read_some` is performed. 258   no call to `stream.read_some` is performed.
262   259  
263   260  
264   @par Await-returns 261   @par Await-returns
265   262  
266   An object of type `io_result<std::size_t>` destructuring as `[ec, n]`. 263   An object of type `io_result<std::size_t>` destructuring as `[ec, n]`.
267   264  
268   If `bool(ec)`, `n` is the position returned by the match condition 265   If `bool(ec)`, `n` is the position returned by the match condition
269   (bytes up to and including the matched delimiter). 266   (bytes up to and including the matched delimiter).
270   267  
271   268  
272   Contingencies: 269   Contingencies:
273   270  
274   @li The first contingency, reported from awaiting @c stream.read_some . 271   @li The first contingency, reported from awaiting @c stream.read_some .
275   @li @c cond::not_found -- when @c dynbuf.size() == @c dynbuf.max_size() 272   @li @c cond::not_found -- when @c dynbuf.size() == @c dynbuf.max_size()
276   and the match condition is not satisfied by data in @c dynbuf.data() . 273   and the match condition is not satisfied by data in @c dynbuf.data() .
277   274  
278   @param stream The stream to read from. The caller retains ownership. 275   @param stream The stream to read from. The caller retains ownership.
279   @param dynbuf The dynamic buffer to append data to. Must remain 276   @param dynbuf The dynamic buffer to append data to. Must remain
280   valid until the operation completes. 277   valid until the operation completes.
281   @param match The match condition callable. Copied into the awaitable. 278   @param match The match condition callable. Copied into the awaitable.
282   @param initial_amount Initial bytes to read per iteration (default 279   @param initial_amount Initial bytes to read per iteration (default
283   2048). Grows by 1.5x when filled. 280   2048). Grows by 1.5x when filled.
284   281  
285   282  
286   283  
287   284  
288   @par Await-throws 285   @par Await-throws
289   286  
290   Whatever operations on @c dunbuf throw. 287   Whatever operations on @c dunbuf throw.
291   288  
292   (Note: types modeling @c DynamicBufferParam provided by Capy throw 289   (Note: types modeling @c DynamicBufferParam provided by Capy throw
293   @c std::bad_alloc from member function 290   @c std::bad_alloc from member function
294   @c prepare .) 291   @c prepare .)
295   292  
296   @par Remarks 293   @par Remarks
297   Supports _IoAwaitable cancellation_. 294   Supports _IoAwaitable cancellation_.
298   295  
299   @par Example 296   @par Example
300   297  
301   @code 298   @code
302   task<> read_http_header( ReadStream auto& stream ) 299   task<> read_http_header( ReadStream auto& stream )
303   { 300   {
304   std::string header; 301   std::string header;
305   auto [ec, n] = co_await read_until( 302   auto [ec, n] = co_await read_until(
306   stream, 303   stream,
307   string_dynamic_buffer( &header ), 304   string_dynamic_buffer( &header ),
308   []( std::string_view data, std::size_t* hint ) { 305   []( std::string_view data, std::size_t* hint ) {
309   auto pos = data.find( "\r\n\r\n" ); 306   auto pos = data.find( "\r\n\r\n" );
310   if( pos != std::string_view::npos ) 307   if( pos != std::string_view::npos )
311   return pos + 4; 308   return pos + 4;
312   if( hint ) 309   if( hint )
313   (*hint) = 3; // partial "\r\n\r" possible 310   (*hint) = 3; // partial "\r\n\r" possible
314   return std::string_view::npos; 311   return std::string_view::npos;
315   } ); 312   } );
316   if( ec ) 313   if( ec )
317   detail::throw_system_error( ec ); 314   detail::throw_system_error( ec );
318   // header contains data through "\r\n\r\n" 315   // header contains data through "\r\n\r\n"
319   } 316   }
320   @endcode 317   @endcode
321   318  
322   @see read_some, MatchCondition, DynamicBufferParam 319   @see read_some, MatchCondition, DynamicBufferParam
323   */ 320   */
324   template<ReadStream Stream, class B, MatchCondition M> 321   template<ReadStream Stream, class B, MatchCondition M>
325   requires DynamicBufferParam<B&&> 322   requires DynamicBufferParam<B&&>
326   detail::read_until_return_t<Stream, B, M> 323   detail::read_until_return_t<Stream, B, M>
HITCBC 327   154 read_until( 324   146 read_until(
328   Stream& stream, 325   Stream& stream,
329   B&& dynbuf, 326   B&& dynbuf,
330   M match, 327   M match,
331   std::size_t initial_amount = 2048) 328   std::size_t initial_amount = 2048)
332   { 329   {
HITCBC 333   154 constexpr bool is_lvalue = std::is_lvalue_reference_v<B&&>; 330   146 constexpr bool is_lvalue = std::is_lvalue_reference_v<B&&>;
334   using BareB = std::remove_reference_t<B>; 331   using BareB = std::remove_reference_t<B>;
335   332  
336   if constexpr(is_lvalue) 333   if constexpr(is_lvalue)
337   return detail::read_until_awaitable<Stream, BareB, M, false>( 334   return detail::read_until_awaitable<Stream, BareB, M, false>(
HITCBC 338   14 stream, std::addressof(dynbuf), std::move(match), initial_amount); 335   14 stream, std::addressof(dynbuf), std::move(match), initial_amount);
339   else 336   else
340   return detail::read_until_awaitable<Stream, BareB, M, true>( 337   return detail::read_until_awaitable<Stream, BareB, M, true>(
HITCBC 341   140 stream, std::move(dynbuf), std::move(match), initial_amount); 338   132 stream, std::move(dynbuf), std::move(match), initial_amount);
342   } 339   }
343   340  
344   /** Asynchronously read until a delimiter string is found. 341   /** Asynchronously read until a delimiter string is found.
345   342  
346   Reads data from the stream until the delimiter is found. This is 343   Reads data from the stream until the delimiter is found. This is
347   a convenience overload equivalent to calling `read_until` with 344   a convenience overload equivalent to calling `read_until` with
348   `match_delim{delim}`. If the delimiter already exists in the 345   `match_delim{delim}`. If the delimiter already exists in the
349   buffer, returns immediately without I/O. 346   buffer, returns immediately without I/O.
350   347  
351   @li The operation completes when: 348   @li The operation completes when:
352   @li The delimiter string is found 349   @li The delimiter string is found
353   @li End-of-stream is reached (`cond::eof`) 350   @li End-of-stream is reached (`cond::eof`)
354   @li The buffer's `max_size()` is reached (`cond::not_found`) 351   @li The buffer's `max_size()` is reached (`cond::not_found`)
355   @li An error occurs 352   @li An error occurs
356   @li The operation is cancelled 353   @li The operation is cancelled
357   354  
358   @par Cancellation 355   @par Cancellation
359   Supports cancellation via `stop_token` propagated through the 356   Supports cancellation via `stop_token` propagated through the
360   IoAwaitable protocol. When cancelled, returns with `cond::canceled`. 357   IoAwaitable protocol. When cancelled, returns with `cond::canceled`.
361   358  
362   @param stream The stream to read from. The caller retains ownership. 359   @param stream The stream to read from. The caller retains ownership.
363   @param buffers The dynamic buffer to append data to. Must remain 360   @param buffers The dynamic buffer to append data to. Must remain
364   valid until the operation completes. 361   valid until the operation completes.
365   @param delim The delimiter string to search for. 362   @param delim The delimiter string to search for.
366   @param initial_amount Initial bytes to read per iteration (default 363   @param initial_amount Initial bytes to read per iteration (default
367   2048). Grows by 1.5x when filled. 364   2048). Grows by 1.5x when filled.
368   365  
369   @return An awaitable that await-returns `(error_code, std::size_t)`. 366   @return An awaitable that await-returns `(error_code, std::size_t)`.
370   On success, `n` is bytes up to and including the delimiter. 367   On success, `n` is bytes up to and including the delimiter.
371   Compare error codes to conditions: 368   Compare error codes to conditions:
372   @li `cond::eof` - EOF before delimiter; `n` is buffer size 369   @li `cond::eof` - EOF before delimiter; `n` is buffer size
373   @li `cond::not_found` - `max_size()` reached before delimiter 370   @li `cond::not_found` - `max_size()` reached before delimiter
374   @li `cond::canceled` - Operation was cancelled 371   @li `cond::canceled` - Operation was cancelled
375   372  
376   @par Example 373   @par Example
377   374  
378   @code 375   @code
379   task<std::string> read_line( ReadStream auto& stream ) 376   task<std::string> read_line( ReadStream auto& stream )
380   { 377   {
381   std::string line; 378   std::string line;
382   auto [ec, n] = co_await read_until( 379   auto [ec, n] = co_await read_until(
383   stream, string_dynamic_buffer( &line ), "\r\n" ); 380   stream, string_dynamic_buffer( &line ), "\r\n" );
384   if( ec == cond::eof ) 381   if( ec == cond::eof )
385   co_return line; // partial line at EOF 382   co_return line; // partial line at EOF
386   if( ec ) 383   if( ec )
387   detail::throw_system_error( ec ); 384   detail::throw_system_error( ec );
388   line.resize( n - 2 ); // remove "\r\n" 385   line.resize( n - 2 ); // remove "\r\n"
389   co_return line; 386   co_return line;
390   } 387   }
391   @endcode 388   @endcode
392   389  
393   @see read_until, match_delim, DynamicBufferParam 390   @see read_until, match_delim, DynamicBufferParam
394   */ 391   */
395   template<ReadStream Stream, class B> 392   template<ReadStream Stream, class B>
396   requires DynamicBufferParam<B&&> 393   requires DynamicBufferParam<B&&>
397   detail::read_until_return_t<Stream, B, match_delim> 394   detail::read_until_return_t<Stream, B, match_delim>
HITCBC 398   118 read_until( 395   118 read_until(
399   Stream& stream, 396   Stream& stream,
400   B&& buffers, 397   B&& buffers,
401   std::string_view delim, 398   std::string_view delim,
402   std::size_t initial_amount = 2048) 399   std::size_t initial_amount = 2048)
403   { 400   {
404   return read_until( 401   return read_until(
405   stream, 402   stream,
406   std::forward<B>(buffers), 403   std::forward<B>(buffers),
407   match_delim{delim}, 404   match_delim{delim},
HITCBC 408   118 initial_amount); 405   118 initial_amount);
409   } 406   }
410   407  
411   } // namespace capy 408   } // namespace capy
412   } // namespace boost 409   } // namespace boost
413   410  
414   #endif 411   #endif