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