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