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